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;