1use 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
25pub 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 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#[derive(Serialize)]
138struct IpoOut {
139 code: String,
140 name: String,
141 list_time: Option<String>,
142 list_timestamp: Option<f64>,
143
144 hk_ipo_price_min: Option<f64>,
146 hk_ipo_price_max: Option<f64>,
147 hk_list_price: Option<f64>,
149 hk_lot_size: Option<i32>,
151 hk_entrance_price: Option<f64>,
153 hk_is_subscribe_status: Option<bool>,
155 hk_apply_end_time: Option<String>,
157
158 us_ipo_price_min: Option<f64>,
160 us_ipo_price_max: Option<f64>,
161 us_issue_size: Option<i64>,
163
164 cn_ipo_price: Option<f64>,
166 cn_apply_code: Option<String>,
168 cn_issue_size: Option<i64>,
170 cn_apply_upper_limit: Option<i64>,
172 cn_industry_pe_rate: Option<f64>,
174 cn_winning_ratio: Option<f64>,
176 cn_issue_pe_rate: Option<f64>,
178 cn_apply_time: Option<String>,
180 cn_winning_time: Option<String>,
182 cn_is_has_won: Option<bool>,
184
185 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_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_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
242pub 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, },
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_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_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_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_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_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_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}