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∉[0, 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/// SGIpoExData / MYIpoExData / JPIpoExData.
120#[derive(Serialize)]
121struct IpoOut {
122    code: String,
123    name: String,
124    list_time: Option<String>,
125    list_timestamp: Option<f64>,
126
127    // ==== HK IPO ex_data (HKIpoExData proto) ====
128    hk_ipo_price_min: Option<f64>,
129    hk_ipo_price_max: Option<f64>,
130    /// 上市价
131    hk_list_price: Option<f64>,
132    /// 每手股数
133    hk_lot_size: Option<i32>,
134    /// 入场费 (港股 IPO 一手所需金额)
135    hk_entrance_price: Option<f64>,
136    /// 是否为认购中状态 (true=认购中, false=待上市)
137    hk_is_subscribe_status: Option<bool>,
138    /// 截止认购时间字符串 (富途认购截止时间会早于交易所公布日期)
139    hk_apply_end_time: Option<String>,
140
141    // ==== US IPO ex_data (USIpoExData proto) ====
142    us_ipo_price_min: Option<f64>,
143    us_ipo_price_max: Option<f64>,
144    /// 美股 IPO 发行量
145    us_issue_size: Option<i64>,
146
147    // ==== CN IPO ex_data (CNIpoExData proto) ====
148    cn_ipo_price: Option<f64>,
149    /// A 股申购代码
150    cn_apply_code: Option<String>,
151    /// 发行总数
152    cn_issue_size: Option<i64>,
153    /// 申购上限
154    cn_apply_upper_limit: Option<i64>,
155    /// 行业市盈率
156    cn_industry_pe_rate: Option<f64>,
157    /// 中签率 (百分比, 如 20 = 20%)
158    cn_winning_ratio: Option<f64>,
159    /// 发行市盈率
160    cn_issue_pe_rate: Option<f64>,
161    /// 申购日期字符串
162    cn_apply_time: Option<String>,
163    /// 公布中签日期字符串
164    cn_winning_time: Option<String>,
165    /// 是否已经公布中签号
166    cn_is_has_won: Option<bool>,
167
168    // ==== SG IPO ex_data (SGIpoExData proto) ====
169    sg_ipo_price_min: Option<f64>,
170    sg_ipo_price_max: Option<f64>,
171    sg_issue_size: Option<i64>,
172    sg_apply_start_time: Option<String>,
173    sg_apply_end_time: Option<String>,
174    sg_winning_time: Option<String>,
175
176    // ==== MY IPO ex_data (MYIpoExData proto) ====
177    my_offer_price: Option<f64>,
178    my_issue_size: Option<i64>,
179    my_apply_start_time: Option<String>,
180    my_apply_end_time: Option<String>,
181    my_winning_time: Option<String>,
182
183    // ==== JP IPO ex_data (JPIpoExData proto) ====
184    jp_ipo_price_min: Option<f64>,
185    jp_ipo_price_max: Option<f64>,
186    jp_issue_size: Option<i64>,
187    jp_lot_size: Option<i32>,
188    jp_eqty_issued_shares: Option<i64>,
189    jp_isin: Option<String>,
190    jp_issued_shares: Option<i64>,
191    jp_industry: Option<String>,
192    jp_market_segment: Option<String>,
193    jp_approval_time: Option<String>,
194    jp_approval_timestamp: Option<f64>,
195    jp_issue_confirm_time: Option<String>,
196    jp_issue_confirm_timestamp: Option<f64>,
197    jp_price_confirm_start_time: Option<String>,
198    jp_price_confirm_start_timestamp: Option<f64>,
199    jp_price_confirm_end_time: Option<String>,
200    jp_price_confirm_end_timestamp: Option<f64>,
201    jp_inquiry_start_time: Option<String>,
202    jp_inquiry_start_timestamp: Option<f64>,
203    jp_inquiry_end_time: Option<String>,
204    jp_inquiry_end_timestamp: Option<f64>,
205    jp_apply_start_time: Option<String>,
206    jp_apply_start_timestamp: Option<f64>,
207    jp_apply_end_time: Option<String>,
208    jp_apply_end_timestamp: Option<f64>,
209    jp_draw_time: Option<String>,
210    jp_draw_timestamp: Option<f64>,
211    jp_winning_time: Option<String>,
212    jp_winning_timestamp: Option<f64>,
213    jp_etf_management_fee_rates: Option<i64>,
214    jp_etf_dividend_times: Option<i64>,
215    jp_etf_dividend_frequency_type: Option<i32>,
216    jp_etf_investing_risk_type: Option<i32>,
217    jp_etf_index_name: Option<String>,
218    jp_etf_company_name: Option<String>,
219    jp_etf_company_introduction_link: Option<String>,
220    jp_etf_company_interview_link: Option<String>,
221    jp_etf_pamphlet_link: Option<String>,
222    jp_etf_introduction_link: Option<String>,
223}
224
225/// 新股 IPO 列表。`market`:1=HK / 11=US / 21=SH / 22=SZ / 31=SG / 41=JP / 61=MY。
226pub async fn get_ipo_list(client: &Arc<FutuClient>, market: i32) -> Result<String> {
227    let req = futu_proto::qot_get_ipo_list::Request {
228        c2s: futu_proto::qot_get_ipo_list::C2s {
229            market,
230            header: None, // v1.4.110 codex Slice 1 schema 占位
231        },
232    };
233    let body = req.encode_to_vec();
234    let frame = client
235        .request(futu_core::proto_id::QOT_GET_IPO_LIST, body)
236        .await?;
237    let resp = futu_proto::qot_get_ipo_list::Response::decode(frame.body.as_ref())
238        .map_err(|e| anyhow!("decode ipo_list: {e}"))?;
239    if resp.ret_type != 0 {
240        bail!("ipo_list ret_type={} msg={:?}", resp.ret_type, resp.ret_msg);
241    }
242    let s2c = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
243    let basic: Vec<IpoOut> = s2c
244        .ipo_list
245        .iter()
246        .map(|i| IpoOut {
247            code: i.basic.security.code.clone(),
248            name: i.basic.name.clone(),
249            list_time: i.basic.list_time.clone(),
250            list_timestamp: i.basic.list_timestamp,
251
252            // HK IPO 字段 (v1.4.98 T1-1 扩 5 字段)
253            hk_ipo_price_min: i.hk_ex_data.as_ref().map(|h| h.ipo_price_min),
254            hk_ipo_price_max: i.hk_ex_data.as_ref().map(|h| h.ipo_price_max),
255            hk_list_price: i.hk_ex_data.as_ref().map(|h| h.list_price),
256            hk_lot_size: i.hk_ex_data.as_ref().map(|h| h.lot_size),
257            hk_entrance_price: i.hk_ex_data.as_ref().map(|h| h.entrance_price),
258            hk_is_subscribe_status: i.hk_ex_data.as_ref().map(|h| h.is_subscribe_status),
259            hk_apply_end_time: i.hk_ex_data.as_ref().and_then(|h| h.apply_end_time.clone()),
260
261            // US IPO 字段 (v1.4.98 T1-1 扩 1 字段)
262            us_ipo_price_min: i.us_ex_data.as_ref().map(|u| u.ipo_price_min),
263            us_ipo_price_max: i.us_ex_data.as_ref().map(|u| u.ipo_price_max),
264            us_issue_size: i.us_ex_data.as_ref().map(|u| u.issue_size),
265
266            // CN IPO 字段 (v1.4.98 T1-1 扩 9 字段, 含 industry_pe / winning_ratio / 申购日期 / 中签结果)
267            cn_ipo_price: i.cn_ex_data.as_ref().map(|c| c.ipo_price),
268            cn_apply_code: i.cn_ex_data.as_ref().map(|c| c.apply_code.clone()),
269            cn_issue_size: i.cn_ex_data.as_ref().map(|c| c.issue_size),
270            cn_apply_upper_limit: i.cn_ex_data.as_ref().map(|c| c.apply_upper_limit),
271            cn_industry_pe_rate: i.cn_ex_data.as_ref().map(|c| c.industry_pe_rate),
272            cn_winning_ratio: i.cn_ex_data.as_ref().map(|c| c.winning_ratio),
273            cn_issue_pe_rate: i.cn_ex_data.as_ref().map(|c| c.issue_pe_rate),
274            cn_apply_time: i.cn_ex_data.as_ref().and_then(|c| c.apply_time.clone()),
275            cn_winning_time: i.cn_ex_data.as_ref().and_then(|c| c.winning_time.clone()),
276            cn_is_has_won: i.cn_ex_data.as_ref().map(|c| c.is_has_won),
277
278            // SG IPO 字段 (v1.4.111 10.7 SG IPO list)
279            sg_ipo_price_min: i.sg_ex_data.as_ref().map(|s| s.ipo_price_min),
280            sg_ipo_price_max: i.sg_ex_data.as_ref().map(|s| s.ipo_price_max),
281            sg_issue_size: i.sg_ex_data.as_ref().map(|s| s.issue_size),
282            sg_apply_start_time: i
283                .sg_ex_data
284                .as_ref()
285                .and_then(|s| s.apply_start_time.clone()),
286            sg_apply_end_time: i.sg_ex_data.as_ref().and_then(|s| s.apply_end_time.clone()),
287            sg_winning_time: i.sg_ex_data.as_ref().and_then(|s| s.winning_time.clone()),
288
289            // MY IPO 字段 (v1.4.111 10.7 MY IPO list)
290            my_offer_price: i.my_ex_data.as_ref().map(|m| m.offer_price),
291            my_issue_size: i.my_ex_data.as_ref().map(|m| m.issue_size),
292            my_apply_start_time: i
293                .my_ex_data
294                .as_ref()
295                .and_then(|m| m.apply_start_time.clone()),
296            my_apply_end_time: i.my_ex_data.as_ref().and_then(|m| m.apply_end_time.clone()),
297            my_winning_time: i.my_ex_data.as_ref().and_then(|m| m.winning_time.clone()),
298
299            // JP IPO 字段 (v1.4.111 10.7 JP CMD20751)
300            jp_ipo_price_min: i.jp_ex_data.as_ref().map(|j| j.ipo_price_min),
301            jp_ipo_price_max: i.jp_ex_data.as_ref().map(|j| j.ipo_price_max),
302            jp_issue_size: i.jp_ex_data.as_ref().map(|j| j.issue_size),
303            jp_lot_size: i.jp_ex_data.as_ref().and_then(|j| j.lot_size),
304            jp_eqty_issued_shares: i.jp_ex_data.as_ref().and_then(|j| j.eqty_issued_shares),
305            jp_isin: i.jp_ex_data.as_ref().and_then(|j| j.isin.clone()),
306            jp_issued_shares: i.jp_ex_data.as_ref().and_then(|j| j.issued_shares),
307            jp_industry: i.jp_ex_data.as_ref().and_then(|j| j.industry.clone()),
308            jp_market_segment: i.jp_ex_data.as_ref().and_then(|j| j.market_segment.clone()),
309            jp_approval_time: i.jp_ex_data.as_ref().and_then(|j| j.approval_time.clone()),
310            jp_approval_timestamp: i.jp_ex_data.as_ref().and_then(|j| j.approval_timestamp),
311            jp_issue_confirm_time: i
312                .jp_ex_data
313                .as_ref()
314                .and_then(|j| j.issue_confirm_time.clone()),
315            jp_issue_confirm_timestamp: i
316                .jp_ex_data
317                .as_ref()
318                .and_then(|j| j.issue_confirm_timestamp),
319            jp_price_confirm_start_time: i
320                .jp_ex_data
321                .as_ref()
322                .and_then(|j| j.price_confirm_start_time.clone()),
323            jp_price_confirm_start_timestamp: i
324                .jp_ex_data
325                .as_ref()
326                .and_then(|j| j.price_confirm_start_timestamp),
327            jp_price_confirm_end_time: i
328                .jp_ex_data
329                .as_ref()
330                .and_then(|j| j.price_confirm_end_time.clone()),
331            jp_price_confirm_end_timestamp: i
332                .jp_ex_data
333                .as_ref()
334                .and_then(|j| j.price_confirm_end_timestamp),
335            jp_inquiry_start_time: i
336                .jp_ex_data
337                .as_ref()
338                .and_then(|j| j.inquiry_start_time.clone()),
339            jp_inquiry_start_timestamp: i
340                .jp_ex_data
341                .as_ref()
342                .and_then(|j| j.inquiry_start_timestamp),
343            jp_inquiry_end_time: i
344                .jp_ex_data
345                .as_ref()
346                .and_then(|j| j.inquiry_end_time.clone()),
347            jp_inquiry_end_timestamp: i.jp_ex_data.as_ref().and_then(|j| j.inquiry_end_timestamp),
348            jp_apply_start_time: i
349                .jp_ex_data
350                .as_ref()
351                .and_then(|j| j.apply_start_time.clone()),
352            jp_apply_start_timestamp: i.jp_ex_data.as_ref().and_then(|j| j.apply_start_timestamp),
353            jp_apply_end_time: i.jp_ex_data.as_ref().and_then(|j| j.apply_end_time.clone()),
354            jp_apply_end_timestamp: i.jp_ex_data.as_ref().and_then(|j| j.apply_end_timestamp),
355            jp_draw_time: i.jp_ex_data.as_ref().and_then(|j| j.draw_time.clone()),
356            jp_draw_timestamp: i.jp_ex_data.as_ref().and_then(|j| j.draw_timestamp),
357            jp_winning_time: i.jp_ex_data.as_ref().and_then(|j| j.winning_time.clone()),
358            jp_winning_timestamp: i.jp_ex_data.as_ref().and_then(|j| j.winning_timestamp),
359            jp_etf_management_fee_rates: i
360                .jp_ex_data
361                .as_ref()
362                .and_then(|j| j.etf_info.as_ref())
363                .and_then(|e| e.management_fee_rates),
364            jp_etf_dividend_times: i
365                .jp_ex_data
366                .as_ref()
367                .and_then(|j| j.etf_info.as_ref())
368                .and_then(|e| e.dividend_times),
369            jp_etf_dividend_frequency_type: i
370                .jp_ex_data
371                .as_ref()
372                .and_then(|j| j.etf_info.as_ref())
373                .and_then(|e| e.dividend_frequency_type),
374            jp_etf_investing_risk_type: i
375                .jp_ex_data
376                .as_ref()
377                .and_then(|j| j.etf_info.as_ref())
378                .and_then(|e| e.investing_risk_type),
379            jp_etf_index_name: i
380                .jp_ex_data
381                .as_ref()
382                .and_then(|j| j.etf_info.as_ref())
383                .and_then(|e| e.index_name.clone()),
384            jp_etf_company_name: i
385                .jp_ex_data
386                .as_ref()
387                .and_then(|j| j.etf_info.as_ref())
388                .and_then(|e| e.company_name.clone()),
389            jp_etf_company_introduction_link: i
390                .jp_ex_data
391                .as_ref()
392                .and_then(|j| j.etf_info.as_ref())
393                .and_then(|e| e.company_introduction_link.clone()),
394            jp_etf_company_interview_link: i
395                .jp_ex_data
396                .as_ref()
397                .and_then(|j| j.etf_info.as_ref())
398                .and_then(|e| e.company_interview_link.clone()),
399            jp_etf_pamphlet_link: i
400                .jp_ex_data
401                .as_ref()
402                .and_then(|j| j.etf_info.as_ref())
403                .and_then(|e| e.etf_pamphlet_link.clone()),
404            jp_etf_introduction_link: i
405                .jp_ex_data
406                .as_ref()
407                .and_then(|j| j.etf_info.as_ref())
408                .and_then(|e| e.etf_introduction_link.clone()),
409        })
410        .collect();
411    Ok(serde_json::to_string_pretty(&basic)?)
412}