1use std::sync::Arc;
2
3use bytes::Bytes;
4use prost::Message as _;
5
6use futu_core::error::{FutuError, Result};
7use futu_domain_qot_indicator::{
8 CatalogPushResult, IndicatorInfo, IndicatorInputValue, IndicatorLanguage, IndicatorParam,
9 IndicatorPushOp, IndicatorRefreshOwner, IndicatorRefreshTicket, RefreshProgress,
10};
11
12use crate::proto_internal::{
13 ft_index_info_common as common, ft_python_indicator_info as py_info,
14 ft_python_indicator_param as py_param,
15};
16use crate::{command_runtime::execute_indicator_read, conn::BackendConn};
17
18use futu_command_spec::IndicatorOperation;
19
20#[must_use]
21pub fn build_mylang_info_request(uid: u64, start_guid: &str) -> common::FTindexDownloadReq {
22 common::FTindexDownloadReq {
23 uid: Some(uid),
24 guid: Vec::new(),
25 last_updated_time: None,
26 start_guid: non_empty(start_guid),
27 }
28}
29
30#[must_use]
31pub fn build_mylang_param_request(uid: u64, start_guid: &str) -> common::FtIndexParamDownloadReq {
32 common::FtIndexParamDownloadReq {
33 uid: Some(uid),
34 guid: Vec::new(),
35 last_updated_time: None,
36 start_guid: non_empty(start_guid),
37 }
38}
39
40#[must_use]
41pub fn build_python_info_request(start_guid: &str) -> py_info::PythonIndicatorInfoDownloadReq {
42 py_info::PythonIndicatorInfoDownloadReq {
43 start_guid: non_empty(start_guid),
44 }
45}
46
47#[must_use]
48pub fn build_python_param_request(start_guid: &str) -> py_param::PythonIndicatorParamDownloadReq {
49 py_param::PythonIndicatorParamDownloadReq {
50 guid: Vec::new(),
51 start_guid: non_empty(start_guid),
52 }
53}
54
55#[must_use]
56pub fn adapt_mylang_info(mut item: common::IndexInfo) -> Option<IndicatorInfo> {
57 if let Some(script) = item.index_script.as_mut() {
64 script.make_ascii_uppercase();
65 }
66 for parameter in &mut item.index_func_param {
67 if let Some(name) = parameter.name.as_mut() {
68 name.make_ascii_uppercase();
69 }
70 }
71 if let Some(short_name) = item
72 .title
73 .as_mut()
74 .and_then(|title| title.short_cut.as_mut())
75 {
76 short_name.make_ascii_uppercase();
77 }
78 let raw_payload = item.encode_to_vec();
79 let guid = item.guid.clone().filter(|guid| !guid.is_empty())?;
80 let title = item.title.as_ref();
81 let short_name = title
82 .and_then(|title| title.short_cut.as_ref())
83 .filter(|name| !name.is_empty())
84 .cloned()
85 .unwrap_or_else(|| cpp_guid_short_name(&guid));
86 Some(IndicatorInfo {
87 language: IndicatorLanguage::MyLang,
88 guid,
89 short_name: Some(short_name),
90 full_name: None,
94 script: item.index_script,
95 revision: item.update_timestamp.or(item.create_timestamp),
96 raw_payload,
97 })
98}
99
100#[must_use]
101pub fn adapt_python_info(item: common::PythonIndicatorInfo) -> Option<IndicatorInfo> {
102 let raw_payload = item.encode_to_vec();
103 let guid = item.guid.clone().filter(|guid| !guid.is_empty())?;
104 Some(IndicatorInfo {
105 language: IndicatorLanguage::Python,
106 guid,
107 short_name: None,
110 full_name: None,
111 script: item.script,
112 revision: item.update_timestamp.or(item.version.map(i64::from)),
113 raw_payload,
114 })
115}
116
117#[must_use]
118pub fn adapt_mylang_param(item: common::IndexParam) -> Option<IndicatorParam> {
119 let raw_payload = item.encode_to_vec();
120 let guid = item.guid.clone().filter(|guid| !guid.is_empty())?;
121 Some(IndicatorParam {
122 language: IndicatorLanguage::MyLang,
123 guid,
124 input_values: item
125 .param_float_val
126 .iter()
127 .map(|value| IndicatorInputValue::Float(format_float(*value)))
128 .collect(),
129 output_styles: item
130 .color_val
131 .iter()
132 .map(|value| Some(i64::from(*value)))
133 .collect(),
134 revision: None,
135 raw_payload,
136 })
137}
138
139#[must_use]
140pub fn adapt_python_param(item: common::PythonIndicatorParam) -> Option<IndicatorParam> {
141 let raw_payload = item.encode_to_vec();
142 let guid = item.guid.clone().filter(|guid| !guid.is_empty())?;
143 Some(IndicatorParam {
144 language: IndicatorLanguage::Python,
145 guid,
146 input_values: item.input.iter().map(python_input_value).collect(),
147 output_styles: item
150 .output_line
151 .iter()
152 .map(|style| style.line_type.map(i64::from))
153 .collect(),
154 revision: None,
155 raw_payload,
156 })
157}
158
159#[must_use]
160pub const fn decode_indicator_push_op(value: i32) -> Option<IndicatorPushOp> {
161 match value {
162 1 => Some(IndicatorPushOp::Add),
163 2 => Some(IndicatorPushOp::Update),
164 3 => Some(IndicatorPushOp::Delete),
165 _ => None,
166 }
167}
168
169pub async fn refresh_indicator_metadata(
170 backend: &BackendConn,
171 cache: &Arc<futu_cache::indicator::IndicatorMetadataCache>,
172 uid: u64,
173 owner: IndicatorRefreshOwner,
174 owner_is_current: &(impl Fn(IndicatorRefreshOwner) -> bool + Sync),
175) -> Result<Arc<futu_domain_qot_indicator::IndicatorCatalogSnapshot>> {
176 ensure_current_owner(owner, owner_is_current)?;
177 let ticket = cache.begin_refresh(owner);
178 refresh_indicator_metadata_with_ticket(backend, cache, uid, ticket, owner_is_current).await
179}
180
181pub async fn refresh_indicator_metadata_with_ticket(
182 backend: &BackendConn,
183 cache: &Arc<futu_cache::indicator::IndicatorMetadataCache>,
184 uid: u64,
185 ticket: IndicatorRefreshTicket,
186 owner_is_current: &(impl Fn(IndicatorRefreshOwner) -> bool + Sync),
187) -> Result<Arc<futu_domain_qot_indicator::IndicatorCatalogSnapshot>> {
188 if let Err(error) = ensure_current_owner(ticket.owner(), owner_is_current) {
189 cache.fail_stream(ticket, IndicatorLanguage::MyLang, true);
190 return Err(error);
191 }
192 let result = tokio::try_join!(
193 download_mylang_info(backend, cache, ticket, uid, owner_is_current),
194 download_mylang_param(backend, cache, ticket, uid, owner_is_current),
195 download_python_info(backend, cache, ticket, owner_is_current),
196 download_python_param(backend, cache, ticket, owner_is_current),
197 );
198 if let Err(error) = result {
199 cache.fail_stream(ticket, IndicatorLanguage::MyLang, true);
200 return Err(error);
201 }
202 let snapshot = cache.snapshot().ok_or_else(|| {
203 FutuError::Codec("indicator four-stream refresh finished without publication".into())
204 })?;
205 if snapshot.owner != ticket.owner() {
206 return Err(FutuError::Codec(
207 "indicator refresh was superseded before composite publication".into(),
208 ));
209 }
210 Ok(snapshot)
211}
212
213pub fn apply_mylang_info_push(
214 cache: &futu_cache::indicator::IndicatorMetadataCache,
215 owner: IndicatorRefreshOwner,
216 body: &[u8],
217) -> Result<CatalogPushResult> {
218 let push = common::InfoPush::decode(body).map_err(FutuError::Proto)?;
219 let op = push_op(push.opr)?;
220 let item = push
221 .item
222 .and_then(adapt_mylang_info)
223 .ok_or_else(|| FutuError::Codec("MyLang info push missing guid item".into()))?;
224 cache
225 .apply_info_push(owner, op, item)
226 .map_err(indicator_state_error)
227}
228
229pub fn apply_mylang_param_push(
230 cache: &futu_cache::indicator::IndicatorMetadataCache,
231 owner: IndicatorRefreshOwner,
232 body: &[u8],
233) -> Result<CatalogPushResult> {
234 let push = common::ParamPush::decode(body).map_err(FutuError::Proto)?;
235 let op = push_op(push.opr)?;
236 let item = push
237 .item
238 .and_then(adapt_mylang_param)
239 .ok_or_else(|| FutuError::Codec("MyLang param push missing guid item".into()))?;
240 cache
241 .apply_param_push(owner, op, item)
242 .map_err(indicator_state_error)
243}
244
245pub fn apply_python_info_push(
246 cache: &futu_cache::indicator::IndicatorMetadataCache,
247 owner: IndicatorRefreshOwner,
248 body: &[u8],
249) -> Result<CatalogPushResult> {
250 let push = py_info::PythonIndicatorInfoPush::decode(body).map_err(FutuError::Proto)?;
251 let op = push_op(push.opr)?;
252 let item = push
253 .item
254 .and_then(adapt_python_info)
255 .ok_or_else(|| FutuError::Codec("Python info push missing guid item".into()))?;
256 cache
257 .apply_info_push(owner, op, item)
258 .map_err(indicator_state_error)
259}
260
261pub fn apply_python_param_push(
262 cache: &futu_cache::indicator::IndicatorMetadataCache,
263 owner: IndicatorRefreshOwner,
264 body: &[u8],
265) -> Result<CatalogPushResult> {
266 let push = py_param::PythonParamPush::decode(body).map_err(FutuError::Proto)?;
267 let op = push_op(push.opr)?;
268 let item = push
269 .item
270 .and_then(adapt_python_param)
271 .ok_or_else(|| FutuError::Codec("Python param push missing guid item".into()))?;
272 cache
273 .apply_param_push(owner, op, item)
274 .map_err(indicator_state_error)
275}
276
277async fn download_mylang_info(
278 backend: &BackendConn,
279 cache: &Arc<futu_cache::indicator::IndicatorMetadataCache>,
280 ticket: IndicatorRefreshTicket,
281 uid: u64,
282 owner_is_current: &(impl Fn(IndicatorRefreshOwner) -> bool + Sync),
283) -> Result<()> {
284 let mut cursor = String::new();
285 loop {
286 let response = execute_indicator_read(
287 backend,
288 IndicatorOperation::MyLangInfoDownload,
289 Bytes::from(build_mylang_info_request(uid, &cursor).encode_to_vec()),
290 )
291 .await?;
292 let page =
293 common::FTindexDownloadRsp::decode(response.body.as_ref()).map_err(FutuError::Proto)?;
294 ensure_page_success(page.ret_code, page.ret_msg.as_deref(), "MyLang info")?;
295 ensure_current_owner(ticket.owner(), owner_is_current)?;
296 let progress = cache
297 .apply_info_page(
298 ticket,
299 IndicatorLanguage::MyLang,
300 page.items
301 .into_iter()
302 .filter_map(adapt_mylang_info)
303 .collect(),
304 page.next_start_guid.as_deref(),
305 )
306 .map_err(indicator_state_error)?;
307 match progress {
308 RefreshProgress::Continue(next) => cursor = next,
309 RefreshProgress::Pending | RefreshProgress::Published(_) => return Ok(()),
310 }
311 }
312}
313
314async fn download_mylang_param(
315 backend: &BackendConn,
316 cache: &Arc<futu_cache::indicator::IndicatorMetadataCache>,
317 ticket: IndicatorRefreshTicket,
318 uid: u64,
319 owner_is_current: &(impl Fn(IndicatorRefreshOwner) -> bool + Sync),
320) -> Result<()> {
321 let mut cursor = String::new();
322 loop {
323 let response = execute_indicator_read(
324 backend,
325 IndicatorOperation::MyLangParamDownload,
326 Bytes::from(build_mylang_param_request(uid, &cursor).encode_to_vec()),
327 )
328 .await?;
329 let page = common::FtIndexParamDownloadRsp::decode(response.body.as_ref())
330 .map_err(FutuError::Proto)?;
331 ensure_page_success(page.ret_code, page.ret_msg.as_deref(), "MyLang param")?;
332 ensure_current_owner(ticket.owner(), owner_is_current)?;
333 let progress = cache
334 .apply_param_page(
335 ticket,
336 IndicatorLanguage::MyLang,
337 page.items
338 .into_iter()
339 .filter_map(adapt_mylang_param)
340 .collect(),
341 page.next_start_guid.as_deref(),
342 )
343 .map_err(indicator_state_error)?;
344 match progress {
345 RefreshProgress::Continue(next) => cursor = next,
346 RefreshProgress::Pending | RefreshProgress::Published(_) => return Ok(()),
347 }
348 }
349}
350
351async fn download_python_info(
352 backend: &BackendConn,
353 cache: &Arc<futu_cache::indicator::IndicatorMetadataCache>,
354 ticket: IndicatorRefreshTicket,
355 owner_is_current: &(impl Fn(IndicatorRefreshOwner) -> bool + Sync),
356) -> Result<()> {
357 let mut cursor = String::new();
358 loop {
359 let response = execute_indicator_read(
360 backend,
361 IndicatorOperation::PythonInfoDownload,
362 Bytes::from(build_python_info_request(&cursor).encode_to_vec()),
363 )
364 .await?;
365 let page = py_info::PythonIndicatorInfoDownloadRsp::decode(response.body.as_ref())
366 .map_err(FutuError::Proto)?;
367 ensure_page_success(page.ret_code, page.ret_msg.as_deref(), "Python info")?;
368 ensure_current_owner(ticket.owner(), owner_is_current)?;
369 let progress = cache
370 .apply_info_page(
371 ticket,
372 IndicatorLanguage::Python,
373 page.items
374 .into_iter()
375 .filter_map(adapt_python_info)
376 .collect(),
377 page.next_start_guid.as_deref(),
378 )
379 .map_err(indicator_state_error)?;
380 match progress {
381 RefreshProgress::Continue(next) => cursor = next,
382 RefreshProgress::Pending | RefreshProgress::Published(_) => return Ok(()),
383 }
384 }
385}
386
387async fn download_python_param(
388 backend: &BackendConn,
389 cache: &Arc<futu_cache::indicator::IndicatorMetadataCache>,
390 ticket: IndicatorRefreshTicket,
391 owner_is_current: &(impl Fn(IndicatorRefreshOwner) -> bool + Sync),
392) -> Result<()> {
393 let mut cursor = String::new();
394 loop {
395 let response = execute_indicator_read(
396 backend,
397 IndicatorOperation::PythonParamDownload,
398 Bytes::from(build_python_param_request(&cursor).encode_to_vec()),
399 )
400 .await?;
401 let page = py_param::PythonIndicatorParamDownloadRsp::decode(response.body.as_ref())
402 .map_err(FutuError::Proto)?;
403 ensure_page_success(page.ret_code, page.ret_msg.as_deref(), "Python param")?;
404 ensure_current_owner(ticket.owner(), owner_is_current)?;
405 let progress = cache
406 .apply_param_page(
407 ticket,
408 IndicatorLanguage::Python,
409 page.items
410 .into_iter()
411 .filter_map(adapt_python_param)
412 .collect(),
413 page.next_start_guid.as_deref(),
414 )
415 .map_err(indicator_state_error)?;
416 match progress {
417 RefreshProgress::Continue(next) => cursor = next,
418 RefreshProgress::Pending | RefreshProgress::Published(_) => return Ok(()),
419 }
420 }
421}
422
423fn ensure_current_owner(
424 owner: IndicatorRefreshOwner,
425 owner_is_current: &(impl Fn(IndicatorRefreshOwner) -> bool + Sync),
426) -> Result<()> {
427 if owner_is_current(owner) {
428 Ok(())
429 } else {
430 Err(FutuError::Codec(
431 "indicator refresh belongs to a retired auth/connection generation".into(),
432 ))
433 }
434}
435
436fn ensure_page_success(code: Option<i32>, message: Option<&str>, label: &str) -> Result<()> {
437 match code.unwrap_or(0) {
442 0 => Ok(()),
443 code => Err(FutuError::ServerError {
444 ret_type: code,
445 msg: message
446 .map(str::to_string)
447 .unwrap_or_else(|| format!("{label} backend rejected request")),
448 }),
449 }
450}
451
452fn push_op(value: Option<i32>) -> Result<IndicatorPushOp> {
453 value
454 .and_then(decode_indicator_push_op)
455 .ok_or_else(|| FutuError::Codec("indicator push has invalid operation".into()))
456}
457
458fn indicator_state_error(error: futu_domain_qot_indicator::IndicatorApplyError) -> FutuError {
459 FutuError::Codec(format!(
460 "indicator metadata state rejected update: {error:?}"
461 ))
462}
463
464fn non_empty(value: &str) -> Option<String> {
465 (!value.is_empty()).then(|| value.to_string())
466}
467
468fn cpp_guid_short_name(guid: &str) -> String {
469 if guid.is_ascii() {
474 guid[..guid.len().min(8)].to_string()
475 } else {
476 guid.chars().take(8).collect()
477 }
478}
479
480fn format_float(value: f32) -> String {
481 let mut text = format!("{value}");
482 if text.contains('.') {
483 while text.ends_with('0') {
484 text.pop();
485 }
486 if text.ends_with('.') {
487 text.pop();
488 }
489 }
490 text
491}
492
493fn python_input_value(value: &common::PythonIndicatorInputParam) -> IndicatorInputValue {
494 let Some(raw_kind) = value.r#type else {
495 return IndicatorInputValue::Missing {
496 declared_type: None,
497 };
498 };
499 let Ok(kind) = common::IndicatorInputType::try_from(raw_kind) else {
500 return IndicatorInputValue::UnknownType(raw_kind);
501 };
502 let missing = || IndicatorInputValue::Missing {
503 declared_type: Some(raw_kind),
504 };
505 match kind {
506 common::IndicatorInputType::InputInt => value
507 .int_value
508 .map(IndicatorInputValue::Int)
509 .unwrap_or_else(missing),
510 common::IndicatorInputType::InputFloat => value
511 .float_value
512 .map(|value| IndicatorInputValue::Float(format_float(value)))
513 .unwrap_or_else(missing),
514 common::IndicatorInputType::InputString => value
515 .string_value
516 .clone()
517 .map(IndicatorInputValue::String)
518 .unwrap_or_else(missing),
519 common::IndicatorInputType::InputBool => value
520 .bool_value
521 .map(IndicatorInputValue::Bool)
522 .unwrap_or_else(missing),
523 common::IndicatorInputType::InputColor => value
524 .int_value
525 .map(IndicatorInputValue::Color)
526 .unwrap_or_else(missing),
527 common::IndicatorInputType::InputShape => value
528 .int_value
529 .map(IndicatorInputValue::Shape)
530 .unwrap_or_else(missing),
531 common::IndicatorInputType::InputLine => value
532 .int_value
533 .map(IndicatorInputValue::Line)
534 .unwrap_or_else(missing),
535 }
536}