1use std::sync::Arc;
18
19use anyhow::{Result, anyhow, bail};
20use futu_net::client::FutuClient;
21use futu_qot::types::{KLType, RehabType};
22use prost::Message;
23use serde::Serialize;
24
25use crate::state::parse_symbol;
26
27fn market_prefix(m: i32) -> &'static str {
28 futu_core::market::qot_market_display_prefix(futu_core::market::QotMarketId::new(m))
29 .unwrap_or("UNK")
30}
31
32#[cfg(test)]
33mod tests;
34
35pub async fn get_capital_flow(
40 client: &Arc<FutuClient>,
41 symbol: &str,
42 period_type: Option<i32>,
43 begin_time: Option<String>,
44 end_time: Option<String>,
45) -> Result<String> {
46 let sec = parse_symbol(symbol)?;
47 let req = futu_proto::qot_get_capital_flow::Request {
48 c2s: futu_proto::qot_get_capital_flow::C2s {
49 security: futu_proto::qot_common::Security {
50 market: sec.market as i32,
51 code: sec.code,
52 },
53 period_type,
54 begin_time,
55 end_time,
56 header: None, },
58 };
59 let body = req.encode_to_vec();
60 let frame = client
61 .request(futu_core::proto_id::QOT_GET_CAPITAL_FLOW, body)
62 .await?;
63 let resp = futu_proto::qot_get_capital_flow::Response::decode(frame.body.as_ref())
64 .map_err(|e| anyhow!("decode capital_flow: {e}"))?;
65 if resp.ret_type != 0 {
66 bail!(
67 "capital_flow ret_type={} msg={:?}",
68 resp.ret_type,
69 resp.ret_msg
70 );
71 }
72 let s2c = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
73 let raw_json = serde_json::to_string_pretty(&serde_json::json!({
78 "flow_item_list": s2c.flow_item_list.iter().map(|f| {
79 serde_json::json!({
80 "in_flow": f.in_flow,
81 "time": f.time,
82 "timestamp": f.timestamp,
83 "main_in_flow": f.main_in_flow,
84 "super_in_flow": f.super_in_flow,
85 "big_in_flow": f.big_in_flow,
86 "mid_in_flow": f.mid_in_flow,
87 "sml_in_flow": f.sml_in_flow,
88 })
89 }).collect::<Vec<_>>(),
90 "last_valid_time": s2c.last_valid_time,
91 "last_valid_timestamp": s2c.last_valid_timestamp,
92 "symbol": symbol,
93 }))?;
94 Ok(raw_json)
95}
96
97#[derive(Serialize)]
102struct CapitalDistributionOut {
103 capital_in_super: f64,
104 capital_in_big: f64,
105 capital_in_mid: f64,
106 capital_in_small: f64,
107 capital_out_super: f64,
108 capital_out_big: f64,
109 capital_out_mid: f64,
110 capital_out_small: f64,
111 update_time: String,
112}
113
114pub async fn get_capital_distribution(client: &Arc<FutuClient>, symbol: &str) -> Result<String> {
115 let sec = parse_symbol(symbol)?;
116 let req = futu_proto::qot_get_capital_distribution::Request {
117 c2s: futu_proto::qot_get_capital_distribution::C2s {
118 security: futu_proto::qot_common::Security {
119 market: sec.market as i32,
120 code: sec.code,
121 },
122 header: None, },
124 };
125 let body = req.encode_to_vec();
126 let frame = client
127 .request(futu_core::proto_id::QOT_GET_CAPITAL_DISTRIBUTION, body)
128 .await?;
129 let resp = futu_proto::qot_get_capital_distribution::Response::decode(frame.body.as_ref())
130 .map_err(|e| anyhow!("decode capital_distribution: {e}"))?;
131 if resp.ret_type != 0 {
132 bail!(
133 "capital_distribution ret_type={} msg={:?}",
134 resp.ret_type,
135 resp.ret_msg
136 );
137 }
138 let s = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
139 let out = CapitalDistributionOut {
140 capital_in_super: s.capital_in_super.unwrap_or(0.0),
141 capital_in_big: s.capital_in_big,
142 capital_in_mid: s.capital_in_mid,
143 capital_in_small: s.capital_in_small,
144 capital_out_super: s.capital_out_super.unwrap_or(0.0),
145 capital_out_big: s.capital_out_big,
146 capital_out_mid: s.capital_out_mid,
147 capital_out_small: s.capital_out_small,
148 update_time: s.update_time.unwrap_or_default(),
149 };
150 Ok(serde_json::to_string_pretty(&out)?)
151}
152
153#[derive(Serialize)]
158struct MarketStateOut {
159 code: String,
160 name: String,
161 market_state: i32,
162}
163
164pub async fn get_market_state(client: &Arc<FutuClient>, symbols: &[String]) -> Result<String> {
165 if symbols.is_empty() {
173 bail!("market_state: symbols empty (必须至少传入 1 个 MARKET.CODE)");
174 }
175 let mut sec_list: Vec<futu_proto::qot_common::Security> = Vec::with_capacity(symbols.len());
176 for (i, s) in symbols.iter().enumerate() {
177 let sec = parse_symbol(s).map_err(|e| {
178 anyhow!(
179 "market_state: symbols[{i}] invalid ({s:?}): {e} — 整体 reject, 不 partial-success"
180 )
181 })?;
182 sec_list.push(futu_proto::qot_common::Security {
183 market: sec.market as i32,
184 code: sec.code,
185 });
186 }
187 let _parsed = futu_qot::symbol_list::parse_required_symbol_list(&sec_list)
190 .map_err(|e| anyhow!("market_state: {e}"))?;
191 let req = futu_proto::qot_get_market_state::Request {
192 c2s: futu_proto::qot_get_market_state::C2s {
193 security_list: sec_list,
194 header: None,
195 },
196 };
197 let body = req.encode_to_vec();
198 let frame = client
199 .request(futu_core::proto_id::QOT_GET_MARKET_STATE, body)
200 .await?;
201 let resp = futu_proto::qot_get_market_state::Response::decode(frame.body.as_ref())
202 .map_err(|e| anyhow!("decode market_state: {e}"))?;
203 if resp.ret_type != 0 {
204 bail!(
205 "market_state ret_type={} msg={:?}",
206 resp.ret_type,
207 resp.ret_msg
208 );
209 }
210 let s = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
211 let out: Vec<MarketStateOut> = s
212 .market_info_list
213 .iter()
214 .map(|m| MarketStateOut {
215 code: format!("{}.{}", market_prefix(m.security.market), m.security.code),
216 name: m.name.clone(),
217 market_state: m.market_state,
218 })
219 .collect();
220 Ok(serde_json::to_string_pretty(&out)?)
221}
222
223fn parse_kl_type_local(s: &str) -> Result<KLType> {
230 let Some(t) = KLType::from_str_alias(s) else {
231 let other = s.trim().to_ascii_lowercase();
232 bail!(
233 "unknown kl_type {other:?} \
234 (day|week|month|quarter|year|1min|3min|5min|10min|15min|30min|60min|120min|180min|240min)"
235 );
236 };
237 Ok(t)
238}
239
240fn parse_rehab_type(s: &str) -> Result<RehabType> {
241 match s.trim().to_ascii_lowercase().as_str() {
242 "none" | "no_rehab" => Ok(RehabType::None),
243 "forward" => Ok(RehabType::Forward),
244 "backward" => Ok(RehabType::Backward),
245 other => bail!("unknown rehab_type {other:?} (none|forward|backward)"),
246 }
247}
248
249#[derive(Serialize)]
250struct HistoryKLineOut {
251 time: String,
252 timestamp: f64,
253 open: f64,
254 high: f64,
255 low: f64,
256 close: f64,
257 volume: i64,
258 turnover: f64,
259 pe: f64,
260 change_rate: f64,
261 turnover_rate: f64,
262}
263
264pub async fn get_history_kline(
270 client: &Arc<FutuClient>,
271 symbol: &str,
272 kl_type_str: &str,
273 rehab_type_str: &str,
274 begin: &str,
275 end: &str,
276 max_count: Option<i32>,
277 need_kl_fields_flag: Option<i64>,
278 extended_time: Option<bool>,
279 session: Option<i32>,
280 next_req_key: Option<&[u8]>,
281) -> Result<String> {
282 let sec = parse_symbol(symbol)?;
283 let kl_type = parse_kl_type_local(kl_type_str)?;
284 let rehab_type = parse_rehab_type(rehab_type_str)?;
285 let result = futu_qot::history_kl::request_history_kl(
286 client,
287 &futu_qot::history_kl::RequestHistoryKLParams {
288 security: &sec,
289 rehab_type,
290 kl_type,
291 begin_time: begin,
292 end_time: end,
293 max_num: max_count,
294 need_kl_fields_flag,
295 next_req_key,
296 extended_time,
297 session,
298 },
299 )
300 .await?;
301 let out: Vec<HistoryKLineOut> = result
302 .kl_list
303 .iter()
304 .map(|k| HistoryKLineOut {
305 time: k.time.clone(),
306 timestamp: k.timestamp,
307 open: k.open_price,
308 high: k.high_price,
309 low: k.low_price,
310 close: k.close_price,
311 volume: k.volume,
312 turnover: k.turnover,
313 pe: k.pe,
314 change_rate: k.change_rate,
315 turnover_rate: k.turnover_rate,
316 })
317 .collect();
318 let next_req_key = result.next_req_key.as_ref().map(|key| {
319 use base64::Engine as _;
320 base64::engine::general_purpose::STANDARD.encode(key)
321 });
322 Ok(serde_json::to_string_pretty(&serde_json::json!({
323 "symbol": symbol,
324 "kl_type": kl_type_str,
325 "rehab_type": rehab_type_str,
326 "extended_time": extended_time,
327 "session": session,
328 "need_kl_fields_flag": need_kl_fields_flag,
329 "next_req_key": next_req_key,
330 "kl_list": out,
331 }))?)
332}
333
334#[derive(Serialize)]
337struct OwnerPlateOut {
338 symbol: String,
339 plates: Vec<PlateInfo>,
340}
341
342#[derive(Serialize)]
343struct PlateInfo {
344 code: String,
345 name: String,
346 plate_type: i32,
347}
348
349pub async fn get_owner_plate(client: &Arc<FutuClient>, symbols: &[String]) -> Result<String> {
351 if symbols.is_empty() {
352 bail!("empty symbols");
353 }
354 let sec_list: Vec<_> = symbols
355 .iter()
356 .map(|s| parse_symbol(s))
357 .collect::<Result<Vec<_>>>()?;
358 let s2c = futu_qot::market_misc::get_owner_plate(client, &sec_list).await?;
359 let out: Vec<OwnerPlateOut> = s2c
360 .owner_plate_list
361 .iter()
362 .map(|entry| {
363 let sym = format!("{:?}.{}", entry.security.market, entry.security.code);
364 OwnerPlateOut {
365 symbol: sym,
366 plates: entry
367 .plate_info_list
368 .iter()
369 .map(|p| PlateInfo {
370 code: p.plate.code.clone(),
371 name: p.name.clone(),
372 plate_type: p.plate_type.unwrap_or(0),
373 })
374 .collect(),
375 }
376 })
377 .collect();
378 Ok(serde_json::to_string_pretty(&out)?)
379}
380
381fn parse_reference_type(s: &str) -> Result<i32> {
384 match s.trim().to_ascii_lowercase().as_str() {
387 "warrant" => Ok(1),
388 "future" | "futures" => Ok(2),
389 "option" => Ok(3),
390 other => bail!("unknown reference_type {other:?} (warrant|future|option)"),
391 }
392}
393
394#[derive(Serialize)]
395struct ReferenceOut {
396 code: String,
397 name: String,
398 lot_size: i32,
399 sec_type: i32,
400}
401
402pub async fn get_reference(
406 client: &Arc<FutuClient>,
407 symbol: &str,
408 reference_type_str: &str,
409) -> Result<String> {
410 let sec = parse_symbol(symbol)?;
411 let ref_type = parse_reference_type(reference_type_str)?;
412 let list = futu_qot::market_misc::get_reference(client, &sec, ref_type).await?;
413 let out: Vec<ReferenceOut> = list
414 .iter()
415 .map(|s| ReferenceOut {
416 code: s.security.code.clone(),
417 name: s.name.clone(),
418 lot_size: s.lot_size,
419 sec_type: s.sec_type,
420 })
421 .collect();
422 Ok(serde_json::to_string_pretty(&out)?)
423}
424
425#[derive(Serialize)]
437struct OptionRow {
438 strike_price: f64,
440 call_symbol: Option<String>,
442 put_symbol: Option<String>,
444 suspend: Option<bool>,
446 market: Option<String>,
448 index_option_type: Option<i32>,
450 expiration_cycle: Option<i32>,
452 option_standard_type: Option<i32>,
454 option_settlement_mode: Option<i32>,
456}
457
458#[derive(Serialize)]
459struct OptionChainEntry {
460 strike_time: String,
461 options: Vec<OptionRow>,
464 call_symbols: Vec<String>,
467 put_symbols: Vec<String>,
469}
470
471pub struct OptionChainInput<'a> {
478 pub owner_symbol: &'a str,
479 pub begin_time: &'a str,
480 pub end_time: &'a str,
481 pub option_type_str: Option<&'a str>,
482 pub data_filter: Option<futu_proto::qot_get_option_chain::DataFilter>,
483}
484
485pub async fn get_option_chain(
486 client: &Arc<FutuClient>,
487 input: OptionChainInput<'_>,
488) -> Result<String> {
489 let owner = parse_symbol(input.owner_symbol)?;
490 let option_type = match input.option_type_str.map(str::trim) {
491 Some("all") | None => Some(0), Some("call") => Some(1),
493 Some("put") => Some(2),
494 Some(other) => bail!("unknown option_type {other:?} (all|call|put)"),
495 };
496 let s2c = futu_qot::market_misc::get_option_chain(
497 client,
498 &owner,
499 input.begin_time,
500 input.end_time,
501 option_type,
502 None,
503 input.data_filter,
504 )
505 .await?;
506 let out: Vec<OptionChainEntry> = s2c
509 .option_chain
510 .iter()
511 .map(|entry| {
512 let mut calls = Vec::new();
513 let mut puts = Vec::new();
514 let mut option_rows: Vec<OptionRow> = Vec::new();
518 for item in &entry.option {
519 if let Some(c) = &item.call {
520 calls.push(c.basic.security.code.clone());
521 }
522 if let Some(p) = &item.put {
523 puts.push(p.basic.security.code.clone());
524 }
525 let ex = item
526 .call
527 .as_ref()
528 .and_then(|c| c.option_ex_data.as_ref())
529 .or_else(|| item.put.as_ref().and_then(|p| p.option_ex_data.as_ref()));
530 if let Some(ex) = ex {
531 option_rows.push(OptionRow {
532 strike_price: ex.strike_price,
533 call_symbol: item.call.as_ref().map(|c| c.basic.security.code.clone()),
534 put_symbol: item.put.as_ref().map(|p| p.basic.security.code.clone()),
535 suspend: Some(ex.suspend),
536 market: Some(ex.market.clone()),
537 index_option_type: ex.index_option_type,
538 expiration_cycle: ex.expiration_cycle,
539 option_standard_type: ex.option_standard_type,
540 option_settlement_mode: ex.option_settlement_mode,
541 });
542 }
543 }
544 OptionChainEntry {
545 strike_time: entry.strike_time.clone(),
546 options: option_rows,
547 call_symbols: calls,
548 put_symbols: puts,
549 }
550 })
551 .collect();
552 Ok(serde_json::to_string_pretty(&out)?)
553}