1use crate::model::{Bar, BarClock, Instrument, Timeframe};
4use chrono::TimeDelta;
5
6pub fn resample(
43 bars: &[Bar],
44 from: impl Into<BarClock>,
45 to: impl Into<BarClock>,
46) -> Option<Vec<Bar>> {
47 let (source, target) = (from.into(), to.into());
48 let (from, to) = (source.timeframe()?, target.timeframe()?);
49 if source == target || (from == to && from.is_trade_based()) {
50 return Some(bars.to_vec());
51 }
52 if from.is_trade_based() || to.is_trade_based() {
53 return None;
54 }
55 if matches!(
57 from,
58 Timeframe::Week1 | Timeframe::Month1 | Timeframe::Weeks(_) | Timeframe::Months(_)
59 ) {
60 return None;
61 }
62 let mut resampled: Vec<Bar> = Vec::new();
63 for bar in bars {
64 let open = source.bar_open(bar.time);
65 let end = source.next_open(open)?;
68 let slot = target.bar_open(open);
69 if target.bar_open(end - TimeDelta::milliseconds(1)) != slot {
70 return None;
71 }
72 match resampled.last_mut() {
73 Some(last) if last.time == slot => *last = super::merge::fold(last, bar)?,
74 _ => resampled.push(Bar {
76 time: slot,
77 colors: None,
78 payload: None,
79 ..bar.clone()
80 }),
81 }
82 }
83 Some(resampled)
84}
85
86pub fn resample_with_session(
101 bars: &[Bar],
102 from: impl Into<BarClock>,
103 to: impl Into<BarClock>,
104 instrument: &Instrument,
105 extended_hours: bool,
106) -> Option<Vec<Bar>> {
107 Some(
108 resample_periods(bars, from, to, instrument, extended_hours)?
109 .into_iter()
110 .map(|(_, bar)| bar)
111 .collect(),
112 )
113}
114
115pub(crate) fn resample_periods(
116 bars: &[Bar],
117 from: impl Into<BarClock>,
118 to: impl Into<BarClock>,
119 instrument: &Instrument,
120 extended_hours: bool,
121) -> Option<Vec<(std::ops::Range<usize>, Bar)>> {
122 use crate::model::{ExchangeSessionProvider, TimezoneId};
123
124 let zone = instrument.time_zone.clone().unwrap_or_else(TimezoneId::utc);
125 let (source, target) = (from.into(), to.into());
126 let (from, to) = (source.timeframe()?, target.timeframe()?);
127 if (instrument.session.is_none()
128 && (instrument.time_zone.is_none() || to.duration_ms() < Timeframe::Day1.duration_ms()))
129 || from.duration_ms() >= Timeframe::Day1.duration_ms()
130 || from.is_trade_based()
131 || to.is_trade_based()
132 {
133 let output = resample(bars, source.clone(), target.clone())?;
134 let mut start = 0;
135 return Some(
136 output
137 .into_iter()
138 .map(|bar| {
139 let end = start
140 + bars[start..].partition_point(|input| {
141 target.bar_open(source.bar_open(input.time)) == bar.time
142 });
143 let range = start..end;
144 start = end;
145 (range, bar)
146 })
147 .collect(),
148 );
149 }
150 let mut session = instrument.session.clone();
151 if !extended_hours && let Some(session) = &mut session {
152 if session.windows.is_empty() && !session.extended_windows.is_empty() {
154 return Some(Vec::new());
155 }
156 session.clear_extended_windows();
157 }
158 let provider = ExchangeSessionProvider::new(zone, session);
159 let opens = provider.opens(bars.iter().map(|bar| bar.time));
160 let slot = |time| {
161 let n = opens.partition_point(|(open, _)| *open <= time);
162 let (open, date) = *opens.get(n.checked_sub(1)?)?;
163 if to.duration_ms() < Timeframe::Day1.duration_ms() {
164 let elapsed = (time - open).num_milliseconds();
165 open.checked_add_signed(TimeDelta::milliseconds(
166 elapsed - elapsed.rem_euclid(to.duration_ms()),
167 ))
168 } else {
169 target.open_on_date(date)
170 }
171 };
172 let mut output: Vec<(std::ops::Range<usize>, Bar)> = Vec::new();
173 for (i, bar) in bars.iter().enumerate() {
174 if !provider.is_open(bar.time, true) {
175 continue;
176 }
177 let period = slot(bar.time)?;
178 let mut end = if instrument.session.is_none() {
179 source.next_open(source.bar_open(bar.time))?
180 } else {
181 bar.time.checked_add_signed(from.duration())?
182 };
183 if let Some(close) = provider.session_close(bar.time, true) {
184 end = end.min(close);
185 }
186 if slot(end.checked_sub_signed(TimeDelta::nanoseconds(1))?)? != period {
187 return None;
188 }
189 match output.last_mut() {
190 Some((range, last)) if last.time == period => {
191 *last = super::merge::fold(last, bar)?;
192 range.end = i + 1;
193 }
194 _ => output.push((
195 i..i + 1,
196 Bar {
197 time: period,
198 colors: None,
199 payload: None,
200 ..bar.clone()
201 },
202 )),
203 }
204 }
205 Some(output)
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211 use chrono::{DateTime, Utc};
212
213 fn bar(time: &str, [open, high, low, close, volume]: [f64; 5]) -> Bar {
214 let time: DateTime<Utc> = time.parse().unwrap();
215 Bar::new(time, open, high, low, close, volume)
216 }
217
218 #[cfg(feature = "timezones")]
219 #[test]
220 fn resamples_selected_exchange_sessions() {
221 use crate::model::{Session, SessionWindow, TimezoneId};
222 use chrono::{NaiveTime, Weekday};
223 let clock = |h, m| NaiveTime::from_hms_opt(h, m, 0).unwrap();
224 let instrument = Instrument::new("SESSION")
225 .with_time_zone(TimezoneId::named("America/New_York").unwrap())
226 .with_session(
227 Session::new([SessionWindow::new(
228 [Weekday::Fri, Weekday::Mon],
229 clock(9, 30),
230 clock(16, 0),
231 )])
232 .with_extended_windows([SessionWindow::new(
233 [Weekday::Fri, Weekday::Mon],
234 clock(8, 30),
235 clock(9, 30),
236 )])
237 .with_holidays(["2024-03-18".parse().unwrap()]),
238 );
239 let input = vec![
240 bar("2024-03-15T12:30:00Z", [9.0, 10.0, 8.0, 9.0, 1.0]),
241 bar("2024-03-15T13:30:00Z", [10.0, 13.0, 9.0, 12.0, 2.0]),
242 bar("2024-03-15T14:30:00Z", [12.0, 15.0, 11.0, 14.0, 3.0]),
243 bar("2024-03-15T15:30:00Z", [14.0, 16.0, 10.0, 11.0, 4.0]),
244 bar("2024-03-15T19:30:00Z", [11.0, 12.0, 7.0, 8.0, 5.0]),
245 bar("2024-03-18T13:30:00Z", [8.0, 9.0, 6.0, 7.0, 6.0]),
246 ];
247 for (extended, expected) in [
248 (
249 false,
250 vec![
251 bar("2024-03-15T13:30:00Z", [10.0, 15.0, 9.0, 14.0, 5.0]),
252 bar("2024-03-15T15:30:00Z", [14.0, 16.0, 10.0, 11.0, 4.0]),
253 bar("2024-03-15T19:30:00Z", [11.0, 12.0, 7.0, 8.0, 5.0]),
254 ],
255 ),
256 (
257 true,
258 vec![
259 bar("2024-03-15T12:30:00Z", [9.0, 13.0, 8.0, 12.0, 3.0]),
260 bar("2024-03-15T14:30:00Z", [12.0, 16.0, 10.0, 11.0, 7.0]),
261 bar("2024-03-15T18:30:00Z", [11.0, 12.0, 7.0, 8.0, 5.0]),
262 ],
263 ),
264 ] {
265 assert_eq!(
266 resample_with_session(
267 &input,
268 Timeframe::Hour1,
269 "2h".parse::<Timeframe>().unwrap(),
270 &instrument,
271 extended
272 ),
273 Some(expected)
274 );
275 }
276 let continuous = Instrument::new("CONTINUOUS")
277 .with_time_zone(TimezoneId::utc())
278 .with_session(Session::new([]));
279 let last = bar("2024-03-15T23:55:00Z", [10.0, 13.0, 9.0, 12.0, 2.0]);
280 assert_eq!(
281 resample_with_session(
282 &[last],
283 "7min".parse::<Timeframe>().unwrap(),
284 Timeframe::Day1,
285 &continuous,
286 false,
287 ),
288 Some(vec![bar(
289 "2024-03-15T00:00:00Z",
290 [10.0, 13.0, 9.0, 12.0, 2.0]
291 )])
292 );
293 #[cfg(feature = "widget")]
294 {
295 let mut chart = crate::widget::ChartBuilder::new().build();
296 chart.set_timeframe("2h".parse().unwrap());
297 chart.set_instrument(Some(instrument.clone()));
298 assert_eq!(chart.bar_clock().bar_open(input[1].time), input[1].time);
299 chart.set_extended_hours(true);
300 assert_eq!(chart.bar_clock().bar_open(input[1].time), input[0].time);
301 }
302 let overnight = Instrument::new("OVERNIGHT")
303 .with_time_zone(TimezoneId::named("America/New_York").unwrap())
304 .with_session(Session::new([SessionWindow::new(
305 [Weekday::Mon, Weekday::Tue],
306 clock(18, 0),
307 clock(17, 0),
308 )]));
309 let input = vec![
310 bar("2024-03-10T22:00:00Z", [10.0, 13.0, 9.0, 12.0, 2.0]),
311 bar("2024-03-11T03:00:00Z", [12.0, 15.0, 11.0, 14.0, 3.0]),
312 bar("2024-03-11T20:00:00Z", [14.0, 16.0, 10.0, 11.0, 4.0]),
313 bar("2024-03-11T21:00:00Z", [99.0, 99.0, 99.0, 99.0, 1.0]),
314 bar("2024-03-11T22:00:00Z", [11.0, 12.0, 7.0, 8.0, 5.0]),
315 ];
316 assert_eq!(
317 resample_with_session(&input, Timeframe::Hour1, Timeframe::Day1, &overnight, false),
318 Some(vec![
319 bar("2024-03-11T00:00:00Z", [10.0, 16.0, 9.0, 11.0, 9.0]),
320 bar("2024-03-12T00:00:00Z", [11.0, 12.0, 7.0, 8.0, 5.0]),
321 ])
322 );
323 let monday = vec![bar("2024-03-11T00:00:00Z", [10.0, 13.0, 9.0, 12.0, 2.0])];
325 assert_eq!(
326 resample_with_session(
327 &monday,
328 Timeframe::Day1,
329 Timeframe::Week1,
330 &overnight,
331 false
332 ),
333 Some(monday.clone())
334 );
335 let tokyo =
337 Instrument::new("TOKYO").with_time_zone(TimezoneId::named("Asia/Tokyo").unwrap());
338 assert_eq!(
339 resample_with_session(&monday, Timeframe::Hour1, Timeframe::Day1, &tokyo, false),
340 Some(monday)
341 );
342 let local =
343 BarClock::new(Timeframe::Day1).with_timezone("America/New_York".parse().unwrap());
344 let selected =
345 resample_with_session(&input, Timeframe::Hour1, local, &overnight, false).unwrap();
346 assert_eq!(
347 selected.iter().map(|bar| bar.time).collect::<Vec<_>>(),
348 [
349 "2024-03-11T04:00:00Z".parse::<DateTime<Utc>>().unwrap(),
350 "2024-03-12T04:00:00Z".parse::<DateTime<Utc>>().unwrap(),
351 ]
352 );
353 }
354
355 #[test]
358 fn resamples_into_bar_clock_slots() {
359 let min7: Timeframe = "7min".parse().unwrap();
360 #[rustfmt::skip]
361 let cases = [
362 (Timeframe::Min1, min7, vec![
364 bar("2024-03-15T23:54:00Z", [10.0, 12.0, 9.0, 11.0, 1.0]),
365 bar("2024-03-15T23:55:00Z", [11.0, 13.0, 10.0, 12.0, 2.0]),
366 bar("2024-03-15T23:56:00Z", [12.0, 15.0, 11.0, 14.0, 3.0]),
367 bar("2024-03-15T23:57:00Z", [14.0, 14.0, 8.0, 9.0, 4.0]),
368 bar("2024-03-15T23:58:00Z", [9.0, 11.0, 9.0, 10.0, 5.0]),
369 bar("2024-03-15T23:59:00Z", [10.0, 12.0, 10.0, 11.0, 6.0]),
370 bar("2024-03-16T00:00:00Z", [11.0, 11.0, 10.0, 10.0, 7.0]),
371 bar("2024-03-16T00:01:00Z", [10.0, 13.0, 10.0, 12.0, 8.0]),
372 ], Some(vec![
373 bar("2024-03-15T23:48:00Z", [10.0, 12.0, 9.0, 11.0, 1.0]),
374 bar("2024-03-15T23:55:00Z", [11.0, 15.0, 8.0, 11.0, 20.0]),
375 bar("2024-03-16T00:00:00Z", [11.0, 13.0, 10.0, 12.0, 15.0]),
376 ])),
377 (min7, Timeframe::Day1, vec![
379 bar("2024-03-15T23:48:00Z", [10.0, 12.0, 9.0, 11.0, 1.0]),
380 bar("2024-03-15T23:55:00Z", [11.0, 15.0, 8.0, 11.0, 20.0]),
381 bar("2024-03-16T00:00:00Z", [11.0, 13.0, 10.0, 12.0, 15.0]),
382 ], Some(vec![
383 bar("2024-03-15T00:00:00Z", [10.0, 15.0, 8.0, 11.0, 21.0]),
384 bar("2024-03-16T00:00:00Z", [11.0, 13.0, 10.0, 12.0, 15.0]),
385 ])),
386 (Timeframe::Min5, min7, vec![
388 bar("2024-03-15T00:00:00Z", [1.0, 1.0, 1.0, 1.0, 1.0]),
389 bar("2024-03-15T00:05:00Z", [1.0, 1.0, 1.0, 1.0, 1.0]),
390 ], None),
391 (Timeframe::Hour1, Timeframe::Hour4, vec![
393 bar("2024-03-15T00:00:00Z", [20.0, 22.0, 19.0, 21.0, 100.0]),
394 bar("2024-03-15T01:00:00Z", [21.0, 25.0, 20.0, 24.0, 200.0]),
395 bar("2024-03-15T03:00:00Z", [24.0, 24.0, 18.0, 19.0, 300.0]),
396 bar("2024-03-15T04:00:00Z", [19.0, 20.0, 17.0, 18.0, 400.0]),
397 ], Some(vec![
398 bar("2024-03-15T00:00:00Z", [20.0, 25.0, 18.0, 19.0, 600.0]),
399 bar("2024-03-15T04:00:00Z", [19.0, 20.0, 17.0, 18.0, 400.0]),
400 ])),
401 (Timeframe::Day1, Timeframe::Week1, vec![
403 bar("2024-03-16T00:00:00Z", [30.0, 31.0, 29.0, 30.0, 1.0]),
404 bar("2024-03-17T00:00:00Z", [30.0, 33.0, 30.0, 32.0, 2.0]),
405 bar("2024-03-18T00:00:00Z", [32.0, 34.0, 31.0, 33.0, 3.0]),
406 bar("2024-03-19T00:00:00Z", [33.0, 33.0, 28.0, 29.0, 4.0]),
407 ], Some(vec![
408 bar("2024-03-11T00:00:00Z", [30.0, 33.0, 29.0, 32.0, 3.0]),
409 bar("2024-03-18T00:00:00Z", [32.0, 34.0, 28.0, 29.0, 7.0]),
410 ])),
411 (Timeframe::Min1, Timeframe::Min5, vec![
413 bar("2024-03-15T00:00:00Z", [1.0, 1.0, 1.0, 1.0, f64::MAX]),
414 bar("2024-03-15T00:01:00Z", [1.0, 1.0, 1.0, 1.0, f64::MAX]),
415 ], None),
416 (Timeframe::Week1, Timeframe::Month1, vec![
418 bar("2024-03-04T00:00:00Z", [1.0, 1.0, 1.0, 1.0, 1.0]),
419 ], None),
420 ("2w".parse::<Timeframe>().unwrap(), "3month".parse::<Timeframe>().unwrap(), vec![
421 bar("2024-03-04T00:00:00Z", [1.0, 1.0, 1.0, 1.0, 1.0]),
422 ], None),
423 ("3month".parse::<Timeframe>().unwrap(), "6month".parse::<Timeframe>().unwrap(), vec![
424 bar("2024-01-01T00:00:00Z", [1.0, 1.0, 1.0, 1.0, 1.0]),
425 ], None),
426 ];
427 for (from, to, bars, expected) in cases {
428 assert_eq!(resample(&bars, from, to), expected, "{from} to {to}");
429 }
430 let Some(zone) = crate::model::TimezoneId::named("America/New_York") else {
431 return;
432 };
433 let local = BarClock::new(Timeframe::Hour4).with_timezone(zone);
434 let input = vec![
436 bar("2024-03-10T05:00:00Z", [100.0, 101.0, 99.0, 100.0, 2.0]),
437 bar("2024-03-10T07:59:00Z", [102.0, 104.0, 101.0, 103.0, 3.0]),
438 bar("2024-03-10T08:00:00Z", [103.0, 105.0, 102.0, 104.0, 4.0]),
439 ];
440 assert_eq!(
441 resample(&input, Timeframe::Min1, local),
442 Some(vec![
443 bar("2024-03-10T05:00:00Z", [100.0, 104.0, 99.0, 103.0, 5.0]),
444 bar("2024-03-10T08:00:00Z", [103.0, 105.0, 102.0, 104.0, 4.0]),
445 ])
446 );
447 }
448}