1use std::sync::Arc;
4use std::time::Duration;
5
6use anyhow::{bail, Result};
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 bids: Vec<Level>,
138 asks: Vec<Level>,
139}
140
141pub async fn get_orderbook(client: &Arc<FutuClient>, symbol: &str, depth: i32) -> Result<String> {
142 let sec = parse_symbol(symbol)?;
143 futu_qot::sub::subscribe(
144 client,
145 std::slice::from_ref(&sec),
146 &[SubType::OrderBook],
147 true,
148 true,
149 )
150 .await?;
151 tokio::time::sleep(Duration::from_millis(300)).await;
152
153 let ob = futu_qot::order_book::get_order_book(client, &sec, depth).await?;
154 let out = OrderBookOut {
155 symbol: format_symbol(&ob.security),
156 bids: ob
157 .bid_list
158 .iter()
159 .map(|e| Level {
160 price: e.price,
161 volume: e.volume,
162 orders: e.order_count,
163 })
164 .collect(),
165 asks: ob
166 .ask_list
167 .iter()
168 .map(|e| Level {
169 price: e.price,
170 volume: e.volume,
171 orders: e.order_count,
172 })
173 .collect(),
174 };
175 Ok(serde_json::to_string_pretty(&out)?)
176}
177
178#[derive(Serialize)]
181struct TickerOut {
182 time: String,
183 sequence: i64,
184 price: f64,
185 volume: i64,
186 turnover: f64,
187 dir: i32,
188 ticker_type: Option<i32>,
189 timestamp: f64,
190}
191
192pub async fn get_ticker(client: &Arc<FutuClient>, symbol: &str, count: i32) -> Result<String> {
193 let sec = parse_symbol(symbol)?;
194 futu_qot::sub::subscribe(
195 client,
196 std::slice::from_ref(&sec),
197 &[SubType::Ticker],
198 true,
199 true,
200 )
201 .await?;
202 tokio::time::sleep(Duration::from_millis(300)).await;
203
204 let result = futu_qot::ticker::get_ticker(client, &sec, count).await?;
205 let out: Vec<TickerOut> = result
206 .ticker_list
207 .iter()
208 .map(|t| TickerOut {
209 time: t.time.clone(),
210 sequence: t.sequence,
211 price: t.price,
212 volume: t.volume,
213 turnover: t.turnover,
214 dir: t.dir,
215 ticker_type: t.ticker_type,
216 timestamp: t.timestamp,
217 })
218 .collect();
219 Ok(serde_json::to_string_pretty(&out)?)
220}
221
222#[derive(Serialize)]
225struct RtOut {
226 time: String,
227 minute: i32,
228 is_blank: bool,
229 price: f64,
230 last_close_price: f64,
231 avg_price: f64,
232 volume: i64,
233 turnover: f64,
234 timestamp: f64,
235}
236
237pub async fn get_rt(client: &Arc<FutuClient>, symbol: &str) -> Result<String> {
238 let sec = parse_symbol(symbol)?;
239 futu_qot::sub::subscribe(
240 client,
241 std::slice::from_ref(&sec),
242 &[SubType::RT],
243 true,
244 true,
245 )
246 .await?;
247 tokio::time::sleep(Duration::from_millis(300)).await;
248
249 let result = futu_qot::rt::get_rt(client, &sec).await?;
250 let out: Vec<RtOut> = result
251 .rt_list
252 .iter()
253 .map(|r| RtOut {
254 time: r.time.clone(),
255 minute: r.minute,
256 is_blank: r.is_blank,
257 price: r.price,
258 last_close_price: r.last_close_price,
259 avg_price: r.avg_price,
260 volume: r.volume,
261 turnover: r.turnover,
262 timestamp: r.timestamp,
263 })
264 .collect();
265 Ok(serde_json::to_string_pretty(&out)?)
266}
267
268#[derive(Serialize)]
271struct StaticOut {
272 symbol: String,
273 id: i64,
274 name: String,
275 sec_type: i32,
276 lot_size: i32,
277 list_time: String,
278 delisting: bool,
279}
280
281pub async fn get_static(client: &Arc<FutuClient>, symbols: &[String]) -> Result<String> {
282 let secs: Result<Vec<_>> = symbols.iter().map(|s| parse_symbol(s)).collect();
283 let secs = secs?;
284
285 let infos = futu_qot::static_info::get_static_info(client, &secs).await?;
286 let out: Vec<StaticOut> = infos
287 .iter()
288 .map(|i| StaticOut {
289 symbol: format_symbol(&i.security),
290 id: i.id,
291 name: i.name.clone(),
292 sec_type: i.sec_type,
293 lot_size: i.lot_size,
294 list_time: i.list_time.clone(),
295 delisting: i.delisting,
296 })
297 .collect();
298 Ok(serde_json::to_string_pretty(&out)?)
299}
300
301#[derive(Serialize)]
304struct BrokerOut {
305 side: &'static str,
306 pos: i32,
307 id: i64,
308 name: String,
309}
310
311pub async fn get_broker(client: &Arc<FutuClient>, symbol: &str) -> Result<String> {
312 let sec = parse_symbol(symbol)?;
313 futu_qot::sub::subscribe(
314 client,
315 std::slice::from_ref(&sec),
316 &[SubType::Broker],
317 true,
318 true,
319 )
320 .await?;
321 tokio::time::sleep(Duration::from_millis(300)).await;
322
323 let data = futu_qot::broker::get_broker(client, &sec).await?;
324 let mut out = Vec::new();
325 for a in &data.ask_list {
326 out.push(BrokerOut {
327 side: "ask",
328 pos: a.pos,
329 id: a.id,
330 name: a.name.clone(),
331 });
332 }
333 for b in &data.bid_list {
334 out.push(BrokerOut {
335 side: "bid",
336 pos: b.pos,
337 id: b.id,
338 name: b.name.clone(),
339 });
340 }
341 Ok(serde_json::to_string_pretty(&out)?)
342}