1use std::sync::Arc;
4
5use anyhow::{anyhow, Context, Result};
6use futu_auth::{KeyRecord, KeyStore, RuntimeCounters};
7use futu_net::client::{ClientConfig, FutuClient, ReconnectingClient};
8use futu_qot::types::{QotMarket, Security};
9use tokio::sync::Mutex;
10
11#[derive(Clone)]
13pub struct ServerState {
14 pub inner: Arc<Mutex<Inner>>,
15 pub enable_trading: bool,
18 pub allow_real_trading: bool,
20 pub key_store: Arc<KeyStore>,
22 pub authed_key: Option<Arc<KeyRecord>>,
24 pub counters: Arc<RuntimeCounters>,
26}
27
28pub struct Inner {
29 pub gateway: String,
30 pub client: Option<Arc<FutuClient>>,
31}
32
33impl ServerState {
34 pub fn new(gateway: String) -> Self {
35 Self {
36 inner: Arc::new(Mutex::new(Inner {
37 gateway,
38 client: None,
39 })),
40 enable_trading: false,
41 allow_real_trading: false,
42 key_store: Arc::new(KeyStore::empty()),
43 authed_key: None,
44 counters: Arc::new(RuntimeCounters::new()),
45 }
46 }
47
48 pub fn with_trading(mut self, enable_trading: bool, allow_real_trading: bool) -> Self {
50 self.enable_trading = enable_trading;
51 self.allow_real_trading = allow_real_trading;
52 self
53 }
54
55 pub fn with_key_store(mut self, store: Arc<KeyStore>) -> Self {
57 self.key_store = store;
58 self
59 }
60
61 pub fn with_authed_key(mut self, key: Option<Arc<KeyRecord>>) -> Self {
63 self.authed_key = key;
64 self
65 }
66
67 pub fn is_scope_mode(&self) -> bool {
69 self.key_store.is_configured()
70 }
71
72 pub async fn client(&self) -> Result<Arc<FutuClient>> {
74 let mut guard = self.inner.lock().await;
75 if let Some(c) = &guard.client {
76 return Ok(c.clone());
77 }
78
79 let config = ClientConfig {
80 addr: guard.gateway.clone(),
81 client_ver: env!("CARGO_PKG_VERSION").to_string(),
82 client_id: "futu-mcp".to_string(),
83 recv_notify: false,
84 rsa_key: None,
85 };
86 let mut reconnector = ReconnectingClient::new(config);
87 let (client, mut push_rx, _info) = reconnector
88 .connect()
89 .await
90 .with_context(|| format!("connect to futu gateway at {}", guard.gateway))?;
91
92 tokio::spawn(async move {
94 while push_rx.recv().await.is_some() {
95 }
97 });
98
99 let arc = Arc::new(client);
100 guard.client = Some(arc.clone());
101 Ok(arc)
102 }
103}
104
105pub fn parse_symbol(s: &str) -> Result<Security> {
109 let (market_str, code) = s.split_once('.').ok_or_else(|| {
110 anyhow!("invalid symbol {s:?}: expected MARKET.CODE (e.g. HK.00700, US.AAPL, SH.600519)")
111 })?;
112 if code.is_empty() {
113 return Err(anyhow!("invalid symbol {s:?}: code part is empty"));
114 }
115 let market = match market_str.to_ascii_uppercase().as_str() {
116 "HK" => QotMarket::HkSecurity,
117 "HK_FUTURE" => QotMarket::HkFuture,
118 "US" => QotMarket::UsSecurity,
119 "SH" => QotMarket::CnshSecurity,
120 "SZ" => QotMarket::CnszSecurity,
121 other => {
122 return Err(anyhow!(
123 "invalid symbol {s:?}: unknown market {other:?} (HK|HK_FUTURE|US|SH|SZ)"
124 ))
125 }
126 };
127 Ok(Security::new(market, code))
128}
129
130pub fn format_symbol(sec: &Security) -> String {
132 let m = match sec.market {
133 QotMarket::HkSecurity => "HK",
134 QotMarket::HkFuture => "HK_FUTURE",
135 QotMarket::UsSecurity => "US",
136 QotMarket::CnshSecurity => "SH",
137 QotMarket::CnszSecurity => "SZ",
138 QotMarket::Unknown => "UNKNOWN",
139 };
140 format!("{m}.{}", sec.code)
141}