1use std::sync::Arc;
4use std::time::Duration;
5
6use anyhow::{Context, Result, bail};
7use futu_net::client::{ClientConfig, FutuClient, PushReceiver, ReconnectingClient};
8use futu_net::reconnect::ReconnectPolicy;
9use futu_qot::types::{Security, SubType};
10
11const CLI_CONNECT_TOTAL_TIMEOUT: Duration = Duration::from_secs(3);
16const CLI_CONNECT_RETRY_DELAY: Duration = Duration::from_millis(200);
17
18pub fn parse_symbol(s: &str) -> Result<Security> {
19 futu_qot::symbol::parse_symbol(s)
20}
21
22pub fn format_symbol(sec: &Security) -> String {
24 futu_qot::symbol::format_symbol(sec)
25}
26
27pub fn parse_sub_type(s: &str) -> Result<SubType> {
29 let t = match s.trim().to_ascii_lowercase().as_str() {
30 "basic" => SubType::Basic,
31 "orderbook" | "order_book" => SubType::OrderBook,
32 "orderbook_odd" | "order_book_odd" | "odd_orderbook" | "odd_lot_orderbook" => {
33 SubType::OrderBookOdd
34 }
35 "ticker" => SubType::Ticker,
36 "rt" => SubType::RT,
37 "kl_day" | "day" => SubType::KLDay,
38 "kl_1min" | "1min" => SubType::KL1Min,
39 "kl_3min" | "3min" => SubType::KL3Min,
40 "kl_5min" | "5min" => SubType::KL5Min,
41 "kl_15min" | "15min" => SubType::KL15Min,
42 "kl_30min" | "30min" => SubType::KL30Min,
43 "kl_60min" | "60min" => SubType::KL60Min,
44 "kl_week" | "week" => SubType::KLWeek,
45 "kl_month" | "month" => SubType::KLMonth,
46 "kl_quarter" | "quarter" => SubType::KLQuarter,
47 "kl_year" | "year" => SubType::KLYear,
48 "broker" => SubType::Broker,
49 "order_detail" | "orderdetail" => SubType::OrderDetail,
50 other => bail!(
51 "unknown sub type {other:?} (supported: basic, orderbook, orderbook_odd, ticker, rt, kl_day, kl_1min, ...)"
52 ),
53 };
54 Ok(t)
55}
56
57pub fn parse_sub_types(csv: &str) -> Result<Vec<SubType>> {
59 csv.split(',')
60 .map(parse_sub_type)
61 .collect::<Result<Vec<_>>>()
62}
63
64pub fn parse_symbol_csv(s: &str) -> Result<Vec<String>> {
77 let trimmed = s.trim();
78 if trimmed.is_empty() {
79 bail!("CSV symbol 列表为空: 必须传入非空 \"MARKET.CODE\" 列表");
80 }
81 let mut out: Vec<String> = Vec::new();
82 for (i, token) in trimmed.split(',').enumerate() {
83 let t = token.trim();
84 if t.is_empty() {
85 bail!("CSV symbol[{i}] 为空 token (输入 \"{s}\"): 整体 reject, 不 silent skip 空项");
86 }
87 out.push(t.to_string());
88 }
89 Ok(out)
90}
91
92pub async fn connect_gateway(
96 addr: &str,
97 client_id: &str,
98) -> Result<(Arc<FutuClient>, PushReceiver)> {
99 let config = ClientConfig {
100 addr: addr.to_string(),
101 client_ver: env!("CARGO_PKG_VERSION").to_string(),
102 client_id: client_id.to_string(),
103 recv_notify: false,
104 rsa_key: None,
105 };
106 let policy = ReconnectPolicy::new(CLI_CONNECT_RETRY_DELAY, CLI_CONNECT_RETRY_DELAY, Some(1));
107 let mut reconnector = ReconnectingClient::new(config).with_policy(policy);
108 let connect_result =
109 tokio::time::timeout(CLI_CONNECT_TOTAL_TIMEOUT, reconnector.connect()).await;
110 let (client, push_rx, _info) = match connect_result {
111 Ok(result) => result.with_context(|| format!("connect to futu gateway at {addr}"))?,
112 Err(_) => bail!(
113 "connect to futu gateway at {addr} timed out after {}s",
114 CLI_CONNECT_TOTAL_TIMEOUT.as_secs()
115 ),
116 };
117 Ok((Arc::new(client), push_rx))
118}
119
120#[cfg(test)]
121mod tests;