1use 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
13fn 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, "ST" => 5, "JP" => 6, "SG" => 7, other => bail!("unknown trade-date market {other:?} (HK|US|CN|NT|ST|JP|SG)"),
30 })
31}
32
33pub(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, "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}