1use anyhow::{Result, anyhow, bail};
6use futu_qot::ipo_calendar::{
7 collect_ipo_calendar_events, is_supported_ipo_event_type, is_yyyymmdd,
8};
9use prost::Message;
10use serde::Serialize;
11use tabled::Tabled;
12
13use crate::common::{connect_gateway, parse_symbol};
14use crate::output::OutputFormat;
15
16use super::trading::parse_ipo_market;
17
18#[derive(Tabled)]
19struct WarrantRow {
20 #[tabled(rename = "Code")]
21 code: String,
22 #[tabled(rename = "Name")]
23 name: String,
24 #[tabled(rename = "Owner")]
25 owner: String,
26 #[tabled(rename = "Cur Price")]
27 cur_price: String,
28 #[tabled(rename = "Strike")]
29 strike: String,
30 #[tabled(rename = "Maturity")]
31 maturity: String,
32}
33
34#[derive(Serialize)]
35struct WarrantJson {
36 code: String,
37 name: String,
38 owner_code: String,
39 cur_price: f64,
40 strike_price: f64,
41 maturity_time: String,
42}
43
44pub async fn run_warrant(
45 gateway: &str,
46 owner_symbol: Option<&str>,
47 begin: i32,
48 num: i32,
49 format: OutputFormat,
50) -> Result<()> {
51 let bounds = futu_core::qot_page_bounds::validate_begin_num(begin, num, 200, "warrant")
54 .map_err(|e| anyhow!("{}", e))?;
55 let owner = match owner_symbol {
56 Some(s) => Some(parse_symbol(s)?),
57 None => None,
58 };
59 let (client, _rx) = connect_gateway(gateway, "futucli-warrant").await?;
60 let req = build_warrant_request(
61 bounds.begin,
62 bounds.num,
63 owner.map(|s| futu_proto::qot_common::Security {
64 market: s.market as i32,
65 code: s.code,
66 }),
67 );
68 let body = req.encode_to_vec();
69 let frame = client
70 .request(futu_core::proto_id::QOT_GET_WARRANT, body)
71 .await?;
72 let resp = futu_proto::qot_get_warrant::Response::decode(frame.body.as_ref())
73 .map_err(|e| anyhow!("decode warrant: {e}"))?;
74 if resp.ret_type != 0 {
75 bail!("warrant ret_type={} msg={:?}", resp.ret_type, resp.ret_msg);
76 }
77 let s2c = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
78 let mut rows = Vec::new();
79 let mut jsons = Vec::new();
80 for w in &s2c.warrant_data_list {
81 rows.push(WarrantRow {
82 code: w.stock.code.clone(),
83 name: w.name.clone(),
84 owner: w.owner.code.clone(),
85 cur_price: format!("{:.3}", w.cur_price),
86 strike: format!("{:.3}", w.strike_price),
87 maturity: w.maturity_time.clone(),
88 });
89 jsons.push(WarrantJson {
90 code: w.stock.code.clone(),
91 name: w.name.clone(),
92 owner_code: w.owner.code.clone(),
93 cur_price: w.cur_price,
94 strike_price: w.strike_price,
95 maturity_time: w.maturity_time.clone(),
96 });
97 }
98 format.print_rows(&rows, &jsons)?;
99 Ok(())
100}
101
102pub(super) fn build_warrant_request(
103 begin: i32,
104 num: i32,
105 owner: Option<futu_proto::qot_common::Security>,
106) -> futu_proto::qot_get_warrant::Request {
107 futu_proto::qot_get_warrant::Request {
108 c2s: futu_proto::qot_get_warrant::C2s {
109 begin,
110 num,
111 sort_field: 10,
114 ascend: false,
115 owner,
116 type_list: vec![],
117 issuer_list: vec![],
118 maturity_time_min: None,
119 maturity_time_max: None,
120 ipo_period: None,
121 price_type: None,
122 status: None,
123 cur_price_min: None,
124 cur_price_max: None,
125 strike_price_min: None,
126 strike_price_max: None,
127 street_min: None,
128 street_max: None,
129 conversion_min: None,
130 conversion_max: None,
131 vol_min: None,
132 vol_max: None,
133 premium_min: None,
134 premium_max: None,
135 leverage_ratio_min: None,
136 leverage_ratio_max: None,
137 delta_min: None,
138 delta_max: None,
139 implied_min: None,
140 implied_max: None,
141 recovery_price_min: None,
142 recovery_price_max: None,
143 price_recovery_ratio_min: None,
144 price_recovery_ratio_max: None,
145 header: None,
146 },
147 }
148}
149
150#[derive(Tabled)]
151struct IpoRow {
152 #[tabled(rename = "Code")]
153 code: String,
154 #[tabled(rename = "Name")]
155 name: String,
156 #[tabled(rename = "List Time")]
157 list_time: String,
158}
159
160#[derive(Serialize)]
161struct IpoJson {
162 code: String,
163 name: String,
164 list_time: Option<String>,
165}
166
167#[derive(Tabled)]
168struct IpoCalendarRow {
169 #[tabled(rename = "Event")]
170 event_type: String,
171 #[tabled(rename = "Code")]
172 code: String,
173 #[tabled(rename = "Name")]
174 name: String,
175 #[tabled(rename = "Date")]
176 date: String,
177 #[tabled(rename = "Time")]
178 time: String,
179}
180
181fn validate_yyyymmdd(field: &str, value: Option<&str>) -> Result<()> {
182 if let Some(value) = value
183 && !is_yyyymmdd(value)
184 {
185 bail!("{field} must be YYYYMMDD, got {value:?}");
186 }
187 Ok(())
188}
189
190pub async fn run_ipo_calendar(
191 gateway: &str,
192 market: &str,
193 events: &[String],
194 begin_date: Option<&str>,
195 end_date: Option<&str>,
196 format: OutputFormat,
197) -> Result<()> {
198 validate_yyyymmdd("begin_date", begin_date)?;
199 validate_yyyymmdd("end_date", end_date)?;
200 if let (Some(begin), Some(end)) = (begin_date, end_date)
201 && begin > end
202 {
203 bail!("begin_date must be earlier than or equal to end_date");
204 }
205 if let Some(event) = events
206 .iter()
207 .find(|event| !is_supported_ipo_event_type(event))
208 {
209 bail!("unsupported IPO calendar event_type {event:?}");
210 }
211 let m = parse_ipo_market(market)?;
212 let (client, _rx) = connect_gateway(gateway, "futucli-ipo-calendar").await?;
213 let req = futu_proto::qot_get_ipo_list::Request {
214 c2s: futu_proto::qot_get_ipo_list::C2s {
215 market: m,
216 header: None,
217 },
218 };
219 let body = req.encode_to_vec();
220 let frame = client
221 .request(futu_core::proto_id::QOT_GET_IPO_LIST, body)
222 .await?;
223 let resp = futu_proto::qot_get_ipo_list::Response::decode(frame.body.as_ref())
224 .map_err(|e| anyhow!("decode ipo_calendar: {e}"))?;
225 if resp.ret_type != 0 {
226 bail!(
227 "ipo_calendar ret_type={} msg={:?}",
228 resp.ret_type,
229 resp.ret_msg
230 );
231 }
232 let s2c = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
233 let calendar_events = collect_ipo_calendar_events(&s2c, events, begin_date, end_date);
234 let rows: Vec<IpoCalendarRow> = calendar_events
235 .iter()
236 .map(|event| IpoCalendarRow {
237 event_type: event.event_type.to_string(),
238 code: event.code.clone(),
239 name: event.name.clone(),
240 date: event.date.clone().unwrap_or_default(),
241 time: event.time.clone().unwrap_or_default(),
242 })
243 .collect();
244 format.print_rows(&rows, &calendar_events)?;
245 Ok(())
246}
247
248pub async fn run_ipo_list(gateway: &str, market: &str, format: OutputFormat) -> Result<()> {
249 let m = parse_ipo_market(market)?;
250 let (client, _rx) = connect_gateway(gateway, "futucli-ipo-list").await?;
251 let req = futu_proto::qot_get_ipo_list::Request {
252 c2s: futu_proto::qot_get_ipo_list::C2s {
253 market: m,
254 header: None, },
256 };
257 let body = req.encode_to_vec();
258 let frame = client
259 .request(futu_core::proto_id::QOT_GET_IPO_LIST, body)
260 .await?;
261 let resp = futu_proto::qot_get_ipo_list::Response::decode(frame.body.as_ref())
262 .map_err(|e| anyhow!("decode ipo_list: {e}"))?;
263 if resp.ret_type != 0 {
264 bail!("ipo_list ret_type={} msg={:?}", resp.ret_type, resp.ret_msg);
265 }
266 let s2c = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
267 let mut rows = Vec::new();
268 let mut jsons = Vec::new();
269 for i in &s2c.ipo_list {
270 rows.push(IpoRow {
271 code: i.basic.security.code.clone(),
272 name: i.basic.name.clone(),
273 list_time: i.basic.list_time.clone().unwrap_or_else(|| "-".into()),
274 });
275 jsons.push(IpoJson {
276 code: i.basic.security.code.clone(),
277 name: i.basic.name.clone(),
278 list_time: i.basic.list_time.clone(),
279 });
280 }
281 format.print_rows(&rows, &jsons)?;
282 Ok(())
283}