Skip to main content

futucli/cmd/analysis/
capital.rs

1//! v1.4.110+ split (from cmd/analysis.rs): capital domain.
2//!
3//! pub items: run_capital_flow,run_capital_distribution.
4
5use anyhow::{Result, anyhow, bail};
6use prost::Message;
7use serde::Serialize;
8use tabled::Tabled;
9
10use crate::common::{connect_gateway, parse_symbol};
11use crate::output::OutputFormat;
12
13// ============================================================
14// capital-flow
15// ============================================================
16
17#[derive(Tabled)]
18struct CapitalFlowRow {
19    #[tabled(rename = "Time")]
20    time: String,
21    #[tabled(rename = "In Flow")]
22    in_flow: String,
23}
24
25#[derive(Serialize)]
26struct CapitalFlowJson {
27    symbol: String,
28    period_type: i32,
29    last_valid_time: String,
30    flow_item_list: Vec<CapitalFlowItemJson>,
31}
32
33#[derive(Serialize)]
34struct CapitalFlowItemJson {
35    timestamp: f64,
36    in_flow: f64,
37}
38
39pub async fn run_capital_flow(
40    gateway: &str,
41    symbol: &str,
42    period_type: i32,
43    begin: Option<&str>,
44    end: Option<&str>,
45    format: OutputFormat,
46) -> Result<()> {
47    let sec = parse_symbol(symbol)?;
48    let (client, _rx) = connect_gateway(gateway, "futucli-capital-flow").await?;
49
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.clone(),
55            },
56            period_type: Some(period_type),
57            begin_time: begin.map(|s| s.to_string()),
58            end_time: end.map(|s| s.to_string()),
59            header: None, // v1.4.110 codex Slice 1 schema 占位
60        },
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    if resp.ret_type != 0 {
68        bail!(
69            "capital_flow ret_type={} msg={:?}",
70            resp.ret_type,
71            resp.ret_msg
72        );
73    }
74    let s2c = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
75
76    let rows: Vec<CapitalFlowRow> = s2c
77        .flow_item_list
78        .iter()
79        .map(|f| CapitalFlowRow {
80            time: ts_to_datetime(f.timestamp.unwrap_or(0.0)),
81            in_flow: format!("{:.2}", f.in_flow),
82        })
83        .collect();
84
85    let json = CapitalFlowJson {
86        symbol: symbol.to_string(),
87        period_type,
88        last_valid_time: s2c.last_valid_time.unwrap_or_default(),
89        flow_item_list: s2c
90            .flow_item_list
91            .iter()
92            .map(|f| CapitalFlowItemJson {
93                timestamp: f.timestamp.unwrap_or(0.0),
94                in_flow: f.in_flow,
95            })
96            .collect(),
97    };
98    format.print_rows(&rows, &[json])?;
99    Ok(())
100}
101
102fn ts_to_datetime(ts: f64) -> String {
103    let secs = ts as i64;
104    chrono::DateTime::<chrono::Local>::from(
105        std::time::UNIX_EPOCH + std::time::Duration::from_secs(secs as u64),
106    )
107    .format("%Y-%m-%d %H:%M:%S")
108    .to_string()
109}
110
111// ============================================================
112// capital-distribution
113// ============================================================
114
115#[derive(Tabled)]
116struct CapitalDistRow {
117    #[tabled(rename = "Tier")]
118    tier: String,
119    #[tabled(rename = "In")]
120    in_amount: String,
121    #[tabled(rename = "Out")]
122    out_amount: String,
123    #[tabled(rename = "Net")]
124    net: String,
125}
126
127#[derive(Serialize)]
128struct CapitalDistJson {
129    symbol: String,
130    capital_in_super: f64,
131    capital_in_big: f64,
132    capital_in_mid: f64,
133    capital_in_small: f64,
134    capital_out_super: f64,
135    capital_out_big: f64,
136    capital_out_mid: f64,
137    capital_out_small: f64,
138    update_time: String,
139}
140
141pub async fn run_capital_distribution(
142    gateway: &str,
143    symbol: &str,
144    format: OutputFormat,
145) -> Result<()> {
146    let sec = parse_symbol(symbol)?;
147    let (client, _rx) = connect_gateway(gateway, "futucli-capital-dist").await?;
148
149    let req = futu_proto::qot_get_capital_distribution::Request {
150        c2s: futu_proto::qot_get_capital_distribution::C2s {
151            security: futu_proto::qot_common::Security {
152                market: sec.market as i32,
153                code: sec.code.clone(),
154            },
155            header: None, // v1.4.110 codex Slice 1 schema 占位
156        },
157    };
158    let body = req.encode_to_vec();
159    let frame = client
160        .request(futu_core::proto_id::QOT_GET_CAPITAL_DISTRIBUTION, body)
161        .await?;
162    let resp = futu_proto::qot_get_capital_distribution::Response::decode(frame.body.as_ref())?;
163    if resp.ret_type != 0 {
164        bail!(
165            "capital_distribution ret_type={} msg={:?}",
166            resp.ret_type,
167            resp.ret_msg
168        );
169    }
170    let s = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
171
172    let super_in = s.capital_in_super.unwrap_or(0.0);
173    let super_out = s.capital_out_super.unwrap_or(0.0);
174    let rows = vec![
175        CapitalDistRow {
176            tier: "Super".into(),
177            in_amount: format!("{super_in:.2}"),
178            out_amount: format!("{super_out:.2}"),
179            net: format!("{:.2}", super_in - super_out),
180        },
181        CapitalDistRow {
182            tier: "Big".into(),
183            in_amount: format!("{:.2}", s.capital_in_big),
184            out_amount: format!("{:.2}", s.capital_out_big),
185            net: format!("{:.2}", s.capital_in_big - s.capital_out_big),
186        },
187        CapitalDistRow {
188            tier: "Mid".into(),
189            in_amount: format!("{:.2}", s.capital_in_mid),
190            out_amount: format!("{:.2}", s.capital_out_mid),
191            net: format!("{:.2}", s.capital_in_mid - s.capital_out_mid),
192        },
193        CapitalDistRow {
194            tier: "Small".into(),
195            in_amount: format!("{:.2}", s.capital_in_small),
196            out_amount: format!("{:.2}", s.capital_out_small),
197            net: format!("{:.2}", s.capital_in_small - s.capital_out_small),
198        },
199    ];
200
201    let json = CapitalDistJson {
202        symbol: symbol.to_string(),
203        capital_in_super: super_in,
204        capital_in_big: s.capital_in_big,
205        capital_in_mid: s.capital_in_mid,
206        capital_in_small: s.capital_in_small,
207        capital_out_super: super_out,
208        capital_out_big: s.capital_out_big,
209        capital_out_mid: s.capital_out_mid,
210        capital_out_small: s.capital_out_small,
211        update_time: s.update_time.unwrap_or_default(),
212    };
213    format.print_rows(&rows, &[json])?;
214    Ok(())
215}