1use crate::model::local_time;
41use crate::model::{Bar, BarClock, TimezoneId};
42use crate::studies::palette;
43use crate::studies::stepper::update_window;
44use crate::studies::{Indicator, IndicatorValues, LabelPlacement, StudyLabel, StudyZone};
45use chrono::{NaiveDate, NaiveTime, TimeDelta, Timelike};
46use egui::Color32;
47use std::ops::Range;
48
49#[derive(Clone)]
54pub struct TradingSessions {
55 names: [String; 3],
56 show: [bool; 3],
57 start: [usize; 3],
59 end: [usize; 3],
61 zones: [TimezoneId; 3],
62 daily: bool,
64 values: IndicatorValues,
65 colors: [Color32; 3],
66 visible: bool,
67 boxes: bool,
68 name_labels: bool,
69 range_labels: bool,
70 average_labels: bool,
71 open_lines: bool,
72 close_lines: bool,
73 average_lines: bool,
74 precision: Option<usize>,
76 label_precision: Option<usize>,
77 sessions: Vec<SessionSummary>,
79 study_zones: Vec<StudyZone>,
80 labels: Vec<StudyLabel>,
81 before_last: Option<[Option<SessionSummary>; 3]>,
83 source_len: usize,
84}
85
86#[derive(Clone)]
88struct SessionSummary {
89 slot: usize,
90 day: NaiveDate,
91 bars: Range<usize>,
92 open: f64,
93 high: f64,
94 low: f64,
95 close: f64,
96 close_sum: f64,
97 closes: usize,
98}
99
100const ZONES: [(&str, i32); 3] = [
103 ("Asia/Tokyo", 9),
104 ("Europe/London", 0),
105 ("America/New_York", -5),
106];
107
108fn zone(name: &str) -> Option<TimezoneId> {
111 TimezoneId::named(name).or_else(|| {
112 let (_, hours) = ZONES.into_iter().find(|(zone, _)| *zone == name)?;
113 TimezoneId::fixed_offset(hours * 3600)
114 })
115}
116
117impl TradingSessions {
118 pub fn new() -> Self {
122 let tint =
123 |color: Color32| Color32::from_rgba_unmultiplied(color.r(), color.g(), color.b(), 38);
124 Self {
125 names: ["Tokyo".into(), "London".into(), "New York".into()],
126 show: [true; 3],
127 start: [9 * 60, 8 * 60 + 30, 9 * 60 + 30],
128 end: [15 * 60, 16 * 60 + 30, 16 * 60],
129 zones: ZONES.map(|(name, _)| zone(name).unwrap()),
130 daily: false,
131 values: IndicatorValues::new(),
132 colors: [
133 tint(palette::BLUE),
134 tint(palette::WARM_ORANGE),
135 tint(palette::TEAL),
136 ],
137 visible: true,
138 boxes: true,
139 name_labels: true,
140 range_labels: false,
141 average_labels: false,
142 open_lines: false,
143 close_lines: false,
144 average_lines: false,
145 precision: None,
146 label_precision: None,
147 sessions: Vec::new(),
148 study_zones: Vec::new(),
149 labels: Vec::new(),
150 before_last: None,
151 source_len: 0,
152 }
153 }
154
155 pub fn with_session(
159 mut self,
160 slot: usize,
161 name: impl Into<String>,
162 start: NaiveTime,
163 end: NaiveTime,
164 zone: TimezoneId,
165 ) -> Self {
166 if slot < 3 {
167 let minutes = |time: NaiveTime| (time.hour() * 60 + time.minute()) as usize;
168 let name: String = name.into();
169 self.names[slot] = name.replace(char::is_control, " ").trim().to_owned();
170 self.start[slot] = minutes(start);
171 self.end[slot] = minutes(end);
172 self.zones[slot] = zone;
173 }
174 self
175 }
176
177 pub fn with_shown(mut self, slot: usize, show: bool) -> Self {
179 if let Some(shown) = self.show.get_mut(slot) {
180 *shown = show;
181 }
182 self
183 }
184
185 pub fn with_boxes(mut self, boxes: bool) -> Self {
188 self.boxes = boxes;
189 self
190 }
191
192 pub fn with_labels(mut self, names: bool, ranges: bool, averages: bool) -> Self {
195 self.name_labels = names;
196 self.range_labels = ranges;
197 self.average_labels = averages;
198 self
199 }
200
201 pub fn with_lines(mut self, opens: bool, closes: bool, averages: bool) -> Self {
204 self.open_lines = opens;
205 self.close_lines = closes;
206 self.average_lines = averages;
207 self
208 }
209}
210
211impl TradingSessions {
212 fn inside(&self, slot: usize, second: u32) -> bool {
214 let (start, end) = (self.start[slot] as u32 * 60, self.end[slot] as u32 * 60);
215 match start.cmp(&end) {
216 std::cmp::Ordering::Less => (start..end).contains(&second),
217 std::cmp::Ordering::Greater => second >= start || second < end,
218 std::cmp::Ordering::Equal => true,
219 }
220 }
221
222 fn scan(
226 &self,
227 slot: usize,
228 data: &[Bar],
229 first: usize,
230 mut current: Option<SessionSummary>,
231 out: &mut Vec<SessionSummary>,
232 ) -> Option<SessionSummary> {
233 let mut before_last = None;
234 let start = TimeDelta::minutes(self.start[slot] as i64);
235 for (at, bar) in data.iter().enumerate().skip(first) {
236 if at + 1 == data.len() {
237 before_last = current.clone();
238 }
239 if ![bar.open, bar.high, bar.low, bar.close]
240 .iter()
241 .all(|price| price.is_finite())
242 {
243 continue;
244 }
245 let local = local_time(self.zones[slot].clone(), bar.time);
246 if !self.inside(slot, local.num_seconds_from_midnight()) {
247 continue;
248 }
249 let day = local
250 .checked_sub_signed(start)
251 .map_or(local.date(), |time| time.date());
252 match &mut current {
253 Some(session) if session.day == day => {
254 session.bars.end = at + 1;
255 session.high = session.high.max(bar.high);
256 session.low = session.low.min(bar.low);
257 session.close = bar.close;
258 session.close_sum += bar.close;
259 session.closes += 1;
260 }
261 _ => {
262 out.extend(current.take());
263 current = Some(SessionSummary {
264 slot,
265 day,
266 bars: at..at + 1,
267 open: bar.open,
268 high: bar.high,
269 low: bar.low,
270 close: bar.close,
271 close_sum: bar.close,
272 closes: 1,
273 });
274 }
275 }
276 }
277 out.extend(current);
278 before_last
279 }
280
281 fn emit(&mut self, session: &SessionSummary) {
283 let average = session.close_sum / session.closes as f64;
284 let plot = 3 + session.slot;
285 let line = |price: f64, plot: usize| StudyZone {
286 bars: session.bars.clone(),
287 top: price,
288 bottom: price,
289 plot,
290 };
291 if self.boxes {
292 self.study_zones.push(StudyZone {
293 bars: session.bars.clone(),
294 top: session.high,
295 bottom: session.low,
296 plot,
297 });
298 }
299 if self.open_lines {
300 self.study_zones.push(line(session.open, plot));
301 }
302 if self.close_lines {
303 self.study_zones.push(line(session.close, plot));
304 }
305 if self.average_lines {
306 self.study_zones.push(line(average, 6 + session.slot));
307 }
308 let precision = self.label_precision.or(self.precision);
309 let price = |value: f64| crate::scales::StudyFormat::Price.format(value, precision);
310 let mut lines = Vec::new();
311 if self.name_labels && !self.names[session.slot].is_empty() {
312 lines.push(self.names[session.slot].clone());
313 }
314 if self.range_labels {
315 lines.push(format!("Range: {}", price(session.high - session.low)));
316 }
317 if self.average_labels {
318 lines.push(format!("Avg: {}", price(average)));
319 }
320 if !lines.is_empty() {
321 self.labels.push(StudyLabel {
322 bar: session.bars.start,
323 price: session.high,
324 text: lines.join("\n"),
325 placement: LabelPlacement::AboveRight,
326 plot: session.slot,
327 });
328 }
329 }
330
331 fn update_sessions(&mut self, data: &[Bar], from: usize) {
335 let from = from.min(self.source_len).min(data.len());
336 if from == data.len() && from == self.source_len {
338 return;
339 }
340 let before_last = self
341 .before_last
342 .take()
343 .filter(|_| from + 1 == self.source_len);
344 self.source_len = data.len();
345 if self.daily {
346 self.sessions.clear();
347 self.study_zones.clear();
348 self.labels.clear();
349 return;
350 }
351 let started = self
353 .sessions
354 .partition_point(|session| session.bars.start < from);
355 let resume: [_; 3] = std::array::from_fn(|slot| {
356 if let Some(before_last) = &before_last {
357 return (from, before_last[slot].clone());
358 }
359 if !self.show[slot] {
360 return (from, None);
361 }
362 let mut latest = self.sessions[..started]
363 .iter()
364 .rev()
365 .filter(|session| session.slot == slot);
366 match (latest.next(), latest.next()) {
367 (Some(last), previous) if last.bars.end > from => {
368 (last.bars.start, previous.cloned())
369 }
370 (last, _) => (from, last.cloned()),
371 }
372 });
373 let cut = |slot: usize| {
375 let (first, current) = &resume[slot];
376 current
377 .as_ref()
378 .map_or(*first, |session| session.bars.start)
379 };
380 let first = cut(0).min(cut(1)).min(cut(2));
381 let keep = self
382 .sessions
383 .partition_point(|session| session.bars.start < first);
384 let mut tail = self.sessions.split_off(keep);
385 tail.retain(|session| session.bars.start < cut(session.slot));
386 let kept = self.labels.partition_point(|label| label.bar < first);
387 self.labels.truncate(kept);
388 let per_session = [
389 self.boxes,
390 self.open_lines,
391 self.close_lines,
392 self.average_lines,
393 ]
394 .iter()
395 .filter(|shown| **shown)
396 .count();
397 self.study_zones.truncate(keep * per_session);
398 let mut before_last: [_; 3] = Default::default();
399 for (slot, (first, current)) in resume.into_iter().enumerate() {
400 if self.show[slot] {
401 before_last[slot] = self.scan(slot, data, first, current, &mut tail);
402 }
403 }
404 self.before_last = (from < data.len()).then_some(before_last);
405 tail.sort_by_key(|session| (session.bars.start, session.slot));
406 for session in &tail {
407 self.emit(session);
408 }
409 self.sessions.append(&mut tail);
410 }
411
412 fn compute(&self, data: &[Bar]) -> IndicatorValues {
413 let mut values = IndicatorValues::missing(data.len());
414 if !data.is_empty() {
415 values.set_value(2, 0, f64::NAN);
417 }
418 if self.daily {
419 return values;
420 }
421 for slot in (0..3).filter(|&slot| self.show[slot]) {
422 let zone = &self.zones[slot];
423 for (i, bar) in data.iter().enumerate() {
424 let second = local_time(zone.clone(), bar.time).num_seconds_from_midnight();
425 if self.inside(slot, second) {
426 values.set_value(slot, i, 1.0);
427 }
428 }
429 }
430 values
431 }
432}
433
434impl Default for TradingSessions {
435 fn default() -> Self {
436 Self::new()
437 }
438}
439
440const SLOTS: [(&str, &str); 3] = [("first", "First"), ("second", "Second"), ("third", "Third")];
442
443impl Indicator for TradingSessions {
444 fn inputs(&self) -> Vec<crate::studies::StudyInput> {
448 use crate::studies::{InputKind, InputValue, StudyInput};
449 let defaults = Self::default();
450 let mut inputs = Vec::with_capacity(22);
451 for (slot, (lower, upper)) in SLOTS.into_iter().enumerate() {
452 let input = |field: &str, label: String, value, default, kind| {
453 StudyInput::new(format!("{field} [{slot}]"), label, value, kind)
454 .with_default(default)
455 };
456 let minutes = InputKind::Length(0, 1439);
457 inputs.extend([
458 input(
459 "show",
460 format!("Show {lower} session"),
461 InputValue::Toggle(self.show[slot]),
462 InputValue::Toggle(defaults.show[slot]),
463 InputKind::Toggle,
464 ),
465 input(
466 "name",
467 format!("{upper} session name"),
468 InputValue::Text(self.names[slot].clone()),
469 InputValue::Text(defaults.names[slot].clone()),
470 InputKind::Text,
471 ),
472 input(
473 "start",
474 format!("{upper} session start, minutes after midnight"),
475 InputValue::Length(self.start[slot]),
476 InputValue::Length(defaults.start[slot]),
477 minutes.clone(),
478 ),
479 input(
480 "end",
481 format!("{upper} session end, minutes after midnight"),
482 InputValue::Length(self.end[slot]),
483 InputValue::Length(defaults.end[slot]),
484 minutes,
485 ),
486 input(
487 "zone",
488 format!("{upper} session time zone"),
489 InputValue::Text(self.zones[slot].name().to_owned()),
490 InputValue::Text(defaults.zones[slot].name().to_owned()),
491 InputKind::Text,
492 ),
493 ]);
494 }
495 for (id, label, value, default) in [
496 ("boxes", "Show session boxes", self.boxes, defaults.boxes),
497 (
498 "name_labels",
499 "Show session names",
500 self.name_labels,
501 defaults.name_labels,
502 ),
503 (
504 "range_labels",
505 "Show session ranges",
506 self.range_labels,
507 defaults.range_labels,
508 ),
509 (
510 "average_labels",
511 "Show session average prices",
512 self.average_labels,
513 defaults.average_labels,
514 ),
515 (
516 "open_lines",
517 "Show session open lines",
518 self.open_lines,
519 defaults.open_lines,
520 ),
521 (
522 "close_lines",
523 "Show session close lines",
524 self.close_lines,
525 defaults.close_lines,
526 ),
527 (
528 "average_lines",
529 "Show session average lines",
530 self.average_lines,
531 defaults.average_lines,
532 ),
533 ] {
534 inputs.push(
535 StudyInput::new(id, label, InputValue::Toggle(value), InputKind::Toggle)
536 .with_default(InputValue::Toggle(default)),
537 );
538 }
539 inputs
540 }
541
542 fn set_input(
543 &mut self,
544 id: &str,
545 value: crate::studies::InputValue,
546 ) -> Result<(), crate::studies::StudyError> {
547 use crate::studies::InputValue;
548 let schema = self
549 .inputs()
550 .into_iter()
551 .find(|input| input.id == id)
552 .ok_or_else(|| crate::studies::StudyError::UnknownInput(id.into()))?;
553 if !schema.accepts(&value) {
554 return Err(crate::studies::StudyError::invalid_input(id));
555 }
556 let option = match id {
557 "boxes" => Some(&mut self.boxes),
558 "name_labels" => Some(&mut self.name_labels),
559 "range_labels" => Some(&mut self.range_labels),
560 "average_labels" => Some(&mut self.average_labels),
561 "open_lines" => Some(&mut self.open_lines),
562 "close_lines" => Some(&mut self.close_lines),
563 "average_lines" => Some(&mut self.average_lines),
564 _ => None,
565 };
566 if let (Some(option), InputValue::Toggle(on)) = (option, &value) {
567 *option = *on;
568 self.calculate(&[]);
569 return Ok(());
570 }
571 let (field, slot) = id
573 .split_once(" [")
574 .ok_or_else(|| crate::studies::StudyError::UnknownInput(id.into()))?;
575 let slot: usize = slot
576 .trim_end_matches(']')
577 .parse()
578 .map_err(|_| crate::studies::StudyError::UnknownInput(id.into()))?;
579 match (field, value) {
580 ("show", InputValue::Toggle(show)) => self.show[slot] = show,
581 ("name", InputValue::Text(name)) => self.names[slot] = name,
582 ("start", InputValue::Length(minutes)) => self.start[slot] = minutes,
583 ("end", InputValue::Length(minutes)) => self.end[slot] = minutes,
584 ("zone", InputValue::Text(name)) => {
585 self.zones[slot] =
586 zone(&name).ok_or_else(|| crate::studies::StudyError::InvalidInput {
587 id: id.into(),
588 reason: "names no known time zone".into(),
589 })?;
590 }
591 _ => return Err(crate::studies::StudyError::UnknownInput(id.into())),
592 }
593 self.calculate(&[]);
594 Ok(())
595 }
596
597 fn name(&self) -> &str {
598 "Trading Sessions"
599 }
600
601 fn desc(&self) -> &str {
602 "Trading Sessions - Background highlight of the Tokyo, London and New York sessions"
603 }
604
605 fn plot_style(&self, line: usize) -> crate::studies::PlotStyle {
608 if (3..9).contains(&line) {
609 return crate::studies::PlotStyle {
610 color: self.colors[line % 3].to_opaque(),
611 width: if line < 6 { 1.0 } else { 2.0 },
612 pattern: crate::model::LineStyle::Solid,
613 plot_type: crate::studies::PlotType::Line,
614 visible: true,
615 };
616 }
617 crate::studies::PlotStyle {
618 color: self.colors.get(line).copied().unwrap_or(Color32::WHITE),
619 width: 1.0,
620 pattern: crate::model::LineStyle::Solid,
621 plot_type: crate::studies::PlotType::BackgroundColor,
622 visible: true,
623 }
624 }
625
626 fn plot_output(&self, line: usize) -> usize {
628 line % 3
629 }
630
631 fn value_format(&self) -> crate::scales::StudyFormat {
632 crate::scales::StudyFormat::Fixed(0)
633 }
634
635 fn calculate(&mut self, data: &[Bar]) {
636 self.update(data, 0);
637 }
638
639 fn update(&mut self, data: &[Bar], from: usize) {
640 let mut values = std::mem::take(&mut self.values);
641 update_window(
642 &mut values,
643 data,
644 from,
645 |_, from| from,
646 |data| self.compute(data),
647 );
648 self.values = values;
649 self.update_sessions(data, from);
650 }
651
652 fn is_incremental(&self) -> bool {
653 true
654 }
655
656 fn set_bar_clock(&mut self, clock: &BarClock) -> bool {
657 let daily = clock.timeframe().is_some_and(|timeframe| {
658 timeframe.duration_ms() >= crate::model::Timeframe::Day1.duration_ms()
659 });
660 let changed = daily != self.daily;
661 self.daily = daily;
662 changed
663 }
664
665 fn set_instrument(&mut self, instrument: Option<&crate::model::Instrument>) -> bool {
666 let precision = instrument
667 .and_then(crate::scales::instrument_price_precision)
668 .map(usize::from);
669 let changed = precision != self.precision;
670 self.precision = precision;
671 changed
672 }
673
674 fn zones(&self) -> &[StudyZone] {
675 &self.study_zones
676 }
677
678 fn zones_cover_whole_bars(&self) -> bool {
679 true
680 }
681
682 fn labels(&self) -> &[StudyLabel] {
683 &self.labels
684 }
685
686 fn set_label_precision(&mut self, precision: Option<usize>) {
689 self.label_precision = precision.filter(|&decimals| decimals != 0);
690 }
691
692 fn values(&self) -> &IndicatorValues {
693 &self.values
694 }
695
696 fn colors(&self) -> Vec<Color32> {
697 self.colors.to_vec()
698 }
699
700 fn set_colors(&mut self, colors: Vec<Color32>) {
701 for (old, new) in self.colors.iter_mut().zip(colors) {
702 *old = new;
703 }
704 }
705
706 fn is_visible(&self) -> bool {
707 self.visible
708 }
709
710 fn set_visible(&mut self, visible: bool) {
711 self.visible = visible;
712 }
713
714 fn clone_box(&self) -> Box<dyn Indicator> {
715 Box::new(self.clone())
716 }
717
718 fn line_names(&self) -> Vec<String> {
719 self.names.to_vec()
720 }
721}
722
723#[cfg(test)]
724mod tests {
725 use super::*;
726 use chrono::{TimeZone, Utc};
727
728 type Row = (u32, u32, f64, f64, f64, f64);
729 type ZoneRow = (Range<usize>, f64, f64, usize);
730 type Case<'a> = (&'a str, Box<dyn Indicator>, Vec<ZoneRow>, Option<&'a str>);
732
733 fn study(name: &str, start: u32, end: u32) -> TradingSessions {
736 let time = |hour| NaiveTime::from_hms_opt(hour, 0, 0).unwrap();
737 TradingSessions::new()
738 .with_session(0, name, time(start), time(end), TimezoneId::utc())
739 .with_shown(1, false)
740 .with_shown(2, false)
741 }
742
743 fn bars(rows: &[Row]) -> Vec<Bar> {
744 rows.iter()
745 .map(|&(day, hour, open, high, low, close)| {
746 let time = Utc.with_ymd_and_hms(2024, 1, day, hour, 0, 0).unwrap();
747 Bar::new(time, open, high, low, close, 1.0)
748 })
749 .collect()
750 }
751
752 fn zones(study: &dyn Indicator) -> Vec<ZoneRow> {
753 let zones = study.zones().iter();
754 zones
755 .map(|zone| (zone.bars.clone(), zone.top, zone.bottom, zone.plot))
756 .collect()
757 }
758
759 const DAY: &[Row] = &[
762 (15, 8, 10.0, 11.0, 9.0, 10.0),
763 (15, 9, 10.0, 12.0, 9.5, 11.0),
764 (15, 10, 11.0, 13.0, 10.0, 12.5),
765 (15, 11, 12.5, 12.8, 8.5, 9.0),
766 (15, 12, 9.0, 10.0, 8.0, 9.5),
767 ];
768
769 #[test]
770 fn a_session_draws_its_box_labels_and_lines_each_with_its_own_toggle() {
771 let average = 32.5 / 3.0;
772 let square = (1..4, 13.0, 8.5, 3);
773 let open = (1..4, 10.0, 10.0, 3);
774 let close = (1..4, 9.0, 9.0, 3);
775 let mean = (1..4, average, average, 6);
776 let all = || {
777 study("Test", 9, 12)
778 .with_labels(true, true, true)
779 .with_lines(true, true, true)
780 };
781 let mut tick = all();
782 let instrument = crate::model::Instrument::new("X").with_tick_size(0.001);
783 tick.set_instrument(Some(&instrument));
784 let mut styled = crate::studies::ConfiguredStudy::new(Box::new(all()));
786 styled.apply_settings(&[], styled.settings()).unwrap();
787 let mut set = crate::studies::ConfiguredStudy::new(Box::new(all()));
788 let mut settings = set.settings();
789 settings.precision = Some(3);
790 set.apply_settings(&[], settings).unwrap();
791 let mut unboxed = study("Test", 9, 12);
792 unboxed
793 .set_input("boxes", crate::studies::InputValue::Toggle(false))
794 .unwrap();
795 let all_text = "Test\nRange: 4.50\nAvg: 10.83";
796 let all_zones = vec![square.clone(), open.clone(), close.clone(), mean.clone()];
797 let rows: Vec<Case> = vec![
798 (
799 "defaults",
800 Box::new(study("Test", 9, 12)),
801 vec![square.clone()],
802 Some("Test"),
803 ),
804 ("all", Box::new(all()), all_zones.clone(), Some(all_text)),
805 (
806 "no box",
807 Box::new(all().with_boxes(false)),
808 vec![open.clone(), close.clone(), mean.clone()],
809 Some(all_text),
810 ),
811 (
812 "no box, through the input",
813 Box::new(unboxed),
814 Vec::new(),
815 Some("Test"),
816 ),
817 (
818 "no labels",
819 Box::new(all().with_labels(false, false, false)),
820 all_zones.clone(),
821 None,
822 ),
823 (
824 "range only",
825 Box::new(all().with_labels(false, true, false)),
826 all_zones.clone(),
827 Some("Range: 4.50"),
828 ),
829 (
830 "no lines",
831 Box::new(all().with_lines(false, false, false)),
832 vec![square.clone()],
833 Some(all_text),
834 ),
835 (
836 "open line only",
837 Box::new(all().with_boxes(false).with_lines(true, false, false)),
838 vec![open.clone()],
839 Some(all_text),
840 ),
841 (
842 "instrument decimals",
843 Box::new(tick),
844 all_zones.clone(),
845 Some("Test\nRange: 4.500\nAvg: 10.833"),
846 ),
847 (
848 "default style precision",
849 Box::new(styled),
850 all_zones.clone(),
851 Some(all_text),
852 ),
853 (
854 "style precision",
855 Box::new(set),
856 all_zones.clone(),
857 Some("Test\nRange: 4.500\nAvg: 10.833"),
858 ),
859 ];
860 let data = bars(DAY);
861 for (name, mut study, want_zones, want_text) in rows {
862 study.calculate(&data);
863 assert_eq!(zones(study.as_ref()), want_zones, "{name}");
864 let labels = study.labels();
865 assert_eq!(labels.first().map(|l| l.text.as_str()), want_text, "{name}");
866 if let Some(label) = labels.first() {
867 assert_eq!(labels.len(), 1, "{name}");
868 assert_eq!(
869 (label.bar, label.price, label.placement, label.plot),
870 (1, 13.0, LabelPlacement::AboveRight, 0),
871 "{name}"
872 );
873 }
874 }
875
876 let mut daily = all();
877 daily.set_bar_clock(&BarClock::new(crate::model::Timeframe::Day1));
878 daily.calculate(&data);
879 assert!(daily.zones().is_empty() && daily.labels().is_empty());
880 }
881
882 #[test]
885 fn an_overnight_session_is_one_session_across_midnight() {
886 let data = bars(&[
887 (15, 21, 1.0, 50.0, 1.0, 1.0),
888 (15, 22, 10.0, 11.0, 9.0, 10.5),
889 (15, 23, 10.5, 12.0, 10.0, 11.5),
890 (16, 0, 11.5, 11.8, 10.2, 11.0),
891 (16, 1, 11.0, 13.0, 10.8, 12.6),
892 (16, 2, 1.0, 50.0, 1.0, 1.0),
893 (16, 3, 1.0, 50.0, 1.0, 1.0),
894 (16, 22, 20.0, 21.0, 19.0, 20.0),
895 ]);
896 let mut night = study("Night", 22, 2).with_labels(true, true, true);
897 night.calculate(&data);
898 assert_eq!(zones(&night), [(1..5, 13.0, 9.0, 3), (7..8, 21.0, 19.0, 3)]);
899 assert_eq!(night.labels()[0].text, "Night\nRange: 4.00\nAvg: 11.40");
900 }
901
902 #[test]
904 fn a_hidden_output_hides_its_box_and_lines() {
905 let mut study = crate::studies::ConfiguredStudy::new(Box::new(TradingSessions::new()));
906 let mut settings = study.settings();
907 settings.plots[0].visible = false;
908 study.apply_settings(&[], settings).unwrap();
909 let visible: Vec<_> = (3..9).map(|line| study.plot_style(line).visible).collect();
910 assert_eq!(visible, [false, true, true, false, true, true]);
911 }
912
913 #[test]
917 fn a_new_bar_leaves_a_finished_session_that_overlaps_another() {
918 let time = |hour| NaiveTime::from_hms_opt(hour, 0, 0).unwrap();
919 let data: Vec<Bar> = (0..27)
920 .map(|hour| {
921 let time =
922 Utc.with_ymd_and_hms(2024, 1, 15, 8, 0, 0).unwrap() + TimeDelta::hours(hour);
923 Bar::new(time, 1.5, 2.0, 1.0, 1.5, 1.0)
924 })
925 .collect();
926 let mut sessions = study("First", 8, 16)
927 .with_session(1, "Second", time(14), time(21), TimezoneId::utc())
928 .with_shown(1, true);
929 sessions.calculate(&data[..26]);
930 sessions.update(&data, 26);
931 assert_eq!(
932 zones(&sessions),
933 [
934 (0..8, 2.0, 1.0, 3),
935 (6..13, 2.0, 1.0, 4),
936 (24..27, 2.0, 1.0, 3)
937 ]
938 );
939 }
940
941 #[test]
945 fn a_session_continues_past_a_bar_without_prices() {
946 let mut data = bars(DAY);
947 let next = data.pop().unwrap();
948 let mut sessions = study("Test", 9, 13);
949 data[3] = Bar::whitespace(data[3].time);
950 sessions.calculate(&data);
951 assert_eq!(zones(&sessions), [(1..3, 13.0, 9.5, 3)]);
952 data.push(next);
953 sessions.update(&data, 4);
954 assert_eq!(zones(&sessions), [(1..5, 13.0, 8.0, 3)]);
955
956 let mut data = bars(&[
957 (15, 9, 10.0, 11.0, 9.0, 10.0),
958 (15, 10, 10.0, 11.0, 9.0, 10.0),
959 (15, 11, 10.0, 11.0, 9.0, 10.0),
960 (16, 9, 10.0, 11.0, 9.0, 10.0),
961 ]);
962 let corrected = Bar::new(data[2].time, 10.0, 20.0, 9.0, 10.0, 1.0);
963 for at in [1, 2] {
964 data[at] = Bar::whitespace(data[at].time);
965 }
966 let mut sessions = study("Test", 9, 12);
967 sessions.calculate(&data);
968 assert_eq!(
969 zones(&sessions),
970 [(0..1, 11.0, 9.0, 3), (3..4, 11.0, 9.0, 3)]
971 );
972 data[2] = corrected;
973 sessions.update(&data, 2);
974 assert_eq!(
975 zones(&sessions),
976 [(0..3, 20.0, 9.0, 3), (3..4, 11.0, 9.0, 3)]
977 );
978 }
979
980 #[test]
983 fn a_last_bar_update_reads_only_that_bar() {
984 let start = Utc.with_ymd_and_hms(2024, 1, 15, 0, 0, 0).unwrap();
985 let mut data: Vec<Bar> = (0..15_840)
986 .map(|i| {
987 let close = 100.0 + (i as f64 / 50.0).sin();
988 let time = start + TimeDelta::seconds(5 * i);
989 Bar::new(time, close, close + 0.5, close - 0.5, close, 1.0)
990 })
991 .collect();
992 let mut sessions = TradingSessions::new();
993 let calculation = std::time::Instant::now();
994 sessions.calculate(&data);
995 let calculation = calculation.elapsed();
996 let last = data.len() - 1;
997 let updates = std::time::Instant::now();
998 for tick in 0..30 {
999 data[last].close = 100.0 + tick as f64 / 100.0;
1000 sessions.update(&data, last);
1001 }
1002 assert!(updates.elapsed() < calculation, "{:?}", updates.elapsed());
1003 }
1004}