Skip to main content

futu_mcp/handlers/reference/
warrant_ipo.rs

1//! mcp/handlers/reference/warrant_ipo — get_warrant + get_ipo_list
2//! (v1.4.110 CC Batch L: 拆自 reference.rs L29-238)
3
4use std::sync::Arc;
5
6use anyhow::{Result, anyhow, bail};
7use futu_net::client::FutuClient;
8use futu_qot::page_bounds::validate_begin_num;
9use prost::Message;
10use serde::Serialize;
11
12use crate::state::parse_symbol;
13
14#[derive(Serialize)]
15struct WarrantOut {
16    code: String,
17    name: String,
18    owner_code: String,
19    cur_price: f64,
20    strike_price: f64,
21    maturity_time: String,
22}
23
24/// 涡轮查询。`owner_symbol` 可选(不传 = 全市场涡轮),默认按成交量降序返 20 条。
25///
26/// v1.4.106 codex 0635 ζ36 F1+F3: 暴露 begin 参数 (分页), 不再静默 clamp num.
27/// 越界 (begin<0 / num∉[1, 200]) 走 `Err` 让调用方看到清晰错误.
28pub async fn get_warrant(
29    client: &Arc<FutuClient>,
30    owner_symbol: Option<&str>,
31    begin: i32,
32    num: i32,
33) -> Result<String> {
34    let bounds = validate_begin_num(begin, num, 200, "warrant").map_err(|e| anyhow!("{}", e))?;
35    let owner = match owner_symbol {
36        Some(s) => Some(parse_symbol(s)?),
37        None => None,
38    };
39    let req = futu_proto::qot_get_warrant::Request {
40        c2s: futu_proto::qot_get_warrant::C2s {
41            begin: bounds.begin,
42            num: bounds.num,
43            // Qot_Common.SortField: 24 = Volume; 其它常见 4=CurPrice, 5=Amplitude
44            sort_field: 24,
45            ascend: false,
46            owner: owner.map(|s| futu_proto::qot_common::Security {
47                market: s.market as i32,
48                code: s.code,
49            }),
50            type_list: vec![],
51            issuer_list: vec![],
52            maturity_time_min: None,
53            maturity_time_max: None,
54            ipo_period: None,
55            price_type: None,
56            status: None,
57            cur_price_min: None,
58            cur_price_max: None,
59            strike_price_min: None,
60            strike_price_max: None,
61            street_min: None,
62            street_max: None,
63            conversion_min: None,
64            conversion_max: None,
65            vol_min: None,
66            vol_max: None,
67            premium_min: None,
68            premium_max: None,
69            leverage_ratio_min: None,
70            leverage_ratio_max: None,
71            delta_min: None,
72            delta_max: None,
73            implied_min: None,
74            implied_max: None,
75            recovery_price_min: None,
76            recovery_price_max: None,
77            price_recovery_ratio_min: None,
78            price_recovery_ratio_max: None,
79            header: None,
80        },
81    };
82    let body = req.encode_to_vec();
83    let frame = client
84        .request(futu_core::proto_id::QOT_GET_WARRANT, body)
85        .await?;
86    let resp = futu_proto::qot_get_warrant::Response::decode(frame.body.as_ref())
87        .map_err(|e| anyhow!("decode warrant: {e}"))?;
88    if resp.ret_type != 0 {
89        bail!("warrant ret_type={} msg={:?}", resp.ret_type, resp.ret_msg);
90    }
91    let s2c = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
92    let out: Vec<WarrantOut> = s2c
93        .warrant_data_list
94        .iter()
95        .map(|w| WarrantOut {
96            code: w.stock.code.clone(),
97            name: w.name.clone(),
98            owner_code: w.owner.code.clone(),
99            cur_price: w.cur_price,
100            strike_price: w.strike_price,
101            maturity_time: w.maturity_time.clone(),
102        })
103        .collect();
104    Ok(serde_json::to_string_pretty(&serde_json::json!({
105        "last_page": s2c.last_page,
106        "all_count": s2c.all_count,
107        "warrant_list": out,
108    }))?)
109}
110
111// ============================================================
112// get_ipo_list / Qot_GetIpoList (CMD 3217)
113// ============================================================
114
115/// v1.4.98 T1-1 (mobile-source-audit): IpoOut 扩 13 字段, 覆盖 CNIpoExData
116/// (10) + HKIpoExData (5) + USIpoExData (1) 全部 ex_data, 让 LLM agent 做
117/// IPO 申购决策有完整数据 (港股入场费 / A 股申购上限 / 中签结果). proto:
118/// `proto/Qot_GetIpoList.proto` CNIpoExData / HKIpoExData / USIpoExData.
119#[derive(Serialize)]
120struct IpoOut {
121    code: String,
122    name: String,
123    list_time: Option<String>,
124    list_timestamp: Option<f64>,
125
126    // ==== HK IPO ex_data (HKIpoExData proto) ====
127    hk_ipo_price_min: Option<f64>,
128    hk_ipo_price_max: Option<f64>,
129    /// 上市价
130    hk_list_price: Option<f64>,
131    /// 每手股数
132    hk_lot_size: Option<i32>,
133    /// 入场费 (港股 IPO 一手所需金额)
134    hk_entrance_price: Option<f64>,
135    /// 是否为认购中状态 (true=认购中, false=待上市)
136    hk_is_subscribe_status: Option<bool>,
137    /// 截止认购时间字符串 (富途认购截止时间会早于交易所公布日期)
138    hk_apply_end_time: Option<String>,
139
140    // ==== US IPO ex_data (USIpoExData proto) ====
141    us_ipo_price_min: Option<f64>,
142    us_ipo_price_max: Option<f64>,
143    /// 美股 IPO 发行量
144    us_issue_size: Option<i64>,
145
146    // ==== CN IPO ex_data (CNIpoExData proto) ====
147    cn_ipo_price: Option<f64>,
148    /// A 股申购代码
149    cn_apply_code: Option<String>,
150    /// 发行总数
151    cn_issue_size: Option<i64>,
152    /// 申购上限
153    cn_apply_upper_limit: Option<i64>,
154    /// 行业市盈率
155    cn_industry_pe_rate: Option<f64>,
156    /// 中签率 (百分比, 如 20 = 20%)
157    cn_winning_ratio: Option<f64>,
158    /// 发行市盈率
159    cn_issue_pe_rate: Option<f64>,
160    /// 申购日期字符串
161    cn_apply_time: Option<String>,
162    /// 公布中签日期字符串
163    cn_winning_time: Option<String>,
164    /// 是否已经公布中签号
165    cn_is_has_won: Option<bool>,
166}
167
168/// 新股 IPO 列表。`market`:1=HK / 11=US / 21=SH / 22=SZ。
169pub async fn get_ipo_list(client: &Arc<FutuClient>, market: i32) -> Result<String> {
170    let req = futu_proto::qot_get_ipo_list::Request {
171        c2s: futu_proto::qot_get_ipo_list::C2s {
172            market,
173            header: None, // v1.4.110 codex Slice 1 schema 占位
174        },
175    };
176    let body = req.encode_to_vec();
177    let frame = client
178        .request(futu_core::proto_id::QOT_GET_IPO_LIST, body)
179        .await?;
180    let resp = futu_proto::qot_get_ipo_list::Response::decode(frame.body.as_ref())
181        .map_err(|e| anyhow!("decode ipo_list: {e}"))?;
182    if resp.ret_type != 0 {
183        bail!("ipo_list ret_type={} msg={:?}", resp.ret_type, resp.ret_msg);
184    }
185    let s2c = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
186    let basic: Vec<IpoOut> = s2c
187        .ipo_list
188        .iter()
189        .map(|i| IpoOut {
190            code: i.basic.security.code.clone(),
191            name: i.basic.name.clone(),
192            list_time: i.basic.list_time.clone(),
193            list_timestamp: i.basic.list_timestamp,
194
195            // HK IPO 字段 (v1.4.98 T1-1 扩 5 字段)
196            hk_ipo_price_min: i.hk_ex_data.as_ref().map(|h| h.ipo_price_min),
197            hk_ipo_price_max: i.hk_ex_data.as_ref().map(|h| h.ipo_price_max),
198            hk_list_price: i.hk_ex_data.as_ref().map(|h| h.list_price),
199            hk_lot_size: i.hk_ex_data.as_ref().map(|h| h.lot_size),
200            hk_entrance_price: i.hk_ex_data.as_ref().map(|h| h.entrance_price),
201            hk_is_subscribe_status: i.hk_ex_data.as_ref().map(|h| h.is_subscribe_status),
202            hk_apply_end_time: i.hk_ex_data.as_ref().and_then(|h| h.apply_end_time.clone()),
203
204            // US IPO 字段 (v1.4.98 T1-1 扩 1 字段)
205            us_ipo_price_min: i.us_ex_data.as_ref().map(|u| u.ipo_price_min),
206            us_ipo_price_max: i.us_ex_data.as_ref().map(|u| u.ipo_price_max),
207            us_issue_size: i.us_ex_data.as_ref().map(|u| u.issue_size),
208
209            // CN IPO 字段 (v1.4.98 T1-1 扩 9 字段, 含 industry_pe / winning_ratio / 申购日期 / 中签结果)
210            cn_ipo_price: i.cn_ex_data.as_ref().map(|c| c.ipo_price),
211            cn_apply_code: i.cn_ex_data.as_ref().map(|c| c.apply_code.clone()),
212            cn_issue_size: i.cn_ex_data.as_ref().map(|c| c.issue_size),
213            cn_apply_upper_limit: i.cn_ex_data.as_ref().map(|c| c.apply_upper_limit),
214            cn_industry_pe_rate: i.cn_ex_data.as_ref().map(|c| c.industry_pe_rate),
215            cn_winning_ratio: i.cn_ex_data.as_ref().map(|c| c.winning_ratio),
216            cn_issue_pe_rate: i.cn_ex_data.as_ref().map(|c| c.issue_pe_rate),
217            cn_apply_time: i.cn_ex_data.as_ref().and_then(|c| c.apply_time.clone()),
218            cn_winning_time: i.cn_ex_data.as_ref().and_then(|c| c.winning_time.clone()),
219            cn_is_has_won: i.cn_ex_data.as_ref().map(|c| c.is_has_won),
220        })
221        .collect();
222    Ok(serde_json::to_string_pretty(&basic)?)
223}