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 match m {
29 1 => "HK",
30 11 => "US",
31 21 => "SH",
32 22 => "SZ",
33 31 => "SG",
34 41 => "JP",
35 42 => "AU",
36 91 => "CC",
37 _ => "UNK",
38 }
39}
40
41#[cfg(test)]
42mod tests;
43
44pub async fn get_capital_flow(
49 client: &Arc<FutuClient>,
50 symbol: &str,
51 period_type: Option<i32>,
52 begin_time: Option<String>,
53 end_time: Option<String>,
54) -> Result<String> {
55 let sec = parse_symbol(symbol)?;
56 let req = futu_proto::qot_get_capital_flow::Request {
57 c2s: futu_proto::qot_get_capital_flow::C2s {
58 security: futu_proto::qot_common::Security {
59 market: sec.market as i32,
60 code: sec.code,
61 },
62 period_type,
63 begin_time,
64 end_time,
65 header: None, },
67 };
68 let body = req.encode_to_vec();
69 let frame = client
70 .request(futu_core::proto_id::QOT_GET_CAPITAL_FLOW, body)
71 .await?;
72 let resp = futu_proto::qot_get_capital_flow::Response::decode(frame.body.as_ref())
73 .map_err(|e| anyhow!("decode capital_flow: {e}"))?;
74 if resp.ret_type != 0 {
75 bail!(
76 "capital_flow ret_type={} msg={:?}",
77 resp.ret_type,
78 resp.ret_msg
79 );
80 }
81 let s2c = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
82 let raw_json = serde_json::to_string_pretty(&serde_json::json!({
87 "flow_item_list": s2c.flow_item_list.iter().map(|f| {
88 serde_json::json!({
89 "in_flow": f.in_flow,
90 "time": f.time,
91 "timestamp": f.timestamp,
92 "main_in_flow": f.main_in_flow,
93 "super_in_flow": f.super_in_flow,
94 "big_in_flow": f.big_in_flow,
95 "mid_in_flow": f.mid_in_flow,
96 "sml_in_flow": f.sml_in_flow,
97 })
98 }).collect::<Vec<_>>(),
99 "last_valid_time": s2c.last_valid_time,
100 "last_valid_timestamp": s2c.last_valid_timestamp,
101 "symbol": symbol,
102 }))?;
103 Ok(raw_json)
104}
105
106#[derive(Serialize)]
111struct CapitalDistributionOut {
112 capital_in_super: f64,
113 capital_in_big: f64,
114 capital_in_mid: f64,
115 capital_in_small: f64,
116 capital_out_super: f64,
117 capital_out_big: f64,
118 capital_out_mid: f64,
119 capital_out_small: f64,
120 update_time: String,
121}
122
123pub async fn get_capital_distribution(client: &Arc<FutuClient>, symbol: &str) -> Result<String> {
124 let sec = parse_symbol(symbol)?;
125 let req = futu_proto::qot_get_capital_distribution::Request {
126 c2s: futu_proto::qot_get_capital_distribution::C2s {
127 security: futu_proto::qot_common::Security {
128 market: sec.market as i32,
129 code: sec.code,
130 },
131 header: None, },
133 };
134 let body = req.encode_to_vec();
135 let frame = client
136 .request(futu_core::proto_id::QOT_GET_CAPITAL_DISTRIBUTION, body)
137 .await?;
138 let resp = futu_proto::qot_get_capital_distribution::Response::decode(frame.body.as_ref())
139 .map_err(|e| anyhow!("decode capital_distribution: {e}"))?;
140 if resp.ret_type != 0 {
141 bail!(
142 "capital_distribution ret_type={} msg={:?}",
143 resp.ret_type,
144 resp.ret_msg
145 );
146 }
147 let s = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
148 let out = CapitalDistributionOut {
149 capital_in_super: s.capital_in_super.unwrap_or(0.0),
150 capital_in_big: s.capital_in_big,
151 capital_in_mid: s.capital_in_mid,
152 capital_in_small: s.capital_in_small,
153 capital_out_super: s.capital_out_super.unwrap_or(0.0),
154 capital_out_big: s.capital_out_big,
155 capital_out_mid: s.capital_out_mid,
156 capital_out_small: s.capital_out_small,
157 update_time: s.update_time.unwrap_or_default(),
158 };
159 Ok(serde_json::to_string_pretty(&out)?)
160}
161
162#[derive(Serialize)]
167struct MarketStateOut {
168 code: String,
169 name: String,
170 market_state: i32,
171}
172
173pub async fn get_market_state(client: &Arc<FutuClient>, symbols: &[String]) -> Result<String> {
174 if symbols.is_empty() {
182 bail!("market_state: symbols empty (必须至少传入 1 个 MARKET.CODE)");
183 }
184 let mut sec_list: Vec<futu_proto::qot_common::Security> = Vec::with_capacity(symbols.len());
185 for (i, s) in symbols.iter().enumerate() {
186 let sec = parse_symbol(s).map_err(|e| {
187 anyhow!(
188 "market_state: symbols[{i}] invalid ({s:?}): {e} — 整体 reject, 不 partial-success"
189 )
190 })?;
191 sec_list.push(futu_proto::qot_common::Security {
192 market: sec.market as i32,
193 code: sec.code,
194 });
195 }
196 let _parsed = futu_qot::symbol_list::parse_required_symbol_list(&sec_list)
199 .map_err(|e| anyhow!("market_state: {e}"))?;
200 let req = futu_proto::qot_get_market_state::Request {
201 c2s: futu_proto::qot_get_market_state::C2s {
202 security_list: sec_list,
203 header: None,
204 },
205 };
206 let body = req.encode_to_vec();
207 let frame = client
208 .request(futu_core::proto_id::QOT_GET_MARKET_STATE, body)
209 .await?;
210 let resp = futu_proto::qot_get_market_state::Response::decode(frame.body.as_ref())
211 .map_err(|e| anyhow!("decode market_state: {e}"))?;
212 if resp.ret_type != 0 {
213 bail!(
214 "market_state ret_type={} msg={:?}",
215 resp.ret_type,
216 resp.ret_msg
217 );
218 }
219 let s = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
220 let out: Vec<MarketStateOut> = s
221 .market_info_list
222 .iter()
223 .map(|m| MarketStateOut {
224 code: format!("{}.{}", market_prefix(m.security.market), m.security.code),
225 name: m.name.clone(),
226 market_state: m.market_state,
227 })
228 .collect();
229 Ok(serde_json::to_string_pretty(&out)?)
230}
231
232fn parse_kl_type_local(s: &str) -> Result<KLType> {
239 match s.trim().to_ascii_lowercase().as_str() {
240 "day" => Ok(KLType::Day),
241 "week" => Ok(KLType::Week),
242 "month" => Ok(KLType::Month),
243 "quarter" => Ok(KLType::Quarter),
244 "year" => Ok(KLType::Year),
245 "1min" => Ok(KLType::Min1),
246 "3min" => Ok(KLType::Min3),
247 "5min" => Ok(KLType::Min5),
248 "15min" => Ok(KLType::Min15),
249 "30min" => Ok(KLType::Min30),
250 "60min" => Ok(KLType::Min60),
251 other => bail!("unknown kl_type {other:?}"),
252 }
253}
254
255fn parse_rehab_type(s: &str) -> Result<RehabType> {
256 match s.trim().to_ascii_lowercase().as_str() {
257 "none" | "no_rehab" => Ok(RehabType::None),
258 "forward" => Ok(RehabType::Forward),
259 "backward" => Ok(RehabType::Backward),
260 other => bail!("unknown rehab_type {other:?} (none|forward|backward)"),
261 }
262}
263
264#[derive(Serialize)]
265struct HistoryKLineOut {
266 time: String,
267 timestamp: f64,
268 open: f64,
269 high: f64,
270 low: f64,
271 close: f64,
272 volume: i64,
273 turnover: f64,
274 pe: f64,
275 change_rate: f64,
276 turnover_rate: f64,
277}
278
279pub async fn get_history_kline(
285 client: &Arc<FutuClient>,
286 symbol: &str,
287 kl_type_str: &str,
288 rehab_type_str: &str,
289 begin: &str,
290 end: &str,
291 max_count: Option<i32>,
292) -> Result<String> {
293 let sec = parse_symbol(symbol)?;
294 let kl_type = parse_kl_type_local(kl_type_str)?;
295 let rehab_type = parse_rehab_type(rehab_type_str)?;
296 let result = futu_qot::history_kl::get_history_kl(
297 client, &sec, rehab_type, kl_type, begin, end, max_count,
298 )
299 .await?;
300 let out: Vec<HistoryKLineOut> = result
301 .kl_list
302 .iter()
303 .map(|k| HistoryKLineOut {
304 time: k.time.clone(),
305 timestamp: k.timestamp,
306 open: k.open_price,
307 high: k.high_price,
308 low: k.low_price,
309 close: k.close_price,
310 volume: k.volume,
311 turnover: k.turnover,
312 pe: k.pe,
313 change_rate: k.change_rate,
314 turnover_rate: k.turnover_rate,
315 })
316 .collect();
317 Ok(serde_json::to_string_pretty(&serde_json::json!({
318 "symbol": symbol,
319 "kl_type": kl_type_str,
320 "rehab_type": rehab_type_str,
321 "kl_list": out,
322 }))?)
323}
324
325#[derive(Serialize)]
328struct OwnerPlateOut {
329 symbol: String,
330 plates: Vec<PlateInfo>,
331}
332
333#[derive(Serialize)]
334struct PlateInfo {
335 code: String,
336 name: String,
337 plate_type: i32,
338}
339
340pub async fn get_owner_plate(client: &Arc<FutuClient>, symbols: &[String]) -> Result<String> {
342 if symbols.is_empty() {
343 bail!("empty symbols");
344 }
345 let sec_list: Vec<_> = symbols
346 .iter()
347 .map(|s| parse_symbol(s))
348 .collect::<Result<Vec<_>>>()?;
349 let s2c = futu_qot::market_misc::get_owner_plate(client, &sec_list).await?;
350 let out: Vec<OwnerPlateOut> = s2c
351 .owner_plate_list
352 .iter()
353 .map(|entry| {
354 let sym = format!("{:?}.{}", entry.security.market, entry.security.code);
355 OwnerPlateOut {
356 symbol: sym,
357 plates: entry
358 .plate_info_list
359 .iter()
360 .map(|p| PlateInfo {
361 code: p.plate.code.clone(),
362 name: p.name.clone(),
363 plate_type: p.plate_type.unwrap_or(0),
364 })
365 .collect(),
366 }
367 })
368 .collect();
369 Ok(serde_json::to_string_pretty(&out)?)
370}
371
372fn parse_reference_type(s: &str) -> Result<i32> {
375 match s.trim().to_ascii_lowercase().as_str() {
378 "warrant" => Ok(1),
379 "future" | "futures" => Ok(2),
380 "option" => Ok(3),
381 other => bail!("unknown reference_type {other:?} (warrant|future|option)"),
382 }
383}
384
385#[derive(Serialize)]
386struct ReferenceOut {
387 code: String,
388 name: String,
389 lot_size: i32,
390 sec_type: i32,
391}
392
393pub async fn get_reference(
397 client: &Arc<FutuClient>,
398 symbol: &str,
399 reference_type_str: &str,
400) -> Result<String> {
401 let sec = parse_symbol(symbol)?;
402 let ref_type = parse_reference_type(reference_type_str)?;
403 let list = futu_qot::market_misc::get_reference(client, &sec, ref_type).await?;
404 let out: Vec<ReferenceOut> = list
405 .iter()
406 .map(|s| ReferenceOut {
407 code: s.security.code.clone(),
408 name: s.name.clone(),
409 lot_size: s.lot_size,
410 sec_type: s.sec_type,
411 })
412 .collect();
413 Ok(serde_json::to_string_pretty(&out)?)
414}
415
416#[derive(Serialize)]
428struct OptionRow {
429 strike_price: f64,
431 call_symbol: Option<String>,
433 put_symbol: Option<String>,
435 suspend: Option<bool>,
437 market: Option<String>,
439 index_option_type: Option<i32>,
441 expiration_cycle: Option<i32>,
443 option_standard_type: Option<i32>,
445 option_settlement_mode: Option<i32>,
447}
448
449#[derive(Serialize)]
450struct OptionChainEntry {
451 strike_time: String,
452 options: Vec<OptionRow>,
455 call_symbols: Vec<String>,
458 put_symbols: Vec<String>,
460}
461
462pub struct OptionChainInput<'a> {
469 pub owner_symbol: &'a str,
470 pub begin_time: &'a str,
471 pub end_time: &'a str,
472 pub option_type_str: Option<&'a str>,
473 pub data_filter: Option<futu_proto::qot_get_option_chain::DataFilter>,
474}
475
476pub async fn get_option_chain(
477 client: &Arc<FutuClient>,
478 input: OptionChainInput<'_>,
479) -> Result<String> {
480 let owner = parse_symbol(input.owner_symbol)?;
481 let option_type = match input.option_type_str.map(str::trim) {
482 Some("all") | None => Some(0), Some("call") => Some(1),
484 Some("put") => Some(2),
485 Some(other) => bail!("unknown option_type {other:?} (all|call|put)"),
486 };
487 let s2c = futu_qot::market_misc::get_option_chain(
488 client,
489 &owner,
490 input.begin_time,
491 input.end_time,
492 option_type,
493 None,
494 input.data_filter,
495 )
496 .await?;
497 let out: Vec<OptionChainEntry> = s2c
500 .option_chain
501 .iter()
502 .map(|entry| {
503 let mut calls = Vec::new();
504 let mut puts = Vec::new();
505 let mut option_rows: Vec<OptionRow> = Vec::new();
509 for item in &entry.option {
510 if let Some(c) = &item.call {
511 calls.push(c.basic.security.code.clone());
512 }
513 if let Some(p) = &item.put {
514 puts.push(p.basic.security.code.clone());
515 }
516 let ex = item
517 .call
518 .as_ref()
519 .and_then(|c| c.option_ex_data.as_ref())
520 .or_else(|| item.put.as_ref().and_then(|p| p.option_ex_data.as_ref()));
521 if let Some(ex) = ex {
522 option_rows.push(OptionRow {
523 strike_price: ex.strike_price,
524 call_symbol: item.call.as_ref().map(|c| c.basic.security.code.clone()),
525 put_symbol: item.put.as_ref().map(|p| p.basic.security.code.clone()),
526 suspend: Some(ex.suspend),
527 market: Some(ex.market.clone()),
528 index_option_type: ex.index_option_type,
529 expiration_cycle: ex.expiration_cycle,
530 option_standard_type: ex.option_standard_type,
531 option_settlement_mode: ex.option_settlement_mode,
532 });
533 }
534 }
535 OptionChainEntry {
536 strike_time: entry.strike_time.clone(),
537 options: option_rows,
538 call_symbols: calls,
539 put_symbols: puts,
540 }
541 })
542 .collect();
543 Ok(serde_json::to_string_pretty(&out)?)
544}