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