1use 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
14pub(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
30pub(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
40pub(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}