1use std::sync::Arc;
4use std::time::Duration;
5
6use anyhow::{Result, bail};
7use futu_net::client::FutuClient;
8use futu_qot::types::{KLType, RehabType, SubType};
9use serde::Serialize;
10
11use crate::state::{format_symbol, parse_symbol};
12
13#[derive(Serialize)]
16struct KLineOut {
17 time: String,
18 timestamp: f64,
19 open: f64,
20 high: f64,
21 low: f64,
22 close: f64,
23 last_close: f64,
24 change_rate: f64,
25 volume: i64,
26 turnover: f64,
27 turnover_rate: f64,
28 pe: f64,
29}
30
31pub fn parse_kl_type(s: &str) -> Result<KLType> {
32 let t = match s.trim().to_ascii_lowercase().as_str() {
33 "day" => KLType::Day,
34 "week" => KLType::Week,
35 "month" => KLType::Month,
36 "quarter" => KLType::Quarter,
37 "year" => KLType::Year,
38 "1min" => KLType::Min1,
39 "3min" => KLType::Min3,
40 "5min" => KLType::Min5,
41 "15min" => KLType::Min15,
42 "30min" => KLType::Min30,
43 "60min" => KLType::Min60,
44 other => bail!("unknown kline type {other:?} (day|week|month|quarter|year|1min|...)"),
45 };
46 Ok(t)
47}
48
49fn estimate_lookback_days(kl_type: KLType, count: i32) -> i32 {
50 let n = count.max(1);
51 match kl_type {
52 KLType::Day => (n as f32 * 1.5).ceil() as i32 + 10,
53 KLType::Week => n * 8 + 30,
54 KLType::Month => n * 32 + 90,
55 KLType::Quarter => n * 95 + 180,
56 KLType::Year => n * 370 + 365,
57 KLType::Min1 => (n as f32 / 240.0).ceil() as i32 + 3,
58 KLType::Min3 => (n as f32 / 80.0).ceil() as i32 + 3,
59 KLType::Min5 => (n as f32 / 48.0).ceil() as i32 + 3,
60 KLType::Min15 => (n as f32 / 16.0).ceil() as i32 + 3,
61 KLType::Min30 => (n as f32 / 8.0).ceil() as i32 + 5,
62 KLType::Min60 => (n as f32 / 4.0).ceil() as i32 + 7,
63 _ => 365,
64 }
65}
66
67pub async fn get_kline(
68 client: &Arc<FutuClient>,
69 symbol: &str,
70 kl_type_str: &str,
71 count: Option<i32>,
72 begin: Option<&str>,
73 end: Option<&str>,
74) -> Result<String> {
75 let sec = parse_symbol(symbol)?;
76 let kl_type = parse_kl_type(kl_type_str)?;
77
78 let today = chrono::Local::now().date_naive();
79 let end_date = match end {
80 Some(s) => chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d")?,
81 None => today,
82 };
83 let n = count.unwrap_or(100);
84 let lookback = estimate_lookback_days(kl_type, n);
85 let begin_date = match begin {
86 Some(s) => chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d")?,
87 None => end_date
88 .checked_sub_days(chrono::Days::new(lookback as u64))
89 .unwrap_or(end_date),
90 };
91
92 let result = futu_qot::history_kl::get_history_kl(
93 client,
94 &sec,
95 RehabType::None,
96 kl_type,
97 &begin_date.format("%Y-%m-%d").to_string(),
98 &end_date.format("%Y-%m-%d").to_string(),
99 Some(n),
100 )
101 .await?;
102
103 let out: Vec<KLineOut> = result
104 .kl_list
105 .iter()
106 .map(|k| KLineOut {
107 time: k.time.clone(),
108 timestamp: k.timestamp,
109 open: k.open_price,
110 high: k.high_price,
111 low: k.low_price,
112 close: k.close_price,
113 last_close: k.last_close_price,
114 change_rate: k.change_rate,
115 volume: k.volume,
116 turnover: k.turnover,
117 turnover_rate: k.turnover_rate,
118 pe: k.pe,
119 })
120 .collect();
121
122 Ok(serde_json::to_string_pretty(&out)?)
123}
124
125#[derive(Serialize)]
128struct Level {
129 price: f64,
130 volume: i64,
131 orders: i32,
132}
133
134#[derive(Serialize)]
135struct OrderBookOut {
136 symbol: String,
137 odd_lot: bool,
138 bids: Vec<Level>,
139 asks: Vec<Level>,
140}
141
142pub async fn get_orderbook(
143 client: &Arc<FutuClient>,
144 symbol: &str,
145 depth: i32,
146 odd_lot: bool,
147) -> Result<String> {
148 let sec = parse_symbol(symbol)?;
149 let sub_type = if odd_lot {
150 SubType::OrderBookOdd
151 } else {
152 SubType::OrderBook
153 };
154 futu_qot::sub::subscribe(client, std::slice::from_ref(&sec), &[sub_type], true, true).await?;
155 tokio::time::sleep(Duration::from_millis(300)).await;
156
157 let ob =
158 futu_qot::order_book::get_order_book_with_type(client, &sec, depth, odd_lot.then_some(1))
159 .await?;
160 let out = OrderBookOut {
161 symbol: format_symbol(&ob.security),
162 odd_lot,
163 bids: ob
164 .bid_list
165 .iter()
166 .map(|e| Level {
167 price: e.price,
168 volume: e.volume,
169 orders: e.order_count,
170 })
171 .collect(),
172 asks: ob
173 .ask_list
174 .iter()
175 .map(|e| Level {
176 price: e.price,
177 volume: e.volume,
178 orders: e.order_count,
179 })
180 .collect(),
181 };
182 Ok(serde_json::to_string_pretty(&out)?)
183}
184
185#[derive(Serialize)]
188struct TickerOut {
189 time: String,
190 sequence: i64,
191 price: f64,
192 volume: i64,
193 turnover: f64,
194 dir: i32,
195 ticker_type: Option<i32>,
196 timestamp: f64,
197}
198
199pub async fn get_ticker(client: &Arc<FutuClient>, symbol: &str, count: i32) -> Result<String> {
200 let sec = parse_symbol(symbol)?;
201 futu_qot::sub::subscribe(
202 client,
203 std::slice::from_ref(&sec),
204 &[SubType::Ticker],
205 true,
206 true,
207 )
208 .await?;
209 tokio::time::sleep(Duration::from_millis(300)).await;
210
211 let result = futu_qot::ticker::get_ticker(client, &sec, count).await?;
212 let out: Vec<TickerOut> = result
213 .ticker_list
214 .iter()
215 .map(|t| TickerOut {
216 time: t.time.clone(),
217 sequence: t.sequence,
218 price: t.price,
219 volume: t.volume,
220 turnover: t.turnover,
221 dir: t.dir,
222 ticker_type: t.ticker_type,
223 timestamp: t.timestamp,
224 })
225 .collect();
226 Ok(serde_json::to_string_pretty(&out)?)
227}
228
229#[derive(Serialize)]
232struct RtOut {
233 time: String,
234 minute: i32,
235 is_blank: bool,
236 price: f64,
237 last_close_price: f64,
238 avg_price: f64,
239 volume: i64,
240 turnover: f64,
241 timestamp: f64,
242}
243
244pub async fn get_rt(client: &Arc<FutuClient>, symbol: &str) -> Result<String> {
245 let sec = parse_symbol(symbol)?;
246 futu_qot::sub::subscribe(
247 client,
248 std::slice::from_ref(&sec),
249 &[SubType::RT],
250 true,
251 true,
252 )
253 .await?;
254 tokio::time::sleep(Duration::from_millis(300)).await;
255
256 let result = futu_qot::rt::get_rt(client, &sec).await?;
257 let out: Vec<RtOut> = result
258 .rt_list
259 .iter()
260 .map(|r| RtOut {
261 time: r.time.clone(),
262 minute: r.minute,
263 is_blank: r.is_blank,
264 price: r.price,
265 last_close_price: r.last_close_price,
266 avg_price: r.avg_price,
267 volume: r.volume,
268 turnover: r.turnover,
269 timestamp: r.timestamp,
270 })
271 .collect();
272 Ok(serde_json::to_string_pretty(&out)?)
273}
274
275#[derive(Serialize)]
278struct StaticOut {
279 symbol: String,
280 id: i64,
281 name: String,
282 sec_type: i32,
283 lot_size: i32,
284 list_time: String,
285 delisting: bool,
286 #[serde(skip_serializing_if = "Option::is_none")]
291 exchange_code: Option<String>,
292}
293
294pub async fn get_static(client: &Arc<FutuClient>, symbols: &[String]) -> Result<String> {
295 let secs: Result<Vec<_>> = symbols.iter().map(|s| parse_symbol(s)).collect();
296 let secs = secs?;
297
298 let infos = futu_qot::static_info::get_static_info(client, &secs).await?;
299 let out: Vec<StaticOut> = infos
300 .iter()
301 .map(|i| StaticOut {
302 symbol: format_symbol(&i.security),
303 id: i.id,
304 name: i.name.clone(),
305 sec_type: i.sec_type,
306 lot_size: i.lot_size,
307 list_time: i.list_time.clone(),
308 delisting: i.delisting,
309 exchange_code: i.exchange_code().map(String::from),
311 })
312 .collect();
313 Ok(serde_json::to_string_pretty(&out)?)
314}
315
316#[derive(Serialize)]
319struct BrokerOut {
320 side: &'static str,
321 pos: i32,
322 id: i64,
323 name: String,
324}
325
326pub async fn get_broker(client: &Arc<FutuClient>, symbol: &str) -> Result<String> {
327 let sec = parse_symbol(symbol)?;
328 futu_qot::sub::subscribe(
329 client,
330 std::slice::from_ref(&sec),
331 &[SubType::Broker],
332 true,
333 true,
334 )
335 .await?;
336 tokio::time::sleep(Duration::from_millis(300)).await;
337
338 let data = futu_qot::broker::get_broker(client, &sec).await?;
339 let mut out = Vec::new();
340 for a in &data.ask_list {
341 out.push(BrokerOut {
342 side: "ask",
343 pos: a.pos,
344 id: a.id,
345 name: a.name.clone(),
346 });
347 }
348 for b in &data.bid_list {
349 out.push(BrokerOut {
350 side: "bid",
351 pos: b.pos,
352 id: b.id,
353 name: b.name.clone(),
354 });
355 }
356 Ok(serde_json::to_string_pretty(&out)?)
357}