Skip to main content

futucli/cmd/analysis/
trading.rs

1//! v1.4.110+ split (from cmd/analysis.rs): trading domain.
2//!
3//! pub items: parse helpers, run_trading_days, run_rehab, run_suspend.
4
5use anyhow::{Result, anyhow, bail};
6use futu_core::{qot_endpoint_market, qot_trade_date_market};
7use prost::Message;
8use serde::Serialize;
9use tabled::Tabled;
10
11use crate::common::{connect_gateway, parse_symbol};
12use crate::output::OutputFormat;
13
14// ============================================================
15// v1.4.30: trading-days / rehab / suspend / user-security /
16//          user-security-groups / warrant / ipo-list / future-info /
17//          stock-filter
18// ============================================================
19
20/// `--market` 字符串 / 数字 → `Qot_Common.TradeDateMarket` 枚举 int.
21pub(super) fn parse_trade_date_market(s: &str) -> Result<i32> {
22    qot_trade_date_market::trade_date_market_from_str_alias(s).ok_or_else(|| {
23        anyhow!(
24            "unknown trade-date market {s:?} (valid = {})",
25            qot_trade_date_market::TRADE_DATE_MARKET_VALID_VALUES
26        )
27    })
28}
29
30/// `--market` 字符串 / 数字 → C++ StockFilter endpoint-local market bucket。
31pub(super) fn parse_qot_market(s: &str) -> Result<i32> {
32    qot_endpoint_market::stock_filter_market_from_str_alias(s).ok_or_else(|| {
33        anyhow!(
34            "unknown qot market {s:?} (valid = {})",
35            qot_endpoint_market::QOT_STOCK_FILTER_MARKET_VALID_VALUES
36        )
37    })
38}
39
40/// `--market` 字符串 / 数字 → C++ GetIpoList endpoint-local market bucket.
41pub(super) fn parse_ipo_market(s: &str) -> Result<i32> {
42    qot_endpoint_market::ipo_market_from_str_alias(s).ok_or_else(|| {
43        anyhow!(
44            "unknown IPO market {s:?} (valid = {})",
45            qot_endpoint_market::QOT_IPO_MARKET_VALID_VALUES
46        )
47    })
48}
49
50#[derive(Tabled)]
51struct TradingDayRow {
52    #[tabled(rename = "Time")]
53    time: String,
54    #[tabled(rename = "Type")]
55    type_label: String,
56}
57
58#[derive(Serialize)]
59struct TradingDayJson {
60    time: String,
61    trade_date_type: i32,
62}
63
64pub async fn run_trading_days(
65    gateway: &str,
66    market: &str,
67    begin: &str,
68    end: &str,
69    format: OutputFormat,
70) -> Result<()> {
71    let m = parse_trade_date_market(market)?;
72    let (client, _rx) = connect_gateway(gateway, "futucli-trading-days").await?;
73    let req = futu_proto::qot_request_trade_date::Request {
74        c2s: futu_proto::qot_request_trade_date::C2s {
75            market: m,
76            begin_time: begin.to_string(),
77            end_time: end.to_string(),
78            security: None,
79            header: None,
80        },
81    };
82    let body = req.encode_to_vec();
83    let frame = client
84        .request(futu_core::proto_id::QOT_REQUEST_TRADE_DATE, body)
85        .await?;
86    let resp = futu_proto::qot_request_trade_date::Response::decode(frame.body.as_ref())
87        .map_err(|e| anyhow!("decode trading_days: {e}"))?;
88    if resp.ret_type != 0 {
89        bail!(
90            "trading_days ret_type={} msg={:?}",
91            resp.ret_type,
92            resp.ret_msg
93        );
94    }
95    let s2c = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
96    let mut rows = Vec::new();
97    let mut jsons = Vec::new();
98    for t in &s2c.trade_date_list {
99        let ty = t.trade_date_type.unwrap_or(0);
100        rows.push(TradingDayRow {
101            time: t.time.clone(),
102            type_label: match ty {
103                0 => "Whole".to_string(),
104                1 => "Morning".to_string(),
105                2 => "Afternoon".to_string(),
106                _ => format!("({ty})"),
107            },
108        });
109        jsons.push(TradingDayJson {
110            time: t.time.clone(),
111            trade_date_type: ty,
112        });
113    }
114    format.print_rows(&rows, &jsons)?;
115    Ok(())
116}
117
118#[derive(Tabled)]
119struct RehabRow {
120    #[tabled(rename = "Time")]
121    time: String,
122    #[tabled(rename = "FwdA")]
123    fwd_a: String,
124    #[tabled(rename = "FwdB")]
125    fwd_b: String,
126    #[tabled(rename = "BwdA")]
127    bwd_a: String,
128    #[tabled(rename = "Dividend")]
129    dividend: String,
130    #[tabled(rename = "ActFlag")]
131    act_flag: i64,
132}
133
134#[derive(Serialize)]
135struct RehabJson {
136    time: String,
137    fwd_factor_a: f64,
138    fwd_factor_b: f64,
139    bwd_factor_a: f64,
140    bwd_factor_b: f64,
141    company_act_flag: i64,
142    dividend: Option<f64>,
143    sp_dividend: Option<f64>,
144    bonus: Option<(i32, i32)>,
145    transfer: Option<(i32, i32)>,
146    allot: Option<(i32, i32, Option<f64>)>,
147}
148
149pub async fn run_rehab(gateway: &str, symbol: &str, format: OutputFormat) -> Result<()> {
150    let sec = parse_symbol(symbol)?;
151    let (client, _rx) = connect_gateway(gateway, "futucli-rehab").await?;
152    let s2c = futu_qot::market_misc::get_rehab(&client, &sec).await?;
153    let mut rows = Vec::new();
154    let mut jsons = Vec::new();
155    for r in &s2c.rehab_list {
156        rows.push(RehabRow {
157            time: r.time.clone(),
158            fwd_a: format!("{:.6}", r.fwd_factor_a),
159            fwd_b: format!("{:.6}", r.fwd_factor_b),
160            bwd_a: format!("{:.6}", r.bwd_factor_a),
161            dividend: r
162                .dividend
163                .map(|d| format!("{d:.4}"))
164                .unwrap_or_else(|| "-".to_string()),
165            act_flag: r.company_act_flag,
166        });
167        jsons.push(RehabJson {
168            time: r.time.clone(),
169            fwd_factor_a: r.fwd_factor_a,
170            fwd_factor_b: r.fwd_factor_b,
171            bwd_factor_a: r.bwd_factor_a,
172            bwd_factor_b: r.bwd_factor_b,
173            company_act_flag: r.company_act_flag,
174            dividend: r.dividend,
175            sp_dividend: r.sp_dividend,
176            bonus: r.bonus_base.zip(r.bonus_ert),
177            transfer: r.transfer_base.zip(r.transfer_ert),
178            allot: r
179                .allot_base
180                .zip(r.allot_ert)
181                .map(|(b, e)| (b, e, r.allot_price)),
182        });
183    }
184    format.print_rows(&rows, &jsons)?;
185    Ok(())
186}
187
188#[derive(Tabled)]
189struct SuspendRow {
190    #[tabled(rename = "Symbol")]
191    symbol: String,
192    #[tabled(rename = "Suspend Days")]
193    days: String,
194}
195
196#[derive(Serialize)]
197struct SuspendJson {
198    symbol: String,
199    suspend_days: Vec<String>,
200}
201
202pub async fn run_suspend(
203    gateway: &str,
204    symbols: &[String],
205    begin: &str,
206    end: &str,
207    format: OutputFormat,
208) -> Result<()> {
209    if symbols.is_empty() {
210        bail!("no symbols");
211    }
212    let secs: Vec<_> = symbols
213        .iter()
214        .map(|s| parse_symbol(s))
215        .collect::<Result<Vec<_>>>()?;
216    let (client, _rx) = connect_gateway(gateway, "futucli-suspend").await?;
217    let s2c = futu_qot::market_misc::get_suspend(&client, &secs, begin, end).await?;
218    let mut rows = Vec::new();
219    let mut jsons = Vec::new();
220    for ss in &s2c.security_suspend_list {
221        let sym = format!("{}.{}", ss.security.market, ss.security.code);
222        let days: Vec<String> = ss.suspend_list.iter().map(|s| s.time.clone()).collect();
223        rows.push(SuspendRow {
224            symbol: sym.clone(),
225            days: if days.is_empty() {
226                "-".into()
227            } else {
228                days.join(", ")
229            },
230        });
231        jsons.push(SuspendJson {
232            symbol: sym,
233            suspend_days: days,
234        });
235    }
236    format.print_rows(&rows, &jsons)?;
237    Ok(())
238}