1use 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
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 = futu_proto::qot_get_warrant::Request {
41 c2s: futu_proto::qot_get_warrant::C2s {
42 begin: bounds.begin,
43 num: bounds.num,
44 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#[derive(Serialize)]
122struct IpoOut {
123 code: String,
124 name: String,
125 list_time: Option<String>,
126 list_timestamp: Option<f64>,
127
128 hk_ipo_price_min: Option<f64>,
130 hk_ipo_price_max: Option<f64>,
131 hk_list_price: Option<f64>,
133 hk_lot_size: Option<i32>,
135 hk_entrance_price: Option<f64>,
137 hk_is_subscribe_status: Option<bool>,
139 hk_apply_end_time: Option<String>,
141
142 us_ipo_price_min: Option<f64>,
144 us_ipo_price_max: Option<f64>,
145 us_issue_size: Option<i64>,
147
148 cn_ipo_price: Option<f64>,
150 cn_apply_code: Option<String>,
152 cn_issue_size: Option<i64>,
154 cn_apply_upper_limit: Option<i64>,
156 cn_industry_pe_rate: Option<f64>,
158 cn_winning_ratio: Option<f64>,
160 cn_issue_pe_rate: Option<f64>,
162 cn_apply_time: Option<String>,
164 cn_winning_time: Option<String>,
166 cn_is_has_won: Option<bool>,
168
169 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_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_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
226pub 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, },
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_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_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_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_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_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_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}