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
13pub(super) fn parse_trade_date_market(s: &str) -> Result<i32> {
21 if let Ok(value) = s.trim().parse::<i32>() {
22 return match value {
23 1..=10 | 11 | 21 | 22 => Ok(value),
24 _ => bail!(
25 "unknown trade-date market {value} \
26 (valid TradeDateMarket = 1..=10; legacy aliases = 11|21|22)"
27 ),
28 };
29 }
30
31 Ok(match s.trim().to_ascii_uppercase().as_str() {
32 "HK" => 1,
33 "US" => 2,
34 "CN" => 3,
35 "NT" => 4, "ST" => 5, "JP_FUTURE" | "JPFUTURE" | "JPF" => 6,
38 "SG_FUTURE" | "SGFUTURE" | "SGF" => 7,
39 "SG" | "SG_STOCK" | "SG_SECURITY" => 8,
40 "MY" | "MY_STOCK" | "MY_SECURITY" => 9,
41 "JP" | "JP_STOCK" | "JP_SECURITY" => 10,
42 other => bail!(
43 "unknown trade-date market {other:?} \
44 (HK|US|CN|NT|ST|JP_FUTURE|SG_FUTURE|SG|MY|JP)"
45 ),
46 })
47}
48
49pub(super) fn parse_qot_market(s: &str) -> Result<i32> {
51 let value = parse_qot_market_value(s)?;
52 match value {
53 1 | 2 | 11 | 21 | 22 | 31 | 41 | 61 => Ok(value),
54 _ => bail!("unknown qot market {value} (valid = 1|2|11|21|22|31|41|61)"),
55 }
56}
57
58pub(super) fn parse_ipo_market(s: &str) -> Result<i32> {
60 let value = parse_qot_market_value(s)?;
61 match value {
62 1 | 2 | 11 | 21 | 22 | 31 | 41 | 61 => Ok(value),
63 _ => bail!("unknown IPO market {value} (valid = 1|2|11|21|22|31|41|61)"),
64 }
65}
66
67fn parse_qot_market_value(s: &str) -> Result<i32> {
68 if let Ok(value) = s.trim().parse::<i32>() {
69 return Ok(value);
70 }
71
72 Ok(match s.trim().to_ascii_uppercase().as_str() {
73 "HK" => 1,
74 "HK_FUTURE" | "HKFUTURE" => 2,
75 "US" => 11,
76 "CN" | "SH" => 21, "SZ" => 22,
78 "SG" => 31,
79 "JP" => 41,
80 "MY" => 61,
81 other => bail!("unknown qot market {other:?} (HK|HK_FUTURE|US|CN|SH|SZ|SG|JP|MY)"),
82 })
83}
84
85#[derive(Tabled)]
86struct TradingDayRow {
87 #[tabled(rename = "Time")]
88 time: String,
89 #[tabled(rename = "Type")]
90 type_label: String,
91}
92
93#[derive(Serialize)]
94struct TradingDayJson {
95 time: String,
96 trade_date_type: i32,
97}
98
99pub async fn run_trading_days(
100 gateway: &str,
101 market: &str,
102 begin: &str,
103 end: &str,
104 format: OutputFormat,
105) -> Result<()> {
106 let m = parse_trade_date_market(market)?;
107 let (client, _rx) = connect_gateway(gateway, "futucli-trading-days").await?;
108 let req = futu_proto::qot_request_trade_date::Request {
109 c2s: futu_proto::qot_request_trade_date::C2s {
110 market: m,
111 begin_time: begin.to_string(),
112 end_time: end.to_string(),
113 security: None,
114 header: None,
115 },
116 };
117 let body = req.encode_to_vec();
118 let frame = client
119 .request(futu_core::proto_id::QOT_REQUEST_TRADE_DATE, body)
120 .await?;
121 let resp = futu_proto::qot_request_trade_date::Response::decode(frame.body.as_ref())
122 .map_err(|e| anyhow!("decode trading_days: {e}"))?;
123 if resp.ret_type != 0 {
124 bail!(
125 "trading_days ret_type={} msg={:?}",
126 resp.ret_type,
127 resp.ret_msg
128 );
129 }
130 let s2c = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
131 let mut rows = Vec::new();
132 let mut jsons = Vec::new();
133 for t in &s2c.trade_date_list {
134 let ty = t.trade_date_type.unwrap_or(0);
135 rows.push(TradingDayRow {
136 time: t.time.clone(),
137 type_label: match ty {
138 0 => "Whole".to_string(),
139 1 => "Morning".to_string(),
140 2 => "Afternoon".to_string(),
141 _ => format!("({ty})"),
142 },
143 });
144 jsons.push(TradingDayJson {
145 time: t.time.clone(),
146 trade_date_type: ty,
147 });
148 }
149 format.print_rows(&rows, &jsons)?;
150 Ok(())
151}
152
153#[derive(Tabled)]
154struct RehabRow {
155 #[tabled(rename = "Time")]
156 time: String,
157 #[tabled(rename = "FwdA")]
158 fwd_a: String,
159 #[tabled(rename = "FwdB")]
160 fwd_b: String,
161 #[tabled(rename = "BwdA")]
162 bwd_a: String,
163 #[tabled(rename = "Dividend")]
164 dividend: String,
165 #[tabled(rename = "ActFlag")]
166 act_flag: i64,
167}
168
169#[derive(Serialize)]
170struct RehabJson {
171 time: String,
172 fwd_factor_a: f64,
173 fwd_factor_b: f64,
174 bwd_factor_a: f64,
175 bwd_factor_b: f64,
176 company_act_flag: i64,
177 dividend: Option<f64>,
178 sp_dividend: Option<f64>,
179 bonus: Option<(i32, i32)>,
180 transfer: Option<(i32, i32)>,
181 allot: Option<(i32, i32, Option<f64>)>,
182}
183
184pub async fn run_rehab(gateway: &str, symbol: &str, format: OutputFormat) -> Result<()> {
185 let sec = parse_symbol(symbol)?;
186 let (client, _rx) = connect_gateway(gateway, "futucli-rehab").await?;
187 let s2c = futu_qot::market_misc::get_rehab(&client, &sec).await?;
188 let mut rows = Vec::new();
189 let mut jsons = Vec::new();
190 for r in &s2c.rehab_list {
191 rows.push(RehabRow {
192 time: r.time.clone(),
193 fwd_a: format!("{:.6}", r.fwd_factor_a),
194 fwd_b: format!("{:.6}", r.fwd_factor_b),
195 bwd_a: format!("{:.6}", r.bwd_factor_a),
196 dividend: r
197 .dividend
198 .map(|d| format!("{d:.4}"))
199 .unwrap_or_else(|| "-".to_string()),
200 act_flag: r.company_act_flag,
201 });
202 jsons.push(RehabJson {
203 time: r.time.clone(),
204 fwd_factor_a: r.fwd_factor_a,
205 fwd_factor_b: r.fwd_factor_b,
206 bwd_factor_a: r.bwd_factor_a,
207 bwd_factor_b: r.bwd_factor_b,
208 company_act_flag: r.company_act_flag,
209 dividend: r.dividend,
210 sp_dividend: r.sp_dividend,
211 bonus: r.bonus_base.zip(r.bonus_ert),
212 transfer: r.transfer_base.zip(r.transfer_ert),
213 allot: r
214 .allot_base
215 .zip(r.allot_ert)
216 .map(|(b, e)| (b, e, r.allot_price)),
217 });
218 }
219 format.print_rows(&rows, &jsons)?;
220 Ok(())
221}
222
223#[derive(Tabled)]
224struct SuspendRow {
225 #[tabled(rename = "Symbol")]
226 symbol: String,
227 #[tabled(rename = "Suspend Days")]
228 days: String,
229}
230
231#[derive(Serialize)]
232struct SuspendJson {
233 symbol: String,
234 suspend_days: Vec<String>,
235}
236
237pub async fn run_suspend(
238 gateway: &str,
239 symbols: &[String],
240 begin: &str,
241 end: &str,
242 format: OutputFormat,
243) -> Result<()> {
244 if symbols.is_empty() {
245 bail!("no symbols");
246 }
247 let secs: Vec<_> = symbols
248 .iter()
249 .map(|s| parse_symbol(s))
250 .collect::<Result<Vec<_>>>()?;
251 let (client, _rx) = connect_gateway(gateway, "futucli-suspend").await?;
252 let s2c = futu_qot::market_misc::get_suspend(&client, &secs, begin, end).await?;
253 let mut rows = Vec::new();
254 let mut jsons = Vec::new();
255 for ss in &s2c.security_suspend_list {
256 let sym = format!("{}.{}", ss.security.market, ss.security.code);
257 let days: Vec<String> = ss.suspend_list.iter().map(|s| s.time.clone()).collect();
258 rows.push(SuspendRow {
259 symbol: sym.clone(),
260 days: if days.is_empty() {
261 "-".into()
262 } else {
263 days.join(", ")
264 },
265 });
266 jsons.push(SuspendJson {
267 symbol: sym,
268 suspend_days: days,
269 });
270 }
271 format.print_rows(&rows, &jsons)?;
272 Ok(())
273}