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