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 prost::Message;
7use serde::Serialize;
8use tabled::Tabled;
9
10use crate::common::{connect_gateway, parse_symbol};
11use crate::output::OutputFormat;
12
13use super::trading::parse_qot_market;
14
15#[derive(Tabled)]
16struct WarrantRow {
17    #[tabled(rename = "Code")]
18    code: String,
19    #[tabled(rename = "Name")]
20    name: String,
21    #[tabled(rename = "Owner")]
22    owner: String,
23    #[tabled(rename = "Cur Price")]
24    cur_price: String,
25    #[tabled(rename = "Strike")]
26    strike: String,
27    #[tabled(rename = "Maturity")]
28    maturity: String,
29}
30
31#[derive(Serialize)]
32struct WarrantJson {
33    code: String,
34    name: String,
35    owner_code: String,
36    cur_price: f64,
37    strike_price: f64,
38    maturity_time: String,
39}
40
41pub async fn run_warrant(
42    gateway: &str,
43    owner_symbol: Option<&str>,
44    begin: i32,
45    num: i32,
46    format: OutputFormat,
47) -> Result<()> {
48    // v1.4.106 codex 0635 ζ36 F1+F3: 暴露 begin (分页), 不再静默 clamp num.
49    // 越界 (begin<0 / num∉[1, 200]) 走 Err 让用户看到清晰错误.
50    let bounds = futu_qot::page_bounds::validate_begin_num(begin, num, 200, "warrant")
51        .map_err(|e| anyhow!("{}", e))?;
52    let owner = match owner_symbol {
53        Some(s) => Some(parse_symbol(s)?),
54        None => None,
55    };
56    let (client, _rx) = connect_gateway(gateway, "futucli-warrant").await?;
57    let req = futu_proto::qot_get_warrant::Request {
58        c2s: futu_proto::qot_get_warrant::C2s {
59            begin: bounds.begin,
60            num: bounds.num,
61            sort_field: 24, // Volume
62            ascend: false,
63            owner: owner.map(|s| futu_proto::qot_common::Security {
64                market: s.market as i32,
65                code: s.code,
66            }),
67            type_list: vec![],
68            issuer_list: vec![],
69            maturity_time_min: None,
70            maturity_time_max: None,
71            ipo_period: None,
72            price_type: None,
73            status: None,
74            cur_price_min: None,
75            cur_price_max: None,
76            strike_price_min: None,
77            strike_price_max: None,
78            street_min: None,
79            street_max: None,
80            conversion_min: None,
81            conversion_max: None,
82            vol_min: None,
83            vol_max: None,
84            premium_min: None,
85            premium_max: None,
86            leverage_ratio_min: None,
87            leverage_ratio_max: None,
88            delta_min: None,
89            delta_max: None,
90            implied_min: None,
91            implied_max: None,
92            recovery_price_min: None,
93            recovery_price_max: None,
94            price_recovery_ratio_min: None,
95            price_recovery_ratio_max: None,
96            header: None,
97        },
98    };
99    let body = req.encode_to_vec();
100    let frame = client
101        .request(futu_core::proto_id::QOT_GET_WARRANT, body)
102        .await?;
103    let resp = futu_proto::qot_get_warrant::Response::decode(frame.body.as_ref())
104        .map_err(|e| anyhow!("decode warrant: {e}"))?;
105    if resp.ret_type != 0 {
106        bail!("warrant ret_type={} msg={:?}", resp.ret_type, resp.ret_msg);
107    }
108    let s2c = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
109    let mut rows = Vec::new();
110    let mut jsons = Vec::new();
111    for w in &s2c.warrant_data_list {
112        rows.push(WarrantRow {
113            code: w.stock.code.clone(),
114            name: w.name.clone(),
115            owner: w.owner.code.clone(),
116            cur_price: format!("{:.3}", w.cur_price),
117            strike: format!("{:.3}", w.strike_price),
118            maturity: w.maturity_time.clone(),
119        });
120        jsons.push(WarrantJson {
121            code: w.stock.code.clone(),
122            name: w.name.clone(),
123            owner_code: w.owner.code.clone(),
124            cur_price: w.cur_price,
125            strike_price: w.strike_price,
126            maturity_time: w.maturity_time.clone(),
127        });
128    }
129    format.print_rows(&rows, &jsons)?;
130    Ok(())
131}
132
133#[derive(Tabled)]
134struct IpoRow {
135    #[tabled(rename = "Code")]
136    code: String,
137    #[tabled(rename = "Name")]
138    name: String,
139    #[tabled(rename = "List Time")]
140    list_time: String,
141}
142
143#[derive(Serialize)]
144struct IpoJson {
145    code: String,
146    name: String,
147    list_time: Option<String>,
148}
149
150pub async fn run_ipo_list(gateway: &str, market: &str, format: OutputFormat) -> Result<()> {
151    let m = parse_qot_market(market)?;
152    let (client, _rx) = connect_gateway(gateway, "futucli-ipo-list").await?;
153    let req = futu_proto::qot_get_ipo_list::Request {
154        c2s: futu_proto::qot_get_ipo_list::C2s {
155            market: m,
156            header: None, // v1.4.110 codex Slice 1 schema 占位
157        },
158    };
159    let body = req.encode_to_vec();
160    let frame = client
161        .request(futu_core::proto_id::QOT_GET_IPO_LIST, body)
162        .await?;
163    let resp = futu_proto::qot_get_ipo_list::Response::decode(frame.body.as_ref())
164        .map_err(|e| anyhow!("decode ipo_list: {e}"))?;
165    if resp.ret_type != 0 {
166        bail!("ipo_list ret_type={} msg={:?}", resp.ret_type, resp.ret_msg);
167    }
168    let s2c = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
169    let mut rows = Vec::new();
170    let mut jsons = Vec::new();
171    for i in &s2c.ipo_list {
172        rows.push(IpoRow {
173            code: i.basic.security.code.clone(),
174            name: i.basic.name.clone(),
175            list_time: i.basic.list_time.clone().unwrap_or_else(|| "-".into()),
176        });
177        jsons.push(IpoJson {
178            code: i.basic.security.code.clone(),
179            name: i.basic.name.clone(),
180            list_time: i.basic.list_time.clone(),
181        });
182    }
183    format.print_rows(&rows, &jsons)?;
184    Ok(())
185}