Skip to main content

futu_qot/
ipo_calendar.rs

1use serde::Serialize;
2
3#[derive(Clone, Debug, PartialEq, Serialize)]
4pub struct IpoCalendarEvent {
5    pub event_type: &'static str,
6    pub market: i32,
7    pub code: String,
8    pub name: String,
9    pub date: Option<String>,
10    pub time: Option<String>,
11    pub timestamp: Option<f64>,
12}
13
14pub fn normalize_ipo_event_type(value: &str) -> String {
15    value.trim().replace('-', "_").to_ascii_lowercase()
16}
17
18pub fn is_supported_ipo_event_type(value: &str) -> bool {
19    matches!(
20        normalize_ipo_event_type(value).as_str(),
21        "list"
22            | "apply"
23            | "apply_start"
24            | "apply_end"
25            | "winning"
26            | "approval"
27            | "issue_confirm"
28            | "price_confirm_start"
29            | "price_confirm_end"
30            | "inquiry_start"
31            | "inquiry_end"
32            | "draw"
33    )
34}
35
36pub fn is_yyyymmdd(value: &str) -> bool {
37    value.len() == 8 && value.bytes().all(|b| b.is_ascii_digit())
38}
39
40pub fn collect_ipo_calendar_events(
41    s2c: &futu_proto::qot_get_ipo_list::S2c,
42    event_types: &[String],
43    begin_date: Option<&str>,
44    end_date: Option<&str>,
45) -> Vec<IpoCalendarEvent> {
46    let mut out = Vec::new();
47    for item in &s2c.ipo_list {
48        push_ipo_calendar_event(
49            &mut out,
50            event_types,
51            begin_date,
52            end_date,
53            item,
54            IpoEventSource {
55                event_type: "list",
56                time: item.basic.list_time.as_deref(),
57                timestamp: item.basic.list_timestamp,
58            },
59        );
60        if let Some(cn) = &item.cn_ex_data {
61            for source in [
62                IpoEventSource {
63                    event_type: "apply",
64                    time: cn.apply_time.as_deref(),
65                    timestamp: cn.apply_timestamp,
66                },
67                IpoEventSource {
68                    event_type: "winning",
69                    time: cn.winning_time.as_deref(),
70                    timestamp: cn.winning_timestamp,
71                },
72            ] {
73                push_ipo_calendar_event(&mut out, event_types, begin_date, end_date, item, source);
74            }
75        }
76        if let Some(hk) = &item.hk_ex_data {
77            push_ipo_calendar_event(
78                &mut out,
79                event_types,
80                begin_date,
81                end_date,
82                item,
83                IpoEventSource {
84                    event_type: "apply_end",
85                    time: hk.apply_end_time.as_deref(),
86                    timestamp: hk.apply_end_timestamp,
87                },
88            );
89        }
90        if let Some(sg) = &item.sg_ex_data {
91            for source in [
92                IpoEventSource {
93                    event_type: "apply_start",
94                    time: sg.apply_start_time.as_deref(),
95                    timestamp: sg.apply_start_timestamp,
96                },
97                IpoEventSource {
98                    event_type: "apply_end",
99                    time: sg.apply_end_time.as_deref(),
100                    timestamp: sg.apply_end_timestamp,
101                },
102                IpoEventSource {
103                    event_type: "winning",
104                    time: sg.winning_time.as_deref(),
105                    timestamp: sg.winning_timestamp,
106                },
107            ] {
108                push_ipo_calendar_event(&mut out, event_types, begin_date, end_date, item, source);
109            }
110        }
111        if let Some(my) = &item.my_ex_data {
112            for source in [
113                IpoEventSource {
114                    event_type: "apply_start",
115                    time: my.apply_start_time.as_deref(),
116                    timestamp: my.apply_start_timestamp,
117                },
118                IpoEventSource {
119                    event_type: "apply_end",
120                    time: my.apply_end_time.as_deref(),
121                    timestamp: my.apply_end_timestamp,
122                },
123                IpoEventSource {
124                    event_type: "winning",
125                    time: my.winning_time.as_deref(),
126                    timestamp: my.winning_timestamp,
127                },
128            ] {
129                push_ipo_calendar_event(&mut out, event_types, begin_date, end_date, item, source);
130            }
131        }
132        if let Some(jp) = &item.jp_ex_data {
133            for source in [
134                IpoEventSource {
135                    event_type: "approval",
136                    time: jp.approval_time.as_deref(),
137                    timestamp: jp.approval_timestamp,
138                },
139                IpoEventSource {
140                    event_type: "issue_confirm",
141                    time: jp.issue_confirm_time.as_deref(),
142                    timestamp: jp.issue_confirm_timestamp,
143                },
144                IpoEventSource {
145                    event_type: "price_confirm_start",
146                    time: jp.price_confirm_start_time.as_deref(),
147                    timestamp: jp.price_confirm_start_timestamp,
148                },
149                IpoEventSource {
150                    event_type: "price_confirm_end",
151                    time: jp.price_confirm_end_time.as_deref(),
152                    timestamp: jp.price_confirm_end_timestamp,
153                },
154                IpoEventSource {
155                    event_type: "inquiry_start",
156                    time: jp.inquiry_start_time.as_deref(),
157                    timestamp: jp.inquiry_start_timestamp,
158                },
159                IpoEventSource {
160                    event_type: "inquiry_end",
161                    time: jp.inquiry_end_time.as_deref(),
162                    timestamp: jp.inquiry_end_timestamp,
163                },
164                IpoEventSource {
165                    event_type: "apply_start",
166                    time: jp.apply_start_time.as_deref(),
167                    timestamp: jp.apply_start_timestamp,
168                },
169                IpoEventSource {
170                    event_type: "apply_end",
171                    time: jp.apply_end_time.as_deref(),
172                    timestamp: jp.apply_end_timestamp,
173                },
174                IpoEventSource {
175                    event_type: "draw",
176                    time: jp.draw_time.as_deref(),
177                    timestamp: jp.draw_timestamp,
178                },
179                IpoEventSource {
180                    event_type: "winning",
181                    time: jp.winning_time.as_deref(),
182                    timestamp: jp.winning_timestamp,
183                },
184            ] {
185                push_ipo_calendar_event(&mut out, event_types, begin_date, end_date, item, source);
186            }
187        }
188    }
189    out
190}
191
192fn requested_ipo_event(events: &[String], event_type: &str) -> bool {
193    events.is_empty()
194        || events
195            .iter()
196            .map(|event| normalize_ipo_event_type(event))
197            .any(|event| event == event_type)
198}
199
200fn yyyymmdd_from_time(value: &str) -> Option<String> {
201    let digits: String = value.chars().filter(|ch| ch.is_ascii_digit()).collect();
202    (digits.len() >= 8).then(|| digits[..8].to_string())
203}
204
205fn include_ipo_event(date_key: Option<&str>, begin: Option<&str>, end: Option<&str>) -> bool {
206    let Some(date_key) = date_key else {
207        return true;
208    };
209    if let Some(begin) = begin
210        && date_key < begin
211    {
212        return false;
213    }
214    if let Some(end) = end
215        && date_key > end
216    {
217        return false;
218    }
219    true
220}
221
222struct IpoEventSource<'a> {
223    event_type: &'static str,
224    time: Option<&'a str>,
225    timestamp: Option<f64>,
226}
227
228fn push_ipo_calendar_event(
229    out: &mut Vec<IpoCalendarEvent>,
230    event_types: &[String],
231    begin_date: Option<&str>,
232    end_date: Option<&str>,
233    item: &futu_proto::qot_get_ipo_list::IpoData,
234    source: IpoEventSource<'_>,
235) {
236    if !requested_ipo_event(event_types, source.event_type) {
237        return;
238    }
239    let date_key = source.time.and_then(yyyymmdd_from_time);
240    if !include_ipo_event(date_key.as_deref(), begin_date, end_date) {
241        return;
242    }
243    out.push(IpoCalendarEvent {
244        event_type: source.event_type,
245        market: item.basic.security.market,
246        code: item.basic.security.code.clone(),
247        name: item.basic.name.clone(),
248        date: date_key,
249        time: source.time.map(str::to_string),
250        timestamp: source.timestamp,
251    });
252}
253
254#[cfg(test)]
255#[path = "ipo_calendar_tests.rs"]
256mod ipo_calendar_tests;