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_core::qot_page_bounds::validate_begin_num;
8use futu_net::client::FutuClient;
9use futu_qot::ipo_calendar::collect_ipo_calendar_events;
10use prost::Message;
11use serde::Serialize;
12
13use crate::state::parse_symbol;
14
15#[derive(Serialize)]
16struct WarrantOut {
17    code: String,
18    name: String,
19    owner_code: String,
20    cur_price: f64,
21    strike_price: f64,
22    maturity_time: String,
23}
24
25/// 涡轮查询。`owner_symbol` 可选(不传 = 全市场涡轮),默认按成交量降序返 20 条。
26///
27/// v1.4.106 codex 0635 ζ36 F1+F3: 暴露 begin 参数 (分页), 不再静默 clamp num.
28/// 越界 (begin<0 / num∉[0, 200]) 走 `Err` 让调用方看到清晰错误.
29pub async fn get_warrant(
30    client: &Arc<FutuClient>,
31    owner_symbol: Option<&str>,
32    begin: i32,
33    num: i32,
34) -> Result<String> {
35    let bounds = validate_begin_num(begin, num, 200, "warrant").map_err(|e| anyhow!("{}", e))?;
36    let owner = match owner_symbol {
37        Some(s) => Some(parse_symbol(s)?),
38        None => None,
39    };
40    let req = build_warrant_request(
41        bounds.begin,
42        bounds.num,
43        owner.map(|s| futu_proto::qot_common::Security {
44            market: s.market as i32,
45            code: s.code,
46        }),
47    );
48    let body = req.encode_to_vec();
49    let frame = client
50        .request(futu_core::proto_id::QOT_GET_WARRANT, body)
51        .await?;
52    let resp = futu_proto::qot_get_warrant::Response::decode(frame.body.as_ref())
53        .map_err(|e| anyhow!("decode warrant: {e}"))?;
54    if resp.ret_type != 0 {
55        bail!("warrant ret_type={} msg={:?}", resp.ret_type, resp.ret_msg);
56    }
57    let s2c = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
58    let out: Vec<WarrantOut> = s2c
59        .warrant_data_list
60        .iter()
61        .map(|w| WarrantOut {
62            code: w.stock.code.clone(),
63            name: w.name.clone(),
64            owner_code: w.owner.code.clone(),
65            cur_price: w.cur_price,
66            strike_price: w.strike_price,
67            maturity_time: w.maturity_time.clone(),
68        })
69        .collect();
70    Ok(serde_json::to_string_pretty(&serde_json::json!({
71        "last_page": s2c.last_page,
72        "all_count": s2c.all_count,
73        "warrant_list": out,
74    }))?)
75}
76
77fn build_warrant_request(
78    begin: i32,
79    num: i32,
80    owner: Option<futu_proto::qot_common::Security>,
81) -> futu_proto::qot_get_warrant::Request {
82    futu_proto::qot_get_warrant::Request {
83        c2s: futu_proto::qot_get_warrant::C2s {
84            begin,
85            num,
86            // Ref: Qot_Common.SortField_Volume = 10. Public sort enum values
87            // are translated to CMD20334 FieldId by the gateway.
88            sort_field: 10,
89            ascend: false,
90            owner,
91            type_list: vec![],
92            issuer_list: vec![],
93            maturity_time_min: None,
94            maturity_time_max: None,
95            ipo_period: None,
96            price_type: None,
97            status: None,
98            cur_price_min: None,
99            cur_price_max: None,
100            strike_price_min: None,
101            strike_price_max: None,
102            street_min: None,
103            street_max: None,
104            conversion_min: None,
105            conversion_max: None,
106            vol_min: None,
107            vol_max: None,
108            premium_min: None,
109            premium_max: None,
110            leverage_ratio_min: None,
111            leverage_ratio_max: None,
112            delta_min: None,
113            delta_max: None,
114            implied_min: None,
115            implied_max: None,
116            recovery_price_min: None,
117            recovery_price_max: None,
118            price_recovery_ratio_min: None,
119            price_recovery_ratio_max: None,
120            header: None,
121        },
122    }
123}
124
125#[cfg(test)]
126mod tests;
127
128// ============================================================
129// get_ipo_list / Qot_GetIpoList (CMD 3217)
130// ============================================================
131
132/// v1.4.98 T1-1 (mobile-source-audit): IpoOut 扩 13 字段, 覆盖 CNIpoExData
133/// (10) + HKIpoExData (5) + USIpoExData (1) 全部 ex_data, 让 LLM agent 做
134/// IPO 申购决策有完整数据 (港股入场费 / A 股申购上限 / 中签结果). proto:
135/// `proto/Qot_GetIpoList.proto` CNIpoExData / HKIpoExData / USIpoExData /
136/// SGIpoExData / MYIpoExData / JPIpoExData.
137#[derive(Serialize)]
138struct IpoOut {
139    code: String,
140    name: String,
141    list_time: Option<String>,
142    list_timestamp: Option<f64>,
143
144    // ==== HK IPO ex_data (HKIpoExData proto) ====
145    hk_ipo_price_min: Option<f64>,
146    hk_ipo_price_max: Option<f64>,
147    /// 上市价
148    hk_list_price: Option<f64>,
149    /// 每手股数
150    hk_lot_size: Option<i32>,
151    /// 入场费 (港股 IPO 一手所需金额)
152    hk_entrance_price: Option<f64>,
153    /// 是否为认购中状态 (true=认购中, false=待上市)
154    hk_is_subscribe_status: Option<bool>,
155    /// 截止认购时间字符串 (富途认购截止时间会早于交易所公布日期)
156    hk_apply_end_time: Option<String>,
157
158    // ==== US IPO ex_data (USIpoExData proto) ====
159    us_ipo_price_min: Option<f64>,
160    us_ipo_price_max: Option<f64>,
161    /// 美股 IPO 发行量
162    us_issue_size: Option<i64>,
163
164    // ==== CN IPO ex_data (CNIpoExData proto) ====
165    cn_ipo_price: Option<f64>,
166    /// A 股申购代码
167    cn_apply_code: Option<String>,
168    /// 发行总数
169    cn_issue_size: Option<i64>,
170    /// 申购上限
171    cn_apply_upper_limit: Option<i64>,
172    /// 行业市盈率
173    cn_industry_pe_rate: Option<f64>,
174    /// 中签率 (百分比, 如 20 = 20%)
175    cn_winning_ratio: Option<f64>,
176    /// 发行市盈率
177    cn_issue_pe_rate: Option<f64>,
178    /// 申购日期字符串
179    cn_apply_time: Option<String>,
180    /// 公布中签日期字符串
181    cn_winning_time: Option<String>,
182    /// 是否已经公布中签号
183    cn_is_has_won: Option<bool>,
184
185    // ==== SG IPO ex_data (SGIpoExData proto) ====
186    sg_ipo_price_min: Option<f64>,
187    sg_ipo_price_max: Option<f64>,
188    sg_issue_size: Option<i64>,
189    sg_apply_start_time: Option<String>,
190    sg_apply_end_time: Option<String>,
191    sg_winning_time: Option<String>,
192
193    // ==== MY IPO ex_data (MYIpoExData proto) ====
194    my_offer_price: Option<f64>,
195    my_issue_size: Option<i64>,
196    my_apply_start_time: Option<String>,
197    my_apply_end_time: Option<String>,
198    my_winning_time: Option<String>,
199
200    // ==== JP IPO ex_data (JPIpoExData proto) ====
201    jp_ipo_price_min: Option<f64>,
202    jp_ipo_price_max: Option<f64>,
203    jp_issue_size: Option<i64>,
204    jp_lot_size: Option<i32>,
205    jp_eqty_issued_shares: Option<i64>,
206    jp_isin: Option<String>,
207    jp_issued_shares: Option<i64>,
208    jp_industry: Option<String>,
209    jp_market_segment: Option<String>,
210    jp_approval_time: Option<String>,
211    jp_approval_timestamp: Option<f64>,
212    jp_issue_confirm_time: Option<String>,
213    jp_issue_confirm_timestamp: Option<f64>,
214    jp_price_confirm_start_time: Option<String>,
215    jp_price_confirm_start_timestamp: Option<f64>,
216    jp_price_confirm_end_time: Option<String>,
217    jp_price_confirm_end_timestamp: Option<f64>,
218    jp_inquiry_start_time: Option<String>,
219    jp_inquiry_start_timestamp: Option<f64>,
220    jp_inquiry_end_time: Option<String>,
221    jp_inquiry_end_timestamp: Option<f64>,
222    jp_apply_start_time: Option<String>,
223    jp_apply_start_timestamp: Option<f64>,
224    jp_apply_end_time: Option<String>,
225    jp_apply_end_timestamp: Option<f64>,
226    jp_draw_time: Option<String>,
227    jp_draw_timestamp: Option<f64>,
228    jp_winning_time: Option<String>,
229    jp_winning_timestamp: Option<f64>,
230    jp_etf_management_fee_rates: Option<i64>,
231    jp_etf_dividend_times: Option<i64>,
232    jp_etf_dividend_frequency_type: Option<i32>,
233    jp_etf_investing_risk_type: Option<i32>,
234    jp_etf_index_name: Option<String>,
235    jp_etf_company_name: Option<String>,
236    jp_etf_company_introduction_link: Option<String>,
237    jp_etf_company_interview_link: Option<String>,
238    jp_etf_pamphlet_link: Option<String>,
239    jp_etf_introduction_link: Option<String>,
240}
241
242/// 新股 IPO 列表。`market`:1=HK / 11=US / 21=SH / 22=SZ / 31=SG / 41=JP / 61=MY。
243pub async fn get_ipo_list(client: &Arc<FutuClient>, market: i32) -> Result<String> {
244    let req = futu_proto::qot_get_ipo_list::Request {
245        c2s: futu_proto::qot_get_ipo_list::C2s {
246            market,
247            header: None, // v1.4.110 codex Slice 1 schema 占位
248        },
249    };
250    let body = req.encode_to_vec();
251    let frame = client
252        .request(futu_core::proto_id::QOT_GET_IPO_LIST, body)
253        .await?;
254    let resp = futu_proto::qot_get_ipo_list::Response::decode(frame.body.as_ref())
255        .map_err(|e| anyhow!("decode ipo_list: {e}"))?;
256    if resp.ret_type != 0 {
257        bail!("ipo_list ret_type={} msg={:?}", resp.ret_type, resp.ret_msg);
258    }
259    let s2c = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
260    let basic: Vec<IpoOut> = s2c
261        .ipo_list
262        .iter()
263        .map(|i| IpoOut {
264            code: i.basic.security.code.clone(),
265            name: i.basic.name.clone(),
266            list_time: i.basic.list_time.clone(),
267            list_timestamp: i.basic.list_timestamp,
268
269            // HK IPO 字段 (v1.4.98 T1-1 扩 5 字段)
270            hk_ipo_price_min: i.hk_ex_data.as_ref().map(|h| h.ipo_price_min),
271            hk_ipo_price_max: i.hk_ex_data.as_ref().map(|h| h.ipo_price_max),
272            hk_list_price: i.hk_ex_data.as_ref().map(|h| h.list_price),
273            hk_lot_size: i.hk_ex_data.as_ref().map(|h| h.lot_size),
274            hk_entrance_price: i.hk_ex_data.as_ref().map(|h| h.entrance_price),
275            hk_is_subscribe_status: i.hk_ex_data.as_ref().map(|h| h.is_subscribe_status),
276            hk_apply_end_time: i.hk_ex_data.as_ref().and_then(|h| h.apply_end_time.clone()),
277
278            // US IPO 字段 (v1.4.98 T1-1 扩 1 字段)
279            us_ipo_price_min: i.us_ex_data.as_ref().map(|u| u.ipo_price_min),
280            us_ipo_price_max: i.us_ex_data.as_ref().map(|u| u.ipo_price_max),
281            us_issue_size: i.us_ex_data.as_ref().map(|u| u.issue_size),
282
283            // CN IPO 字段 (v1.4.98 T1-1 扩 9 字段, 含 industry_pe / winning_ratio / 申购日期 / 中签结果)
284            cn_ipo_price: i.cn_ex_data.as_ref().map(|c| c.ipo_price),
285            cn_apply_code: i.cn_ex_data.as_ref().map(|c| c.apply_code.clone()),
286            cn_issue_size: i.cn_ex_data.as_ref().map(|c| c.issue_size),
287            cn_apply_upper_limit: i.cn_ex_data.as_ref().map(|c| c.apply_upper_limit),
288            cn_industry_pe_rate: i.cn_ex_data.as_ref().map(|c| c.industry_pe_rate),
289            cn_winning_ratio: i.cn_ex_data.as_ref().map(|c| c.winning_ratio),
290            cn_issue_pe_rate: i.cn_ex_data.as_ref().map(|c| c.issue_pe_rate),
291            cn_apply_time: i.cn_ex_data.as_ref().and_then(|c| c.apply_time.clone()),
292            cn_winning_time: i.cn_ex_data.as_ref().and_then(|c| c.winning_time.clone()),
293            cn_is_has_won: i.cn_ex_data.as_ref().map(|c| c.is_has_won),
294
295            // SG IPO 字段 (v1.4.111 10.7 SG IPO list)
296            sg_ipo_price_min: i.sg_ex_data.as_ref().map(|s| s.ipo_price_min),
297            sg_ipo_price_max: i.sg_ex_data.as_ref().map(|s| s.ipo_price_max),
298            sg_issue_size: i.sg_ex_data.as_ref().map(|s| s.issue_size),
299            sg_apply_start_time: i
300                .sg_ex_data
301                .as_ref()
302                .and_then(|s| s.apply_start_time.clone()),
303            sg_apply_end_time: i.sg_ex_data.as_ref().and_then(|s| s.apply_end_time.clone()),
304            sg_winning_time: i.sg_ex_data.as_ref().and_then(|s| s.winning_time.clone()),
305
306            // MY IPO 字段 (v1.4.111 10.7 MY IPO list)
307            my_offer_price: i.my_ex_data.as_ref().map(|m| m.offer_price),
308            my_issue_size: i.my_ex_data.as_ref().map(|m| m.issue_size),
309            my_apply_start_time: i
310                .my_ex_data
311                .as_ref()
312                .and_then(|m| m.apply_start_time.clone()),
313            my_apply_end_time: i.my_ex_data.as_ref().and_then(|m| m.apply_end_time.clone()),
314            my_winning_time: i.my_ex_data.as_ref().and_then(|m| m.winning_time.clone()),
315
316            // JP IPO 字段 (v1.4.111 10.7 JP CMD20751)
317            jp_ipo_price_min: i.jp_ex_data.as_ref().map(|j| j.ipo_price_min),
318            jp_ipo_price_max: i.jp_ex_data.as_ref().map(|j| j.ipo_price_max),
319            jp_issue_size: i.jp_ex_data.as_ref().map(|j| j.issue_size),
320            jp_lot_size: i.jp_ex_data.as_ref().and_then(|j| j.lot_size),
321            jp_eqty_issued_shares: i.jp_ex_data.as_ref().and_then(|j| j.eqty_issued_shares),
322            jp_isin: i.jp_ex_data.as_ref().and_then(|j| j.isin.clone()),
323            jp_issued_shares: i.jp_ex_data.as_ref().and_then(|j| j.issued_shares),
324            jp_industry: i.jp_ex_data.as_ref().and_then(|j| j.industry.clone()),
325            jp_market_segment: i.jp_ex_data.as_ref().and_then(|j| j.market_segment.clone()),
326            jp_approval_time: i.jp_ex_data.as_ref().and_then(|j| j.approval_time.clone()),
327            jp_approval_timestamp: i.jp_ex_data.as_ref().and_then(|j| j.approval_timestamp),
328            jp_issue_confirm_time: i
329                .jp_ex_data
330                .as_ref()
331                .and_then(|j| j.issue_confirm_time.clone()),
332            jp_issue_confirm_timestamp: i
333                .jp_ex_data
334                .as_ref()
335                .and_then(|j| j.issue_confirm_timestamp),
336            jp_price_confirm_start_time: i
337                .jp_ex_data
338                .as_ref()
339                .and_then(|j| j.price_confirm_start_time.clone()),
340            jp_price_confirm_start_timestamp: i
341                .jp_ex_data
342                .as_ref()
343                .and_then(|j| j.price_confirm_start_timestamp),
344            jp_price_confirm_end_time: i
345                .jp_ex_data
346                .as_ref()
347                .and_then(|j| j.price_confirm_end_time.clone()),
348            jp_price_confirm_end_timestamp: i
349                .jp_ex_data
350                .as_ref()
351                .and_then(|j| j.price_confirm_end_timestamp),
352            jp_inquiry_start_time: i
353                .jp_ex_data
354                .as_ref()
355                .and_then(|j| j.inquiry_start_time.clone()),
356            jp_inquiry_start_timestamp: i
357                .jp_ex_data
358                .as_ref()
359                .and_then(|j| j.inquiry_start_timestamp),
360            jp_inquiry_end_time: i
361                .jp_ex_data
362                .as_ref()
363                .and_then(|j| j.inquiry_end_time.clone()),
364            jp_inquiry_end_timestamp: i.jp_ex_data.as_ref().and_then(|j| j.inquiry_end_timestamp),
365            jp_apply_start_time: i
366                .jp_ex_data
367                .as_ref()
368                .and_then(|j| j.apply_start_time.clone()),
369            jp_apply_start_timestamp: i.jp_ex_data.as_ref().and_then(|j| j.apply_start_timestamp),
370            jp_apply_end_time: i.jp_ex_data.as_ref().and_then(|j| j.apply_end_time.clone()),
371            jp_apply_end_timestamp: i.jp_ex_data.as_ref().and_then(|j| j.apply_end_timestamp),
372            jp_draw_time: i.jp_ex_data.as_ref().and_then(|j| j.draw_time.clone()),
373            jp_draw_timestamp: i.jp_ex_data.as_ref().and_then(|j| j.draw_timestamp),
374            jp_winning_time: i.jp_ex_data.as_ref().and_then(|j| j.winning_time.clone()),
375            jp_winning_timestamp: i.jp_ex_data.as_ref().and_then(|j| j.winning_timestamp),
376            jp_etf_management_fee_rates: i
377                .jp_ex_data
378                .as_ref()
379                .and_then(|j| j.etf_info.as_ref())
380                .and_then(|e| e.management_fee_rates),
381            jp_etf_dividend_times: i
382                .jp_ex_data
383                .as_ref()
384                .and_then(|j| j.etf_info.as_ref())
385                .and_then(|e| e.dividend_times),
386            jp_etf_dividend_frequency_type: i
387                .jp_ex_data
388                .as_ref()
389                .and_then(|j| j.etf_info.as_ref())
390                .and_then(|e| e.dividend_frequency_type),
391            jp_etf_investing_risk_type: i
392                .jp_ex_data
393                .as_ref()
394                .and_then(|j| j.etf_info.as_ref())
395                .and_then(|e| e.investing_risk_type),
396            jp_etf_index_name: i
397                .jp_ex_data
398                .as_ref()
399                .and_then(|j| j.etf_info.as_ref())
400                .and_then(|e| e.index_name.clone()),
401            jp_etf_company_name: i
402                .jp_ex_data
403                .as_ref()
404                .and_then(|j| j.etf_info.as_ref())
405                .and_then(|e| e.company_name.clone()),
406            jp_etf_company_introduction_link: i
407                .jp_ex_data
408                .as_ref()
409                .and_then(|j| j.etf_info.as_ref())
410                .and_then(|e| e.company_introduction_link.clone()),
411            jp_etf_company_interview_link: i
412                .jp_ex_data
413                .as_ref()
414                .and_then(|j| j.etf_info.as_ref())
415                .and_then(|e| e.company_interview_link.clone()),
416            jp_etf_pamphlet_link: i
417                .jp_ex_data
418                .as_ref()
419                .and_then(|j| j.etf_info.as_ref())
420                .and_then(|e| e.etf_pamphlet_link.clone()),
421            jp_etf_introduction_link: i
422                .jp_ex_data
423                .as_ref()
424                .and_then(|j| j.etf_info.as_ref())
425                .and_then(|e| e.etf_introduction_link.clone()),
426        })
427        .collect();
428    Ok(serde_json::to_string_pretty(&basic)?)
429}
430
431pub async fn get_ipo_calendar(
432    client: &Arc<FutuClient>,
433    market: i32,
434    events: &[String],
435    begin_date: Option<&str>,
436    end_date: Option<&str>,
437) -> Result<String> {
438    let req = futu_proto::qot_get_ipo_list::Request {
439        c2s: futu_proto::qot_get_ipo_list::C2s {
440            market,
441            header: None,
442        },
443    };
444    let body = req.encode_to_vec();
445    let frame = client
446        .request(futu_core::proto_id::QOT_GET_IPO_LIST, body)
447        .await?;
448    let resp = futu_proto::qot_get_ipo_list::Response::decode(frame.body.as_ref())
449        .map_err(|e| anyhow!("decode ipo_calendar: {e}"))?;
450    if resp.ret_type != 0 {
451        bail!(
452            "ipo_calendar ret_type={} msg={:?}",
453            resp.ret_type,
454            resp.ret_msg
455        );
456    }
457    let s2c = resp.s2c.ok_or_else(|| anyhow!("missing s2c"))?;
458    let events = collect_ipo_calendar_events(&s2c, events, begin_date, end_date);
459    Ok(serde_json::to_string_pretty(&serde_json::json!({
460        "market": market,
461        "events": events,
462    }))?)
463}