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