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