Skip to main content

futucli/cmd/analysis/
warrant_ipo.rs

1//! v1.4.110+ split (from cmd/analysis.rs): warrant_ipo domain.
2//!
3//! pub items: run_warrant, run_ipo_list.
4
5use 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    // v1.4.111 codex: 暴露 begin (分页), 不静默 clamp num.
52    // 越界 (begin<0 / num∉[0, 200]) 走 Err; num=0 对齐 C++ 为空页请求.
53    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, // Volume
65            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, // v1.4.110 codex Slice 1 schema 占位
241        },
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}