1use std::sync::Arc;
4use std::time::Duration;
5
6use anyhow::{Context, Result, anyhow, bail};
7use futu_core::{qot_subscription, qot_symbol};
8use futu_net::client::{ClientConfig, FutuClient, PushReceiver, ReconnectingClient};
9use futu_net::reconnect::ReconnectPolicy;
10use futu_qot::types::{Security, SubType};
11
12use crate::qot_sdk_adapter;
13
14const CLI_CONNECT_TOTAL_TIMEOUT: Duration = Duration::from_secs(6);
24const CLI_CONNECT_RETRY_DELAY: Duration = Duration::from_millis(200);
25const CLI_CONNECT_MAX_RETRIES: u32 = 4;
26
27pub fn parse_symbol(s: &str) -> Result<Security> {
28 let parsed = qot_symbol::parse_qot_symbol_parts(s).map_err(|err| anyhow!("{err}"))?;
29 Ok(qot_sdk_adapter::security_from_parsed_symbol(parsed))
30}
31
32pub fn format_symbol(sec: &Security) -> String {
34 qot_symbol::format_qot_symbol(sec.market as i32, &sec.code)
35}
36
37pub fn parse_sub_type(s: &str) -> Result<SubType> {
39 let Some(t) = qot_subscription::qot_sub_type_from_str_alias(s)
40 .and_then(qot_sdk_adapter::sub_type_from_id)
41 else {
42 let other = s.trim().to_ascii_lowercase();
43 bail!(
44 "unknown sub type {other:?} (supported: basic, orderbook, orderbook_odd, ticker, rt, kl_day, kl_1min, kl_10min, kl_120min, ...)"
45 );
46 };
47 Ok(t)
48}
49
50pub fn parse_sub_types(csv: &str) -> Result<Vec<SubType>> {
52 csv.split(',')
53 .map(parse_sub_type)
54 .collect::<Result<Vec<_>>>()
55}
56
57pub fn parse_symbol_csv(s: &str) -> Result<Vec<String>> {
70 let trimmed = s.trim();
71 if trimmed.is_empty() {
72 bail!("CSV symbol 列表为空: 必须传入非空 \"MARKET.CODE\" 列表");
73 }
74 let mut out: Vec<String> = Vec::new();
75 for (i, token) in trimmed.split(',').enumerate() {
76 let t = token.trim();
77 if t.is_empty() {
78 bail!("CSV symbol[{i}] 为空 token (输入 \"{s}\"): 整体 reject, 不 silent skip 空项");
79 }
80 out.push(t.to_string());
81 }
82 Ok(out)
83}
84
85pub async fn connect_gateway(
89 addr: &str,
90 client_id: &str,
91) -> Result<(Arc<FutuClient>, PushReceiver)> {
92 let config = ClientConfig {
93 addr: addr.to_string(),
94 client_ver: env!("CARGO_PKG_VERSION").to_string(),
95 client_id: client_id.to_string(),
96 recv_notify: false,
97 rsa_key: None,
98 };
99 let policy = ReconnectPolicy::new(
100 CLI_CONNECT_RETRY_DELAY,
101 CLI_CONNECT_RETRY_DELAY,
102 Some(CLI_CONNECT_MAX_RETRIES),
103 );
104 let mut reconnector = ReconnectingClient::new(config).with_policy(policy);
105 let connect_result =
106 tokio::time::timeout(CLI_CONNECT_TOTAL_TIMEOUT, reconnector.connect()).await;
107 let (client, push_rx, _info) = match connect_result {
108 Ok(result) => result.with_context(|| format!("connect to futu gateway at {addr}"))?,
109 Err(_) => bail!(
110 "connect to futu gateway at {addr} timed out after {}s",
111 CLI_CONNECT_TOTAL_TIMEOUT.as_secs()
112 ),
113 };
114 Ok((Arc::new(client), push_rx))
115}
116
117#[cfg(test)]
118mod tests;