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