1use crate::error::StreamError;
12use crate::tick::NormalizedTick;
13use rust_decimal::Decimal;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
17pub enum Timeframe {
18 Seconds(u64),
20 Minutes(u64),
22 Hours(u64),
24}
25
26impl Timeframe {
27 pub fn duration_ms(self) -> u64 {
29 match self {
30 Timeframe::Seconds(s) => s * 1_000,
31 Timeframe::Minutes(m) => m * 60 * 1_000,
32 Timeframe::Hours(h) => h * 3600 * 1_000,
33 }
34 }
35
36 pub fn bar_start_ms(self, ts_ms: u64) -> u64 {
38 let dur = self.duration_ms();
39 (ts_ms / dur) * dur
40 }
41
42 pub fn from_duration_ms(ms: u64) -> Option<Timeframe> {
48 if ms == 0 {
49 return None;
50 }
51 if ms % 3_600_000 == 0 {
52 return Some(Timeframe::Hours(ms / 3_600_000));
53 }
54 if ms % 60_000 == 0 {
55 return Some(Timeframe::Minutes(ms / 60_000));
56 }
57 if ms % 1_000 == 0 {
58 return Some(Timeframe::Seconds(ms / 1_000));
59 }
60 None
61 }
62}
63
64impl PartialOrd for Timeframe {
65 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
66 Some(self.cmp(other))
67 }
68}
69
70impl Ord for Timeframe {
71 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
75 self.duration_ms().cmp(&other.duration_ms())
76 }
77}
78
79impl std::fmt::Display for Timeframe {
80 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81 match self {
82 Timeframe::Seconds(s) => write!(f, "{s}s"),
83 Timeframe::Minutes(m) => write!(f, "{m}m"),
84 Timeframe::Hours(h) => write!(f, "{h}h"),
85 }
86 }
87}
88
89impl std::str::FromStr for Timeframe {
90 type Err = crate::error::StreamError;
91
92 fn from_str(s: &str) -> Result<Self, Self::Err> {
104 let s = s.trim();
105 if s.is_empty() {
106 return Err(crate::error::StreamError::ConfigError {
107 reason: "timeframe string is empty".into(),
108 });
109 }
110 let (digits, suffix) = s.split_at(s.len() - 1);
111 let n: u64 = digits.parse().map_err(|_| crate::error::StreamError::ConfigError {
112 reason: format!("invalid timeframe numeric part '{digits}' in '{s}'"),
113 })?;
114 if n == 0 {
115 return Err(crate::error::StreamError::ConfigError {
116 reason: format!("timeframe value must be > 0, got '{s}'"),
117 });
118 }
119 match suffix {
120 "s" => Ok(Timeframe::Seconds(n)),
121 "m" => Ok(Timeframe::Minutes(n)),
122 "h" => Ok(Timeframe::Hours(n)),
123 other => Err(crate::error::StreamError::ConfigError {
124 reason: format!(
125 "unknown timeframe suffix '{other}' in '{s}'; expected s, m, or h"
126 ),
127 }),
128 }
129 }
130}
131
132#[derive(Debug, Clone, Copy, PartialEq, Eq)]
134pub enum BarDirection {
135 Bullish,
137 Bearish,
139 Neutral,
141}
142
143#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
145pub struct OhlcvBar {
146 pub symbol: String,
148 pub timeframe: Timeframe,
150 pub bar_start_ms: u64,
152 pub open: Decimal,
154 pub high: Decimal,
156 pub low: Decimal,
158 pub close: Decimal,
160 pub volume: Decimal,
162 pub trade_count: u64,
164 pub is_complete: bool,
166 pub is_gap_fill: bool,
171 pub vwap: Option<Decimal>,
173}
174
175impl OhlcvBar {
176 pub fn range(&self) -> Decimal {
178 self.high - self.low
179 }
180
181 pub fn body(&self) -> Decimal {
185 (self.close - self.open).abs()
186 }
187
188 pub fn body_high(&self) -> Decimal {
192 self.open.max(self.close)
193 }
194
195 pub fn body_low(&self) -> Decimal {
199 self.open.min(self.close)
200 }
201
202 pub fn is_bullish(&self) -> bool {
204 self.close > self.open
205 }
206
207 pub fn is_bearish(&self) -> bool {
209 self.close < self.open
210 }
211
212 pub fn has_upper_wick(&self) -> bool {
214 self.wick_upper() > Decimal::ZERO
215 }
216
217 pub fn has_lower_wick(&self) -> bool {
219 self.wick_lower() > Decimal::ZERO
220 }
221
222 pub fn body_direction(&self) -> BarDirection {
227 use std::cmp::Ordering;
228 match self.close.cmp(&self.open) {
229 Ordering::Greater => BarDirection::Bullish,
230 Ordering::Less => BarDirection::Bearish,
231 Ordering::Equal => BarDirection::Neutral,
232 }
233 }
234
235 pub fn is_doji(&self, epsilon: Decimal) -> bool {
240 self.body() <= epsilon
241 }
242
243 pub fn wick_upper(&self) -> Decimal {
247 self.high - self.body_high()
248 }
249
250 pub fn wick_lower(&self) -> Decimal {
254 self.body_low() - self.low
255 }
256
257 pub fn price_change(&self) -> Decimal {
262 self.close - self.open
263 }
264
265 pub fn typical_price(&self) -> Decimal {
270 (self.high + self.low + self.close) / Decimal::from(3)
271 }
272
273 pub fn close_location_value(&self) -> Option<f64> {
281 use rust_decimal::prelude::ToPrimitive;
282 let range = self.range();
283 if range.is_zero() {
284 return None;
285 }
286 ((self.close - self.low - (self.high - self.close)) / range).to_f64()
287 }
288
289 pub fn median_price(&self) -> Decimal {
293 (self.high + self.low) / Decimal::from(2)
294 }
295
296 pub fn weighted_close(&self) -> Decimal {
301 (self.high + self.low + self.close + self.close) / Decimal::from(4)
302 }
303
304 pub fn price_change_pct(&self) -> Option<f64> {
309 use rust_decimal::prelude::ToPrimitive;
310 if self.open.is_zero() {
311 return None;
312 }
313 let pct = self.price_change() / self.open * Decimal::from(100);
314 pct.to_f64()
315 }
316
317 pub fn body_ratio(&self) -> Option<f64> {
323 use rust_decimal::prelude::ToPrimitive;
324 let range = self.range();
325 if range.is_zero() {
326 return None;
327 }
328 (self.body() / range).to_f64()
329 }
330
331 pub fn true_range(&self, prev_close: Decimal) -> Decimal {
336 let hl = self.range();
337 let hpc = (self.high - prev_close).abs();
338 let lpc = (self.low - prev_close).abs();
339 hl.max(hpc).max(lpc)
340 }
341
342 #[deprecated(since = "2.2.0", note = "Use `is_inside_bar` instead")]
348 pub fn inside_bar(&self, prev: &OhlcvBar) -> bool {
349 self.is_inside_bar(prev)
350 }
351
352 pub fn outside_bar(&self, prev: &OhlcvBar) -> bool {
357 self.high > prev.high && self.low < prev.low
358 }
359
360 pub fn wick_ratio(&self) -> Option<f64> {
365 use rust_decimal::prelude::ToPrimitive;
366 let range = self.range();
367 if range.is_zero() {
368 return None;
369 }
370 ((self.wick_upper() + self.wick_lower()) / range).to_f64()
371 }
372
373 pub fn is_hammer(&self) -> bool {
382 let range = self.range();
383 if range.is_zero() {
384 return false;
385 }
386 let body = self.body();
387 let wick_lo = self.wick_lower();
388 let wick_hi = self.wick_upper();
389 let three = Decimal::from(3);
390 let six = Decimal::from(6);
391 let ten = Decimal::from(10);
392 body * ten <= range * three
396 && wick_lo * ten >= range * six
397 && wick_hi * ten <= range
398 }
399
400 pub fn is_shooting_star(&self) -> bool {
410 let range = self.range();
411 if range.is_zero() {
412 return false;
413 }
414 let body = self.body();
415 let wick_lo = self.wick_lower();
416 let wick_hi = self.wick_upper();
417 let three = Decimal::from(3);
418 let six = Decimal::from(6);
419 let ten = Decimal::from(10);
420 body * ten <= range * three
424 && wick_hi * ten >= range * six
425 && wick_lo * ten <= range
426 }
427
428 pub fn gap_from(&self, prev: &OhlcvBar) -> Decimal {
433 self.open - prev.close
434 }
435
436 pub fn is_gap_up(&self, prev: &OhlcvBar) -> bool {
438 self.open > prev.close
439 }
440
441 pub fn is_gap_down(&self, prev: &OhlcvBar) -> bool {
443 self.open < prev.close
444 }
445
446 pub fn bar_midpoint(&self) -> Decimal {
451 (self.open + self.close) / Decimal::from(2)
452 }
453
454 pub fn body_to_range_ratio(&self) -> Option<Decimal> {
458 let r = self.range();
459 if r.is_zero() {
460 return None;
461 }
462 Some(self.body() / r)
463 }
464
465 pub fn is_long_upper_wick(&self) -> bool {
469 self.wick_upper() > self.body()
470 }
471
472 pub fn is_long_lower_wick(&self) -> bool {
476 self.wick_lower() > self.body()
477 }
478
479 #[deprecated(since = "2.2.0", note = "Use `body()` instead")]
483 pub fn price_change_abs(&self) -> Decimal {
484 self.body()
485 }
486
487 pub fn upper_shadow(&self) -> Decimal {
491 self.wick_upper()
492 }
493
494 pub fn lower_shadow(&self) -> Decimal {
498 self.wick_lower()
499 }
500
501 pub fn is_spinning_top(&self, body_pct: Decimal) -> bool {
510 let range = self.range();
511 if range.is_zero() {
512 return false;
513 }
514 let body = self.body();
515 let max_body = range * body_pct;
516 body <= max_body && self.wick_upper() > body && self.wick_lower() > body
517 }
518
519 pub fn hlc3(&self) -> Decimal {
521 self.typical_price()
522 }
523
524 pub fn ohlc4(&self) -> Decimal {
529 (self.open + self.high + self.low + self.close) / Decimal::from(4)
530 }
531
532 pub fn is_marubozu(&self) -> bool {
539 self.wick_upper().is_zero() && self.wick_lower().is_zero()
540 }
541
542 pub fn is_engulfing(&self, prev: &OhlcvBar) -> bool {
553 self.body_low() < prev.body_low() && self.body_high() > prev.body_high()
554 }
555
556 pub fn is_harami(&self, prev: &OhlcvBar) -> bool {
562 self.body_low() > prev.body_low() && self.body_high() < prev.body_high()
563 }
564
565 pub fn tail_length(&self) -> Decimal {
570 self.wick_upper().max(self.wick_lower())
571 }
572
573 pub fn is_inside_bar(&self, prev: &OhlcvBar) -> bool {
579 self.high < prev.high && self.low > prev.low
580 }
581
582 pub fn gap_up(&self, prev: &OhlcvBar) -> bool {
584 self.open > prev.high
585 }
586
587 pub fn gap_down(&self, prev: &OhlcvBar) -> bool {
589 self.open < prev.low
590 }
591
592 #[deprecated(since = "2.2.0", note = "Use `body()` instead")]
596 pub fn body_size(&self) -> Decimal {
597 self.body()
598 }
599
600 pub fn volume_delta(&self, prev: &OhlcvBar) -> Decimal {
602 self.volume - prev.volume
603 }
604
605 pub fn is_consolidating(&self, prev: &OhlcvBar) -> bool {
609 let prev_range = prev.range();
610 if prev_range.is_zero() {
611 return false;
612 }
613 self.range() < prev_range / Decimal::TWO
614 }
615
616 pub fn mean_volume(bars: &[OhlcvBar]) -> Option<Decimal> {
620 if bars.is_empty() {
621 return None;
622 }
623 Some(Self::sum_volume(bars) / Decimal::from(bars.len() as u64))
624 }
625
626 pub fn vwap_deviation(&self) -> Option<f64> {
630 use rust_decimal::prelude::ToPrimitive;
631 let vwap = self.vwap?;
632 if vwap.is_zero() {
633 return None;
634 }
635 ((self.close - vwap).abs() / vwap).to_f64()
636 }
637
638 pub fn relative_volume(&self, avg_volume: Decimal) -> Option<f64> {
642 use rust_decimal::prelude::ToPrimitive;
643 if avg_volume.is_zero() {
644 return None;
645 }
646 (self.volume / avg_volume).to_f64()
647 }
648
649 pub fn intraday_reversal(&self, prev: &OhlcvBar) -> bool {
655 let prev_bullish = prev.close > prev.open;
656 let this_bearish = self.close < self.open;
657 let prev_bearish = prev.close < prev.open;
658 let this_bullish = self.close > self.open;
659 (prev_bullish && this_bearish && self.open >= prev.close)
660 || (prev_bearish && this_bullish && self.open <= prev.close)
661 }
662
663 pub fn range_pct(&self) -> Option<f64> {
667 use rust_decimal::prelude::ToPrimitive;
668 if self.open.is_zero() {
669 return None;
670 }
671 let range = self.range() / self.open;
672 range.to_f64().map(|v| v * 100.0)
673 }
674
675 #[deprecated(since = "2.2.0", note = "Use `outside_bar()` instead")]
680 pub fn is_outside_bar(&self, prev: &OhlcvBar) -> bool {
681 self.outside_bar(prev)
682 }
683
684 pub fn high_low_midpoint(&self) -> Decimal {
688 self.median_price()
689 }
690
691 pub fn high_close_ratio(&self) -> Option<f64> {
696 use rust_decimal::prelude::ToPrimitive;
697 if self.high.is_zero() {
698 return None;
699 }
700 (self.close / self.high).to_f64()
701 }
702
703 pub fn lower_shadow_pct(&self) -> Option<f64> {
707 use rust_decimal::prelude::ToPrimitive;
708 let range = self.range();
709 if range.is_zero() {
710 return None;
711 }
712 (self.lower_shadow() / range).to_f64()
713 }
714
715 pub fn open_close_ratio(&self) -> Option<f64> {
719 use rust_decimal::prelude::ToPrimitive;
720 if self.open.is_zero() {
721 return None;
722 }
723 (self.close / self.open).to_f64()
724 }
725
726 pub fn is_wide_range_bar(&self, threshold: Decimal) -> bool {
728 self.range() > threshold
729 }
730
731 pub fn close_to_low_ratio(&self) -> Option<f64> {
737 use rust_decimal::prelude::ToPrimitive;
738 let range = self.range();
739 if range.is_zero() {
740 return None;
741 }
742 ((self.close - self.low) / range).to_f64()
743 }
744
745 pub fn volume_per_trade(&self) -> Option<Decimal> {
749 if self.trade_count == 0 {
750 return None;
751 }
752 Some(self.volume / Decimal::from(self.trade_count as u64))
753 }
754
755 pub fn price_range_overlap(&self, other: &OhlcvBar) -> bool {
759 self.high >= other.low && other.high >= self.low
760 }
761
762 pub fn bar_height_pct(&self) -> Option<f64> {
767 use rust_decimal::prelude::ToPrimitive;
768 if self.open.is_zero() {
769 return None;
770 }
771 (self.range() / self.open).to_f64()
772 }
773
774 pub fn bar_type(&self) -> &'static str {
779 if self.close == self.open {
780 "doji"
781 } else if self.close > self.open {
782 "bullish"
783 } else {
784 "bearish"
785 }
786 }
787
788 pub fn body_pct(&self) -> Option<Decimal> {
793 let range = self.range();
794 if range.is_zero() {
795 return None;
796 }
797 Some(self.body() / range * Decimal::ONE_HUNDRED)
798 }
799
800 pub fn is_bullish_hammer(&self) -> bool {
806 let body = self.body();
807 if body.is_zero() {
808 return false;
809 }
810 let lower = self.wick_lower();
811 let upper = self.wick_upper();
812 lower >= body * Decimal::TWO && upper <= body
813 }
814
815 pub fn upper_wick_pct(&self) -> Option<Decimal> {
819 let range = self.range();
820 if range.is_zero() {
821 return None;
822 }
823 Some(self.wick_upper() / range * Decimal::ONE_HUNDRED)
824 }
825
826 pub fn lower_wick_pct(&self) -> Option<Decimal> {
830 let range = self.range();
831 if range.is_zero() {
832 return None;
833 }
834 Some(self.wick_lower() / range * Decimal::ONE_HUNDRED)
835 }
836
837 pub fn is_bearish_engulfing(&self, prev: &OhlcvBar) -> bool {
841 self.is_bearish() && self.is_engulfing(prev)
842 }
843
844 pub fn is_bullish_engulfing(&self, prev: &OhlcvBar) -> bool {
848 self.is_bullish() && self.is_engulfing(prev)
849 }
850
851 #[deprecated(since = "2.2.0", note = "Use `gap_from()` instead")]
856 pub fn close_gap(&self, prev: &OhlcvBar) -> Decimal {
857 self.gap_from(prev)
858 }
859
860 pub fn close_above_midpoint(&self) -> bool {
862 self.close > self.high_low_midpoint()
863 }
864
865 pub fn close_momentum(&self, prev: &OhlcvBar) -> Decimal {
869 self.close - prev.close
870 }
871
872 #[deprecated(since = "2.2.0", note = "Use `range()` instead")]
876 pub fn bar_range(&self) -> Decimal {
877 self.range()
878 }
879
880 pub fn bar_duration_ms(&self) -> u64 {
882 self.timeframe.duration_ms()
883 }
884
885 pub fn is_gravestone_doji(&self, epsilon: Decimal) -> bool {
890 self.body() <= epsilon && (self.close - self.low).abs() <= epsilon
891 }
892
893 pub fn is_dragonfly_doji(&self, epsilon: Decimal) -> bool {
898 self.body() <= epsilon && (self.high - self.close).abs() <= epsilon
899 }
900
901 pub fn is_flat(&self) -> bool {
906 self.range().is_zero()
907 }
908
909 #[deprecated(since = "2.2.0", note = "Use `true_range()` instead")]
913 pub fn true_range_with_prev(&self, prev_close: Decimal) -> Decimal {
914 self.true_range(prev_close)
915 }
916
917 #[deprecated(since = "2.2.0", note = "Use `high_close_ratio()` instead")]
921 pub fn close_to_high_ratio(&self) -> Option<f64> {
922 self.high_close_ratio()
923 }
924
925 #[deprecated(since = "2.2.0", note = "Use `open_close_ratio()` instead")]
929 pub fn close_open_ratio(&self) -> Option<f64> {
930 self.open_close_ratio()
931 }
932
933 pub fn price_at_pct(&self, pct: f64) -> Decimal {
938 use rust_decimal::prelude::FromPrimitive;
939 let pct_clamped = pct.clamp(0.0, 1.0);
940 let factor = Decimal::from_f64(pct_clamped).unwrap_or(Decimal::ZERO);
941 self.low + self.range() * factor
942 }
943
944 pub fn average_true_range(bars: &[OhlcvBar]) -> Option<Decimal> {
950 if bars.len() < 2 {
951 return None;
952 }
953 let sum: Decimal = (1..bars.len())
954 .map(|i| bars[i].true_range(bars[i - 1].close))
955 .sum();
956 Some(sum / Decimal::from((bars.len() - 1) as u64))
957 }
958
959 pub fn average_body(bars: &[OhlcvBar]) -> Option<Decimal> {
963 if bars.is_empty() {
964 return None;
965 }
966 let sum: Decimal = bars.iter().map(|b| b.body()).sum();
967 Some(sum / Decimal::from(bars.len() as u64))
968 }
969
970 pub fn highest_high(bars: &[OhlcvBar]) -> Option<Decimal> {
975 bars.iter().map(|b| b.high).reduce(Decimal::max)
976 }
977
978 pub fn lowest_low(bars: &[OhlcvBar]) -> Option<Decimal> {
983 bars.iter().map(|b| b.low).reduce(Decimal::min)
984 }
985
986 pub fn highest_close(bars: &[OhlcvBar]) -> Option<Decimal> {
991 bars.iter().map(|b| b.close).reduce(Decimal::max)
992 }
993
994 pub fn lowest_close(bars: &[OhlcvBar]) -> Option<Decimal> {
999 bars.iter().map(|b| b.close).reduce(Decimal::min)
1000 }
1001
1002 pub fn close_range(bars: &[OhlcvBar]) -> Option<Decimal> {
1006 let hi = Self::highest_close(bars)?;
1007 let lo = Self::lowest_close(bars)?;
1008 Some(hi - lo)
1009 }
1010
1011 pub fn momentum(bars: &[OhlcvBar], n: usize) -> Option<f64> {
1016 use rust_decimal::prelude::ToPrimitive;
1017 let len = bars.len();
1018 if len <= n {
1019 return None;
1020 }
1021 let current = bars[len - 1].close;
1022 let prior = bars[len - 1 - n].close;
1023 if prior.is_zero() {
1024 return None;
1025 }
1026 ((current - prior) / prior).to_f64()
1027 }
1028
1029 pub fn sum_volume(bars: &[OhlcvBar]) -> Decimal {
1035 bars.iter().map(|b| b.volume).sum()
1036 }
1037
1038 pub fn bullish_count(bars: &[OhlcvBar]) -> usize {
1040 bars.iter().filter(|b| b.is_bullish()).count()
1041 }
1042
1043 pub fn bearish_count(bars: &[OhlcvBar]) -> usize {
1045 bars.iter().filter(|b| b.is_bearish()).count()
1046 }
1047
1048 pub fn bullish_streak(bars: &[OhlcvBar]) -> usize {
1053 bars.iter().rev().take_while(|b| b.is_bullish()).count()
1054 }
1055
1056 pub fn bearish_streak(bars: &[OhlcvBar]) -> usize {
1061 bars.iter().rev().take_while(|b| b.is_bearish()).count()
1062 }
1063
1064 pub fn win_rate(bars: &[OhlcvBar]) -> Option<f64> {
1068 if bars.is_empty() {
1069 return None;
1070 }
1071 Some(Self::bullish_count(bars) as f64 / bars.len() as f64)
1072 }
1073
1074 pub fn max_drawdown(bars: &[OhlcvBar]) -> Option<f64> {
1082 use rust_decimal::prelude::ToPrimitive;
1083 if bars.len() < 2 {
1084 return None;
1085 }
1086 let mut peak = bars[0].close;
1087 let mut max_dd = 0.0_f64;
1088 for bar in &bars[1..] {
1089 if bar.close > peak {
1090 peak = bar.close;
1091 } else if !peak.is_zero() {
1092 let dd = ((peak - bar.close) / peak).to_f64().unwrap_or(0.0);
1093 if dd > max_dd {
1094 max_dd = dd;
1095 }
1096 }
1097 }
1098 Some(max_dd)
1099 }
1100
1101 pub fn linear_regression_slope(bars: &[OhlcvBar]) -> Option<f64> {
1110 use rust_decimal::prelude::ToPrimitive;
1111 let ys: Vec<f64> = bars.iter().filter_map(|b| b.close.to_f64()).collect();
1112 Self::ols_slope_indexed(&ys, bars.len())
1113 }
1114
1115 pub fn volume_slope(bars: &[OhlcvBar]) -> Option<f64> {
1120 use rust_decimal::prelude::ToPrimitive;
1121 let ys: Vec<f64> = bars.iter().filter_map(|b| b.volume.to_f64()).collect();
1122 Self::ols_slope_indexed(&ys, bars.len())
1123 }
1124
1125 fn ols_slope_indexed(ys: &[f64], expected_n: usize) -> Option<f64> {
1129 if ys.len() < expected_n || expected_n < 2 {
1130 return None;
1131 }
1132 let n_f = expected_n as f64;
1133 let x_mean = (n_f - 1.0) / 2.0;
1134 let y_mean = ys.iter().sum::<f64>() / n_f;
1135 let numerator: f64 = ys.iter().enumerate().map(|(i, y)| (i as f64 - x_mean) * (y - y_mean)).sum();
1136 let denominator: f64 = ys.iter().enumerate().map(|(i, _)| (i as f64 - x_mean).powi(2)).sum();
1137 if denominator == 0.0 {
1138 return None;
1139 }
1140 Some(numerator / denominator)
1141 }
1142
1143 pub fn mean_close(bars: &[OhlcvBar]) -> Option<Decimal> {
1147 if bars.is_empty() {
1148 return None;
1149 }
1150 let sum: Decimal = bars.iter().map(|b| b.close).sum();
1151 Some(sum / Decimal::from(bars.len() as u64))
1152 }
1153
1154 pub fn close_std_dev(bars: &[OhlcvBar]) -> Option<f64> {
1159 use rust_decimal::prelude::ToPrimitive;
1160 let n = bars.len();
1161 if n < 2 {
1162 return None;
1163 }
1164 let mean = Self::mean_close(bars)?.to_f64()?;
1165 let variance: f64 = bars.iter()
1166 .filter_map(|b| b.close.to_f64())
1167 .map(|c| (c - mean).powi(2))
1168 .sum::<f64>() / n as f64;
1169 Some(variance.sqrt())
1170 }
1171
1172 pub fn price_efficiency_ratio(bars: &[OhlcvBar]) -> Option<f64> {
1178 use rust_decimal::prelude::ToPrimitive;
1179 let n = bars.len();
1180 if n < 2 {
1181 return None;
1182 }
1183 let net_move = (bars[n - 1].close - bars[0].close).abs();
1184 let total_path: Decimal = bars.iter().map(|b| b.range()).sum();
1185 if total_path.is_zero() {
1186 return None;
1187 }
1188 (net_move / total_path).to_f64()
1189 }
1190
1191 pub fn mean_clv(bars: &[OhlcvBar]) -> Option<f64> {
1194 if bars.is_empty() {
1195 return None;
1196 }
1197 let clvs: Vec<f64> = bars.iter().filter_map(|b| b.close_location_value()).collect();
1198 if clvs.is_empty() {
1199 return None;
1200 }
1201 Some(clvs.iter().sum::<f64>() / clvs.len() as f64)
1202 }
1203
1204 pub fn mean_range(bars: &[OhlcvBar]) -> Option<Decimal> {
1208 if bars.is_empty() {
1209 return None;
1210 }
1211 let total: Decimal = bars.iter().map(|b| b.range()).sum();
1212 Some(total / Decimal::from(bars.len() as u64))
1213 }
1214
1215 pub fn close_z_score(bars: &[OhlcvBar], value: Decimal) -> Option<f64> {
1220 use rust_decimal::prelude::ToPrimitive;
1221 let mean = Self::mean_close(bars)?;
1222 let std_dev = Self::close_std_dev(bars)?;
1223 if std_dev == 0.0 {
1224 return None;
1225 }
1226 ((value - mean) / Decimal::try_from(std_dev).ok()?).to_f64()
1227 }
1228
1229 pub fn bollinger_band_width(bars: &[OhlcvBar]) -> Option<f64> {
1233 use rust_decimal::prelude::ToPrimitive;
1234 let mean = Self::mean_close(bars)?;
1235 if mean.is_zero() {
1236 return None;
1237 }
1238 let std_dev = Self::close_std_dev(bars)?;
1239 let width = 2.0 * std_dev / mean.to_f64()?;
1240 Some(width)
1241 }
1242
1243 pub fn up_down_ratio(bars: &[OhlcvBar]) -> Option<f64> {
1247 let down = Self::bearish_count(bars);
1248 if down == 0 {
1249 return None;
1250 }
1251 Some(Self::bullish_count(bars) as f64 / down as f64)
1252 }
1253
1254 pub fn volume_weighted_close(bars: &[OhlcvBar]) -> Option<Decimal> {
1258 let total_volume = Self::sum_volume(bars);
1259 if total_volume.is_zero() {
1260 return None;
1261 }
1262 let weighted_sum: Decimal = bars.iter().map(|b| b.close * b.volume).sum();
1263 Some(weighted_sum / total_volume)
1264 }
1265
1266 pub fn rolling_return(bars: &[OhlcvBar]) -> Option<f64> {
1271 use rust_decimal::prelude::ToPrimitive;
1272 let n = bars.len();
1273 if n < 2 {
1274 return None;
1275 }
1276 let first = bars[0].close;
1277 let last = bars[n - 1].close;
1278 if first.is_zero() {
1279 return None;
1280 }
1281 ((last - first) / first).to_f64()
1282 }
1283
1284 pub fn average_high(bars: &[OhlcvBar]) -> Option<Decimal> {
1288 if bars.is_empty() {
1289 return None;
1290 }
1291 let total: Decimal = bars.iter().map(|b| b.high).sum();
1292 Some(total / Decimal::from(bars.len() as u64))
1293 }
1294
1295 pub fn average_low(bars: &[OhlcvBar]) -> Option<Decimal> {
1299 if bars.is_empty() {
1300 return None;
1301 }
1302 let total: Decimal = bars.iter().map(|b| b.low).sum();
1303 Some(total / Decimal::from(bars.len() as u64))
1304 }
1305
1306 pub fn min_body(bars: &[OhlcvBar]) -> Option<Decimal> {
1310 bars.iter().map(|b| b.body()).reduce(Decimal::min)
1311 }
1312
1313 pub fn max_body(bars: &[OhlcvBar]) -> Option<Decimal> {
1317 bars.iter().map(|b| b.body()).reduce(Decimal::max)
1318 }
1319
1320 pub fn atr_pct(bars: &[OhlcvBar]) -> Option<f64> {
1324 use rust_decimal::prelude::ToPrimitive;
1325 let atr = Self::average_true_range(bars)?;
1326 let mean = Self::mean_close(bars)?;
1327 if mean.is_zero() {
1328 return None;
1329 }
1330 (atr / mean).to_f64()
1331 }
1332
1333 pub fn breakout_count(bars: &[OhlcvBar]) -> usize {
1336 if bars.len() < 2 {
1337 return 0;
1338 }
1339 bars.windows(2)
1340 .filter(|w| w[1].close > w[0].high)
1341 .count()
1342 }
1343
1344 pub fn doji_count(bars: &[OhlcvBar], epsilon: Decimal) -> usize {
1346 bars.iter().filter(|b| b.is_doji(epsilon)).count()
1347 }
1348
1349 pub fn channel_width(bars: &[OhlcvBar]) -> Option<Decimal> {
1353 let hi = Self::highest_high(bars)?;
1354 let lo = Self::lowest_low(bars)?;
1355 Some(hi - lo)
1356 }
1357
1358 pub fn sma(bars: &[OhlcvBar], n: usize) -> Option<Decimal> {
1362 if n == 0 || bars.len() < n {
1363 return None;
1364 }
1365 let window = &bars[bars.len() - n..];
1366 let sum: Decimal = window.iter().map(|b| b.close).sum();
1367 Some(sum / Decimal::from(n as u64))
1368 }
1369
1370 pub fn mean_wick_ratio(bars: &[OhlcvBar]) -> Option<f64> {
1375 let ratios: Vec<f64> = bars.iter().filter_map(|b| b.wick_ratio()).collect();
1376 if ratios.is_empty() {
1377 return None;
1378 }
1379 Some(ratios.iter().sum::<f64>() / ratios.len() as f64)
1380 }
1381
1382 pub fn bullish_volume(bars: &[OhlcvBar]) -> Decimal {
1384 bars.iter().filter(|b| b.is_bullish()).map(|b| b.volume).sum()
1385 }
1386
1387 pub fn bearish_volume(bars: &[OhlcvBar]) -> Decimal {
1389 bars.iter().filter(|b| b.is_bearish()).map(|b| b.volume).sum()
1390 }
1391
1392 pub fn close_above_mid_count(bars: &[OhlcvBar]) -> usize {
1394 bars.iter().filter(|b| b.close > b.high_low_midpoint()).count()
1395 }
1396
1397 pub fn ema(bars: &[OhlcvBar], alpha: f64) -> Option<f64> {
1403 use rust_decimal::prelude::ToPrimitive;
1404 let alpha = alpha.clamp(1e-9, 1.0);
1405 let mut iter = bars.iter();
1406 let first = iter.next()?.close.to_f64()?;
1407 let result = iter.fold(first, |acc, b| {
1408 let c = b.close.to_f64().unwrap_or(acc);
1409 alpha * c + (1.0 - alpha) * acc
1410 });
1411 Some(result)
1412 }
1413
1414 pub fn highest_open(bars: &[OhlcvBar]) -> Option<Decimal> {
1418 bars.iter().map(|b| b.open).reduce(Decimal::max)
1419 }
1420
1421 pub fn lowest_open(bars: &[OhlcvBar]) -> Option<Decimal> {
1425 bars.iter().map(|b| b.open).reduce(Decimal::min)
1426 }
1427
1428 pub fn rising_close_count(bars: &[OhlcvBar]) -> usize {
1431 if bars.len() < 2 {
1432 return 0;
1433 }
1434 bars.windows(2).filter(|w| w[1].close > w[0].close).count()
1435 }
1436
1437 pub fn mean_body_ratio(bars: &[OhlcvBar]) -> Option<f64> {
1442 let ratios: Vec<f64> = bars.iter().filter_map(|b| b.body_ratio()).collect();
1443 if ratios.is_empty() {
1444 return None;
1445 }
1446 Some(ratios.iter().sum::<f64>() / ratios.len() as f64)
1447 }
1448
1449 pub fn volume_std_dev(bars: &[OhlcvBar]) -> Option<f64> {
1453 use rust_decimal::prelude::ToPrimitive;
1454 let n = bars.len();
1455 if n < 2 {
1456 return None;
1457 }
1458 let vols: Vec<f64> = bars.iter().filter_map(|b| b.volume.to_f64()).collect();
1459 if vols.len() < 2 {
1460 return None;
1461 }
1462 let mean = vols.iter().sum::<f64>() / vols.len() as f64;
1463 let variance = vols.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / (vols.len() - 1) as f64;
1464 Some(variance.sqrt())
1465 }
1466
1467 pub fn max_volume_bar(bars: &[OhlcvBar]) -> Option<&OhlcvBar> {
1471 bars.iter().max_by(|a, b| a.volume.cmp(&b.volume))
1472 }
1473
1474 pub fn min_volume_bar(bars: &[OhlcvBar]) -> Option<&OhlcvBar> {
1478 bars.iter().min_by(|a, b| a.volume.cmp(&b.volume))
1479 }
1480
1481 pub fn gap_sum(bars: &[OhlcvBar]) -> Decimal {
1485 if bars.len() < 2 {
1486 return Decimal::ZERO;
1487 }
1488 bars.windows(2).map(|w| w[1].open - w[0].close).sum()
1489 }
1490
1491 pub fn three_white_soldiers(bars: &[OhlcvBar]) -> bool {
1494 if bars.len() < 3 {
1495 return false;
1496 }
1497 let last3 = &bars[bars.len() - 3..];
1498 last3[0].close > last3[0].open
1499 && last3[1].close > last3[1].open
1500 && last3[2].close > last3[2].open
1501 && last3[1].close > last3[0].close
1502 && last3[2].close > last3[1].close
1503 }
1504
1505 pub fn three_black_crows(bars: &[OhlcvBar]) -> bool {
1508 if bars.len() < 3 {
1509 return false;
1510 }
1511 let last3 = &bars[bars.len() - 3..];
1512 last3[0].close < last3[0].open
1513 && last3[1].close < last3[1].open
1514 && last3[2].close < last3[2].open
1515 && last3[1].close < last3[0].close
1516 && last3[2].close < last3[1].close
1517 }
1518
1519 pub fn is_gap_bar(bar: &OhlcvBar, prev_close: Decimal) -> bool {
1522 bar.open != prev_close
1523 }
1524
1525 pub fn gap_bars_count(bars: &[OhlcvBar]) -> usize {
1527 if bars.len() < 2 {
1528 return 0;
1529 }
1530 bars.windows(2).filter(|w| w[1].open != w[0].close).count()
1531 }
1532
1533 pub fn bar_efficiency(bar: &OhlcvBar) -> Option<f64> {
1537 use rust_decimal::prelude::ToPrimitive;
1538 let range = bar.range();
1539 if range.is_zero() {
1540 return None;
1541 }
1542 (bar.body() / range).to_f64()
1543 }
1544
1545 pub fn wicks_sum(bars: &[OhlcvBar]) -> Decimal {
1549 bars.iter().map(|b| b.wick_upper() + b.wick_lower()).sum()
1550 }
1551
1552 pub fn avg_close_to_high(bars: &[OhlcvBar]) -> Option<f64> {
1556 use rust_decimal::prelude::ToPrimitive;
1557 if bars.is_empty() {
1558 return None;
1559 }
1560 let sum: Decimal = bars.iter().map(|b| b.high - b.close).sum();
1561 (sum / Decimal::from(bars.len() as u32)).to_f64()
1562 }
1563
1564 pub fn avg_range(bars: &[OhlcvBar]) -> Option<f64> {
1568 use rust_decimal::prelude::ToPrimitive;
1569 if bars.is_empty() {
1570 return None;
1571 }
1572 let sum: Decimal = bars.iter().map(|b| b.range()).sum();
1573 (sum / Decimal::from(bars.len() as u32)).to_f64()
1574 }
1575
1576 pub fn max_close(bars: &[OhlcvBar]) -> Option<Decimal> {
1580 bars.iter().map(|b| b.close).reduce(Decimal::max)
1581 }
1582
1583 pub fn min_close(bars: &[OhlcvBar]) -> Option<Decimal> {
1587 bars.iter().map(|b| b.close).reduce(Decimal::min)
1588 }
1589
1590 pub fn trend_strength(bars: &[OhlcvBar]) -> Option<f64> {
1594 if bars.len() < 2 {
1595 return None;
1596 }
1597 let moves = bars.len() - 1;
1598 let up = bars.windows(2).filter(|w| w[1].close > w[0].close).count();
1599 Some(up as f64 / moves as f64)
1600 }
1601
1602 pub fn net_change(bars: &[OhlcvBar]) -> Option<Decimal> {
1606 bars.last().map(|b| b.price_change())
1607 }
1608
1609 pub fn open_to_close_pct(bars: &[OhlcvBar]) -> Option<f64> {
1613 use rust_decimal::prelude::ToPrimitive;
1614 let bar = bars.last()?;
1615 if bar.open.is_zero() {
1616 return None;
1617 }
1618 (bar.price_change() / bar.open * Decimal::ONE_HUNDRED).to_f64()
1619 }
1620
1621 pub fn high_to_low_pct(bars: &[OhlcvBar]) -> Option<f64> {
1625 use rust_decimal::prelude::ToPrimitive;
1626 let bar = bars.last()?;
1627 if bar.high.is_zero() {
1628 return None;
1629 }
1630 (bar.range() / bar.high * Decimal::ONE_HUNDRED).to_f64()
1631 }
1632
1633 pub fn consecutive_highs(bars: &[OhlcvBar]) -> usize {
1635 if bars.len() < 2 {
1636 return 0;
1637 }
1638 let mut count = 0;
1639 for w in bars.windows(2).rev() {
1640 if w[1].high > w[0].high {
1641 count += 1;
1642 } else {
1643 break;
1644 }
1645 }
1646 count
1647 }
1648
1649 pub fn consecutive_lows(bars: &[OhlcvBar]) -> usize {
1651 if bars.len() < 2 {
1652 return 0;
1653 }
1654 let mut count = 0;
1655 for w in bars.windows(2).rev() {
1656 if w[1].low < w[0].low {
1657 count += 1;
1658 } else {
1659 break;
1660 }
1661 }
1662 count
1663 }
1664
1665 pub fn volume_change_pct(bars: &[OhlcvBar]) -> Option<f64> {
1669 use rust_decimal::prelude::ToPrimitive;
1670 if bars.len() < 2 {
1671 return None;
1672 }
1673 let prior = bars[bars.len() - 2].volume;
1674 if prior.is_zero() {
1675 return None;
1676 }
1677 let current = bars[bars.len() - 1].volume;
1678 ((current - prior) / prior * Decimal::ONE_HUNDRED).to_f64()
1679 }
1680
1681 pub fn open_gap_pct(bars: &[OhlcvBar]) -> Option<f64> {
1685 use rust_decimal::prelude::ToPrimitive;
1686 if bars.len() < 2 {
1687 return None;
1688 }
1689 let prev_close = bars[bars.len() - 2].close;
1690 if prev_close.is_zero() {
1691 return None;
1692 }
1693 let current_open = bars[bars.len() - 1].open;
1694 ((current_open - prev_close) / prev_close * Decimal::ONE_HUNDRED).to_f64()
1695 }
1696
1697 pub fn volume_cumulative(bars: &[OhlcvBar]) -> Decimal {
1699 bars.iter().map(|b| b.volume).sum()
1700 }
1701
1702 pub fn price_position(bars: &[OhlcvBar]) -> Option<f64> {
1707 use rust_decimal::prelude::ToPrimitive;
1708 let hi = Self::highest_high(bars)?;
1709 let lo = Self::lowest_low(bars)?;
1710 let range = hi - lo;
1711 if range.is_zero() {
1712 return None;
1713 }
1714 let last_close = bars.last()?.close;
1715 ((last_close - lo) / range).to_f64()
1716 }
1717
1718 pub fn is_trending_up(bars: &[OhlcvBar], n: usize) -> bool {
1723 if n < 2 || bars.len() < n {
1724 return false;
1725 }
1726 bars[bars.len() - n..].windows(2).all(|w| w[1].close > w[0].close)
1727 }
1728
1729 pub fn is_trending_down(bars: &[OhlcvBar], n: usize) -> bool {
1734 if n < 2 || bars.len() < n {
1735 return false;
1736 }
1737 bars[bars.len() - n..].windows(2).all(|w| w[1].close < w[0].close)
1738 }
1739
1740 pub fn volume_acceleration(bars: &[OhlcvBar]) -> Option<f64> {
1745 use rust_decimal::prelude::ToPrimitive;
1746 if bars.len() < 2 {
1747 return None;
1748 }
1749 let prev = bars[bars.len() - 2].volume;
1750 if prev.is_zero() {
1751 return None;
1752 }
1753 let curr = bars[bars.len() - 1].volume;
1754 ((curr - prev) / prev * Decimal::ONE_HUNDRED).to_f64()
1755 }
1756
1757 pub fn wick_body_ratio(bars: &[OhlcvBar]) -> Option<f64> {
1762 use rust_decimal::prelude::ToPrimitive;
1763 let valid: Vec<f64> = bars.iter().filter_map(|b| {
1764 let body = b.body();
1765 if body.is_zero() {
1766 return None;
1767 }
1768 let wicks = b.wick_upper() + b.wick_lower();
1769 (wicks / body).to_f64()
1770 }).collect();
1771 if valid.is_empty() {
1772 return None;
1773 }
1774 Some(valid.iter().sum::<f64>() / valid.len() as f64)
1775 }
1776
1777 pub fn close_above_open_count(bars: &[OhlcvBar]) -> usize {
1779 bars.iter().filter(|b| b.close > b.open).count()
1780 }
1781
1782 pub fn volume_price_correlation(bars: &[OhlcvBar]) -> Option<f64> {
1787 use rust_decimal::prelude::ToPrimitive;
1788 let n = bars.len();
1789 if n < 2 {
1790 return None;
1791 }
1792 let vols: Vec<f64> = bars.iter().filter_map(|b| b.volume.to_f64()).collect();
1793 let closes: Vec<f64> = bars.iter().filter_map(|b| b.close.to_f64()).collect();
1794 if vols.len() != n || closes.len() != n {
1795 return None;
1796 }
1797 let nf = n as f64;
1798 let mean_v = vols.iter().sum::<f64>() / nf;
1799 let mean_c = closes.iter().sum::<f64>() / nf;
1800 let cov: f64 = vols.iter().zip(closes.iter()).map(|(v, c)| (v - mean_v) * (c - mean_c)).sum::<f64>() / nf;
1801 let std_v = (vols.iter().map(|v| (v - mean_v).powi(2)).sum::<f64>() / nf).sqrt();
1802 let std_c = (closes.iter().map(|c| (c - mean_c).powi(2)).sum::<f64>() / nf).sqrt();
1803 if std_v == 0.0 || std_c == 0.0 {
1804 return None;
1805 }
1806 Some(cov / (std_v * std_c))
1807 }
1808
1809 pub fn body_consistency(bars: &[OhlcvBar]) -> Option<f64> {
1813 if bars.is_empty() {
1814 return None;
1815 }
1816 let valid: Vec<_> = bars.iter().filter(|b| !b.range().is_zero()).collect();
1817 if valid.is_empty() {
1818 return None;
1819 }
1820 let consistent = valid.iter().filter(|b| {
1821 b.body() * Decimal::TWO > b.range()
1822 }).count();
1823 Some(consistent as f64 / valid.len() as f64)
1824 }
1825
1826 pub fn close_volatility_ratio(bars: &[OhlcvBar]) -> Option<f64> {
1831 use rust_decimal::prelude::ToPrimitive;
1832 let mean = Self::mean_close(bars)?;
1833 if mean.is_zero() {
1834 return None;
1835 }
1836 let std = Self::close_std_dev(bars)?;
1837 let mean_f = mean.to_f64()?;
1838 Some(std / mean_f.abs())
1839 }
1840
1841 pub fn close_momentum_score(bars: &[OhlcvBar]) -> Option<f64> {
1846 let mean = Self::mean_close(bars)?;
1847 let above = bars.iter().filter(|b| b.close > mean).count();
1848 Some(above as f64 / bars.len() as f64)
1849 }
1850
1851 pub fn range_expansion_count(bars: &[OhlcvBar]) -> usize {
1856 if bars.len() < 2 {
1857 return 0;
1858 }
1859 bars.windows(2).filter(|w| w[1].range() > w[0].range()).count()
1860 }
1861
1862 pub fn gap_count(bars: &[OhlcvBar]) -> usize {
1865 if bars.len() < 2 {
1866 return 0;
1867 }
1868 bars.windows(2).filter(|w| w[1].open != w[0].close).count()
1869 }
1870
1871 pub fn avg_wick_size(bars: &[OhlcvBar]) -> Option<f64> {
1875 use rust_decimal::prelude::ToPrimitive;
1876 if bars.is_empty() {
1877 return None;
1878 }
1879 let total: f64 = bars.iter()
1880 .filter_map(|b| (b.wick_upper() + b.wick_lower()).to_f64())
1881 .sum();
1882 Some(total / bars.len() as f64)
1883 }
1884
1885 pub fn mean_volume_ratio(bars: &[OhlcvBar]) -> Vec<Option<f64>> {
1891 use rust_decimal::prelude::ToPrimitive;
1892 if bars.is_empty() {
1893 return vec![];
1894 }
1895 let mean = match Self::mean_volume(bars) {
1896 Some(m) if !m.is_zero() => m,
1897 _ => return bars.iter().map(|_| None).collect(),
1898 };
1899 bars.iter().map(|b| (b.volume / mean).to_f64()).collect()
1900 }
1901
1902 pub fn close_above_high_ma(bars: &[OhlcvBar], n: usize) -> usize {
1906 if n < 1 || bars.len() < n {
1907 return 0;
1908 }
1909 let high_ma: Decimal = bars.iter().take(n).map(|b| b.high).sum::<Decimal>()
1910 / Decimal::from(n as u32);
1911 bars[n - 1..].iter().filter(|b| b.close > high_ma).count()
1912 }
1913
1914 pub fn price_compression_ratio(bars: &[OhlcvBar]) -> Option<f64> {
1920 use rust_decimal::prelude::ToPrimitive;
1921 let mean_body = Self::average_body(bars)?;
1922 let mean_range = Self::mean_range(bars)?;
1923 if mean_range.is_zero() {
1924 return None;
1925 }
1926 (mean_body / mean_range).to_f64()
1927 }
1928
1929 pub fn open_close_spread(bars: &[OhlcvBar]) -> Option<f64> {
1933 use rust_decimal::prelude::ToPrimitive;
1934 if bars.is_empty() {
1935 return None;
1936 }
1937 let total: f64 = bars.iter()
1938 .filter_map(|b| (b.close - b.open).abs().to_f64())
1939 .sum();
1940 Some(total / bars.len() as f64)
1941 }
1942
1943 pub fn max_consecutive_gains(bars: &[OhlcvBar]) -> usize {
1945 let mut max_run = 0usize;
1946 let mut current = 0usize;
1947 for w in bars.windows(2) {
1948 if w[1].close > w[0].close {
1949 current += 1;
1950 if current > max_run {
1951 max_run = current;
1952 }
1953 } else {
1954 current = 0;
1955 }
1956 }
1957 max_run
1958 }
1959
1960 pub fn max_consecutive_losses(bars: &[OhlcvBar]) -> usize {
1962 let mut max_run = 0usize;
1963 let mut current = 0usize;
1964 for w in bars.windows(2) {
1965 if w[1].close < w[0].close {
1966 current += 1;
1967 if current > max_run {
1968 max_run = current;
1969 }
1970 } else {
1971 current = 0;
1972 }
1973 }
1974 max_run
1975 }
1976
1977 pub fn price_path_length(bars: &[OhlcvBar]) -> Option<f64> {
1983 use rust_decimal::prelude::ToPrimitive;
1984 if bars.len() < 2 {
1985 return None;
1986 }
1987 let total: f64 = bars.windows(2)
1988 .filter_map(|w| (w[1].close - w[0].close).abs().to_f64())
1989 .sum();
1990 Some(total)
1991 }
1992
1993 pub fn close_reversion_count(bars: &[OhlcvBar]) -> usize {
1998 let mean = match Self::mean_close(bars) {
1999 Some(m) => m,
2000 None => return 0,
2001 };
2002 if bars.len() < 2 {
2003 return 0;
2004 }
2005 bars.windows(2).filter(|w| {
2006 let prev = w[0].close;
2007 let curr = w[1].close;
2008 if prev < mean {
2010 curr > prev && curr <= mean
2011 } else {
2012 curr < prev && curr >= mean
2013 }
2014 }).count()
2015 }
2016
2017 pub fn atr_ratio(bars: &[OhlcvBar]) -> Option<f64> {
2022 use rust_decimal::prelude::ToPrimitive;
2023 let atr = Self::average_true_range(bars)?;
2024 let mean = Self::mean_close(bars)?;
2025 if mean.is_zero() {
2026 return None;
2027 }
2028 (atr / mean * Decimal::ONE_HUNDRED).to_f64()
2029 }
2030
2031 pub fn volume_trend_strength(bars: &[OhlcvBar]) -> Option<f64> {
2036 use rust_decimal::prelude::ToPrimitive;
2037 let n = bars.len();
2038 if n < 2 {
2039 return None;
2040 }
2041 let nf = n as f64;
2042 let indices: Vec<f64> = (0..n).map(|i| i as f64).collect();
2043 let vols: Vec<f64> = bars.iter().filter_map(|b| b.volume.to_f64()).collect();
2044 if vols.len() != n {
2045 return None;
2046 }
2047 let mean_i = indices.iter().sum::<f64>() / nf;
2048 let mean_v = vols.iter().sum::<f64>() / nf;
2049 let cov: f64 = indices.iter().zip(vols.iter()).map(|(i, v)| (i - mean_i) * (v - mean_v)).sum::<f64>() / nf;
2050 let std_i = (indices.iter().map(|i| (i - mean_i).powi(2)).sum::<f64>() / nf).sqrt();
2051 let std_v = (vols.iter().map(|v| (v - mean_v).powi(2)).sum::<f64>() / nf).sqrt();
2052 if std_i == 0.0 || std_v == 0.0 {
2053 return None;
2054 }
2055 Some(cov / (std_i * std_v))
2056 }
2057
2058 pub fn high_close_spread(bars: &[OhlcvBar]) -> Option<f64> {
2063 use rust_decimal::prelude::ToPrimitive;
2064 if bars.is_empty() {
2065 return None;
2066 }
2067 let total: f64 = bars.iter().filter_map(|b| (b.high - b.close).to_f64()).sum();
2068 Some(total / bars.len() as f64)
2069 }
2070
2071 pub fn open_range(bars: &[OhlcvBar]) -> Option<f64> {
2077 use rust_decimal::prelude::ToPrimitive;
2078 if bars.is_empty() {
2079 return None;
2080 }
2081 let total: f64 = bars.iter().filter_map(|b| (b.close - b.open).abs().to_f64()).sum();
2082 Some(total / bars.len() as f64)
2083 }
2084
2085 pub fn normalized_close(bars: &[OhlcvBar]) -> Option<f64> {
2090 use rust_decimal::prelude::ToPrimitive;
2091 let min = Self::min_close(bars)?;
2092 let max = Self::max_close(bars)?;
2093 let range = max - min;
2094 if range.is_zero() {
2095 return None;
2096 }
2097 let last = bars.last()?.close;
2098 ((last - min) / range).to_f64()
2099 }
2100
2101 pub fn price_channel_position(bars: &[OhlcvBar]) -> Option<f64> {
2107 Self::price_position(bars)
2108 }
2109
2110 pub fn candle_score(bars: &[OhlcvBar]) -> Option<f64> {
2115 if bars.is_empty() {
2116 return None;
2117 }
2118 let strong = bars.iter().filter(|b| {
2119 b.is_bullish()
2120 && !b.range().is_zero()
2121 && b.body() * Decimal::TWO > b.range()
2122 && b.close_above_midpoint()
2123 }).count();
2124 Some(strong as f64 / bars.len() as f64)
2125 }
2126
2127 pub fn bar_speed(bars: &[OhlcvBar]) -> Option<f64> {
2132 if bars.is_empty() {
2133 return None;
2134 }
2135 let total_ticks: u64 = bars.iter().map(|b| b.trade_count).sum();
2136 let total_ms: u64 = bars.iter().map(|b| b.bar_duration_ms()).sum();
2137 if total_ms == 0 {
2138 return None;
2139 }
2140 Some(total_ticks as f64 / total_ms as f64)
2141 }
2142
2143 pub fn higher_highs_count(bars: &[OhlcvBar]) -> usize {
2147 if bars.len() < 2 {
2148 return 0;
2149 }
2150 bars.windows(2).filter(|w| w[1].high > w[0].high).count()
2151 }
2152
2153 pub fn lower_lows_count(bars: &[OhlcvBar]) -> usize {
2157 if bars.len() < 2 {
2158 return 0;
2159 }
2160 bars.windows(2).filter(|w| w[1].low < w[0].low).count()
2161 }
2162
2163 pub fn close_minus_open_pct(bars: &[OhlcvBar]) -> Option<f64> {
2167 use rust_decimal::prelude::ToPrimitive;
2168 let values: Vec<f64> = bars.iter().filter_map(|b| {
2169 if b.open.is_zero() { return None; }
2170 ((b.close - b.open) / b.open * Decimal::ONE_HUNDRED).to_f64()
2171 }).collect();
2172 if values.is_empty() {
2173 return None;
2174 }
2175 Some(values.iter().sum::<f64>() / values.len() as f64)
2176 }
2177
2178 pub fn volume_per_range(bars: &[OhlcvBar]) -> Option<f64> {
2183 use rust_decimal::prelude::ToPrimitive;
2184 let values: Vec<f64> = bars.iter().filter_map(|b| {
2185 let r = b.range();
2186 if r.is_zero() { return None; }
2187 (b.volume / r).to_f64()
2188 }).collect();
2189 if values.is_empty() {
2190 return None;
2191 }
2192 Some(values.iter().sum::<f64>() / values.len() as f64)
2193 }
2194
2195 pub fn body_fraction(bars: &[OhlcvBar]) -> Option<f64> {
2199 use rust_decimal::prelude::ToPrimitive;
2200 let fracs: Vec<f64> = bars.iter().filter_map(|b| {
2201 let range = b.range();
2202 if range.is_zero() { return None; }
2203 let body = (b.close - b.open).abs();
2204 (body / range).to_f64()
2205 }).collect();
2206 if fracs.is_empty() {
2207 return None;
2208 }
2209 Some(fracs.iter().sum::<f64>() / fracs.len() as f64)
2210 }
2211
2212 pub fn bullish_ratio(bars: &[OhlcvBar]) -> Option<f64> {
2216 if bars.is_empty() {
2217 return None;
2218 }
2219 let bullish = bars.iter().filter(|b| b.close > b.open).count();
2220 Some(bullish as f64 / bars.len() as f64)
2221 }
2222
2223 pub fn peak_close(bars: &[OhlcvBar]) -> Option<Decimal> {
2227 bars.iter().map(|b| b.close).reduce(Decimal::max)
2228 }
2229
2230 pub fn trough_close(bars: &[OhlcvBar]) -> Option<Decimal> {
2234 bars.iter().map(|b| b.close).reduce(Decimal::min)
2235 }
2236
2237 pub fn up_volume_fraction(bars: &[OhlcvBar]) -> Option<f64> {
2241 use rust_decimal::prelude::ToPrimitive;
2242 if bars.is_empty() {
2243 return None;
2244 }
2245 let total: Decimal = bars.iter().map(|b| b.volume).sum();
2246 if total.is_zero() {
2247 return None;
2248 }
2249 let up_vol: Decimal = bars.iter()
2250 .filter(|b| b.close > b.open)
2251 .map(|b| b.volume)
2252 .sum();
2253 (up_vol / total).to_f64()
2254 }
2255
2256 pub fn tail_upper_fraction(bars: &[OhlcvBar]) -> Option<f64> {
2261 use rust_decimal::prelude::ToPrimitive;
2262 let fracs: Vec<f64> = bars.iter().filter_map(|b| {
2263 let range = b.range();
2264 if range.is_zero() { return None; }
2265 let body_top = b.open.max(b.close);
2266 let upper_wick = b.high - body_top;
2267 (upper_wick / range).to_f64()
2268 }).collect();
2269 if fracs.is_empty() {
2270 return None;
2271 }
2272 Some(fracs.iter().sum::<f64>() / fracs.len() as f64)
2273 }
2274
2275 pub fn tail_lower_fraction(bars: &[OhlcvBar]) -> Option<f64> {
2280 use rust_decimal::prelude::ToPrimitive;
2281 let fracs: Vec<f64> = bars.iter().filter_map(|b| {
2282 let range = b.range();
2283 if range.is_zero() { return None; }
2284 let body_bot = b.open.min(b.close);
2285 let lower_wick = body_bot - b.low;
2286 (lower_wick / range).to_f64()
2287 }).collect();
2288 if fracs.is_empty() {
2289 return None;
2290 }
2291 Some(fracs.iter().sum::<f64>() / fracs.len() as f64)
2292 }
2293
2294 pub fn range_std_dev(bars: &[OhlcvBar]) -> Option<f64> {
2299 use rust_decimal::prelude::ToPrimitive;
2300 if bars.len() < 2 {
2301 return None;
2302 }
2303 let vals: Vec<f64> = bars.iter().filter_map(|b| b.range().to_f64()).collect();
2304 if vals.len() < 2 {
2305 return None;
2306 }
2307 let n = vals.len() as f64;
2308 let mean = vals.iter().sum::<f64>() / n;
2309 let variance = vals.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / (n - 1.0);
2310 Some(variance.sqrt())
2311 }
2312
2313 pub fn close_to_range_position(bars: &[OhlcvBar]) -> Option<f64> {
2322 use rust_decimal::prelude::ToPrimitive;
2323 let vals: Vec<f64> = bars
2324 .iter()
2325 .filter_map(|b| {
2326 let r = b.range();
2327 if r.is_zero() {
2328 return None;
2329 }
2330 ((b.close - b.low) / r).to_f64()
2331 })
2332 .collect();
2333 if vals.is_empty() {
2334 return None;
2335 }
2336 Some(vals.iter().sum::<f64>() / vals.len() as f64)
2337 }
2338
2339 pub fn volume_oscillator(bars: &[OhlcvBar], short_n: usize, long_n: usize) -> Option<f64> {
2348 use rust_decimal::prelude::ToPrimitive;
2349 if short_n == 0 || long_n == 0 || short_n >= long_n || bars.len() < long_n {
2350 return None;
2351 }
2352 let recent = &bars[bars.len() - short_n..];
2353 let long_slice = &bars[bars.len() - long_n..];
2354 let short_avg: f64 =
2355 recent.iter().filter_map(|b| b.volume.to_f64()).sum::<f64>() / short_n as f64;
2356 let long_sum: Vec<f64> = long_slice.iter().filter_map(|b| b.volume.to_f64()).collect();
2357 if long_sum.is_empty() {
2358 return None;
2359 }
2360 let long_avg = long_sum.iter().sum::<f64>() / long_sum.len() as f64;
2361 if long_avg == 0.0 {
2362 return None;
2363 }
2364 Some((short_avg - long_avg) / long_avg)
2365 }
2366
2367 pub fn direction_reversal_count(bars: &[OhlcvBar]) -> usize {
2373 if bars.len() < 2 {
2374 return 0;
2375 }
2376 let mut count = 0usize;
2377 let mut prev_bullish: Option<bool> = None;
2378 for b in bars {
2379 let bullish = b.close > b.open;
2380 if let Some(pb) = prev_bullish {
2381 if bullish != pb {
2382 count += 1;
2383 }
2384 }
2385 prev_bullish = Some(bullish);
2386 }
2387 count
2388 }
2389
2390 pub fn upper_wick_dominance_fraction(bars: &[OhlcvBar]) -> Option<f64> {
2395 if bars.is_empty() {
2396 return None;
2397 }
2398 let count = bars.iter().filter(|b| b.wick_upper() > b.wick_lower()).count();
2399 Some(count as f64 / bars.len() as f64)
2400 }
2401
2402 pub fn avg_open_to_high_ratio(bars: &[OhlcvBar]) -> Option<f64> {
2407 use rust_decimal::prelude::ToPrimitive;
2408 let vals: Vec<f64> = bars
2409 .iter()
2410 .filter_map(|b| {
2411 let r = b.range();
2412 if r.is_zero() {
2413 return None;
2414 }
2415 ((b.high - b.open) / r).to_f64()
2416 })
2417 .collect();
2418 if vals.is_empty() {
2419 return None;
2420 }
2421 Some(vals.iter().sum::<f64>() / vals.len() as f64)
2422 }
2423
2424 pub fn volume_weighted_range(bars: &[OhlcvBar]) -> Option<f64> {
2428 use rust_decimal::prelude::ToPrimitive;
2429 if bars.is_empty() {
2430 return None;
2431 }
2432 let mut numerator = 0f64;
2433 let mut denom = 0f64;
2434 for b in bars {
2435 let r = b.range().to_f64()?;
2436 let v = b.volume.to_f64()?;
2437 numerator += r * v;
2438 denom += v;
2439 }
2440 if denom == 0.0 {
2441 return None;
2442 }
2443 Some(numerator / denom)
2444 }
2445
2446 pub fn bar_strength_index(bars: &[OhlcvBar]) -> Option<f64> {
2454 let vals: Vec<f64> =
2455 bars.iter().filter_map(|b| b.close_location_value()).collect();
2456 if vals.is_empty() {
2457 return None;
2458 }
2459 Some(vals.iter().sum::<f64>() / vals.len() as f64)
2460 }
2461
2462 pub fn shadow_to_body_ratio(bars: &[OhlcvBar]) -> Option<f64> {
2468 use rust_decimal::prelude::ToPrimitive;
2469 if bars.is_empty() {
2470 return None;
2471 }
2472 let total_wick: Decimal = bars.iter().map(|b| b.wick_upper() + b.wick_lower()).sum();
2473 let total_body: Decimal = bars.iter().map(|b| b.body()).sum();
2474 if total_body.is_zero() {
2475 return None;
2476 }
2477 (total_wick / total_body).to_f64()
2478 }
2479
2480 pub fn first_last_close_pct(bars: &[OhlcvBar]) -> Option<f64> {
2485 use rust_decimal::prelude::ToPrimitive;
2486 let first = bars.first()?;
2487 let last = bars.last()?;
2488 if first.close.is_zero() {
2489 return None;
2490 }
2491 ((last.close - first.close) / first.close * Decimal::ONE_HUNDRED).to_f64()
2492 }
2493
2494 pub fn open_to_close_volatility(bars: &[OhlcvBar]) -> Option<f64> {
2499 use rust_decimal::prelude::ToPrimitive;
2500 if bars.len() < 2 {
2501 return None;
2502 }
2503 let returns: Vec<f64> = bars
2504 .iter()
2505 .filter_map(|b| {
2506 if b.open.is_zero() {
2507 return None;
2508 }
2509 ((b.close - b.open) / b.open).to_f64()
2510 })
2511 .collect();
2512 if returns.len() < 2 {
2513 return None;
2514 }
2515 let n = returns.len() as f64;
2516 let mean = returns.iter().sum::<f64>() / n;
2517 let variance = returns.iter().map(|r| (r - mean).powi(2)).sum::<f64>() / (n - 1.0);
2518 Some(variance.sqrt())
2519 }
2520
2521 pub fn close_recovery_ratio(bars: &[OhlcvBar]) -> Option<f64> {
2530 use rust_decimal::prelude::ToPrimitive;
2531 let vals: Vec<f64> = bars
2532 .iter()
2533 .filter_map(|b| {
2534 let r = b.range();
2535 if r.is_zero() {
2536 return None;
2537 }
2538 ((b.close - b.low) / r).to_f64()
2539 })
2540 .collect();
2541 if vals.is_empty() {
2542 return None;
2543 }
2544 Some(vals.iter().sum::<f64>() / vals.len() as f64)
2545 }
2546
2547 pub fn median_range(bars: &[OhlcvBar]) -> Option<Decimal> {
2552 if bars.is_empty() {
2553 return None;
2554 }
2555 let mut ranges: Vec<Decimal> = bars.iter().map(|b| b.range()).collect();
2556 ranges.sort();
2557 let n = ranges.len();
2558 if n % 2 == 1 {
2559 Some(ranges[n / 2])
2560 } else {
2561 Some((ranges[n / 2 - 1] + ranges[n / 2]) / Decimal::from(2u64))
2562 }
2563 }
2564
2565 pub fn mean_typical_price(bars: &[OhlcvBar]) -> Option<Decimal> {
2570 if bars.is_empty() {
2571 return None;
2572 }
2573 let sum: Decimal = bars.iter().map(|b| b.typical_price()).sum();
2574 Some(sum / Decimal::from(bars.len() as u64))
2575 }
2576
2577 pub fn directional_volume_ratio(bars: &[OhlcvBar]) -> Option<f64> {
2583 use rust_decimal::prelude::ToPrimitive;
2584 let bull = Self::bullish_volume(bars);
2585 let bear = Self::bearish_volume(bars);
2586 let total = bull + bear;
2587 if total.is_zero() {
2588 return None;
2589 }
2590 (bull / total).to_f64()
2591 }
2592
2593 pub fn inside_bar_fraction(bars: &[OhlcvBar]) -> Option<f64> {
2598 if bars.len() < 2 {
2599 return None;
2600 }
2601 let inside = bars.windows(2).filter(|w| w[1].is_inside_bar(&w[0])).count();
2602 Some(inside as f64 / (bars.len() - 1) as f64)
2603 }
2604
2605 pub fn body_momentum(bars: &[OhlcvBar]) -> Decimal {
2611 bars.iter()
2612 .map(|b| b.close - b.open)
2613 .sum()
2614 }
2615
2616 pub fn avg_trade_count(bars: &[OhlcvBar]) -> Option<f64> {
2620 if bars.is_empty() {
2621 return None;
2622 }
2623 let total: u64 = bars.iter().map(|b| b.trade_count).sum();
2624 Some(total as f64 / bars.len() as f64)
2625 }
2626
2627 pub fn max_trade_count(bars: &[OhlcvBar]) -> Option<u64> {
2631 bars.iter().map(|b| b.trade_count).max()
2632 }
2633
2634}
2635
2636impl std::fmt::Display for OhlcvBar {
2637 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2638 write!(
2639 f,
2640 "{} {} [{}/{}/{}/{} v={}]",
2641 self.symbol, self.timeframe, self.open, self.high, self.low, self.close, self.volume
2642 )
2643 }
2644}
2645
2646pub struct OhlcvAggregator {
2648 symbol: String,
2649 timeframe: Timeframe,
2650 current_bar: Option<OhlcvBar>,
2651 last_bar: Option<OhlcvBar>,
2653 emit_empty_bars: bool,
2657 bars_emitted: u64,
2659 price_volume_sum: Decimal,
2661 total_volume: Decimal,
2663 peak_volume: Option<Decimal>,
2665 min_volume: Option<Decimal>,
2667}
2668
2669impl OhlcvAggregator {
2670 pub fn new(symbol: impl Into<String>, timeframe: Timeframe) -> Result<Self, StreamError> {
2675 let tf_dur = timeframe.duration_ms();
2676 if tf_dur == 0 {
2677 return Err(StreamError::ConfigError {
2678 reason: "OhlcvAggregator timeframe duration must be > 0".into(),
2679 });
2680 }
2681 Ok(Self {
2682 symbol: symbol.into(),
2683 timeframe,
2684 current_bar: None,
2685 last_bar: None,
2686 emit_empty_bars: false,
2687 bars_emitted: 0,
2688 price_volume_sum: Decimal::ZERO,
2689 total_volume: Decimal::ZERO,
2690 peak_volume: None,
2691 min_volume: None,
2692 })
2693 }
2694
2695 pub fn with_emit_empty_bars(mut self, enabled: bool) -> Self {
2697 self.emit_empty_bars = enabled;
2698 self
2699 }
2700
2701 #[must_use = "completed bars are returned; ignoring them loses bar data"]
2710 #[inline]
2711 pub fn feed(&mut self, tick: &NormalizedTick) -> Result<Vec<OhlcvBar>, StreamError> {
2712 if tick.symbol != self.symbol {
2713 return Err(StreamError::AggregationError {
2714 reason: format!(
2715 "tick symbol '{}' does not match aggregator '{}'",
2716 tick.symbol, self.symbol
2717 ),
2718 });
2719 }
2720
2721 let tick_ts = tick.exchange_ts_ms.unwrap_or(tick.received_at_ms);
2723 let bar_start = self.timeframe.bar_start_ms(tick_ts);
2724 let mut emitted: Vec<OhlcvBar> = Vec::new();
2725
2726 let bar_window_changed = self
2728 .current_bar
2729 .as_ref()
2730 .map_or(false, |b| b.bar_start_ms != bar_start);
2731
2732 if bar_window_changed {
2733 let mut completed = self.current_bar.take().unwrap_or_else(|| unreachable!());
2735 completed.is_complete = true;
2736 let prev_close = completed.close;
2737 let prev_start = completed.bar_start_ms;
2738 emitted.push(completed);
2739
2740 if self.emit_empty_bars {
2742 let dur = self.timeframe.duration_ms();
2743 let mut gap_start = prev_start + dur;
2744 while gap_start < bar_start {
2745 emitted.push(OhlcvBar {
2746 symbol: self.symbol.clone(),
2747 timeframe: self.timeframe,
2748 bar_start_ms: gap_start,
2749 open: prev_close,
2750 high: prev_close,
2751 low: prev_close,
2752 close: prev_close,
2753 volume: Decimal::ZERO,
2754 trade_count: 0,
2755 is_complete: true,
2756 is_gap_fill: true,
2757 vwap: None,
2758 });
2759 gap_start += dur;
2760 }
2761 }
2762 }
2763
2764 let tick_value = tick.value();
2766 if self.current_bar.is_some() {
2767 self.price_volume_sum += tick_value;
2768 } else {
2769 self.price_volume_sum = tick_value;
2770 }
2771
2772 match &mut self.current_bar {
2773 Some(bar) => {
2774 if tick.price > bar.high {
2775 bar.high = tick.price;
2776 }
2777 if tick.price < bar.low {
2778 bar.low = tick.price;
2779 }
2780 bar.close = tick.price;
2781 bar.volume += tick.quantity;
2782 bar.trade_count += 1;
2783 bar.vwap = if bar.volume.is_zero() {
2784 None
2785 } else {
2786 Some(self.price_volume_sum / bar.volume)
2787 };
2788 }
2789 None => {
2790 self.current_bar = Some(OhlcvBar {
2791 symbol: self.symbol.clone(),
2792 timeframe: self.timeframe,
2793 bar_start_ms: bar_start,
2794 open: tick.price,
2795 high: tick.price,
2796 low: tick.price,
2797 close: tick.price,
2798 volume: tick.quantity,
2799 trade_count: 1,
2800 is_complete: false,
2801 is_gap_fill: false,
2802 vwap: Some(tick.price), });
2804 }
2805 }
2806 self.bars_emitted += emitted.len() as u64;
2807 for b in &emitted {
2808 self.total_volume += b.volume;
2809 self.peak_volume = Some(match self.peak_volume {
2810 Some(prev) => prev.max(b.volume),
2811 None => b.volume,
2812 });
2813 self.min_volume = Some(match self.min_volume {
2814 Some(prev) => prev.min(b.volume),
2815 None => b.volume,
2816 });
2817 }
2818 if let Some(b) = emitted.last() {
2819 self.last_bar = Some(b.clone());
2820 }
2821 Ok(emitted)
2822 }
2823
2824 pub fn current_bar(&self) -> Option<&OhlcvBar> {
2826 self.current_bar.as_ref()
2827 }
2828
2829 #[must_use = "the flushed bar is returned; ignoring it loses the partial bar"]
2831 pub fn flush(&mut self) -> Option<OhlcvBar> {
2832 let mut bar = self.current_bar.take()?;
2833 bar.is_complete = true;
2834 self.bars_emitted += 1;
2835 self.total_volume += bar.volume;
2836 self.peak_volume = Some(match self.peak_volume {
2837 Some(prev) => prev.max(bar.volume),
2838 None => bar.volume,
2839 });
2840 self.min_volume = Some(match self.min_volume {
2841 Some(prev) => prev.min(bar.volume),
2842 None => bar.volume,
2843 });
2844 self.last_bar = Some(bar.clone());
2845 Some(bar)
2846 }
2847
2848 pub fn last_bar(&self) -> Option<&OhlcvBar> {
2853 self.last_bar.as_ref()
2854 }
2855
2856 pub fn bar_count(&self) -> u64 {
2858 self.bars_emitted
2859 }
2860
2861 pub fn reset(&mut self) {
2866 self.current_bar = None;
2867 self.last_bar = None;
2868 self.bars_emitted = 0;
2869 self.price_volume_sum = Decimal::ZERO;
2870 self.total_volume = Decimal::ZERO;
2871 self.peak_volume = None;
2872 self.min_volume = None;
2873 }
2874
2875 pub fn total_volume(&self) -> Decimal {
2880 self.total_volume
2881 }
2882
2883 pub fn peak_volume(&self) -> Option<Decimal> {
2888 self.peak_volume
2889 }
2890
2891 pub fn min_volume(&self) -> Option<Decimal> {
2896 self.min_volume
2897 }
2898
2899 pub fn volume_range(&self) -> Option<(Decimal, Decimal)> {
2904 Some((self.min_volume?, self.peak_volume?))
2905 }
2906
2907 pub fn average_volume(&self) -> Option<Decimal> {
2911 if self.bars_emitted == 0 {
2912 return None;
2913 }
2914 Some(self.total_volume / Decimal::from(self.bars_emitted))
2915 }
2916
2917 pub fn symbol(&self) -> &str {
2919 &self.symbol
2920 }
2921
2922 pub fn timeframe(&self) -> Timeframe {
2924 self.timeframe
2925 }
2926
2927 pub fn window_progress(&self, now_ms: u64) -> Option<f64> {
2933 let bar = self.current_bar.as_ref()?;
2934 let elapsed = now_ms.saturating_sub(bar.bar_start_ms);
2935 let duration = self.timeframe.duration_ms();
2936 let progress = elapsed as f64 / duration as f64;
2937 Some(progress.clamp(0.0, 1.0))
2938 }
2939
2940 pub fn is_active(&self) -> bool {
2943 self.current_bar.is_some()
2944 }
2945
2946 pub fn vwap_current(&self) -> Option<Decimal> {
2951 let bar = self.current_bar.as_ref()?;
2952 if bar.volume.is_zero() {
2953 return None;
2954 }
2955 Some(self.price_volume_sum / bar.volume)
2956 }
2957}
2958
2959#[cfg(test)]
2960#[allow(deprecated)]
2961mod tests {
2962 use super::*;
2963 use crate::tick::{Exchange, NormalizedTick, TradeSide};
2964 use rust_decimal_macros::dec;
2965
2966 fn make_tick(symbol: &str, price: Decimal, qty: Decimal, ts_ms: u64) -> NormalizedTick {
2967 NormalizedTick {
2968 exchange: Exchange::Binance,
2969 symbol: symbol.to_string(),
2970 price,
2971 quantity: qty,
2972 side: Some(TradeSide::Buy),
2973 trade_id: None,
2974 exchange_ts_ms: None,
2975 received_at_ms: ts_ms,
2976 }
2977 }
2978
2979 fn make_tick_with_exchange_ts(
2980 symbol: &str,
2981 price: Decimal,
2982 qty: Decimal,
2983 exchange_ts_ms: u64,
2984 received_at_ms: u64,
2985 ) -> NormalizedTick {
2986 NormalizedTick {
2987 exchange: Exchange::Binance,
2988 symbol: symbol.to_string(),
2989 price,
2990 quantity: qty,
2991 side: Some(TradeSide::Buy),
2992 trade_id: None,
2993 exchange_ts_ms: Some(exchange_ts_ms),
2994 received_at_ms,
2995 }
2996 }
2997
2998 fn agg(symbol: &str, tf: Timeframe) -> OhlcvAggregator {
2999 OhlcvAggregator::new(symbol, tf).unwrap()
3000 }
3001
3002 #[test]
3003 fn test_timeframe_seconds_duration_ms() {
3004 assert_eq!(Timeframe::Seconds(30).duration_ms(), 30_000);
3005 }
3006
3007 #[test]
3008 fn test_timeframe_minutes_duration_ms() {
3009 assert_eq!(Timeframe::Minutes(5).duration_ms(), 300_000);
3010 }
3011
3012 #[test]
3013 fn test_timeframe_hours_duration_ms() {
3014 assert_eq!(Timeframe::Hours(1).duration_ms(), 3_600_000);
3015 }
3016
3017 #[test]
3018 fn test_timeframe_bar_start_ms_aligns() {
3019 let tf = Timeframe::Minutes(1);
3020 let ts = 61_500; assert_eq!(tf.bar_start_ms(ts), 60_000);
3022 }
3023
3024 #[test]
3025 fn test_timeframe_display() {
3026 assert_eq!(Timeframe::Seconds(30).to_string(), "30s");
3027 assert_eq!(Timeframe::Minutes(5).to_string(), "5m");
3028 assert_eq!(Timeframe::Hours(4).to_string(), "4h");
3029 }
3030
3031 #[test]
3032 fn test_timeframe_ord_seconds_lt_minutes() {
3033 assert!(Timeframe::Seconds(30) < Timeframe::Minutes(1));
3034 }
3035
3036 #[test]
3037 fn test_timeframe_ord_minutes_lt_hours() {
3038 assert!(Timeframe::Minutes(59) < Timeframe::Hours(1));
3039 }
3040
3041 #[test]
3042 fn test_timeframe_ord_same_duration_equal() {
3043 assert_eq!(Timeframe::Seconds(60), Timeframe::Seconds(60));
3044 assert_eq!(
3045 Timeframe::Seconds(3600).cmp(&Timeframe::Hours(1)),
3046 std::cmp::Ordering::Equal
3047 );
3048 }
3049
3050 #[test]
3051 fn test_timeframe_ord_sort() {
3052 let mut tfs = vec![
3053 Timeframe::Hours(1),
3054 Timeframe::Seconds(30),
3055 Timeframe::Minutes(5),
3056 ];
3057 tfs.sort();
3058 assert_eq!(tfs[0], Timeframe::Seconds(30));
3059 assert_eq!(tfs[1], Timeframe::Minutes(5));
3060 assert_eq!(tfs[2], Timeframe::Hours(1));
3061 }
3062
3063 #[test]
3064 fn test_ohlcv_aggregator_first_tick_sets_ohlcv() {
3065 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
3066 let tick = make_tick("BTC-USD", dec!(50000), dec!(1), 60_000);
3067 let result = agg.feed(&tick).unwrap();
3068 assert!(result.is_empty()); let bar = agg.current_bar().unwrap();
3070 assert_eq!(bar.open, dec!(50000));
3071 assert_eq!(bar.high, dec!(50000));
3072 assert_eq!(bar.low, dec!(50000));
3073 assert_eq!(bar.close, dec!(50000));
3074 assert_eq!(bar.volume, dec!(1));
3075 assert_eq!(bar.trade_count, 1);
3076 }
3077
3078 #[test]
3079 fn test_ohlcv_aggregator_high_low_tracking() {
3080 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
3081 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1), 60_000))
3082 .unwrap();
3083 agg.feed(&make_tick("BTC-USD", dec!(51000), dec!(1), 60_100))
3084 .unwrap();
3085 agg.feed(&make_tick("BTC-USD", dec!(49500), dec!(1), 60_200))
3086 .unwrap();
3087 let bar = agg.current_bar().unwrap();
3088 assert_eq!(bar.high, dec!(51000));
3089 assert_eq!(bar.low, dec!(49500));
3090 assert_eq!(bar.close, dec!(49500));
3091 assert_eq!(bar.trade_count, 3);
3092 }
3093
3094 #[test]
3095 fn test_ohlcv_aggregator_bar_completes_on_new_window() {
3096 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
3097 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1), 60_000))
3098 .unwrap();
3099 agg.feed(&make_tick("BTC-USD", dec!(50100), dec!(2), 60_500))
3100 .unwrap();
3101 let mut bars = agg
3103 .feed(&make_tick("BTC-USD", dec!(50200), dec!(1), 120_000))
3104 .unwrap();
3105 assert_eq!(bars.len(), 1);
3106 let bar = bars.remove(0);
3107 assert!(bar.is_complete);
3108 assert_eq!(bar.open, dec!(50000));
3109 assert_eq!(bar.close, dec!(50100));
3110 assert_eq!(bar.volume, dec!(3));
3111 assert_eq!(bar.bar_start_ms, 60_000);
3112 }
3113
3114 #[test]
3115 fn test_ohlcv_aggregator_new_bar_started_after_completion() {
3116 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
3117 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1), 60_000))
3118 .unwrap();
3119 agg.feed(&make_tick("BTC-USD", dec!(50200), dec!(1), 120_000))
3120 .unwrap();
3121 let bar = agg.current_bar().unwrap();
3122 assert_eq!(bar.open, dec!(50200));
3123 assert_eq!(bar.bar_start_ms, 120_000);
3124 }
3125
3126 #[test]
3127 fn test_ohlcv_aggregator_flush_marks_complete() {
3128 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
3129 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1), 60_000))
3130 .unwrap();
3131 let flushed = agg.flush().unwrap();
3132 assert!(flushed.is_complete);
3133 assert!(agg.current_bar().is_none());
3134 }
3135
3136 #[test]
3137 fn test_ohlcv_aggregator_flush_empty_returns_none() {
3138 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
3139 assert!(agg.flush().is_none());
3140 }
3141
3142 #[test]
3143 fn test_ohlcv_aggregator_wrong_symbol_returns_error() {
3144 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
3145 let tick = make_tick("ETH-USD", dec!(3000), dec!(1), 60_000);
3146 let result = agg.feed(&tick);
3147 assert!(matches!(result, Err(StreamError::AggregationError { .. })));
3148 }
3149
3150 #[test]
3151 fn test_ohlcv_aggregator_volume_accumulates() {
3152 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
3153 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1.5), 60_000))
3154 .unwrap();
3155 agg.feed(&make_tick("BTC-USD", dec!(50100), dec!(2.5), 60_100))
3156 .unwrap();
3157 let bar = agg.current_bar().unwrap();
3158 assert_eq!(bar.volume, dec!(4));
3159 }
3160
3161 #[test]
3162 fn test_ohlcv_bar_symbol_and_timeframe() {
3163 let mut agg = agg("BTC-USD", Timeframe::Minutes(5));
3164 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1), 300_000))
3165 .unwrap();
3166 let bar = agg.current_bar().unwrap();
3167 assert_eq!(bar.symbol, "BTC-USD");
3168 assert_eq!(bar.timeframe, Timeframe::Minutes(5));
3169 }
3170
3171 #[test]
3172 fn test_ohlcv_aggregator_symbol_accessor() {
3173 let agg = agg("ETH-USD", Timeframe::Hours(1));
3174 assert_eq!(agg.symbol(), "ETH-USD");
3175 assert_eq!(agg.timeframe(), Timeframe::Hours(1));
3176 }
3177
3178 #[test]
3179 fn test_bar_aligned_by_exchange_ts_not_received_ts() {
3180 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
3184 let tick = make_tick_with_exchange_ts("BTC-USD", dec!(50000), dec!(1), 60_500, 120_100);
3185 agg.feed(&tick).unwrap();
3186 let bar = agg.current_bar().unwrap();
3187 assert_eq!(bar.bar_start_ms, 60_000, "bar should use exchange_ts_ms");
3188 }
3189
3190 #[test]
3191 fn test_bar_falls_back_to_received_ts_when_no_exchange_ts() {
3192 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
3193 let tick = make_tick("BTC-USD", dec!(50000), dec!(1), 75_000);
3194 agg.feed(&tick).unwrap();
3195 let bar = agg.current_bar().unwrap();
3196 assert_eq!(bar.bar_start_ms, 60_000);
3197 }
3198
3199 #[test]
3202 fn test_emit_empty_bars_no_gap_no_empties() {
3203 let mut agg = OhlcvAggregator::new("BTC-USD", Timeframe::Minutes(1))
3205 .unwrap()
3206 .with_emit_empty_bars(true);
3207 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1), 60_000))
3208 .unwrap();
3209 let bars = agg
3210 .feed(&make_tick("BTC-USD", dec!(50100), dec!(1), 120_000))
3211 .unwrap();
3212 assert_eq!(bars.len(), 1);
3214 assert_eq!(bars[0].bar_start_ms, 60_000);
3215 assert_eq!(bars[0].volume, dec!(1));
3216 }
3217
3218 #[test]
3219 fn test_emit_empty_bars_two_skipped_windows() {
3220 let mut agg = OhlcvAggregator::new("BTC-USD", Timeframe::Minutes(1))
3223 .unwrap()
3224 .with_emit_empty_bars(true);
3225 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1), 60_000))
3226 .unwrap();
3227 let bars = agg
3228 .feed(&make_tick("BTC-USD", dec!(51000), dec!(1), 240_000))
3229 .unwrap();
3230 assert_eq!(bars.len(), 3);
3232 assert_eq!(bars[0].bar_start_ms, 60_000);
3233 assert!(!bars[0].volume.is_zero()); assert_eq!(bars[1].bar_start_ms, 120_000);
3235 assert!(bars[1].volume.is_zero()); assert_eq!(bars[1].trade_count, 0);
3237 assert_eq!(bars[1].open, dec!(50000)); assert_eq!(bars[2].bar_start_ms, 180_000);
3239 assert!(bars[2].volume.is_zero()); }
3241
3242 #[test]
3243 fn test_emit_empty_bars_disabled_no_empties_on_gap() {
3244 let mut agg = OhlcvAggregator::new("BTC-USD", Timeframe::Minutes(1))
3245 .unwrap()
3246 .with_emit_empty_bars(false);
3247 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1), 60_000))
3248 .unwrap();
3249 let bars = agg
3250 .feed(&make_tick("BTC-USD", dec!(51000), dec!(1), 240_000))
3251 .unwrap();
3252 assert_eq!(bars.len(), 1); }
3254
3255 #[test]
3256 fn test_emit_empty_bars_is_complete_true() {
3257 let mut agg = OhlcvAggregator::new("BTC-USD", Timeframe::Minutes(1))
3258 .unwrap()
3259 .with_emit_empty_bars(true);
3260 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1), 60_000))
3261 .unwrap();
3262 let bars = agg
3263 .feed(&make_tick("BTC-USD", dec!(51000), dec!(1), 240_000))
3264 .unwrap();
3265 for bar in &bars {
3266 assert!(bar.is_complete, "all emitted bars must be marked complete");
3267 }
3268 }
3269
3270 #[test]
3271 fn test_ohlcv_bar_display() {
3272 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
3273 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1), 60_000))
3274 .unwrap();
3275 let bar = agg.current_bar().unwrap();
3276 let s = bar.to_string();
3277 assert!(s.contains("BTC-USD"));
3278 assert!(s.contains("1m"));
3279 assert!(s.contains("50000"));
3280 }
3281
3282 #[test]
3283 fn test_bar_count_increments_on_feed() {
3284 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
3285 assert_eq!(agg.bar_count(), 0);
3286 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1), 60_000))
3287 .unwrap();
3288 agg.feed(&make_tick("BTC-USD", dec!(50100), dec!(1), 120_000))
3289 .unwrap();
3290 assert_eq!(agg.bar_count(), 1);
3291 }
3292
3293 #[test]
3294 fn test_bar_count_increments_on_flush() {
3295 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
3296 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1), 60_000))
3297 .unwrap();
3298 agg.flush().unwrap();
3299 assert_eq!(agg.bar_count(), 1);
3300 }
3301
3302 #[test]
3303 fn test_ohlcv_bar_range() {
3304 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
3305 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1), 60_000))
3306 .unwrap();
3307 agg.feed(&make_tick("BTC-USD", dec!(51000), dec!(1), 60_100))
3308 .unwrap();
3309 agg.feed(&make_tick("BTC-USD", dec!(49500), dec!(1), 60_200))
3310 .unwrap();
3311 let bar = agg.current_bar().unwrap();
3312 assert_eq!(bar.range(), dec!(1500)); }
3314
3315 #[test]
3316 fn test_ohlcv_bar_body_bullish() {
3317 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
3318 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1), 60_000))
3319 .unwrap();
3320 agg.feed(&make_tick("BTC-USD", dec!(50500), dec!(1), 60_100))
3321 .unwrap();
3322 let bar = agg.current_bar().unwrap();
3323 assert_eq!(bar.body(), dec!(500));
3325 }
3326
3327 #[test]
3328 fn test_ohlcv_bar_body_bearish() {
3329 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
3330 agg.feed(&make_tick("BTC-USD", dec!(50500), dec!(1), 60_000))
3331 .unwrap();
3332 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1), 60_100))
3333 .unwrap();
3334 let bar = agg.current_bar().unwrap();
3335 assert_eq!(bar.body(), dec!(500));
3337 }
3338
3339 #[test]
3340 fn test_aggregator_reset_clears_bar_and_count() {
3341 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
3342 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1), 60_000))
3343 .unwrap();
3344 agg.feed(&make_tick("BTC-USD", dec!(50100), dec!(1), 120_000))
3345 .unwrap();
3346 assert_eq!(agg.bar_count(), 1);
3347 assert!(agg.current_bar().is_some());
3348 agg.reset();
3349 assert_eq!(agg.bar_count(), 0);
3350 assert!(agg.current_bar().is_none());
3351 }
3352
3353 #[test]
3354 fn test_ohlcv_bar_is_bullish_when_close_gt_open() {
3355 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
3356 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1), 60_000))
3357 .unwrap();
3358 agg.feed(&make_tick("BTC-USD", dec!(51000), dec!(1), 60_100))
3359 .unwrap();
3360 let bar = agg.current_bar().unwrap();
3361 assert!(bar.is_bullish());
3362 assert!(!bar.is_bearish());
3363 }
3364
3365 #[test]
3366 fn test_ohlcv_bar_is_bearish_when_close_lt_open() {
3367 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
3368 agg.feed(&make_tick("BTC-USD", dec!(51000), dec!(1), 60_000))
3369 .unwrap();
3370 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1), 60_100))
3371 .unwrap();
3372 let bar = agg.current_bar().unwrap();
3373 assert!(bar.is_bearish());
3374 assert!(!bar.is_bullish());
3375 }
3376
3377 #[test]
3378 fn test_ohlcv_bar_neither_bullish_nor_bearish_on_equal_open_close() {
3379 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
3380 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1), 60_000))
3381 .unwrap();
3382 let bar = agg.current_bar().unwrap();
3384 assert!(!bar.is_bullish());
3385 assert!(!bar.is_bearish());
3386 }
3387
3388 #[test]
3389 fn test_ohlcv_bar_vwap_single_tick_equals_price() {
3390 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
3391 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(2), 60_000))
3392 .unwrap();
3393 let bar = agg.current_bar().unwrap();
3394 assert_eq!(bar.vwap, Some(dec!(50000)));
3395 }
3396
3397 #[test]
3398 fn test_ohlcv_bar_vwap_two_equal_price_ticks() {
3399 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
3400 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1), 60_000))
3401 .unwrap();
3402 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(3), 60_100))
3403 .unwrap();
3404 let bar = agg.current_bar().unwrap();
3405 assert_eq!(bar.vwap, Some(dec!(50000)));
3407 }
3408
3409 #[test]
3410 fn test_ohlcv_bar_vwap_two_different_price_ticks() {
3411 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
3412 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1), 60_000))
3413 .unwrap();
3414 agg.feed(&make_tick("BTC-USD", dec!(51000), dec!(1), 60_100))
3415 .unwrap();
3416 let bar = agg.current_bar().unwrap();
3417 assert_eq!(bar.vwap, Some(dec!(50500)));
3419 }
3420
3421 #[test]
3422 fn test_ohlcv_bar_vwap_gap_fill_is_none() {
3423 let mut agg = OhlcvAggregator::new("BTC-USD", Timeframe::Minutes(1))
3424 .unwrap()
3425 .with_emit_empty_bars(true);
3426 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1), 60_000))
3427 .unwrap();
3428 let bars = agg
3429 .feed(&make_tick("BTC-USD", dec!(51000), dec!(1), 240_000))
3430 .unwrap();
3431 assert!(bars[0].vwap.is_some());
3433 assert!(bars[1].vwap.is_none());
3434 assert!(bars[2].vwap.is_none());
3435 }
3436
3437 #[test]
3438 fn test_aggregator_reset_allows_fresh_start() {
3439 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
3440 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1), 60_000))
3441 .unwrap();
3442 agg.reset();
3443 agg.feed(&make_tick("BTC-USD", dec!(99999), dec!(2), 60_000))
3444 .unwrap();
3445 let bar = agg.current_bar().unwrap();
3446 assert_eq!(bar.open, dec!(99999));
3447 }
3448
3449 #[test]
3452 fn test_from_duration_ms_hours() {
3453 assert_eq!(Timeframe::from_duration_ms(3_600_000), Some(Timeframe::Hours(1)));
3454 assert_eq!(Timeframe::from_duration_ms(7_200_000), Some(Timeframe::Hours(2)));
3455 }
3456
3457 #[test]
3458 fn test_from_duration_ms_minutes() {
3459 assert_eq!(Timeframe::from_duration_ms(300_000), Some(Timeframe::Minutes(5)));
3460 assert_eq!(Timeframe::from_duration_ms(60_000), Some(Timeframe::Minutes(1)));
3461 }
3462
3463 #[test]
3464 fn test_from_duration_ms_seconds() {
3465 assert_eq!(Timeframe::from_duration_ms(15_000), Some(Timeframe::Seconds(15)));
3466 assert_eq!(Timeframe::from_duration_ms(1_000), Some(Timeframe::Seconds(1)));
3467 }
3468
3469 #[test]
3470 fn test_from_duration_ms_zero_returns_none() {
3471 assert_eq!(Timeframe::from_duration_ms(0), None);
3472 }
3473
3474 #[test]
3475 fn test_from_duration_ms_non_whole_second_returns_none() {
3476 assert_eq!(Timeframe::from_duration_ms(1_500), None);
3477 }
3478
3479 #[test]
3480 fn test_from_duration_ms_roundtrip() {
3481 for tf in [Timeframe::Seconds(30), Timeframe::Minutes(5), Timeframe::Hours(4)] {
3482 assert_eq!(Timeframe::from_duration_ms(tf.duration_ms()), Some(tf));
3483 }
3484 }
3485
3486 #[test]
3489 fn test_is_doji_exact_zero_body() {
3490 let bar = OhlcvBar {
3491 symbol: "X".into(), timeframe: Timeframe::Minutes(1),
3492 bar_start_ms: 0, open: dec!(100), high: dec!(105),
3493 low: dec!(95), close: dec!(100),
3494 volume: dec!(1), trade_count: 1, is_complete: true,
3495 is_gap_fill: false, vwap: None,
3496 };
3497 assert!(bar.is_doji(Decimal::ZERO));
3498 }
3499
3500 #[test]
3501 fn test_is_doji_small_epsilon() {
3502 let bar = OhlcvBar {
3503 symbol: "X".into(), timeframe: Timeframe::Minutes(1),
3504 bar_start_ms: 0, open: dec!(100), high: dec!(105),
3505 low: dec!(95), close: dec!(100.005),
3506 volume: dec!(1), trade_count: 1, is_complete: true,
3507 is_gap_fill: false, vwap: None,
3508 };
3509 assert!(bar.is_doji(dec!(0.01)));
3510 assert!(!bar.is_doji(Decimal::ZERO));
3511 }
3512
3513 #[test]
3514 fn test_wick_upper_bullish() {
3515 let bar = OhlcvBar {
3517 symbol: "X".into(), timeframe: Timeframe::Minutes(1),
3518 bar_start_ms: 0, open: dec!(100), high: dec!(107),
3519 low: dec!(98), close: dec!(104),
3520 volume: dec!(1), trade_count: 1, is_complete: true,
3521 is_gap_fill: false, vwap: None,
3522 };
3523 assert_eq!(bar.wick_upper(), dec!(3));
3524 }
3525
3526 #[test]
3527 fn test_wick_lower_bearish() {
3528 let bar = OhlcvBar {
3530 symbol: "X".into(), timeframe: Timeframe::Minutes(1),
3531 bar_start_ms: 0, open: dec!(104), high: dec!(107),
3532 low: dec!(97), close: dec!(100),
3533 volume: dec!(1), trade_count: 1, is_complete: true,
3534 is_gap_fill: false, vwap: None,
3535 };
3536 assert_eq!(bar.wick_lower(), dec!(3));
3537 }
3538
3539 #[test]
3542 fn test_window_progress_none_when_no_bar() {
3543 let agg = agg("BTC-USD", Timeframe::Minutes(1));
3544 assert!(agg.window_progress(60_000).is_none());
3545 }
3546
3547 #[test]
3548 fn test_window_progress_at_start_is_zero() {
3549 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
3550 agg.feed(&make_tick("BTC-USD", dec!(100), dec!(1), 60_000)).unwrap();
3552 assert_eq!(agg.window_progress(60_000), Some(0.0));
3553 }
3554
3555 #[test]
3556 fn test_window_progress_midpoint() {
3557 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
3558 agg.feed(&make_tick("BTC-USD", dec!(100), dec!(1), 60_000)).unwrap();
3559 let progress = agg.window_progress(90_000).unwrap();
3561 assert!((progress - 0.5).abs() < 1e-9);
3562 }
3563
3564 #[test]
3565 fn test_window_progress_clamps_at_one() {
3566 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
3567 agg.feed(&make_tick("BTC-USD", dec!(100), dec!(1), 60_000)).unwrap();
3568 assert_eq!(agg.window_progress(150_000), Some(1.0));
3570 }
3571
3572 #[test]
3575 fn test_price_change_bullish_is_positive() {
3576 let bar = make_bar(dec!(100), dec!(110), dec!(98), dec!(105));
3577 assert_eq!(bar.price_change(), dec!(5));
3578 }
3579
3580 #[test]
3581 fn test_price_change_bearish_is_negative() {
3582 let bar = make_bar(dec!(105), dec!(110), dec!(98), dec!(100));
3583 assert_eq!(bar.price_change(), dec!(-5));
3584 }
3585
3586 #[test]
3587 fn test_price_change_doji_is_zero() {
3588 let bar = make_bar(dec!(100), dec!(102), dec!(98), dec!(100));
3589 assert_eq!(bar.price_change(), dec!(0));
3590 }
3591
3592 #[test]
3595 fn test_total_volume_zero_before_completion() {
3596 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
3597 agg.feed(&make_tick("BTC-USD", dec!(100), dec!(2), 60_000)).unwrap();
3598 assert_eq!(agg.total_volume(), dec!(0));
3600 }
3601
3602 #[test]
3603 fn test_total_volume_accumulates_across_bars() {
3604 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
3605 agg.feed(&make_tick("BTC-USD", dec!(100), dec!(2), 60_000)).unwrap();
3607 agg.feed(&make_tick("BTC-USD", dec!(101), dec!(3), 120_000)).unwrap();
3609 assert_eq!(agg.total_volume(), dec!(2));
3611 agg.feed(&make_tick("BTC-USD", dec!(102), dec!(5), 180_000)).unwrap();
3613 assert_eq!(agg.total_volume(), dec!(5)); }
3615
3616 #[test]
3617 fn test_total_volume_reset_clears() {
3618 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
3619 agg.feed(&make_tick("BTC-USD", dec!(100), dec!(2), 60_000)).unwrap();
3620 agg.feed(&make_tick("BTC-USD", dec!(101), dec!(3), 120_000)).unwrap();
3621 agg.reset();
3622 assert_eq!(agg.total_volume(), dec!(0));
3623 }
3624
3625 fn make_bar(open: Decimal, high: Decimal, low: Decimal, close: Decimal) -> OhlcvBar {
3628 OhlcvBar {
3629 symbol: "X".into(),
3630 timeframe: Timeframe::Minutes(1),
3631 bar_start_ms: 0,
3632 open,
3633 high,
3634 low,
3635 close,
3636 volume: dec!(1),
3637 trade_count: 1,
3638 is_complete: true,
3639 is_gap_fill: false,
3640 vwap: None,
3641 }
3642 }
3643
3644 #[test]
3645 fn test_typical_price() {
3646 let bar = make_bar(dec!(9), dec!(12), dec!(8), dec!(10));
3648 assert_eq!(bar.typical_price(), dec!(10));
3649 }
3650
3651 #[test]
3652 fn test_median_price() {
3653 let bar = make_bar(dec!(9), dec!(12), dec!(8), dec!(10));
3655 assert_eq!(bar.median_price(), dec!(10));
3656 }
3657
3658 #[test]
3659 fn test_typical_price_differs_from_median() {
3660 let bar = make_bar(dec!(8), dec!(10), dec!(6), dec!(10));
3662 assert_eq!(bar.median_price(), dec!(8));
3663 assert!(bar.typical_price() > bar.median_price());
3664 }
3665
3666 #[test]
3667 fn test_close_location_value_at_high() {
3668 let bar = make_bar(dec!(100), dec!(110), dec!(90), dec!(110));
3670 let clv = bar.close_location_value().unwrap();
3671 assert!((clv - 1.0).abs() < 1e-9, "expected 1.0 got {clv}");
3672 }
3673
3674 #[test]
3675 fn test_close_location_value_at_low() {
3676 let bar = make_bar(dec!(100), dec!(110), dec!(90), dec!(90));
3678 let clv = bar.close_location_value().unwrap();
3679 assert!((clv + 1.0).abs() < 1e-9, "expected -1.0 got {clv}");
3680 }
3681
3682 #[test]
3683 fn test_close_location_value_midpoint_is_zero() {
3684 let bar = make_bar(dec!(100), dec!(110), dec!(90), dec!(100));
3686 let clv = bar.close_location_value().unwrap();
3687 assert!(clv.abs() < 1e-9, "expected 0.0 got {clv}");
3688 }
3689
3690 #[test]
3691 fn test_close_location_value_zero_range_returns_none() {
3692 let bar = make_bar(dec!(100), dec!(100), dec!(100), dec!(100));
3693 assert!(bar.close_location_value().is_none());
3694 }
3695
3696 #[test]
3697 fn test_body_direction_bullish() {
3698 let bar = make_bar(dec!(90), dec!(110), dec!(85), dec!(105));
3699 assert_eq!(bar.body_direction(), BarDirection::Bullish);
3700 }
3701
3702 #[test]
3703 fn test_body_direction_bearish() {
3704 let bar = make_bar(dec!(105), dec!(110), dec!(85), dec!(90));
3705 assert_eq!(bar.body_direction(), BarDirection::Bearish);
3706 }
3707
3708 #[test]
3709 fn test_body_direction_neutral() {
3710 let bar = make_bar(dec!(100), dec!(110), dec!(85), dec!(100));
3711 assert_eq!(bar.body_direction(), BarDirection::Neutral);
3712 }
3713
3714 #[test]
3717 fn test_last_bar_none_before_completion() {
3718 let agg = agg("BTC-USD", Timeframe::Minutes(1));
3719 assert!(agg.last_bar().is_none());
3720 }
3721
3722 #[test]
3723 fn test_last_bar_set_after_bar_completion() {
3724 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
3725 agg.feed(&make_tick("BTC-USD", dec!(100), dec!(1), 60_000)).unwrap();
3727 agg.feed(&make_tick("BTC-USD", dec!(200), dec!(1), 120_000)).unwrap();
3729 let last = agg.last_bar().unwrap();
3730 assert!(last.is_complete);
3731 assert_eq!(last.close, dec!(100));
3732 }
3733
3734 #[test]
3735 fn test_last_bar_set_after_flush() {
3736 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
3737 agg.feed(&make_tick("BTC-USD", dec!(50), dec!(1), 60_000)).unwrap();
3738 let flushed = agg.flush().unwrap();
3739 assert_eq!(agg.last_bar().unwrap().close, flushed.close);
3740 }
3741
3742 #[test]
3743 fn test_last_bar_cleared_on_reset() {
3744 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
3745 agg.feed(&make_tick("BTC-USD", dec!(100), dec!(1), 60_000)).unwrap();
3746 agg.feed(&make_tick("BTC-USD", dec!(200), dec!(1), 120_000)).unwrap();
3747 assert!(agg.last_bar().is_some());
3748 agg.reset();
3749 assert!(agg.last_bar().is_none());
3750 }
3751
3752 #[test]
3755 fn test_weighted_close_basic() {
3756 let bar = make_bar(dec!(9), dec!(12), dec!(8), dec!(10));
3758 assert_eq!(bar.weighted_close(), dec!(10));
3759 }
3760
3761 #[test]
3762 fn test_weighted_close_weights_close_more_than_typical() {
3763 let bar = make_bar(dec!(50), dec!(100), dec!(0), dec!(80));
3765 assert_eq!(bar.weighted_close(), dec!(65));
3766 }
3767
3768 #[test]
3769 fn test_price_change_pct_bullish() {
3770 let bar = make_bar(dec!(100), dec!(115), dec!(95), dec!(110));
3772 let pct = bar.price_change_pct().unwrap();
3773 assert!((pct - 10.0).abs() < 1e-9, "expected 10.0 got {pct}");
3774 }
3775
3776 #[test]
3777 fn test_price_change_pct_bearish() {
3778 let bar = make_bar(dec!(200), dec!(210), dec!(175), dec!(180));
3780 let pct = bar.price_change_pct().unwrap();
3781 assert!((pct - (-10.0)).abs() < 1e-9, "expected -10.0 got {pct}");
3782 }
3783
3784 #[test]
3785 fn test_price_change_pct_zero_open_returns_none() {
3786 let bar = make_bar(dec!(0), dec!(5), dec!(0), dec!(3));
3787 assert!(bar.price_change_pct().is_none());
3788 }
3789
3790 #[test]
3791 fn test_wick_ratio_all_wicks() {
3792 let bar = make_bar(dec!(5), dec!(10), dec!(0), dec!(5));
3794 let r = bar.wick_ratio().unwrap();
3795 assert!((r - 1.0).abs() < 1e-9, "expected 1.0 got {r}");
3796 }
3797
3798 #[test]
3799 fn test_wick_ratio_no_wicks() {
3800 let bar = make_bar(dec!(0), dec!(10), dec!(0), dec!(10));
3802 let r = bar.wick_ratio().unwrap();
3803 assert!((r - 0.0).abs() < 1e-9, "expected 0.0 got {r}");
3804 }
3805
3806 #[test]
3807 fn test_wick_ratio_zero_range_returns_none() {
3808 let bar = make_bar(dec!(5), dec!(5), dec!(5), dec!(5));
3810 assert!(bar.wick_ratio().is_none());
3811 }
3812
3813 #[test]
3816 fn test_body_ratio_no_wicks_is_one() {
3817 let bar = make_bar(dec!(0), dec!(10), dec!(0), dec!(10));
3819 let r = bar.body_ratio().unwrap();
3820 assert!((r - 1.0).abs() < 1e-9);
3821 }
3822
3823 #[test]
3824 fn test_body_ratio_all_wicks_is_zero() {
3825 let bar = make_bar(dec!(5), dec!(10), dec!(0), dec!(5));
3827 let r = bar.body_ratio().unwrap();
3828 assert!((r - 0.0).abs() < 1e-9);
3829 }
3830
3831 #[test]
3832 fn test_body_ratio_zero_range_returns_none() {
3833 let bar = make_bar(dec!(5), dec!(5), dec!(5), dec!(5));
3834 assert!(bar.body_ratio().is_none());
3835 }
3836
3837 #[test]
3838 fn test_body_ratio_plus_wick_ratio_equals_one() {
3839 let bar = make_bar(dec!(4), dec!(10), dec!(0), dec!(8));
3841 let body = bar.body_ratio().unwrap();
3842 let wick = bar.wick_ratio().unwrap();
3843 assert!((body + wick - 1.0).abs() < 1e-9);
3844 }
3845
3846 #[test]
3849 fn test_average_volume_none_before_bars() {
3850 let agg = agg("BTC-USD", Timeframe::Minutes(1));
3851 assert!(agg.average_volume().is_none());
3852 }
3853
3854 #[test]
3855 fn test_average_volume_one_bar() {
3856 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
3857 agg.feed(&make_tick("BTC-USD", dec!(100), dec!(4), 60_000)).unwrap();
3858 agg.feed(&make_tick("BTC-USD", dec!(101), dec!(1), 120_000)).unwrap();
3859 assert_eq!(agg.average_volume(), Some(dec!(4)));
3861 }
3862
3863 #[test]
3864 fn test_average_volume_two_bars() {
3865 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
3866 agg.feed(&make_tick("BTC-USD", dec!(100), dec!(4), 60_000)).unwrap();
3867 agg.feed(&make_tick("BTC-USD", dec!(101), dec!(6), 120_000)).unwrap();
3868 agg.feed(&make_tick("BTC-USD", dec!(102), dec!(1), 180_000)).unwrap();
3869 assert_eq!(agg.average_volume(), Some(dec!(5)));
3871 }
3872
3873 #[test]
3876 fn test_true_range_no_gap() {
3877 let bar = make_bar(dec!(9), dec!(12), dec!(8), dec!(11));
3879 assert_eq!(bar.true_range(dec!(10)), dec!(4));
3880 }
3881
3882 #[test]
3883 fn test_true_range_gap_up() {
3884 let bar = make_bar(dec!(12), dec!(15), dec!(12), dec!(13));
3886 assert_eq!(bar.true_range(dec!(10)), dec!(5));
3887 }
3888
3889 #[test]
3890 fn test_true_range_gap_down() {
3891 let bar = make_bar(dec!(7), dec!(8), dec!(5), dec!(6));
3893 assert_eq!(bar.true_range(dec!(12)), dec!(7));
3894 }
3895
3896 #[test]
3897 fn test_inside_bar_true_when_contained() {
3898 let prev = make_bar(dec!(9), dec!(15), dec!(5), dec!(12));
3899 let curr = make_bar(dec!(10), dec!(14), dec!(6), dec!(11));
3900 assert!(curr.is_inside_bar(&prev));
3901 }
3902
3903 #[test]
3904 fn test_inside_bar_false_when_not_contained() {
3905 let prev = make_bar(dec!(9), dec!(15), dec!(5), dec!(12));
3906 let curr = make_bar(dec!(10), dec!(16), dec!(6), dec!(11));
3907 assert!(!curr.is_inside_bar(&prev));
3908 }
3909
3910 #[test]
3911 fn test_outside_bar_true_when_engulfing() {
3912 let prev = make_bar(dec!(9), dec!(12), dec!(8), dec!(11));
3913 let curr = make_bar(dec!(10), dec!(14), dec!(6), dec!(11));
3914 assert!(curr.outside_bar(&prev));
3915 }
3916
3917 #[test]
3918 fn test_outside_bar_false_when_not_engulfing() {
3919 let prev = make_bar(dec!(9), dec!(12), dec!(8), dec!(11));
3920 let curr = make_bar(dec!(10), dec!(11), dec!(9), dec!(10));
3921 assert!(!curr.outside_bar(&prev));
3922 }
3923
3924 #[test]
3927 fn test_is_hammer_classic() {
3928 let bar = make_bar(dec!(9), dec!(10), dec!(0), dec!(9));
3931 assert!(bar.is_hammer());
3932 }
3933
3934 #[test]
3935 fn test_is_hammer_false_large_upper_wick() {
3936 let bar = make_bar(dec!(5), dec!(10), dec!(0), dec!(5));
3938 assert!(!bar.is_hammer());
3939 }
3940
3941 #[test]
3942 fn test_is_hammer_false_zero_range() {
3943 let bar = make_bar(dec!(5), dec!(5), dec!(5), dec!(5));
3944 assert!(!bar.is_hammer());
3945 }
3946
3947 #[test]
3950 fn test_peak_volume_none_before_completion() {
3951 let agg = agg("BTC-USD", Timeframe::Minutes(1));
3952 assert!(agg.peak_volume().is_none());
3953 }
3954
3955 #[test]
3956 fn test_peak_volume_tracks_maximum() {
3957 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
3958 agg.feed(&make_tick("BTC-USD", dec!(100), dec!(3), 60_000)).unwrap();
3960 agg.feed(&make_tick("BTC-USD", dec!(101), dec!(10), 120_000)).unwrap();
3962 assert_eq!(agg.peak_volume(), Some(dec!(3)));
3963 agg.feed(&make_tick("BTC-USD", dec!(102), dec!(1), 180_000)).unwrap();
3965 assert_eq!(agg.peak_volume(), Some(dec!(10)));
3966 }
3967
3968 #[test]
3969 fn test_peak_volume_reset_clears() {
3970 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
3971 agg.feed(&make_tick("BTC-USD", dec!(100), dec!(5), 60_000)).unwrap();
3972 agg.feed(&make_tick("BTC-USD", dec!(101), dec!(1), 120_000)).unwrap();
3973 agg.reset();
3974 assert!(agg.peak_volume().is_none());
3975 }
3976
3977 #[test]
3978 fn test_peak_volume_via_flush() {
3979 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
3980 agg.feed(&make_tick("BTC-USD", dec!(100), dec!(7), 60_000)).unwrap();
3981 agg.flush();
3982 assert_eq!(agg.peak_volume(), Some(dec!(7)));
3983 }
3984
3985 #[test]
3988 fn test_is_shooting_star_classic() {
3989 let bar = make_bar(dec!(1), dec!(10), dec!(0), dec!(1));
3992 assert!(bar.is_shooting_star());
3993 }
3994
3995 #[test]
3996 fn test_is_shooting_star_false_large_lower_wick() {
3997 let bar = make_bar(dec!(5), dec!(10), dec!(0), dec!(5));
3999 assert!(!bar.is_shooting_star());
4000 }
4001
4002 #[test]
4003 fn test_is_shooting_star_false_zero_range() {
4004 let bar = make_bar(dec!(5), dec!(5), dec!(5), dec!(5));
4005 assert!(!bar.is_shooting_star());
4006 }
4007
4008 #[test]
4009 fn test_hammer_and_shooting_star_are_mutually_exclusive_for_typical_bars() {
4010 let hammer = make_bar(dec!(9), dec!(10), dec!(0), dec!(9));
4012 let star = make_bar(dec!(1), dec!(10), dec!(0), dec!(1));
4014 assert!(hammer.is_hammer() && !hammer.is_shooting_star());
4015 assert!(star.is_shooting_star() && !star.is_hammer());
4016 }
4017
4018 #[test]
4021 fn test_min_volume_none_before_completion() {
4022 let agg = agg("BTC-USD", Timeframe::Minutes(1));
4023 assert!(agg.min_volume().is_none());
4024 }
4025
4026 #[test]
4027 fn test_min_volume_tracks_minimum() {
4028 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4029 agg.feed(&make_tick("BTC-USD", dec!(100), dec!(10), 60_000)).unwrap();
4031 agg.feed(&make_tick("BTC-USD", dec!(101), dec!(1), 120_000)).unwrap();
4032 assert_eq!(agg.min_volume(), Some(dec!(10)));
4033 agg.feed(&make_tick("BTC-USD", dec!(102), dec!(5), 180_000)).unwrap();
4035 assert_eq!(agg.min_volume(), Some(dec!(1)));
4036 }
4037
4038 #[test]
4039 fn test_min_volume_reset_clears() {
4040 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4041 agg.feed(&make_tick("BTC-USD", dec!(100), dec!(5), 60_000)).unwrap();
4042 agg.feed(&make_tick("BTC-USD", dec!(101), dec!(1), 120_000)).unwrap();
4043 agg.reset();
4044 assert!(agg.min_volume().is_none());
4045 }
4046
4047 #[test]
4050 fn test_is_gap_up_true() {
4051 let prev = make_bar(dec!(5), dec!(10), dec!(4), dec!(8));
4052 let curr = make_bar(dec!(9), dec!(12), dec!(8), dec!(11)); assert!(curr.is_gap_up(&prev));
4054 }
4055
4056 #[test]
4057 fn test_is_gap_up_false_when_equal() {
4058 let prev = make_bar(dec!(5), dec!(10), dec!(4), dec!(8));
4059 let curr = make_bar(dec!(8), dec!(12), dec!(7), dec!(11)); assert!(!curr.is_gap_up(&prev));
4061 }
4062
4063 #[test]
4064 fn test_is_gap_down_true() {
4065 let prev = make_bar(dec!(5), dec!(10), dec!(4), dec!(8));
4066 let curr = make_bar(dec!(7), dec!(8), dec!(6), dec!(7)); assert!(curr.is_gap_down(&prev));
4068 }
4069
4070 #[test]
4071 fn test_is_gap_down_false_when_equal() {
4072 let prev = make_bar(dec!(5), dec!(10), dec!(4), dec!(8));
4073 let curr = make_bar(dec!(8), dec!(9), dec!(7), dec!(8)); assert!(!curr.is_gap_down(&prev));
4075 }
4076
4077 #[test]
4080 fn test_volume_range_none_before_completion() {
4081 let agg = agg("BTC-USD", Timeframe::Minutes(1));
4082 assert!(agg.volume_range().is_none());
4083 }
4084
4085 #[test]
4086 fn test_volume_range_after_two_bars() {
4087 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4088 agg.feed(&make_tick("BTC-USD", dec!(100), dec!(3), 60_000)).unwrap();
4089 agg.feed(&make_tick("BTC-USD", dec!(101), dec!(10), 120_000)).unwrap();
4090 agg.feed(&make_tick("BTC-USD", dec!(102), dec!(1), 180_000)).unwrap();
4091 assert_eq!(agg.volume_range(), Some((dec!(3), dec!(10))));
4093 }
4094
4095 fn make_ohlcv_bar(open: Decimal, high: Decimal, low: Decimal, close: Decimal) -> OhlcvBar {
4098 OhlcvBar {
4099 symbol: "X".into(),
4100 timeframe: Timeframe::Minutes(1),
4101 open,
4102 high,
4103 low,
4104 close,
4105 volume: dec!(1),
4106 bar_start_ms: 0,
4107 trade_count: 1,
4108 is_complete: false,
4109 is_gap_fill: false,
4110 vwap: None,
4111 }
4112 }
4113
4114 #[test]
4115 fn test_body_to_range_ratio_bullish_full_body() {
4116 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(100), dec!(110));
4118 assert_eq!(bar.body_to_range_ratio(), Some(dec!(1)));
4119 }
4120
4121 #[test]
4122 fn test_body_to_range_ratio_doji_like() {
4123 let bar = make_ohlcv_bar(dec!(100), dec!(102), dec!(98), dec!(100));
4125 assert_eq!(bar.body_to_range_ratio(), Some(dec!(0)));
4126 }
4127
4128 #[test]
4129 fn test_body_to_range_ratio_none_when_range_zero() {
4130 let bar = make_ohlcv_bar(dec!(100), dec!(100), dec!(100), dec!(100));
4131 assert!(bar.body_to_range_ratio().is_none());
4132 }
4133
4134 #[test]
4137 fn test_is_active_false_before_any_ticks() {
4138 let agg = agg("BTC-USD", Timeframe::Minutes(1));
4139 assert!(!agg.is_active());
4140 }
4141
4142 #[test]
4143 fn test_is_active_true_after_first_tick() {
4144 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4145 agg.feed(&make_tick("BTC-USD", dec!(100), dec!(1), 1_000)).unwrap();
4146 assert!(agg.is_active());
4147 }
4148
4149 #[test]
4150 fn test_is_active_false_after_flush() {
4151 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4152 agg.feed(&make_tick("BTC-USD", dec!(100), dec!(1), 1_000)).unwrap();
4153 agg.flush();
4154 assert!(!agg.is_active());
4155 }
4156
4157 #[test]
4160 fn test_is_long_upper_wick_true_when_upper_wick_dominates() {
4161 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(100), dec!(101));
4163 assert!(bar.is_long_upper_wick());
4164 }
4165
4166 #[test]
4167 fn test_is_long_upper_wick_false_for_full_body() {
4168 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(100), dec!(110));
4170 assert!(!bar.is_long_upper_wick());
4171 }
4172
4173 #[test]
4174 fn test_is_long_upper_wick_false_when_equal() {
4175 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(100), dec!(105));
4177 assert!(!bar.is_long_upper_wick());
4178 }
4179
4180 #[test]
4183 fn test_price_change_abs_bullish_bar() {
4184 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(100), dec!(108));
4185 assert_eq!(bar.price_change_abs(), dec!(8));
4186 }
4187
4188 #[test]
4189 fn test_price_change_abs_bearish_bar() {
4190 let bar = make_ohlcv_bar(dec!(110), dec!(110), dec!(100), dec!(102));
4191 assert_eq!(bar.price_change_abs(), dec!(8));
4192 }
4193
4194 #[test]
4195 fn test_price_change_abs_doji_zero() {
4196 let bar = make_ohlcv_bar(dec!(100), dec!(105), dec!(95), dec!(100));
4197 assert_eq!(bar.price_change_abs(), dec!(0));
4198 }
4199
4200 #[test]
4203 fn test_vwap_current_none_before_any_ticks() {
4204 let agg = agg("BTC-USD", Timeframe::Minutes(1));
4205 assert!(agg.vwap_current().is_none());
4206 }
4207
4208 #[test]
4209 fn test_vwap_current_equals_price_for_single_tick() {
4210 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4211 agg.feed(&make_tick("BTC-USD", dec!(200), dec!(5), 1_000)).unwrap();
4212 assert_eq!(agg.vwap_current(), Some(dec!(200)));
4214 }
4215
4216 #[test]
4217 fn test_vwap_current_weighted_average() {
4218 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4219 agg.feed(&make_tick("BTC-USD", dec!(100), dec!(1), 1_000)).unwrap();
4220 agg.feed(&make_tick("BTC-USD", dec!(200), dec!(3), 2_000)).unwrap();
4221 assert_eq!(agg.vwap_current(), Some(dec!(175)));
4223 }
4224
4225 fn bar(o: i64, h: i64, l: i64, c: i64) -> OhlcvBar {
4228 OhlcvBar {
4229 symbol: "X".into(),
4230 timeframe: Timeframe::Minutes(1),
4231 open: Decimal::from(o),
4232 high: Decimal::from(h),
4233 low: Decimal::from(l),
4234 close: Decimal::from(c),
4235 volume: Decimal::ZERO,
4236 bar_start_ms: 0,
4237 trade_count: 0,
4238 is_complete: false,
4239 is_gap_fill: false,
4240 vwap: None,
4241 }
4242 }
4243
4244 #[test]
4245 fn test_upper_shadow_equals_wick_upper() {
4246 let b = bar(100, 120, 90, 110);
4247 assert_eq!(b.upper_shadow(), b.wick_upper());
4248 assert_eq!(b.upper_shadow(), Decimal::from(10)); }
4250
4251 #[test]
4252 fn test_lower_shadow_equals_wick_lower() {
4253 let b = bar(100, 120, 90, 110);
4254 assert_eq!(b.lower_shadow(), b.wick_lower());
4255 assert_eq!(b.lower_shadow(), Decimal::from(10)); }
4257
4258 #[test]
4259 fn test_is_spinning_top_true_when_small_body_large_wicks() {
4260 let b = bar(100, 130, 80, 110);
4265 assert!(b.is_spinning_top(dec!(0.3)));
4266 }
4267
4268 #[test]
4269 fn test_is_spinning_top_false_when_body_too_large() {
4270 let b = bar(80, 130, 80, 120);
4272 assert!(!b.is_spinning_top(dec!(0.3)));
4273 }
4274
4275 #[test]
4276 fn test_is_spinning_top_false_when_zero_range() {
4277 let b = bar(100, 100, 100, 100);
4278 assert!(!b.is_spinning_top(dec!(0.3)));
4279 }
4280
4281 #[test]
4282 fn test_hlc3_equals_typical_price() {
4283 let b = bar(100, 120, 80, 110);
4284 assert_eq!(b.hlc3(), b.typical_price());
4285 assert_eq!(b.hlc3(), (Decimal::from(120) + Decimal::from(80) + Decimal::from(110)) / Decimal::from(3));
4287 }
4288
4289 #[test]
4292 fn test_is_bearish_true_when_close_below_open() {
4293 let bar = make_ohlcv_bar(dec!(110), dec!(115), dec!(100), dec!(105));
4294 assert!(bar.is_bearish());
4295 }
4296
4297 #[test]
4298 fn test_is_bearish_false_when_close_above_open() {
4299 let bar = make_ohlcv_bar(dec!(100), dec!(115), dec!(95), dec!(110));
4300 assert!(!bar.is_bearish());
4301 }
4302
4303 #[test]
4304 fn test_is_bearish_false_when_doji() {
4305 let bar = make_ohlcv_bar(dec!(100), dec!(105), dec!(95), dec!(100));
4306 assert!(!bar.is_bearish());
4307 }
4308
4309 #[test]
4312 fn test_wick_ratio_zero_for_full_body_no_wicks() {
4313 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(100), dec!(110));
4315 let ratio = bar.wick_ratio().unwrap();
4316 assert!(ratio.abs() < 1e-10);
4317 }
4318
4319 #[test]
4320 fn test_wick_ratio_one_for_pure_wick_doji() {
4321 let bar = make_ohlcv_bar(dec!(105), dec!(110), dec!(100), dec!(105));
4323 let ratio = bar.wick_ratio().unwrap();
4324 assert!((ratio - 1.0).abs() < 1e-10);
4325 }
4326
4327 #[test]
4328 fn test_wick_ratio_none_for_zero_range_bar() {
4329 let bar = make_ohlcv_bar(dec!(100), dec!(100), dec!(100), dec!(100));
4330 assert!(bar.wick_ratio().is_none());
4331 }
4332
4333 #[test]
4336 fn test_is_bullish_true_when_close_above_open() {
4337 let bar = make_ohlcv_bar(dec!(100), dec!(115), dec!(95), dec!(110));
4338 assert!(bar.is_bullish());
4339 }
4340
4341 #[test]
4342 fn test_is_bullish_false_when_close_below_open() {
4343 let bar = make_ohlcv_bar(dec!(110), dec!(115), dec!(100), dec!(105));
4344 assert!(!bar.is_bullish());
4345 }
4346
4347 #[test]
4348 fn test_is_bullish_false_when_doji() {
4349 let bar = make_ohlcv_bar(dec!(100), dec!(105), dec!(95), dec!(100));
4350 assert!(!bar.is_bullish());
4351 }
4352
4353 #[test]
4356 fn test_bar_duration_ms_one_minute() {
4357 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
4358 assert_eq!(bar.bar_duration_ms(), 60_000);
4359 }
4360
4361 #[test]
4362 fn test_bar_duration_ms_consistent_with_timeframe() {
4363 let mut bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
4364 bar.timeframe = Timeframe::Hours(1);
4365 assert_eq!(bar.bar_duration_ms(), 3_600_000);
4366 }
4367
4368 #[test]
4369 fn test_bar_duration_ms_seconds_timeframe() {
4370 let mut bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
4371 bar.timeframe = Timeframe::Seconds(30);
4372 assert_eq!(bar.bar_duration_ms(), 30_000);
4373 }
4374
4375 #[test]
4378 fn test_ohlc4_equals_average_of_all_four_prices() {
4379 let b = bar(100, 120, 80, 110);
4380 let expected = (Decimal::from(100) + Decimal::from(120) + Decimal::from(80) + Decimal::from(110))
4382 / Decimal::from(4);
4383 assert_eq!(b.ohlc4(), expected);
4384 }
4385
4386 #[test]
4387 fn test_is_marubozu_true_when_no_wicks() {
4388 let b = bar(100, 110, 100, 110);
4390 assert!(b.is_marubozu());
4391 }
4392
4393 #[test]
4394 fn test_is_marubozu_false_when_has_upper_wick() {
4395 let b = bar(100, 115, 100, 110);
4396 assert!(!b.is_marubozu());
4397 }
4398
4399 #[test]
4400 fn test_is_marubozu_false_when_has_lower_wick() {
4401 let b = bar(100, 110, 95, 110);
4402 assert!(!b.is_marubozu());
4403 }
4404
4405 #[test]
4408 fn test_is_harami_true_when_body_inside_prev_body() {
4409 let prev = bar(98, 115, 90, 108); let curr = bar(100, 110, 95, 105); assert!(curr.is_harami(&prev));
4412 }
4413
4414 #[test]
4415 fn test_is_harami_false_when_body_engulfs_prev() {
4416 let prev = bar(100, 110, 95, 105); let curr = bar(98, 115, 90, 108); assert!(!curr.is_harami(&prev));
4419 }
4420
4421 #[test]
4422 fn test_is_harami_false_when_bodies_equal() {
4423 let prev = bar(100, 110, 90, 105);
4424 let curr = bar(100, 110, 90, 105); assert!(!curr.is_harami(&prev));
4426 }
4427
4428 #[test]
4429 fn test_tail_length_upper_wick_longer() {
4430 let b = bar(100, 120, 95, 105);
4432 assert_eq!(b.tail_length(), Decimal::from(15));
4433 }
4434
4435 #[test]
4436 fn test_tail_length_lower_wick_longer() {
4437 let b = bar(105, 110, 80, 100);
4439 assert_eq!(b.tail_length(), Decimal::from(20));
4440 }
4441
4442 #[test]
4443 fn test_tail_length_zero_for_marubozu() {
4444 let b = bar(100, 110, 100, 110);
4446 assert!(b.tail_length().is_zero());
4447 }
4448
4449 #[test]
4452 fn test_is_inside_bar_true_when_range_within_prev() {
4453 let prev = bar(90, 120, 80, 110); let curr = bar(95, 115, 85, 100); assert!(curr.is_inside_bar(&prev));
4456 }
4457
4458 #[test]
4459 fn test_is_inside_bar_false_when_high_exceeds_prev_high() {
4460 let prev = bar(90, 110, 80, 100); let curr = bar(95, 112, 85, 100); assert!(!curr.is_inside_bar(&prev));
4463 }
4464
4465 #[test]
4466 fn test_is_inside_bar_false_when_equal_range() {
4467 let prev = bar(90, 110, 80, 100);
4468 let curr = bar(90, 110, 80, 100); assert!(!curr.is_inside_bar(&prev));
4470 }
4471
4472 #[test]
4473 fn test_bar_type_bullish() {
4474 let b = bar(100, 110, 90, 105); assert_eq!(b.bar_type(), "bullish");
4476 }
4477
4478 #[test]
4479 fn test_bar_type_bearish() {
4480 let b = bar(105, 110, 90, 100); assert_eq!(b.bar_type(), "bearish");
4482 }
4483
4484 #[test]
4485 fn test_bar_type_doji() {
4486 let b = bar(100, 110, 90, 100); assert_eq!(b.bar_type(), "doji");
4488 }
4489
4490 #[test]
4493 fn test_body_pct_none_for_zero_range() {
4494 let b = bar(100, 100, 100, 100);
4495 assert!(b.body_pct().is_none());
4496 }
4497
4498 #[test]
4499 fn test_body_pct_100_for_marubozu() {
4500 let b = bar(100, 110, 100, 110);
4502 assert_eq!(b.body_pct().unwrap(), Decimal::ONE_HUNDRED);
4503 }
4504
4505 #[test]
4506 fn test_body_pct_50_for_half_body() {
4507 let b = bar(100, 110, 100, 105);
4509 assert_eq!(b.body_pct().unwrap(), Decimal::from(50));
4510 }
4511
4512 #[test]
4513 fn test_is_bullish_hammer_true_for_classic_hammer() {
4514 let b = bar(108, 110, 100, 109);
4517 assert!(b.is_bullish_hammer());
4518 }
4519
4520 #[test]
4521 fn test_is_bullish_hammer_false_when_lower_wick_not_long_enough() {
4522 let b = bar(100, 110, 98, 108);
4524 assert!(!b.is_bullish_hammer());
4525 }
4526
4527 #[test]
4528 fn test_is_bullish_hammer_false_for_doji() {
4529 let b = bar(100, 110, 90, 100); assert!(!b.is_bullish_hammer());
4531 }
4532
4533 #[test]
4535 fn test_is_marubozu_true_when_full_body() {
4536 let b = bar(100, 110, 100, 110);
4538 assert!(b.is_marubozu());
4539 }
4540
4541 #[test]
4542 fn test_is_marubozu_false_when_large_wicks() {
4543 let b = bar(100, 120, 80, 110);
4545 assert!(!b.is_marubozu());
4546 }
4547
4548 #[test]
4549 fn test_is_marubozu_true_for_zero_range_flat_bar() {
4550 let b = bar(100, 100, 100, 100);
4552 assert!(b.is_marubozu());
4553 }
4554
4555 #[test]
4557 fn test_upper_wick_pct_zero_when_no_upper_wick() {
4558 let b = bar(100, 110, 90, 110);
4560 let pct = b.upper_wick_pct().unwrap();
4561 assert!(pct.is_zero(), "expected 0, got {pct}");
4562 }
4563
4564 #[test]
4565 fn test_upper_wick_pct_50_when_half_range() {
4566 let b = bar(100, 120, 100, 110);
4568 let pct = b.upper_wick_pct().unwrap();
4569 assert_eq!(pct, dec!(50));
4570 }
4571
4572 #[test]
4573 fn test_upper_wick_pct_none_for_zero_range() {
4574 let b = bar(100, 100, 100, 100);
4575 assert!(b.upper_wick_pct().is_none());
4576 }
4577
4578 #[test]
4580 fn test_lower_wick_pct_zero_when_no_lower_wick() {
4581 let b = bar(100, 110, 100, 105);
4583 let pct = b.lower_wick_pct().unwrap();
4584 assert!(pct.is_zero(), "expected 0, got {pct}");
4585 }
4586
4587 #[test]
4588 fn test_lower_wick_pct_50_when_half_range() {
4589 let b = bar(110, 120, 100, 115);
4591 let pct = b.lower_wick_pct().unwrap();
4592 assert_eq!(pct, dec!(50));
4593 }
4594
4595 #[test]
4596 fn test_lower_wick_pct_none_for_zero_range() {
4597 let b = bar(100, 100, 100, 100);
4598 assert!(b.lower_wick_pct().is_none());
4599 }
4600
4601 #[test]
4603 fn test_is_bearish_engulfing_true_for_bearish_engulf() {
4604 let prev = bar(100, 115, 95, 110); let curr = bar(112, 115, 88, 90); assert!(curr.is_bearish_engulfing(&prev));
4607 }
4608
4609 #[test]
4610 fn test_is_bearish_engulfing_false_for_bullish_engulf() {
4611 let prev = bar(110, 115, 95, 100); let curr = bar(98, 120, 95, 115); assert!(!curr.is_bearish_engulfing(&prev));
4614 }
4615
4616 #[test]
4617 fn test_is_engulfing_true_when_body_contains_prev_body() {
4618 let prev = bar(100, 110, 95, 105); let curr = bar(98, 115, 95, 108); assert!(curr.is_engulfing(&prev));
4621 }
4622
4623 #[test]
4624 fn test_is_engulfing_false_when_only_partial_overlap() {
4625 let prev = bar(100, 115, 90, 112); let curr = bar(101, 115, 90, 113); assert!(!curr.is_engulfing(&prev));
4628 }
4629
4630 #[test]
4631 fn test_is_engulfing_false_for_equal_bodies() {
4632 let prev = bar(100, 110, 90, 108);
4633 let curr = bar(100, 110, 90, 108); assert!(!curr.is_engulfing(&prev));
4635 }
4636
4637 #[test]
4640 fn test_has_upper_wick_true_when_high_above_max_oc() {
4641 let bar = make_ohlcv_bar(dec!(100), dec!(115), dec!(100), dec!(110));
4643 assert!(bar.has_upper_wick());
4644 }
4645
4646 #[test]
4647 fn test_has_upper_wick_false_for_full_body() {
4648 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(100), dec!(110));
4650 assert!(!bar.has_upper_wick());
4651 }
4652
4653 #[test]
4654 fn test_has_lower_wick_true_when_low_below_min_oc() {
4655 let bar = make_ohlcv_bar(dec!(105), dec!(110), dec!(100), dec!(110));
4657 assert!(bar.has_lower_wick());
4658 }
4659
4660 #[test]
4661 fn test_has_lower_wick_false_for_full_body() {
4662 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(100), dec!(110));
4664 assert!(!bar.has_lower_wick());
4665 }
4666
4667 #[test]
4670 fn test_is_gravestone_doji_true() {
4671 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(100), dec!(100));
4673 assert!(bar.is_gravestone_doji(dec!(0)));
4674 }
4675
4676 #[test]
4677 fn test_is_gravestone_doji_false_when_close_above_low() {
4678 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(99), dec!(105));
4680 assert!(!bar.is_gravestone_doji(dec!(1)));
4681 }
4682
4683 #[test]
4686 fn test_is_dragonfly_doji_true() {
4687 let bar = make_ohlcv_bar(dec!(110), dec!(110), dec!(100), dec!(110));
4689 assert!(bar.is_dragonfly_doji(dec!(0)));
4690 }
4691
4692 #[test]
4693 fn test_is_dragonfly_doji_false_when_close_below_high() {
4694 let bar = make_ohlcv_bar(dec!(105), dec!(110), dec!(100), dec!(105));
4696 assert!(!bar.is_dragonfly_doji(dec!(1)));
4697 }
4698
4699 #[test]
4702 fn test_is_flat_true() {
4703 let bar = make_ohlcv_bar(dec!(100), dec!(100), dec!(100), dec!(100));
4704 assert!(bar.is_flat());
4705 }
4706
4707 #[test]
4708 fn test_is_flat_false_when_range_exists() {
4709 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
4710 assert!(!bar.is_flat());
4711 }
4712
4713 #[test]
4714 fn test_close_to_high_ratio_normal() {
4715 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(110));
4716 let r = bar.close_to_high_ratio().unwrap();
4718 assert!((r - 1.0).abs() < 1e-9);
4719 }
4720
4721 #[test]
4722 fn test_close_to_high_ratio_none_when_high_zero() {
4723 let bar = make_ohlcv_bar(dec!(0), dec!(0), dec!(0), dec!(0));
4724 assert!(bar.close_to_high_ratio().is_none());
4725 }
4726
4727 #[test]
4728 fn test_close_open_ratio_normal() {
4729 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(110));
4731 let r = bar.close_open_ratio().unwrap();
4732 assert!((r - 1.1).abs() < 1e-9);
4733 }
4734
4735 #[test]
4736 fn test_close_open_ratio_none_when_open_zero() {
4737 let bar = make_ohlcv_bar(dec!(0), dec!(10), dec!(0), dec!(5));
4738 assert!(bar.close_open_ratio().is_none());
4739 }
4740
4741 #[test]
4744 fn test_true_range_simple_hl_dominates() {
4745 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
4747 assert_eq!(bar.true_range_with_prev(dec!(100)), dec!(20));
4748 }
4749
4750 #[test]
4751 fn test_true_range_gap_up_dominates() {
4752 let bar = make_ohlcv_bar(dec!(91), dec!(100), dec!(90), dec!(95));
4754 assert_eq!(bar.true_range_with_prev(dec!(80)), dec!(20));
4755 }
4756
4757 #[test]
4758 fn test_true_range_gap_down_dominates() {
4759 let bar = make_ohlcv_bar(dec!(98), dec!(100), dec!(95), dec!(97));
4761 assert_eq!(bar.true_range_with_prev(dec!(120)), dec!(25));
4762 }
4763
4764 #[test]
4767 fn test_is_outside_bar_true() {
4768 let prev = make_ohlcv_bar(dec!(100), dec!(105), dec!(95), dec!(100));
4769 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
4770 assert!(bar.is_outside_bar(&prev));
4771 }
4772
4773 #[test]
4774 fn test_is_outside_bar_false_when_inside() {
4775 let prev = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
4776 let bar = make_ohlcv_bar(dec!(100), dec!(105), dec!(95), dec!(100));
4777 assert!(!bar.is_outside_bar(&prev));
4778 }
4779
4780 #[test]
4781 fn test_high_low_midpoint_correct() {
4782 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
4783 assert_eq!(bar.high_low_midpoint(), dec!(100));
4785 }
4786
4787 #[test]
4788 fn test_high_low_midpoint_uneven() {
4789 let bar = make_ohlcv_bar(dec!(100), dec!(111), dec!(90), dec!(100));
4790 assert_eq!(bar.high_low_midpoint(), dec!(100.5));
4792 }
4793
4794 #[test]
4797 fn test_gap_up_true() {
4798 let prev = make_ohlcv_bar(dec!(95), dec!(100), dec!(90), dec!(98));
4799 let bar = make_ohlcv_bar(dec!(102), dec!(110), dec!(101), dec!(108));
4800 assert!(bar.gap_up(&prev));
4801 }
4802
4803 #[test]
4804 fn test_gap_up_false_when_no_gap() {
4805 let prev = make_ohlcv_bar(dec!(95), dec!(100), dec!(90), dec!(98));
4806 let bar = make_ohlcv_bar(dec!(99), dec!(105), dec!(98), dec!(104));
4807 assert!(!bar.gap_up(&prev));
4808 }
4809
4810 #[test]
4811 fn test_gap_down_true() {
4812 let prev = make_ohlcv_bar(dec!(95), dec!(100), dec!(90), dec!(92));
4813 let bar = make_ohlcv_bar(dec!(88), dec!(89), dec!(85), dec!(86));
4814 assert!(bar.gap_down(&prev));
4815 }
4816
4817 #[test]
4818 fn test_gap_down_false_when_no_gap() {
4819 let prev = make_ohlcv_bar(dec!(95), dec!(100), dec!(90), dec!(92));
4820 let bar = make_ohlcv_bar(dec!(91), dec!(95), dec!(89), dec!(93));
4821 assert!(!bar.gap_down(&prev));
4822 }
4823
4824 #[test]
4827 fn test_range_pct_correct() {
4828 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
4830 let pct = bar.range_pct().unwrap();
4831 assert!((pct - 20.0).abs() < 1e-9);
4832 }
4833
4834 #[test]
4835 fn test_range_pct_none_when_open_zero() {
4836 let bar = make_ohlcv_bar(dec!(0), dec!(10), dec!(0), dec!(5));
4837 assert!(bar.range_pct().is_none());
4838 }
4839
4840 #[test]
4841 fn test_range_pct_zero_for_flat_bar() {
4842 let bar = make_ohlcv_bar(dec!(100), dec!(100), dec!(100), dec!(100));
4844 let pct = bar.range_pct().unwrap();
4845 assert_eq!(pct, 0.0);
4846 }
4847
4848 #[test]
4851 fn test_body_size_bullish_bar() {
4852 let bar = make_ohlcv_bar(dec!(100), dec!(115), dec!(95), dec!(110));
4854 assert_eq!(bar.body_size(), dec!(10));
4855 }
4856
4857 #[test]
4858 fn test_body_size_bearish_bar() {
4859 let bar = make_ohlcv_bar(dec!(110), dec!(115), dec!(95), dec!(100));
4861 assert_eq!(bar.body_size(), dec!(10));
4862 }
4863
4864 #[test]
4865 fn test_body_size_doji() {
4866 let bar = make_ohlcv_bar(dec!(100), dec!(105), dec!(95), dec!(100));
4868 assert_eq!(bar.body_size(), dec!(0));
4869 }
4870
4871 #[test]
4874 fn test_volume_delta_positive_when_increasing() {
4875 let mut prev = make_ohlcv_bar(dec!(100), dec!(105), dec!(95), dec!(102));
4876 prev.volume = dec!(1000);
4877 let mut bar = make_ohlcv_bar(dec!(102), dec!(110), dec!(98), dec!(108));
4878 bar.volume = dec!(1500);
4879 assert_eq!(bar.volume_delta(&prev), dec!(500));
4880 }
4881
4882 #[test]
4883 fn test_volume_delta_negative_when_decreasing() {
4884 let mut prev = make_ohlcv_bar(dec!(100), dec!(105), dec!(95), dec!(102));
4885 prev.volume = dec!(1500);
4886 let mut bar = make_ohlcv_bar(dec!(102), dec!(110), dec!(98), dec!(108));
4887 bar.volume = dec!(1000);
4888 assert_eq!(bar.volume_delta(&prev), dec!(-500));
4889 }
4890
4891 #[test]
4892 fn test_is_consolidating_true_when_small_range() {
4893 let prev = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)); let bar = make_ohlcv_bar(dec!(102), dec!(106), dec!(100), dec!(104)); assert!(bar.is_consolidating(&prev));
4896 }
4897
4898 #[test]
4899 fn test_is_consolidating_false_when_large_range() {
4900 let prev = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)); let bar = make_ohlcv_bar(dec!(102), dec!(115), dec!(95), dec!(110)); assert!(!bar.is_consolidating(&prev));
4903 }
4904
4905 #[test]
4908 fn test_relative_volume_correct() {
4909 let bar = make_ohlcv_bar(dec!(100), dec!(105), dec!(95), dec!(103));
4910 let rv = bar.relative_volume(dec!(2)).unwrap();
4912 assert!((rv - 0.5).abs() < 1e-9);
4913 }
4914
4915 #[test]
4916 fn test_relative_volume_none_when_avg_zero() {
4917 let bar = make_ohlcv_bar(dec!(100), dec!(105), dec!(95), dec!(103));
4918 assert!(bar.relative_volume(dec!(0)).is_none());
4919 }
4920
4921 #[test]
4922 fn test_intraday_reversal_true_for_bullish_then_bearish() {
4923 let prev = make_ohlcv_bar(dec!(100), dec!(108), dec!(99), dec!(105));
4925 let bar = make_ohlcv_bar(dec!(105), dec!(107), dec!(97), dec!(98));
4927 assert!(bar.intraday_reversal(&prev));
4928 }
4929
4930 #[test]
4931 fn test_intraday_reversal_false_for_continuation() {
4932 let prev = make_ohlcv_bar(dec!(100), dec!(108), dec!(99), dec!(105));
4934 let bar = make_ohlcv_bar(dec!(104), dec!(115), dec!(103), dec!(113));
4935 assert!(!bar.intraday_reversal(&prev));
4936 }
4937
4938 #[test]
4941 fn test_price_at_pct_zero_returns_low() {
4942 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
4943 assert_eq!(bar.price_at_pct(0.0), dec!(90));
4944 }
4945
4946 #[test]
4947 fn test_price_at_pct_one_returns_high() {
4948 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
4949 assert_eq!(bar.price_at_pct(1.0), dec!(110));
4950 }
4951
4952 #[test]
4953 fn test_price_at_pct_half_returns_midpoint() {
4954 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
4955 assert_eq!(bar.price_at_pct(0.5), dec!(100));
4957 }
4958
4959 #[test]
4960 fn test_price_at_pct_clamped_above_one() {
4961 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
4962 assert_eq!(bar.price_at_pct(2.0), dec!(110));
4963 }
4964
4965 #[test]
4968 fn test_average_true_range_none_when_fewer_than_two_bars() {
4969 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
4970 assert!(OhlcvBar::average_true_range(&[bar]).is_none());
4971 assert!(OhlcvBar::average_true_range(&[]).is_none());
4972 }
4973
4974 #[test]
4975 fn test_average_true_range_two_bars_no_gap() {
4976 let bar1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
4979 let bar2 = make_ohlcv_bar(dec!(105), dec!(115), dec!(95), dec!(110));
4980 let atr = OhlcvBar::average_true_range(&[bar1, bar2]).unwrap();
4981 assert_eq!(atr, dec!(20)); }
4983
4984 #[test]
4985 fn test_average_true_range_three_bars_mean() {
4986 let bar1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
4990 let bar2 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
4991 let bar3 = make_ohlcv_bar(dec!(110), dec!(120), dec!(100), dec!(115));
4992 let atr = OhlcvBar::average_true_range(&[bar1, bar2, bar3]).unwrap();
4993 assert_eq!(atr, dec!(20));
4994 }
4995
4996 #[test]
4999 fn test_average_body_none_when_empty() {
5000 assert!(OhlcvBar::average_body(&[]).is_none());
5001 }
5002
5003 #[test]
5004 fn test_average_body_single_bar() {
5005 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(108));
5006 assert_eq!(OhlcvBar::average_body(&[bar]), Some(dec!(8)));
5008 }
5009
5010 #[test]
5011 fn test_average_body_multiple_bars() {
5012 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(110)); let b2 = make_ohlcv_bar(dec!(110), dec!(120), dec!(100), dec!(100)); let b3 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(120)); let avg = OhlcvBar::average_body(&[b1, b2, b3]).unwrap();
5016 assert_eq!(avg, dec!(40) / dec!(3));
5018 }
5019
5020 #[test]
5023 fn test_bullish_count_zero_for_empty_slice() {
5024 assert_eq!(OhlcvBar::bullish_count(&[]), 0);
5025 }
5026
5027 #[test]
5028 fn test_bullish_count_all_bullish() {
5029 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(108)); let b2 = make_ohlcv_bar(dec!(108), dec!(120), dec!(105), dec!(115)); assert_eq!(OhlcvBar::bullish_count(&[b1, b2]), 2);
5032 }
5033
5034 #[test]
5035 fn test_bearish_count_correct() {
5036 let bull = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(108));
5037 let bear = make_ohlcv_bar(dec!(108), dec!(110), dec!(90), dec!(95));
5038 let doji = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
5039 assert_eq!(OhlcvBar::bearish_count(&[bull, bear, doji]), 1);
5040 }
5041
5042 #[test]
5043 fn test_win_rate_none_when_empty() {
5044 assert!(OhlcvBar::win_rate(&[]).is_none());
5045 }
5046
5047 #[test]
5048 fn test_win_rate_all_bullish() {
5049 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(108));
5050 let b2 = make_ohlcv_bar(dec!(108), dec!(115), dec!(105), dec!(112));
5051 let wr = OhlcvBar::win_rate(&[b1, b2]).unwrap();
5052 assert!((wr - 1.0).abs() < 1e-9);
5053 }
5054
5055 #[test]
5056 fn test_win_rate_half_and_half() {
5057 let bull = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(108));
5058 let bear = make_ohlcv_bar(dec!(108), dec!(110), dec!(90), dec!(95));
5059 let wr = OhlcvBar::win_rate(&[bull, bear]).unwrap();
5060 assert!((wr - 0.5).abs() < 1e-9);
5061 }
5062
5063 #[test]
5066 fn test_bullish_streak_zero_for_empty_slice() {
5067 assert_eq!(OhlcvBar::bullish_streak(&[]), 0);
5068 }
5069
5070 #[test]
5071 fn test_bullish_streak_zero_when_last_bar_bearish() {
5072 let bull = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(108));
5073 let bear = make_ohlcv_bar(dec!(108), dec!(110), dec!(90), dec!(95));
5074 assert_eq!(OhlcvBar::bullish_streak(&[bull, bear]), 0);
5075 }
5076
5077 #[test]
5078 fn test_bullish_streak_counts_consecutive_tail() {
5079 let bear = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(90)); let bull1 = make_ohlcv_bar(dec!(90), dec!(105), dec!(88), dec!(102)); let bull2 = make_ohlcv_bar(dec!(102), dec!(115), dec!(100), dec!(110)); assert_eq!(OhlcvBar::bullish_streak(&[bear, bull1, bull2]), 2);
5083 }
5084
5085 #[test]
5086 fn test_bearish_streak_counts_consecutive_tail() {
5087 let bull = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(108)); let bear1 = make_ohlcv_bar(dec!(108), dec!(109), dec!(90), dec!(95)); let bear2 = make_ohlcv_bar(dec!(95), dec!(96), dec!(80), dec!(85)); assert_eq!(OhlcvBar::bearish_streak(&[bull, bear1, bear2]), 2);
5091 }
5092
5093 #[test]
5096 fn test_max_drawdown_none_when_fewer_than_2_bars() {
5097 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
5098 assert!(OhlcvBar::max_drawdown(&[bar]).is_none());
5099 assert!(OhlcvBar::max_drawdown(&[]).is_none());
5100 }
5101
5102 #[test]
5103 fn test_max_drawdown_zero_when_monotone_increasing() {
5104 let b1 = make_ohlcv_bar(dec!(100), dec!(105), dec!(98), dec!(100));
5105 let b2 = make_ohlcv_bar(dec!(100), dec!(110), dec!(99), dec!(105));
5106 let b3 = make_ohlcv_bar(dec!(105), dec!(115), dec!(104), dec!(110));
5107 let dd = OhlcvBar::max_drawdown(&[b1, b2, b3]).unwrap();
5108 assert_eq!(dd, 0.0);
5109 }
5110
5111 #[test]
5112 fn test_max_drawdown_correct_after_peak_then_drop() {
5113 let b1 = make_ohlcv_bar(dec!(100), dec!(102), dec!(98), dec!(100));
5115 let b2 = make_ohlcv_bar(dec!(100), dec!(125), dec!(99), dec!(120));
5116 let b3 = make_ohlcv_bar(dec!(120), dec!(121), dec!(88), dec!(90));
5117 let dd = OhlcvBar::max_drawdown(&[b1, b2, b3]).unwrap();
5118 assert!((dd - 0.25).abs() < 1e-9, "expected 0.25, got {dd}");
5119 }
5120
5121 #[test]
5124 fn test_mean_volume_none_when_empty() {
5125 assert!(OhlcvBar::mean_volume(&[]).is_none());
5126 }
5127
5128 #[test]
5129 fn test_mean_volume_single_bar() {
5130 let mut bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
5131 bar.volume = dec!(200);
5132 assert_eq!(OhlcvBar::mean_volume(&[bar]), Some(dec!(200)));
5133 }
5134
5135 #[test]
5136 fn test_mean_volume_multiple_bars() {
5137 let mut b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
5138 b1.volume = dec!(100);
5139 let mut b2 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
5140 b2.volume = dec!(200);
5141 let mut b3 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
5142 b3.volume = dec!(300);
5143 assert_eq!(OhlcvBar::mean_volume(&[b1, b2, b3]), Some(dec!(200)));
5144 }
5145
5146 #[test]
5149 fn test_vwap_deviation_none_when_vwap_not_set() {
5150 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
5151 assert!(bar.vwap_deviation().is_none());
5152 }
5153
5154 #[test]
5155 fn test_vwap_deviation_zero_when_close_equals_vwap() {
5156 let mut bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
5157 bar.vwap = Some(dec!(100));
5158 assert_eq!(bar.vwap_deviation(), Some(0.0));
5159 }
5160
5161 #[test]
5162 fn test_vwap_deviation_correct_value() {
5163 let mut bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(110));
5164 bar.vwap = Some(dec!(100));
5165 let dev = bar.vwap_deviation().unwrap();
5167 assert!((dev - 0.1).abs() < 1e-10);
5168 }
5169
5170 #[test]
5173 fn test_high_close_ratio_none_when_high_zero() {
5174 let bar = OhlcvBar {
5175 symbol: "X".into(),
5176 timeframe: Timeframe::Minutes(1),
5177 open: dec!(0),
5178 high: dec!(0),
5179 low: dec!(0),
5180 close: dec!(0),
5181 volume: dec!(1),
5182 bar_start_ms: 0,
5183 trade_count: 1,
5184 is_complete: false,
5185 is_gap_fill: false,
5186 vwap: None,
5187 };
5188 assert!(bar.high_close_ratio().is_none());
5189 }
5190
5191 #[test]
5192 fn test_high_close_ratio_one_when_close_equals_high() {
5193 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(110));
5194 let ratio = bar.high_close_ratio().unwrap();
5195 assert!((ratio - 1.0).abs() < 1e-10);
5196 }
5197
5198 #[test]
5199 fn test_high_close_ratio_less_than_one_when_close_below_high() {
5200 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(99));
5201 let ratio = bar.high_close_ratio().unwrap();
5202 assert!(ratio < 1.0);
5203 }
5204
5205 #[test]
5208 fn test_lower_shadow_pct_none_when_range_zero() {
5209 let bar = make_ohlcv_bar(dec!(100), dec!(100), dec!(100), dec!(100));
5210 assert!(bar.lower_shadow_pct().is_none());
5211 }
5212
5213 #[test]
5214 fn test_lower_shadow_pct_zero_when_no_lower_shadow() {
5215 let bar = make_ohlcv_bar(dec!(90), dec!(110), dec!(90), dec!(110));
5217 let pct = bar.lower_shadow_pct().unwrap();
5218 assert!(pct.abs() < 1e-10);
5219 }
5220
5221 #[test]
5222 fn test_lower_shadow_pct_correct_value() {
5223 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
5225 let pct = bar.lower_shadow_pct().unwrap();
5226 assert!((pct - 0.5).abs() < 1e-10);
5227 }
5228
5229 #[test]
5232 fn test_open_close_ratio_none_when_open_zero() {
5233 let bar = make_ohlcv_bar(dec!(0), dec!(10), dec!(0), dec!(5));
5234 assert!(bar.open_close_ratio().is_none());
5235 }
5236
5237 #[test]
5238 fn test_open_close_ratio_one_when_flat() {
5239 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
5240 let ratio = bar.open_close_ratio().unwrap();
5241 assert!((ratio - 1.0).abs() < 1e-10);
5242 }
5243
5244 #[test]
5245 fn test_open_close_ratio_above_one_for_bullish_bar() {
5246 let bar = make_ohlcv_bar(dec!(100), dec!(115), dec!(95), dec!(110));
5247 let ratio = bar.open_close_ratio().unwrap();
5248 assert!(ratio > 1.0);
5249 }
5250
5251 #[test]
5254 fn test_is_wide_range_bar_true_when_range_exceeds_threshold() {
5255 let bar = make_ohlcv_bar(dec!(100), dec!(120), dec!(95), dec!(110)); assert!(bar.is_wide_range_bar(dec!(20)));
5257 }
5258
5259 #[test]
5260 fn test_is_wide_range_bar_false_when_range_equals_threshold() {
5261 let bar = make_ohlcv_bar(dec!(100), dec!(120), dec!(100), dec!(110)); assert!(!bar.is_wide_range_bar(dec!(20)));
5263 }
5264
5265 #[test]
5268 fn test_close_to_low_ratio_none_when_range_zero() {
5269 let bar = make_ohlcv_bar(dec!(100), dec!(100), dec!(100), dec!(100));
5270 assert!(bar.close_to_low_ratio().is_none());
5271 }
5272
5273 #[test]
5274 fn test_close_to_low_ratio_one_when_closed_at_high() {
5275 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(110));
5276 let ratio = bar.close_to_low_ratio().unwrap();
5277 assert!((ratio - 1.0).abs() < 1e-10);
5278 }
5279
5280 #[test]
5281 fn test_close_to_low_ratio_zero_when_closed_at_low() {
5282 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(90));
5283 let ratio = bar.close_to_low_ratio().unwrap();
5284 assert!(ratio.abs() < 1e-10);
5285 }
5286
5287 #[test]
5288 fn test_close_to_low_ratio_half_at_midpoint() {
5289 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
5290 let ratio = bar.close_to_low_ratio().unwrap();
5292 assert!((ratio - 0.5).abs() < 1e-10);
5293 }
5294
5295 #[test]
5298 fn test_volume_per_trade_none_when_trade_count_zero() {
5299 let mut bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
5300 bar.trade_count = 0;
5301 assert!(bar.volume_per_trade().is_none());
5302 }
5303
5304 #[test]
5305 fn test_volume_per_trade_correct_value() {
5306 let mut bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
5307 bar.volume = dec!(500);
5308 bar.trade_count = 5;
5309 assert_eq!(bar.volume_per_trade(), Some(dec!(100)));
5310 }
5311
5312 #[test]
5315 fn test_price_range_overlap_true_when_ranges_overlap() {
5316 let a = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
5317 let b = make_ohlcv_bar(dec!(105), dec!(120), dec!(95), dec!(110));
5318 assert!(a.price_range_overlap(&b));
5319 }
5320
5321 #[test]
5322 fn test_price_range_overlap_false_when_no_overlap() {
5323 let a = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
5324 let b = make_ohlcv_bar(dec!(120), dec!(130), dec!(115), dec!(125));
5325 assert!(!a.price_range_overlap(&b));
5326 }
5327
5328 #[test]
5329 fn test_price_range_overlap_true_at_exact_touch() {
5330 let a = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
5331 let b = make_ohlcv_bar(dec!(115), dec!(125), dec!(110), dec!(120));
5332 assert!(a.price_range_overlap(&b));
5333 }
5334
5335 #[test]
5338 fn test_bar_height_pct_none_when_open_zero() {
5339 let bar = make_ohlcv_bar(dec!(0), dec!(10), dec!(0), dec!(5));
5340 assert!(bar.bar_height_pct().is_none());
5341 }
5342
5343 #[test]
5344 fn test_bar_height_pct_correct_value() {
5345 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100)); let pct = bar.bar_height_pct().unwrap();
5348 assert!((pct - 0.2).abs() < 1e-10);
5349 }
5350
5351 #[test]
5354 fn test_is_bullish_engulfing_true_for_valid_pattern() {
5355 let prev = make_ohlcv_bar(dec!(110), dec!(115), dec!(95), dec!(100));
5357 let bar = make_ohlcv_bar(dec!(98), dec!(115), dec!(95), dec!(112));
5358 assert!(bar.is_bullish_engulfing(&prev));
5359 }
5360
5361 #[test]
5362 fn test_is_bullish_engulfing_false_when_bearish() {
5363 let prev = make_ohlcv_bar(dec!(110), dec!(115), dec!(95), dec!(100));
5364 let bar = make_ohlcv_bar(dec!(108), dec!(115), dec!(95), dec!(95));
5365 assert!(!bar.is_bullish_engulfing(&prev));
5366 }
5367
5368 #[test]
5371 fn test_close_gap_positive_for_gap_up() {
5372 let prev = make_ohlcv_bar(dec!(100), dec!(105), dec!(95), dec!(102));
5373 let bar = make_ohlcv_bar(dec!(106), dec!(110), dec!(103), dec!(108)); assert_eq!(bar.close_gap(&prev), dec!(4));
5375 }
5376
5377 #[test]
5378 fn test_close_gap_negative_for_gap_down() {
5379 let prev = make_ohlcv_bar(dec!(100), dec!(105), dec!(95), dec!(102));
5380 let bar = make_ohlcv_bar(dec!(98), dec!(100), dec!(95), dec!(97)); assert_eq!(bar.close_gap(&prev), dec!(-4));
5382 }
5383
5384 #[test]
5385 fn test_close_gap_zero_when_no_gap() {
5386 let prev = make_ohlcv_bar(dec!(100), dec!(105), dec!(95), dec!(102));
5387 let bar = make_ohlcv_bar(dec!(102), dec!(110), dec!(100), dec!(108));
5388 assert_eq!(bar.close_gap(&prev), dec!(0));
5389 }
5390
5391 #[test]
5394 fn test_close_above_midpoint_true_when_above_mid() {
5395 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
5397 assert!(bar.close_above_midpoint());
5398 }
5399
5400 #[test]
5401 fn test_close_above_midpoint_false_when_at_mid() {
5402 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100)); assert!(!bar.close_above_midpoint());
5404 }
5405
5406 #[test]
5409 fn test_close_momentum_positive_when_rising() {
5410 let prev = make_ohlcv_bar(dec!(100), dec!(105), dec!(95), dec!(100));
5411 let bar = make_ohlcv_bar(dec!(100), dec!(115), dec!(98), dec!(110));
5412 assert_eq!(bar.close_momentum(&prev), dec!(10));
5413 }
5414
5415 #[test]
5416 fn test_close_momentum_zero_when_unchanged() {
5417 let prev = make_ohlcv_bar(dec!(100), dec!(105), dec!(95), dec!(100));
5418 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(95), dec!(100));
5419 assert_eq!(bar.close_momentum(&prev), dec!(0));
5420 }
5421
5422 #[test]
5425 fn test_bar_range_correct() {
5426 let bar = make_ohlcv_bar(dec!(100), dec!(120), dec!(90), dec!(110));
5427 assert_eq!(bar.bar_range(), dec!(30));
5428 }
5429
5430 #[test]
5433 fn test_linear_regression_slope_none_for_single_bar() {
5434 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
5435 assert!(OhlcvBar::linear_regression_slope(&[bar]).is_none());
5436 }
5437
5438 #[test]
5439 fn test_linear_regression_slope_positive_for_rising_closes() {
5440 let bars = vec![
5441 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100)),
5442 make_ohlcv_bar(dec!(100), dec!(115), dec!(95), dec!(110)),
5443 make_ohlcv_bar(dec!(110), dec!(120), dec!(105), dec!(120)),
5444 ];
5445 let slope = OhlcvBar::linear_regression_slope(&bars).unwrap();
5446 assert!(slope > 0.0, "slope should be positive for rising closes");
5447 }
5448
5449 #[test]
5450 fn test_linear_regression_slope_negative_for_falling_closes() {
5451 let bars = vec![
5452 make_ohlcv_bar(dec!(120), dec!(125), dec!(115), dec!(120)),
5453 make_ohlcv_bar(dec!(120), dec!(115), dec!(105), dec!(110)),
5454 make_ohlcv_bar(dec!(110), dec!(108), dec!(95), dec!(100)),
5455 ];
5456 let slope = OhlcvBar::linear_regression_slope(&bars).unwrap();
5457 assert!(slope < 0.0, "slope should be negative for falling closes");
5458 }
5459
5460 #[test]
5461 fn test_linear_regression_slope_near_zero_for_flat_closes() {
5462 let bars = vec![
5463 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100)),
5464 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100)),
5465 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100)),
5466 ];
5467 let slope = OhlcvBar::linear_regression_slope(&bars).unwrap();
5468 assert!(slope.abs() < 1e-10, "slope should be ~0 for identical closes");
5469 }
5470
5471 #[test]
5474 fn test_volume_slope_none_for_single_bar() {
5475 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
5476 assert!(OhlcvBar::volume_slope(&[bar]).is_none());
5477 }
5478
5479 #[test]
5480 fn test_volume_slope_positive_for_rising_volume() {
5481 let make_bar_with_vol = |v: u64| {
5482 let mut b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
5483 b.volume = Decimal::from(v);
5484 b
5485 };
5486 let bars = vec![make_bar_with_vol(100), make_bar_with_vol(200), make_bar_with_vol(300)];
5487 assert!(OhlcvBar::volume_slope(&bars).unwrap() > 0.0);
5488 }
5489
5490 #[test]
5493 fn test_highest_close_none_for_empty_slice() {
5494 assert!(OhlcvBar::highest_close(&[]).is_none());
5495 }
5496
5497 #[test]
5498 fn test_highest_close_returns_max_close() {
5499 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
5500 let b2 = make_ohlcv_bar(dec!(100), dec!(120), dec!(95), dec!(115));
5501 let b3 = make_ohlcv_bar(dec!(100), dec!(108), dec!(90), dec!(102));
5502 assert_eq!(OhlcvBar::highest_close(&[b1, b2, b3]), Some(dec!(115)));
5503 }
5504
5505 #[test]
5506 fn test_lowest_close_none_for_empty_slice() {
5507 assert!(OhlcvBar::lowest_close(&[]).is_none());
5508 }
5509
5510 #[test]
5511 fn test_lowest_close_returns_min_close() {
5512 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
5513 let b2 = make_ohlcv_bar(dec!(100), dec!(120), dec!(95), dec!(115));
5514 let b3 = make_ohlcv_bar(dec!(100), dec!(108), dec!(90), dec!(102));
5515 assert_eq!(OhlcvBar::lowest_close(&[b1, b2, b3]), Some(dec!(102)));
5516 }
5517
5518 #[test]
5521 fn test_close_range_none_for_empty_slice() {
5522 assert!(OhlcvBar::close_range(&[]).is_none());
5523 }
5524
5525 #[test]
5526 fn test_close_range_correct() {
5527 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(102));
5528 let b2 = make_ohlcv_bar(dec!(100), dec!(120), dec!(95), dec!(115));
5529 assert_eq!(OhlcvBar::close_range(&[b1, b2]), Some(dec!(13)));
5531 }
5532
5533 #[test]
5534 fn test_momentum_none_for_insufficient_bars() {
5535 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
5536 assert!(OhlcvBar::momentum(&[bar], 1).is_none());
5537 }
5538
5539 #[test]
5540 fn test_momentum_positive_for_rising_close() {
5541 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
5542 let b2 = make_ohlcv_bar(dec!(100), dec!(115), dec!(95), dec!(110));
5543 let mom = OhlcvBar::momentum(&[b1, b2], 1).unwrap();
5545 assert!((mom - 0.1).abs() < 1e-10);
5546 }
5547
5548 #[test]
5549 fn test_momentum_negative_for_falling_close() {
5550 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(110));
5551 let b2 = make_ohlcv_bar(dec!(100), dec!(108), dec!(88), dec!(99));
5552 let mom = OhlcvBar::momentum(&[b1, b2], 1).unwrap();
5554 assert!(mom < 0.0);
5555 }
5556
5557 #[test]
5560 fn test_mean_close_none_for_empty_slice() {
5561 assert!(OhlcvBar::mean_close(&[]).is_none());
5562 }
5563
5564 #[test]
5565 fn test_mean_close_single_bar() {
5566 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
5567 assert_eq!(OhlcvBar::mean_close(&[bar]), Some(dec!(105)));
5568 }
5569
5570 #[test]
5571 fn test_mean_close_multiple_bars() {
5572 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
5573 let b2 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(110));
5574 let b3 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(120));
5575 assert_eq!(OhlcvBar::mean_close(&[b1, b2, b3]), Some(dec!(110)));
5577 }
5578
5579 #[test]
5582 fn test_close_std_dev_none_for_single_bar() {
5583 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
5584 assert!(OhlcvBar::close_std_dev(&[bar]).is_none());
5585 }
5586
5587 #[test]
5588 fn test_close_std_dev_zero_for_identical_closes() {
5589 let bars = vec![
5590 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100)),
5591 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100)),
5592 ];
5593 let sd = OhlcvBar::close_std_dev(&bars).unwrap();
5594 assert!(sd.abs() < 1e-10, "std_dev should be ~0 for identical closes");
5595 }
5596
5597 #[test]
5598 fn test_close_std_dev_positive_for_varied_closes() {
5599 let bars = vec![
5600 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(90)),
5601 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(110)),
5602 ];
5603 assert!(OhlcvBar::close_std_dev(&bars).unwrap() > 0.0);
5604 }
5605
5606 #[test]
5609 fn test_price_efficiency_ratio_none_for_single_bar() {
5610 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
5611 assert!(OhlcvBar::price_efficiency_ratio(&[bar]).is_none());
5612 }
5613
5614 #[test]
5615 fn test_price_efficiency_ratio_one_for_trending_price() {
5616 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
5618 let b2 = make_ohlcv_bar(dec!(110), dec!(120), dec!(100), dec!(110));
5619 let b3 = make_ohlcv_bar(dec!(120), dec!(130), dec!(110), dec!(120));
5620 let ratio = OhlcvBar::price_efficiency_ratio(&[b1, b2, b3]).unwrap();
5622 assert!(ratio > 0.0 && ratio <= 1.0);
5623 }
5624
5625 #[test]
5626 fn test_price_efficiency_ratio_none_for_zero_total_range() {
5627 let bars = vec![
5629 make_ohlcv_bar(dec!(100), dec!(100), dec!(100), dec!(100)),
5630 make_ohlcv_bar(dec!(100), dec!(100), dec!(100), dec!(100)),
5631 ];
5632 assert!(OhlcvBar::price_efficiency_ratio(&bars).is_none());
5633 }
5634
5635 #[test]
5638 fn test_clv_plus_one_when_close_at_high() {
5639 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(110));
5641 let clv = bar.close_location_value().unwrap();
5642 assert!((clv - 1.0).abs() < 1e-10, "CLV should be 1.0 when close == high, got {clv}");
5643 }
5644
5645 #[test]
5646 fn test_clv_minus_one_when_close_at_low() {
5647 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(90));
5648 let clv = bar.close_location_value().unwrap();
5649 assert!((clv + 1.0).abs() < 1e-10, "CLV should be -1.0 when close == low, got {clv}");
5650 }
5651
5652 #[test]
5653 fn test_clv_zero_when_close_at_midpoint() {
5654 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
5655 let clv = bar.close_location_value().unwrap();
5656 assert!(clv.abs() < 1e-10, "CLV should be 0 at midpoint, got {clv}");
5657 }
5658
5659 #[test]
5660 fn test_clv_none_for_zero_range_bar() {
5661 let bar = make_ohlcv_bar(dec!(100), dec!(100), dec!(100), dec!(100));
5662 assert!(bar.close_location_value().is_none());
5663 }
5664
5665 #[test]
5666 fn test_mean_clv_none_for_empty_slice() {
5667 assert!(OhlcvBar::mean_clv(&[]).is_none());
5668 }
5669
5670 #[test]
5671 fn test_mean_clv_positive_for_bullish_closes() {
5672 let bars = vec![
5673 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(108)), make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(106)), ];
5676 let clv = OhlcvBar::mean_clv(&bars).unwrap();
5677 assert!(clv > 0.0, "mean CLV should be positive when closes are near highs");
5678 }
5679
5680 #[test]
5681 fn test_mean_range_none_for_empty_slice() {
5682 assert!(OhlcvBar::mean_range(&[]).is_none());
5683 }
5684
5685 #[test]
5686 fn test_mean_range_single_bar() {
5687 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
5688 assert_eq!(OhlcvBar::mean_range(&[bar]), Some(dec!(20)));
5689 }
5690
5691 #[test]
5692 fn test_mean_range_multiple_bars() {
5693 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)); let b2 = make_ohlcv_bar(dec!(100), dec!(120), dec!(80), dec!(100)); assert_eq!(OhlcvBar::mean_range(&[b1, b2]), Some(dec!(30)));
5696 }
5697
5698 #[test]
5699 fn test_close_z_score_none_for_empty_slice() {
5700 assert!(OhlcvBar::close_z_score(&[], dec!(100)).is_none());
5701 }
5702
5703 #[test]
5704 fn test_close_z_score_of_mean_is_zero() {
5705 let bars = vec![
5706 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100)),
5707 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100)),
5708 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(110)),
5709 ];
5710 let mean = (dec!(100) + dec!(100) + dec!(110)) / dec!(3);
5712 let z = OhlcvBar::close_z_score(&bars, mean).unwrap();
5713 assert!(z.abs() < 1e-6);
5714 }
5715
5716 #[test]
5717 fn test_close_z_score_positive_above_mean() {
5718 let bars = vec![
5719 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100)),
5720 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100)),
5721 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(110)),
5722 ];
5723 let z = OhlcvBar::close_z_score(&bars, dec!(120)).unwrap();
5724 assert!(z > 0.0);
5725 }
5726
5727 #[test]
5728 fn test_bollinger_band_width_none_for_empty_slice() {
5729 assert!(OhlcvBar::bollinger_band_width(&[]).is_none());
5730 }
5731
5732 #[test]
5733 fn test_bollinger_band_width_zero_for_identical_closes() {
5734 let bars = vec![
5735 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100)),
5736 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100)),
5737 ];
5738 assert_eq!(OhlcvBar::bollinger_band_width(&bars), Some(0.0));
5739 }
5740
5741 #[test]
5742 fn test_bollinger_band_width_positive_for_varying_closes() {
5743 let bars = vec![
5744 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(90)),
5745 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100)),
5746 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(110)),
5747 ];
5748 let bw = OhlcvBar::bollinger_band_width(&bars).unwrap();
5749 assert!(bw > 0.0);
5750 }
5751
5752 #[test]
5753 fn test_up_down_ratio_none_for_no_bearish_bars() {
5754 let bars = vec![
5755 make_ohlcv_bar(dec!(90), dec!(110), dec!(85), dec!(105)), ];
5757 assert!(OhlcvBar::up_down_ratio(&bars).is_none());
5758 }
5759
5760 #[test]
5761 fn test_up_down_ratio_two_to_one() {
5762 let bull = make_ohlcv_bar(dec!(90), dec!(110), dec!(85), dec!(105));
5763 let bear = make_ohlcv_bar(dec!(110), dec!(115), dec!(85), dec!(95));
5764 let bars = vec![bull.clone(), bull, bear];
5765 let ratio = OhlcvBar::up_down_ratio(&bars).unwrap();
5766 assert!((ratio - 2.0).abs() < 1e-9);
5767 }
5768
5769 #[test]
5772 fn test_volume_weighted_close_none_for_empty_slice() {
5773 assert!(OhlcvBar::volume_weighted_close(&[]).is_none());
5774 }
5775
5776 #[test]
5777 fn test_volume_weighted_close_single_bar() {
5778 let mut bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
5779 bar.volume = dec!(10);
5780 assert_eq!(OhlcvBar::volume_weighted_close(&[bar]), Some(dec!(105)));
5781 }
5782
5783 #[test]
5784 fn test_volume_weighted_close_weights_by_volume() {
5785 let mut b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
5786 b1.volume = dec!(1);
5787 let mut b2 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(200));
5788 b2.volume = dec!(3);
5789 assert_eq!(OhlcvBar::volume_weighted_close(&[b1, b2]), Some(dec!(175)));
5791 }
5792
5793 #[test]
5796 fn test_rolling_return_none_for_empty_slice() {
5797 assert!(OhlcvBar::rolling_return(&[]).is_none());
5798 }
5799
5800 #[test]
5801 fn test_rolling_return_none_for_single_bar() {
5802 assert!(OhlcvBar::rolling_return(&[make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105))]).is_none());
5803 }
5804
5805 #[test]
5806 fn test_rolling_return_positive_when_close_rises() {
5807 let b1 = make_ohlcv_bar(dec!(100), dec!(105), dec!(95), dec!(100));
5808 let b2 = make_ohlcv_bar(dec!(100), dec!(115), dec!(95), dec!(110));
5809 let ret = OhlcvBar::rolling_return(&[b1, b2]).unwrap();
5810 assert!((ret - 0.1).abs() < 1e-9);
5811 }
5812
5813 #[test]
5816 fn test_average_high_none_for_empty_slice() {
5817 assert!(OhlcvBar::average_high(&[]).is_none());
5818 }
5819
5820 #[test]
5821 fn test_average_high_single_bar() {
5822 let bar = make_ohlcv_bar(dec!(100), dec!(120), dec!(90), dec!(105));
5823 assert_eq!(OhlcvBar::average_high(&[bar]), Some(dec!(120)));
5824 }
5825
5826 #[test]
5827 fn test_average_high_multiple_bars() {
5828 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
5829 let b2 = make_ohlcv_bar(dec!(100), dec!(130), dec!(90), dec!(105));
5830 assert_eq!(OhlcvBar::average_high(&[b1, b2]), Some(dec!(120)));
5831 }
5832
5833 #[test]
5834 fn test_average_low_none_for_empty_slice() {
5835 assert!(OhlcvBar::average_low(&[]).is_none());
5836 }
5837
5838 #[test]
5839 fn test_average_low_single_bar() {
5840 let bar = make_ohlcv_bar(dec!(100), dec!(120), dec!(80), dec!(105));
5841 assert_eq!(OhlcvBar::average_low(&[bar]), Some(dec!(80)));
5842 }
5843
5844 #[test]
5845 fn test_average_low_multiple_bars() {
5846 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(80), dec!(105));
5847 let b2 = make_ohlcv_bar(dec!(100), dec!(130), dec!(60), dec!(105));
5848 assert_eq!(OhlcvBar::average_low(&[b1, b2]), Some(dec!(70)));
5849 }
5850
5851 #[test]
5854 fn test_min_body_none_for_empty_slice() {
5855 assert!(OhlcvBar::min_body(&[]).is_none());
5856 }
5857
5858 #[test]
5859 fn test_min_body_returns_smallest_body() {
5860 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)); let b2 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(115)); assert_eq!(OhlcvBar::min_body(&[b1, b2]), Some(dec!(5)));
5863 }
5864
5865 #[test]
5866 fn test_max_body_none_for_empty_slice() {
5867 assert!(OhlcvBar::max_body(&[]).is_none());
5868 }
5869
5870 #[test]
5871 fn test_max_body_returns_largest_body() {
5872 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)); let b2 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(115)); assert_eq!(OhlcvBar::max_body(&[b1, b2]), Some(dec!(15)));
5875 }
5876
5877 #[test]
5880 fn test_atr_pct_none_for_single_bar() {
5881 assert!(OhlcvBar::atr_pct(&[make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105))]).is_none());
5882 }
5883
5884 #[test]
5885 fn test_atr_pct_positive_for_normal_bars() {
5886 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
5887 let b2 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
5888 let pct = OhlcvBar::atr_pct(&[b1, b2]).unwrap();
5889 assert!(pct > 0.0);
5890 }
5891
5892 #[test]
5895 fn test_breakout_count_zero_for_empty_slice() {
5896 assert_eq!(OhlcvBar::breakout_count(&[]), 0);
5897 }
5898
5899 #[test]
5900 fn test_breakout_count_zero_for_single_bar() {
5901 assert_eq!(OhlcvBar::breakout_count(&[make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105))]), 0);
5902 }
5903
5904 #[test]
5905 fn test_breakout_count_detects_close_above_prev_high() {
5906 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(108));
5908 let b2 = make_ohlcv_bar(dec!(108), dec!(120), dec!(105), dec!(115));
5909 assert_eq!(OhlcvBar::breakout_count(&[b1, b2]), 1);
5910 }
5911
5912 #[test]
5913 fn test_breakout_count_zero_when_close_at_prev_high() {
5914 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(108));
5916 let b2 = make_ohlcv_bar(dec!(108), dec!(115), dec!(105), dec!(110));
5917 assert_eq!(OhlcvBar::breakout_count(&[b1, b2]), 0);
5918 }
5919
5920 #[test]
5923 fn test_doji_count_zero_for_empty_slice() {
5924 assert_eq!(OhlcvBar::doji_count(&[], dec!(0.001)), 0);
5925 }
5926
5927 #[test]
5928 fn test_doji_count_detects_doji_bars() {
5929 let doji = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100)); let non_doji = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(110)); assert_eq!(OhlcvBar::doji_count(&[doji, non_doji], dec!(1)), 1);
5932 }
5933
5934 #[test]
5937 fn test_channel_width_none_for_empty_slice() {
5938 assert!(OhlcvBar::channel_width(&[]).is_none());
5939 }
5940
5941 #[test]
5942 fn test_channel_width_correct() {
5943 let b1 = make_ohlcv_bar(dec!(100), dec!(120), dec!(90), dec!(105));
5944 let b2 = make_ohlcv_bar(dec!(100), dec!(115), dec!(80), dec!(100));
5945 assert_eq!(OhlcvBar::channel_width(&[b1, b2]), Some(dec!(40)));
5947 }
5948
5949 #[test]
5952 fn test_sma_none_for_zero_period() {
5953 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
5954 assert!(OhlcvBar::sma(&[bar], 0).is_none());
5955 }
5956
5957 #[test]
5958 fn test_sma_none_when_fewer_bars_than_period() {
5959 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
5960 assert!(OhlcvBar::sma(&[bar], 3).is_none());
5961 }
5962
5963 #[test]
5964 fn test_sma_correct_for_last_n_bars() {
5965 let bars = vec![
5966 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100)),
5967 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(110)),
5968 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(120)),
5969 ];
5970 assert_eq!(OhlcvBar::sma(&bars, 3), Some(dec!(110)));
5972 }
5973
5974 #[test]
5977 fn test_mean_wick_ratio_none_for_empty_slice() {
5978 assert!(OhlcvBar::mean_wick_ratio(&[]).is_none());
5979 }
5980
5981 #[test]
5982 fn test_mean_wick_ratio_in_range_zero_to_one() {
5983 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
5984 let b2 = make_ohlcv_bar(dec!(100), dec!(115), dec!(85), dec!(100));
5985 let ratio = OhlcvBar::mean_wick_ratio(&[b1, b2]).unwrap();
5986 assert!(ratio >= 0.0 && ratio <= 1.0);
5987 }
5988
5989 #[test]
5992 fn test_bullish_volume_zero_for_empty_slice() {
5993 assert_eq!(OhlcvBar::bullish_volume(&[]), dec!(0));
5994 }
5995
5996 #[test]
5997 fn test_bullish_volume_sums_bullish_bars() {
5998 let mut bull = make_ohlcv_bar(dec!(90), dec!(110), dec!(85), dec!(105));
5999 bull.volume = dec!(100);
6000 let mut bear = make_ohlcv_bar(dec!(110), dec!(115), dec!(85), dec!(95));
6001 bear.volume = dec!(50);
6002 assert_eq!(OhlcvBar::bullish_volume(&[bull, bear]), dec!(100));
6003 }
6004
6005 #[test]
6006 fn test_bearish_volume_zero_for_empty_slice() {
6007 assert_eq!(OhlcvBar::bearish_volume(&[]), dec!(0));
6008 }
6009
6010 #[test]
6011 fn test_bearish_volume_sums_bearish_bars() {
6012 let mut bull = make_ohlcv_bar(dec!(90), dec!(110), dec!(85), dec!(105));
6013 bull.volume = dec!(100);
6014 let mut bear = make_ohlcv_bar(dec!(110), dec!(115), dec!(85), dec!(95));
6015 bear.volume = dec!(50);
6016 assert_eq!(OhlcvBar::bearish_volume(&[bull, bear]), dec!(50));
6017 }
6018
6019 #[test]
6022 fn test_close_above_mid_count_zero_for_empty_slice() {
6023 assert_eq!(OhlcvBar::close_above_mid_count(&[]), 0);
6024 }
6025
6026 #[test]
6027 fn test_close_above_mid_count_correct() {
6028 let above_mid = make_ohlcv_bar(dec!(100), dec!(120), dec!(80), dec!(110)); let at_mid = make_ohlcv_bar(dec!(100), dec!(120), dec!(80), dec!(100)); let below_mid = make_ohlcv_bar(dec!(100), dec!(120), dec!(80), dec!(85)); assert_eq!(OhlcvBar::close_above_mid_count(&[above_mid, at_mid, below_mid]), 1);
6032 }
6033
6034 #[test]
6037 fn test_ema_none_for_empty_slice() {
6038 assert!(OhlcvBar::ema(&[], 0.5).is_none());
6039 }
6040
6041 #[test]
6042 fn test_ema_single_bar_equals_close() {
6043 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
6044 let e = OhlcvBar::ema(&[bar], 0.5).unwrap();
6045 assert!((e - 105.0).abs() < 1e-9);
6046 }
6047
6048 #[test]
6049 fn test_ema_alpha_one_equals_last_close() {
6050 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
6051 let b2 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(200));
6052 let e = OhlcvBar::ema(&[b1, b2], 1.0).unwrap();
6053 assert!((e - 200.0).abs() < 1e-9);
6054 }
6055
6056 #[test]
6059 fn test_highest_open_none_for_empty_slice() {
6060 assert!(OhlcvBar::highest_open(&[]).is_none());
6061 }
6062
6063 #[test]
6064 fn test_highest_open_returns_max() {
6065 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
6066 let b2 = make_ohlcv_bar(dec!(130), dec!(140), dec!(120), dec!(135));
6067 assert_eq!(OhlcvBar::highest_open(&[b1, b2]), Some(dec!(130)));
6068 }
6069
6070 #[test]
6071 fn test_lowest_open_none_for_empty_slice() {
6072 assert!(OhlcvBar::lowest_open(&[]).is_none());
6073 }
6074
6075 #[test]
6076 fn test_lowest_open_returns_min() {
6077 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
6078 let b2 = make_ohlcv_bar(dec!(130), dec!(140), dec!(120), dec!(135));
6079 assert_eq!(OhlcvBar::lowest_open(&[b1, b2]), Some(dec!(100)));
6080 }
6081
6082 #[test]
6085 fn test_rising_close_count_zero_for_empty_slice() {
6086 assert_eq!(OhlcvBar::rising_close_count(&[]), 0);
6087 }
6088
6089 #[test]
6090 fn test_rising_close_count_zero_for_single_bar() {
6091 assert_eq!(OhlcvBar::rising_close_count(&[make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105))]), 0);
6092 }
6093
6094 #[test]
6095 fn test_rising_close_count_correct() {
6096 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
6097 let b2 = make_ohlcv_bar(dec!(100), dec!(115), dec!(90), dec!(110)); let b3 = make_ohlcv_bar(dec!(100), dec!(115), dec!(90), dec!(105)); let b4 = make_ohlcv_bar(dec!(100), dec!(120), dec!(90), dec!(115)); assert_eq!(OhlcvBar::rising_close_count(&[b1, b2, b3, b4]), 2);
6101 }
6102
6103 #[test]
6106 fn test_mean_body_ratio_none_for_empty_slice() {
6107 assert!(OhlcvBar::mean_body_ratio(&[]).is_none());
6108 }
6109
6110 #[test]
6111 fn test_mean_body_ratio_in_range_zero_to_one() {
6112 let b1 = make_ohlcv_bar(dec!(100), dec!(120), dec!(80), dec!(110));
6113 let b2 = make_ohlcv_bar(dec!(100), dec!(120), dec!(80), dec!(100));
6114 let ratio = OhlcvBar::mean_body_ratio(&[b1, b2]).unwrap();
6115 assert!(ratio >= 0.0 && ratio <= 1.0);
6116 }
6117
6118 #[test]
6121 fn test_volume_std_dev_none_for_single_bar() {
6122 let mut b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
6123 b.volume = dec!(100);
6124 assert!(OhlcvBar::volume_std_dev(&[b]).is_none());
6125 }
6126
6127 #[test]
6128 fn test_volume_std_dev_zero_for_identical_volumes() {
6129 let mut b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)); b1.volume = dec!(50);
6130 let mut b2 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)); b2.volume = dec!(50);
6131 assert_eq!(OhlcvBar::volume_std_dev(&[b1, b2]), Some(0.0));
6132 }
6133
6134 #[test]
6135 fn test_volume_std_dev_positive_for_varied_volumes() {
6136 let mut b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)); b1.volume = dec!(10);
6137 let mut b2 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)); b2.volume = dec!(100);
6138 let std = OhlcvBar::volume_std_dev(&[b1, b2]).unwrap();
6139 assert!(std > 0.0);
6140 }
6141
6142 #[test]
6145 fn test_max_volume_bar_none_for_empty_slice() {
6146 assert!(OhlcvBar::max_volume_bar(&[]).is_none());
6147 }
6148
6149 #[test]
6150 fn test_max_volume_bar_returns_highest_volume() {
6151 let mut b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)); b1.volume = dec!(10);
6152 let mut b2 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)); b2.volume = dec!(100);
6153 let bars = [b1, b2];
6154 let bar = OhlcvBar::max_volume_bar(&bars).unwrap();
6155 assert_eq!(bar.volume, dec!(100));
6156 }
6157
6158 #[test]
6159 fn test_min_volume_bar_returns_lowest_volume() {
6160 let mut b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)); b1.volume = dec!(10);
6161 let mut b2 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)); b2.volume = dec!(100);
6162 let bars = [b1, b2];
6163 let bar = OhlcvBar::min_volume_bar(&bars).unwrap();
6164 assert_eq!(bar.volume, dec!(10));
6165 }
6166
6167 #[test]
6170 fn test_gap_sum_zero_for_empty_slice() {
6171 assert_eq!(OhlcvBar::gap_sum(&[]), dec!(0));
6172 }
6173
6174 #[test]
6175 fn test_gap_sum_zero_for_single_bar() {
6176 assert_eq!(OhlcvBar::gap_sum(&[make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105))]), dec!(0));
6177 }
6178
6179 #[test]
6180 fn test_gap_sum_positive_for_gap_up_sequence() {
6181 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
6183 let b2 = make_ohlcv_bar(dec!(110), dec!(120), dec!(105), dec!(115));
6184 assert_eq!(OhlcvBar::gap_sum(&[b1, b2]), dec!(10));
6185 }
6186
6187 #[test]
6188 fn test_gap_sum_negative_for_gap_down_sequence() {
6189 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
6191 let b2 = make_ohlcv_bar(dec!(90), dec!(95), dec!(80), dec!(85));
6192 assert_eq!(OhlcvBar::gap_sum(&[b1, b2]), dec!(-10));
6193 }
6194
6195 #[test]
6198 fn test_three_white_soldiers_false_for_fewer_than_3_bars() {
6199 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
6200 assert!(!OhlcvBar::three_white_soldiers(&[b]));
6201 }
6202
6203 #[test]
6204 fn test_three_white_soldiers_true_for_classic_pattern() {
6205 let b1 = make_ohlcv_bar(dec!(100), dec!(115), dec!(98), dec!(112));
6206 let b2 = make_ohlcv_bar(dec!(112), dec!(128), dec!(110), dec!(125));
6207 let b3 = make_ohlcv_bar(dec!(125), dec!(142), dec!(123), dec!(140));
6208 assert!(OhlcvBar::three_white_soldiers(&[b1, b2, b3]));
6209 }
6210
6211 #[test]
6212 fn test_three_white_soldiers_false_for_bearish_bar_in_sequence() {
6213 let b1 = make_ohlcv_bar(dec!(100), dec!(115), dec!(98), dec!(112));
6215 let b2 = make_ohlcv_bar(dec!(115), dec!(120), dec!(105), dec!(108));
6216 let b3 = make_ohlcv_bar(dec!(108), dec!(130), dec!(106), dec!(128));
6217 assert!(!OhlcvBar::three_white_soldiers(&[b1, b2, b3]));
6218 }
6219
6220 #[test]
6223 fn test_three_black_crows_false_for_fewer_than_3_bars() {
6224 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(95));
6225 assert!(!OhlcvBar::three_black_crows(&[b]));
6226 }
6227
6228 #[test]
6229 fn test_three_black_crows_true_for_classic_pattern() {
6230 let b1 = make_ohlcv_bar(dec!(140), dec!(142), dec!(110), dec!(112));
6231 let b2 = make_ohlcv_bar(dec!(112), dec!(114), dec!(95), dec!(97));
6232 let b3 = make_ohlcv_bar(dec!(97), dec!(99), dec!(80), dec!(82));
6233 assert!(OhlcvBar::three_black_crows(&[b1, b2, b3]));
6234 }
6235
6236 #[test]
6237 fn test_three_black_crows_false_for_bullish_bar_in_sequence() {
6238 let b1 = make_ohlcv_bar(dec!(140), dec!(142), dec!(110), dec!(112));
6240 let b2 = make_ohlcv_bar(dec!(108), dec!(120), dec!(106), dec!(118));
6241 let b3 = make_ohlcv_bar(dec!(115), dec!(116), dec!(90), dec!(92));
6242 assert!(!OhlcvBar::three_black_crows(&[b1, b2, b3]));
6243 }
6244
6245 #[test]
6248 fn test_is_gap_bar_true_when_open_differs_from_prev_close() {
6249 let bar = make_ohlcv_bar(dec!(105), dec!(115), dec!(100), dec!(110));
6250 assert!(OhlcvBar::is_gap_bar(&bar, dec!(100)));
6251 }
6252
6253 #[test]
6254 fn test_is_gap_bar_false_when_open_equals_prev_close() {
6255 let bar = make_ohlcv_bar(dec!(100), dec!(115), dec!(98), dec!(110));
6256 assert!(!OhlcvBar::is_gap_bar(&bar, dec!(100)));
6257 }
6258
6259 #[test]
6262 fn test_gap_bars_count_zero_for_empty_slice() {
6263 assert_eq!(OhlcvBar::gap_bars_count(&[]), 0);
6264 }
6265
6266 #[test]
6267 fn test_gap_bars_count_zero_when_no_gaps() {
6268 let b1 = make_ohlcv_bar(dec!(90), dec!(105), dec!(88), dec!(100));
6270 let b2 = make_ohlcv_bar(dec!(100), dec!(115), dec!(98), dec!(110));
6271 assert_eq!(OhlcvBar::gap_bars_count(&[b1, b2]), 0);
6272 }
6273
6274 #[test]
6275 fn test_gap_bars_count_counts_all_gaps() {
6276 let b1 = make_ohlcv_bar(dec!(90), dec!(105), dec!(88), dec!(100));
6277 let b2 = make_ohlcv_bar(dec!(105), dec!(115), dec!(103), dec!(110)); let b3 = make_ohlcv_bar(dec!(110), dec!(120), dec!(108), dec!(115)); let b4 = make_ohlcv_bar(dec!(120), dec!(130), dec!(118), dec!(128)); assert_eq!(OhlcvBar::gap_bars_count(&[b1, b2, b3, b4]), 2);
6281 }
6282
6283 #[test]
6286 fn test_inside_bar_true_when_range_inside_prior_v2() {
6287 let prior = make_ohlcv_bar(dec!(100), dec!(120), dec!(80), dec!(110));
6288 let bar = make_ohlcv_bar(dec!(105), dec!(115), dec!(90), dec!(108));
6289 assert!(bar.inside_bar(&prior));
6290 }
6291
6292 #[test]
6293 fn test_inside_bar_false_when_high_exceeds_prior_v2() {
6294 let prior = make_ohlcv_bar(dec!(100), dec!(120), dec!(80), dec!(110));
6295 let bar = make_ohlcv_bar(dec!(105), dec!(125), dec!(90), dec!(118));
6296 assert!(!bar.inside_bar(&prior));
6297 }
6298
6299 #[test]
6300 fn test_outside_bar_true_when_range_engulfs_prior_v2() {
6301 let prior = make_ohlcv_bar(dec!(100), dec!(115), dec!(90), dec!(108));
6302 let bar = make_ohlcv_bar(dec!(95), dec!(120), dec!(85), dec!(112));
6303 assert!(bar.outside_bar(&prior));
6304 }
6305
6306 #[test]
6307 fn test_outside_bar_false_when_range_is_inside_v2() {
6308 let prior = make_ohlcv_bar(dec!(100), dec!(120), dec!(80), dec!(110));
6309 let bar = make_ohlcv_bar(dec!(105), dec!(115), dec!(90), dec!(108));
6310 assert!(!bar.outside_bar(&prior));
6311 }
6312
6313 #[test]
6316 fn test_bar_efficiency_none_for_zero_range_bar() {
6317 let bar = make_ohlcv_bar(dec!(100), dec!(100), dec!(100), dec!(100));
6318 assert!(OhlcvBar::bar_efficiency(&bar).is_none());
6319 }
6320
6321 #[test]
6322 fn test_bar_efficiency_one_for_full_trend_bar() {
6323 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(100), dec!(110));
6325 let eff = OhlcvBar::bar_efficiency(&bar).unwrap();
6326 assert!((eff - 1.0).abs() < 1e-9);
6327 }
6328
6329 #[test]
6330 fn test_bar_efficiency_between_zero_and_one() {
6331 let bar = make_ohlcv_bar(dec!(100), dec!(115), dec!(95), dec!(108));
6332 let eff = OhlcvBar::bar_efficiency(&bar).unwrap();
6333 assert!(eff >= 0.0 && eff <= 1.0);
6334 }
6335
6336 #[test]
6339 fn test_wicks_sum_zero_for_empty_slice() {
6340 assert_eq!(OhlcvBar::wicks_sum(&[]), dec!(0));
6341 }
6342
6343 #[test]
6344 fn test_wicks_sum_correct_for_doji_like_bar() {
6345 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
6347 assert_eq!(OhlcvBar::wicks_sum(&[bar]), dec!(20));
6348 }
6349
6350 #[test]
6353 fn test_avg_close_to_high_none_for_empty_slice() {
6354 assert!(OhlcvBar::avg_close_to_high(&[]).is_none());
6355 }
6356
6357 #[test]
6358 fn test_avg_close_to_high_correct_for_two_bars() {
6359 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(95), dec!(105));
6361 let b2 = make_ohlcv_bar(dec!(110), dec!(120), dec!(108), dec!(115));
6362 let avg = OhlcvBar::avg_close_to_high(&[b1, b2]).unwrap();
6363 assert!((avg - 5.0).abs() < 1e-9);
6364 }
6365
6366 #[test]
6369 fn test_avg_range_r65_none_for_empty() {
6370 assert!(OhlcvBar::avg_range(&[]).is_none());
6371 }
6372
6373 #[test]
6374 fn test_avg_range_r65_correct() {
6375 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
6376 let b2 = make_ohlcv_bar(dec!(105), dec!(120), dec!(100), dec!(115));
6377 let avg = OhlcvBar::avg_range(&[b1, b2]).unwrap();
6378 assert!((avg - 20.0).abs() < 1e-9);
6379 }
6380
6381 #[test]
6384 fn test_max_close_r65_none_empty() {
6385 assert!(OhlcvBar::max_close(&[]).is_none());
6386 }
6387
6388 #[test]
6389 fn test_max_close_r65_highest() {
6390 let b1 = make_ohlcv_bar(dec!(100), dec!(115), dec!(95), dec!(110));
6391 let b2 = make_ohlcv_bar(dec!(110), dec!(130), dec!(108), dec!(125));
6392 let b3 = make_ohlcv_bar(dec!(115), dec!(120), dec!(112), dec!(118));
6393 assert_eq!(OhlcvBar::max_close(&[b1, b2, b3]), Some(dec!(125)));
6394 }
6395
6396 #[test]
6397 fn test_min_close_r65_lowest() {
6398 let b1 = make_ohlcv_bar(dec!(100), dec!(115), dec!(95), dec!(110));
6399 let b2 = make_ohlcv_bar(dec!(110), dec!(130), dec!(108), dec!(125));
6400 let b3 = make_ohlcv_bar(dec!(90), dec!(105), dec!(88), dec!(95));
6401 assert_eq!(OhlcvBar::min_close(&[b1, b2, b3]), Some(dec!(95)));
6402 }
6403
6404 #[test]
6407 fn test_trend_strength_r65_none_single() {
6408 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
6409 assert!(OhlcvBar::trend_strength(&[b]).is_none());
6410 }
6411
6412 #[test]
6413 fn test_trend_strength_r65_one_bullish() {
6414 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(95), dec!(105));
6415 let b2 = make_ohlcv_bar(dec!(105), dec!(120), dec!(103), dec!(115));
6416 let b3 = make_ohlcv_bar(dec!(115), dec!(130), dec!(113), dec!(128));
6417 let s = OhlcvBar::trend_strength(&[b1, b2, b3]).unwrap();
6418 assert!((s - 1.0).abs() < 1e-9);
6419 }
6420
6421 #[test]
6422 fn test_trend_strength_r65_zero_bearish() {
6423 let b1 = make_ohlcv_bar(dec!(128), dec!(130), dec!(113), dec!(128));
6424 let b2 = make_ohlcv_bar(dec!(115), dec!(120), dec!(103), dec!(110));
6425 let b3 = make_ohlcv_bar(dec!(105), dec!(110), dec!(95), dec!(100));
6426 let s = OhlcvBar::trend_strength(&[b1, b2, b3]).unwrap();
6427 assert!((s - 0.0).abs() < 1e-9);
6428 }
6429
6430 #[test]
6433 fn test_net_change_none_for_empty() {
6434 assert!(OhlcvBar::net_change(&[]).is_none());
6435 }
6436
6437 #[test]
6438 fn test_net_change_positive_for_bullish_bar() {
6439 let b = make_ohlcv_bar(dec!(100), dec!(115), dec!(98), dec!(112));
6440 assert_eq!(OhlcvBar::net_change(&[b]), Some(dec!(12)));
6441 }
6442
6443 #[test]
6444 fn test_net_change_negative_for_bearish_bar() {
6445 let b = make_ohlcv_bar(dec!(110), dec!(112), dec!(95), dec!(100));
6446 assert_eq!(OhlcvBar::net_change(&[b]), Some(dec!(-10)));
6447 }
6448
6449 #[test]
6452 fn test_open_to_close_pct_none_for_empty() {
6453 assert!(OhlcvBar::open_to_close_pct(&[]).is_none());
6454 }
6455
6456 #[test]
6457 fn test_open_to_close_pct_correct() {
6458 let b = make_ohlcv_bar(dec!(100), dec!(115), dec!(98), dec!(110));
6460 let pct = OhlcvBar::open_to_close_pct(&[b]).unwrap();
6461 assert!((pct - 10.0).abs() < 1e-9);
6462 }
6463
6464 #[test]
6467 fn test_high_to_low_pct_none_for_empty() {
6468 assert!(OhlcvBar::high_to_low_pct(&[]).is_none());
6469 }
6470
6471 #[test]
6472 fn test_high_to_low_pct_correct() {
6473 let b = make_ohlcv_bar(dec!(150), dec!(200), dec!(100), dec!(160));
6475 let pct = OhlcvBar::high_to_low_pct(&[b]).unwrap();
6476 assert!((pct - 50.0).abs() < 1e-9);
6477 }
6478
6479 #[test]
6482 fn test_consecutive_highs_zero_for_single_bar() {
6483 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
6484 assert_eq!(OhlcvBar::consecutive_highs(&[b]), 0);
6485 }
6486
6487 #[test]
6488 fn test_consecutive_highs_counts_trailing_highs() {
6489 let b1 = make_ohlcv_bar(dec!(95), dec!(110), dec!(90), dec!(105));
6491 let b2 = make_ohlcv_bar(dec!(105), dec!(120), dec!(103), dec!(115));
6492 let b3 = make_ohlcv_bar(dec!(115), dec!(130), dec!(113), dec!(125));
6493 assert_eq!(OhlcvBar::consecutive_highs(&[b1, b2, b3]), 2);
6494 }
6495
6496 #[test]
6497 fn test_consecutive_lows_counts_trailing_lows() {
6498 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
6500 let b2 = make_ohlcv_bar(dec!(95), dec!(108), dec!(80), dec!(100));
6501 let b3 = make_ohlcv_bar(dec!(90), dec!(102), dec!(70), dec!(95));
6502 assert_eq!(OhlcvBar::consecutive_lows(&[b1, b2, b3]), 2);
6503 }
6504
6505 #[test]
6508 fn test_volume_change_pct_none_for_single_bar() {
6509 let mut b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
6510 b.volume = dec!(100);
6511 assert!(OhlcvBar::volume_change_pct(&[b]).is_none());
6512 }
6513
6514 #[test]
6515 fn test_volume_change_pct_correct() {
6516 let mut b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)); b1.volume = dec!(100);
6518 let mut b2 = make_ohlcv_bar(dec!(105), dec!(115), dec!(103), dec!(110)); b2.volume = dec!(150);
6519 let pct = OhlcvBar::volume_change_pct(&[b1, b2]).unwrap();
6520 assert!((pct - 50.0).abs() < 1e-9);
6521 }
6522
6523 #[test]
6526 fn test_clv_r67_plus_one_at_high() {
6527 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(110));
6529 let clv = b.close_location_value().unwrap();
6530 assert!((clv - 1.0).abs() < 1e-9);
6531 }
6532
6533 #[test]
6534 fn test_clv_r67_minus_one_at_low() {
6535 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(90));
6537 let clv = b.close_location_value().unwrap();
6538 assert!((clv - (-1.0)).abs() < 1e-9);
6539 }
6540
6541 #[test]
6542 fn test_clv_r67_none_for_zero_range() {
6543 let b = make_ohlcv_bar(dec!(100), dec!(100), dec!(100), dec!(100));
6544 assert!(b.close_location_value().is_none());
6545 }
6546
6547 #[test]
6550 fn test_body_pct_r67_none_for_zero_range() {
6551 let b = make_ohlcv_bar(dec!(100), dec!(100), dec!(100), dec!(100));
6552 assert!(b.body_pct().is_none());
6553 }
6554
6555 #[test]
6556 fn test_body_pct_r67_100_for_full_body() {
6557 let b = make_ohlcv_bar(dec!(90), dec!(110), dec!(90), dec!(110));
6559 assert_eq!(b.body_pct(), Some(dec!(100)));
6560 }
6561
6562 #[test]
6565 fn test_bullish_count_r67_correct() {
6566 let b1 = make_ohlcv_bar(dec!(100), dec!(115), dec!(98), dec!(112)); let b2 = make_ohlcv_bar(dec!(112), dec!(120), dec!(105), dec!(108)); let b3 = make_ohlcv_bar(dec!(108), dec!(125), dec!(106), dec!(120)); assert_eq!(OhlcvBar::bullish_count(&[b1, b2, b3]), 2);
6570 }
6571
6572 #[test]
6573 fn test_bearish_count_r67_correct() {
6574 let b1 = make_ohlcv_bar(dec!(115), dec!(118), dec!(100), dec!(105)); let b2 = make_ohlcv_bar(dec!(105), dec!(115), dec!(103), dec!(112)); assert_eq!(OhlcvBar::bearish_count(&[b1, b2]), 1);
6577 }
6578
6579 #[test]
6582 fn test_open_gap_pct_none_for_single_bar() {
6583 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
6584 assert!(OhlcvBar::open_gap_pct(&[b]).is_none());
6585 }
6586
6587 #[test]
6588 fn test_open_gap_pct_positive_for_gap_up() {
6589 let b1 = make_ohlcv_bar(dec!(90), dec!(105), dec!(88), dec!(100));
6591 let b2 = make_ohlcv_bar(dec!(105), dec!(115), dec!(103), dec!(110));
6592 let pct = OhlcvBar::open_gap_pct(&[b1, b2]).unwrap();
6593 assert!((pct - 5.0).abs() < 1e-9);
6594 }
6595
6596 #[test]
6599 fn test_volume_cumulative_zero_for_empty() {
6600 assert_eq!(OhlcvBar::volume_cumulative(&[]), dec!(0));
6601 }
6602
6603 #[test]
6604 fn test_volume_cumulative_sums_all_volumes() {
6605 let mut b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)); b1.volume = dec!(100);
6606 let mut b2 = make_ohlcv_bar(dec!(105), dec!(115), dec!(103), dec!(110)); b2.volume = dec!(200);
6607 assert_eq!(OhlcvBar::volume_cumulative(&[b1, b2]), dec!(300));
6608 }
6609
6610 #[test]
6613 fn test_price_position_none_for_empty() {
6614 assert!(OhlcvBar::price_position(&[]).is_none());
6615 }
6616
6617 #[test]
6618 fn test_price_position_one_when_close_at_highest() {
6619 let b1 = make_ohlcv_bar(dec!(85), dec!(100), dec!(80), dec!(95));
6621 let b2 = make_ohlcv_bar(dec!(110), dec!(120), dec!(100), dec!(120));
6622 let pos = OhlcvBar::price_position(&[b1, b2]).unwrap();
6623 assert!((pos - 1.0).abs() < 1e-9);
6624 }
6625
6626 #[test]
6629 fn test_close_above_open_count_zero_for_empty() {
6630 assert_eq!(OhlcvBar::close_above_open_count(&[]), 0);
6631 }
6632
6633 #[test]
6634 fn test_close_above_open_count_correct() {
6635 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(95), dec!(108));
6637 let b2 = make_ohlcv_bar(dec!(110), dec!(115), dec!(100), dec!(102));
6638 assert_eq!(OhlcvBar::close_above_open_count(&[b1, b2]), 1);
6639 }
6640
6641 #[test]
6644 fn test_volume_price_correlation_none_for_single_bar() {
6645 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
6646 assert!(OhlcvBar::volume_price_correlation(&[b]).is_none());
6647 }
6648
6649 #[test]
6650 fn test_volume_price_correlation_positive_for_comoving() {
6651 let mut b1 = make_ohlcv_bar(dec!(100), dec!(105), dec!(98), dec!(102)); b1.volume = dec!(100);
6653 let mut b2 = make_ohlcv_bar(dec!(102), dec!(110), dec!(100), dec!(108)); b2.volume = dec!(200);
6654 let corr = OhlcvBar::volume_price_correlation(&[b1, b2]).unwrap();
6655 assert!(corr > 0.0, "expected positive correlation, got {}", corr);
6656 }
6657
6658 #[test]
6661 fn test_body_consistency_none_for_empty() {
6662 assert!(OhlcvBar::body_consistency(&[]).is_none());
6663 }
6664
6665 #[test]
6666 fn test_body_consistency_one_for_all_big_bodies() {
6667 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(100), dec!(108));
6669 let b2 = make_ohlcv_bar(dec!(110), dec!(120), dec!(110), dec!(118));
6670 let r = OhlcvBar::body_consistency(&[b1, b2]).unwrap();
6671 assert!((r - 1.0).abs() < 1e-9, "expected 1.0, got {}", r);
6672 }
6673
6674 #[test]
6677 fn test_close_volatility_ratio_none_for_single_bar() {
6678 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
6679 assert!(OhlcvBar::close_volatility_ratio(&[b]).is_none());
6680 }
6681
6682 #[test]
6683 fn test_close_volatility_ratio_positive_for_varied_closes() {
6684 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
6685 let b2 = make_ohlcv_bar(dec!(110), dec!(120), dec!(105), dec!(115));
6686 let r = OhlcvBar::close_volatility_ratio(&[b1, b2]).unwrap();
6687 assert!(r > 0.0, "expected positive ratio, got {}", r);
6688 }
6689
6690 #[test]
6691 fn test_close_volatility_ratio_zero_for_identical_closes() {
6692 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
6693 let b2 = make_ohlcv_bar(dec!(100), dec!(112), dec!(88), dec!(105));
6694 let r = OhlcvBar::close_volatility_ratio(&[b1, b2]).unwrap();
6695 assert!((r - 0.0).abs() < 1e-9, "expected 0.0 for identical closes, got {}", r);
6696 }
6697
6698 #[test]
6701 fn test_is_trending_up_false_for_empty() {
6702 assert!(!OhlcvBar::is_trending_up(&[], 3));
6703 }
6704
6705 #[test]
6706 fn test_is_trending_up_false_for_n_less_than_2() {
6707 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
6708 assert!(!OhlcvBar::is_trending_up(&[b], 1));
6709 }
6710
6711 #[test]
6712 fn test_is_trending_up_true_for_rising_closes() {
6713 let b1 = make_ohlcv_bar(dec!(100), dec!(105), dec!(98), dec!(102));
6714 let b2 = make_ohlcv_bar(dec!(102), dec!(110), dec!(100), dec!(107));
6715 let b3 = make_ohlcv_bar(dec!(107), dec!(115), dec!(105), dec!(112));
6716 assert!(OhlcvBar::is_trending_up(&[b1, b2, b3], 3));
6717 }
6718
6719 #[test]
6720 fn test_is_trending_down_true_for_falling_closes() {
6721 let b1 = make_ohlcv_bar(dec!(112), dec!(115), dec!(105), dec!(110));
6722 let b2 = make_ohlcv_bar(dec!(110), dec!(112), dec!(100), dec!(105));
6723 let b3 = make_ohlcv_bar(dec!(105), dec!(108), dec!(95), dec!(98));
6724 assert!(OhlcvBar::is_trending_down(&[b1, b2, b3], 3));
6725 }
6726
6727 #[test]
6730 fn test_volume_acceleration_none_for_single_bar() {
6731 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
6732 assert!(OhlcvBar::volume_acceleration(&[b]).is_none());
6733 }
6734
6735 #[test]
6736 fn test_volume_acceleration_positive_when_volume_rises() {
6737 let mut b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)); b1.volume = dec!(100);
6738 let mut b2 = make_ohlcv_bar(dec!(105), dec!(115), dec!(103), dec!(110)); b2.volume = dec!(150);
6739 let acc = OhlcvBar::volume_acceleration(&[b1, b2]).unwrap();
6740 assert!(acc > 0.0, "volume rose so acceleration should be positive, got {}", acc);
6741 }
6742
6743 #[test]
6746 fn test_wick_body_ratio_none_for_empty() {
6747 assert!(OhlcvBar::wick_body_ratio(&[]).is_none());
6748 }
6749
6750 #[test]
6751 fn test_wick_body_ratio_none_for_doji_bar() {
6752 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
6754 assert!(OhlcvBar::wick_body_ratio(&[b]).is_none());
6755 }
6756
6757 #[test]
6758 fn test_wick_body_ratio_positive_for_wicked_bar() {
6759 let b = make_ohlcv_bar(dec!(100), dec!(115), dec!(95), dec!(105));
6761 let r = OhlcvBar::wick_body_ratio(&[b]).unwrap();
6762 assert!(r > 0.0, "expected positive wick/body ratio, got {}", r);
6763 }
6764
6765 #[test]
6768 fn test_close_momentum_score_none_for_empty() {
6769 assert!(OhlcvBar::close_momentum_score(&[]).is_none());
6770 }
6771
6772 #[test]
6773 fn test_close_momentum_score_half_for_symmetric() {
6774 let b1 = make_ohlcv_bar(dec!(88), dec!(95), dec!(85), dec!(90));
6776 let b2 = make_ohlcv_bar(dec!(108), dec!(115), dec!(105), dec!(110));
6777 let score = OhlcvBar::close_momentum_score(&[b1, b2]).unwrap();
6778 assert!((score - 0.5).abs() < 1e-9, "expected 0.5, got {}", score);
6779 }
6780
6781 #[test]
6784 fn test_range_expansion_count_zero_for_single_bar() {
6785 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
6786 assert_eq!(OhlcvBar::range_expansion_count(&[b]), 0);
6787 }
6788
6789 #[test]
6790 fn test_range_expansion_count_correct() {
6791 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
6793 let b2 = make_ohlcv_bar(dec!(105), dec!(120), dec!(90), dec!(110));
6794 assert_eq!(OhlcvBar::range_expansion_count(&[b1, b2]), 1);
6795 }
6796
6797 #[test]
6800 fn test_gap_count_zero_for_single_bar() {
6801 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
6802 assert_eq!(OhlcvBar::gap_count(&[b]), 0);
6803 }
6804
6805 #[test]
6806 fn test_gap_count_detects_gap() {
6807 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
6808 let b2 = make_ohlcv_bar(dec!(108), dec!(115), dec!(106), dec!(112));
6810 assert_eq!(OhlcvBar::gap_count(&[b1, b2]), 1);
6811 }
6812
6813 #[test]
6814 fn test_gap_count_zero_when_open_equals_close() {
6815 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
6816 let b2 = make_ohlcv_bar(dec!(105), dec!(115), dec!(103), dec!(112));
6818 assert_eq!(OhlcvBar::gap_count(&[b1, b2]), 0);
6819 }
6820
6821 #[test]
6824 fn test_avg_wick_size_none_for_empty() {
6825 assert!(OhlcvBar::avg_wick_size(&[]).is_none());
6826 }
6827
6828 #[test]
6829 fn test_avg_wick_size_correct() {
6830 let b = make_ohlcv_bar(dec!(100), dec!(115), dec!(95), dec!(105));
6833 let ws = OhlcvBar::avg_wick_size(&[b]).unwrap();
6834 assert!((ws - 15.0).abs() < 1e-6, "expected 15.0, got {}", ws);
6835 }
6836
6837 #[test]
6840 fn test_mean_volume_ratio_empty_for_empty_slice() {
6841 assert!(OhlcvBar::mean_volume_ratio(&[]).is_empty());
6842 }
6843
6844 #[test]
6845 fn test_mean_volume_ratio_sums_to_n_times_mean() {
6846 let mut b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)); b1.volume = dec!(100);
6847 let mut b2 = make_ohlcv_bar(dec!(105), dec!(115), dec!(103), dec!(110)); b2.volume = dec!(300);
6848 let ratios = OhlcvBar::mean_volume_ratio(&[b1, b2]);
6850 assert_eq!(ratios.len(), 2);
6851 let r0 = ratios[0].unwrap();
6852 let r1 = ratios[1].unwrap();
6853 assert!((r0 - 0.5).abs() < 1e-6, "expected 0.5, got {}", r0);
6854 assert!((r1 - 1.5).abs() < 1e-6, "expected 1.5, got {}", r1);
6855 }
6856
6857 #[test]
6860 fn test_price_compression_ratio_none_for_zero_range() {
6861 let b = make_ohlcv_bar(dec!(100), dec!(100), dec!(100), dec!(100));
6863 assert!(OhlcvBar::price_compression_ratio(&[b]).is_none());
6864 }
6865
6866 #[test]
6867 fn test_price_compression_ratio_in_range() {
6868 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(108));
6869 let r = OhlcvBar::price_compression_ratio(&[b]).unwrap();
6870 assert!(r >= 0.0 && r <= 1.0, "expected value in [0,1], got {}", r);
6871 }
6872
6873 #[test]
6876 fn test_open_close_spread_none_for_empty() {
6877 assert!(OhlcvBar::open_close_spread(&[]).is_none());
6878 }
6879
6880 #[test]
6881 fn test_open_close_spread_zero_for_doji() {
6882 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
6883 let s = OhlcvBar::open_close_spread(&[b]).unwrap();
6884 assert!((s - 0.0).abs() < 1e-9, "doji should have spread=0, got {}", s);
6885 }
6886
6887 #[test]
6888 fn test_open_close_spread_positive_for_directional_bar() {
6889 let b = make_ohlcv_bar(dec!(100), dec!(115), dec!(98), dec!(110));
6890 let s = OhlcvBar::open_close_spread(&[b]).unwrap();
6891 assert!(s > 0.0, "directional bar should have positive spread, got {}", s);
6892 }
6893
6894 #[test]
6897 fn test_close_above_high_ma_zero_for_too_few_bars() {
6898 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
6899 assert_eq!(OhlcvBar::close_above_high_ma(&[b], 2), 0);
6900 }
6901
6902 #[test]
6903 fn test_close_above_high_ma_detects_breakout() {
6904 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
6906 let b2 = make_ohlcv_bar(dec!(110), dec!(120), dec!(108), dec!(118));
6907 assert_eq!(OhlcvBar::close_above_high_ma(&[b1, b2], 2), 1);
6908 }
6909
6910 #[test]
6913 fn test_max_consecutive_gains_zero_for_single_bar() {
6914 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
6915 assert_eq!(OhlcvBar::max_consecutive_gains(&[b]), 0);
6916 }
6917
6918 #[test]
6919 fn test_max_consecutive_gains_correct() {
6920 let b1 = make_ohlcv_bar(dec!(98), dec!(102), dec!(96), dec!(100));
6922 let b2 = make_ohlcv_bar(dec!(100), dec!(108), dec!(99), dec!(105));
6923 let b3 = make_ohlcv_bar(dec!(105), dec!(112), dec!(104), dec!(110));
6924 let b4 = make_ohlcv_bar(dec!(110), dec!(111), dec!(105), dec!(108));
6925 let b5 = make_ohlcv_bar(dec!(108), dec!(116), dec!(107), dec!(115));
6926 assert_eq!(OhlcvBar::max_consecutive_gains(&[b1, b2, b3, b4, b5]), 2);
6927 }
6928
6929 #[test]
6930 fn test_max_consecutive_losses_correct() {
6931 let b1 = make_ohlcv_bar(dec!(112), dec!(115), dec!(108), dec!(110));
6933 let b2 = make_ohlcv_bar(dec!(110), dec!(112), dec!(103), dec!(105));
6934 let b3 = make_ohlcv_bar(dec!(105), dec!(108), dec!(98), dec!(100));
6935 let b4 = make_ohlcv_bar(dec!(100), dec!(112), dec!(98), dec!(108));
6936 assert_eq!(OhlcvBar::max_consecutive_losses(&[b1, b2, b3, b4]), 2);
6937 }
6938
6939 #[test]
6942 fn test_price_path_length_none_for_single_bar() {
6943 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
6944 assert!(OhlcvBar::price_path_length(&[b]).is_none());
6945 }
6946
6947 #[test]
6948 fn test_price_path_length_correct() {
6949 let b1 = make_ohlcv_bar(dec!(98), dec!(102), dec!(96), dec!(100));
6951 let b2 = make_ohlcv_bar(dec!(100), dec!(112), dec!(99), dec!(110));
6952 let b3 = make_ohlcv_bar(dec!(110), dec!(112), dec!(103), dec!(105));
6953 let len = OhlcvBar::price_path_length(&[b1, b2, b3]).unwrap();
6954 assert!((len - 15.0).abs() < 1e-6, "expected 15.0, got {}", len);
6955 }
6956
6957 #[test]
6960 fn test_close_reversion_count_zero_for_single_bar() {
6961 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
6962 assert_eq!(OhlcvBar::close_reversion_count(&[b]), 0);
6963 }
6964
6965 #[test]
6966 fn test_close_reversion_count_returns_usize() {
6967 let b1 = make_ohlcv_bar(dec!(98), dec!(102), dec!(96), dec!(100));
6968 let b2 = make_ohlcv_bar(dec!(100), dec!(112), dec!(99), dec!(110));
6969 let b3 = make_ohlcv_bar(dec!(110), dec!(112), dec!(103), dec!(105));
6970 let _ = OhlcvBar::close_reversion_count(&[b1, b2, b3]);
6972 }
6973
6974 #[test]
6977 fn test_atr_ratio_none_for_single_bar() {
6978 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
6979 assert!(OhlcvBar::atr_ratio(&[b]).is_none());
6980 }
6981
6982 #[test]
6983 fn test_atr_ratio_positive_for_valid_bars() {
6984 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
6985 let b2 = make_ohlcv_bar(dec!(105), dec!(115), dec!(103), dec!(110));
6986 let r = OhlcvBar::atr_ratio(&[b1, b2]).unwrap();
6987 assert!(r > 0.0, "expected positive ATR ratio, got {}", r);
6988 }
6989
6990 #[test]
6993 fn test_volume_trend_strength_none_for_single_bar() {
6994 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
6995 assert!(OhlcvBar::volume_trend_strength(&[b]).is_none());
6996 }
6997
6998 #[test]
6999 fn test_volume_trend_strength_positive_for_rising_volume() {
7000 let mut b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)); b1.volume = dec!(100);
7001 let mut b2 = make_ohlcv_bar(dec!(105), dec!(115), dec!(103), dec!(110)); b2.volume = dec!(200);
7002 let mut b3 = make_ohlcv_bar(dec!(110), dec!(120), dec!(108), dec!(115)); b3.volume = dec!(300);
7003 let s = OhlcvBar::volume_trend_strength(&[b1, b2, b3]).unwrap();
7004 assert!(s > 0.0, "rising volume should give positive strength, got {}", s);
7005 }
7006
7007 #[test]
7010 fn test_high_close_spread_none_for_empty() {
7011 assert!(OhlcvBar::high_close_spread(&[]).is_none());
7012 }
7013
7014 #[test]
7015 fn test_high_close_spread_zero_when_close_equals_high() {
7016 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(110));
7018 let s = OhlcvBar::high_close_spread(&[b]).unwrap();
7019 assert!((s - 0.0).abs() < 1e-9, "expected 0.0, got {}", s);
7020 }
7021
7022 #[test]
7023 fn test_high_close_spread_positive_for_wicked_bar() {
7024 let b = make_ohlcv_bar(dec!(100), dec!(115), dec!(95), dec!(105));
7025 let s = OhlcvBar::high_close_spread(&[b]).unwrap();
7026 assert!(s > 0.0, "expected positive spread, got {}", s);
7027 }
7028
7029 #[test]
7032 fn test_open_range_none_for_empty() {
7033 assert!(OhlcvBar::open_range(&[]).is_none());
7034 }
7035
7036 #[test]
7037 fn test_open_range_zero_for_doji() {
7038 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
7039 let r = OhlcvBar::open_range(&[b]).unwrap();
7040 assert!((r - 0.0).abs() < 1e-9, "doji should have open_range=0, got {}", r);
7041 }
7042
7043 #[test]
7044 fn test_open_range_positive_for_directional() {
7045 let b = make_ohlcv_bar(dec!(100), dec!(115), dec!(98), dec!(110));
7046 let r = OhlcvBar::open_range(&[b]).unwrap();
7047 assert!(r > 0.0, "directional bar should have positive open_range, got {}", r);
7048 }
7049
7050 #[test]
7053 fn test_normalized_close_none_for_single_bar() {
7054 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
7055 assert!(OhlcvBar::normalized_close(&[b]).is_none());
7056 }
7057
7058 #[test]
7059 fn test_normalized_close_one_when_last_close_is_max() {
7060 let b1 = make_ohlcv_bar(dec!(98), dec!(105), dec!(96), dec!(100));
7061 let b2 = make_ohlcv_bar(dec!(100), dec!(115), dec!(99), dec!(110));
7062 let nc = OhlcvBar::normalized_close(&[b1, b2]).unwrap();
7063 assert!((nc - 1.0).abs() < 1e-9, "last close = max should give 1.0, got {}", nc);
7064 }
7065
7066 #[test]
7067 fn test_normalized_close_zero_when_last_close_is_min() {
7068 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(90));
7069 let b2 = make_ohlcv_bar(dec!(90), dec!(105), dec!(88), dec!(100));
7070 let nc = OhlcvBar::normalized_close(&[b1, b2]).unwrap();
7073 assert!(nc >= 0.0 && nc <= 1.0, "normalized close should be in [0,1], got {}", nc);
7074 }
7075
7076 #[test]
7079 fn test_candle_score_none_for_empty() {
7080 assert!(OhlcvBar::candle_score(&[]).is_none());
7081 }
7082
7083 #[test]
7084 fn test_candle_score_one_for_strong_bull_bar() {
7085 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(99), dec!(108));
7087 let s = OhlcvBar::candle_score(&[b]).unwrap();
7088 assert_eq!(s, 1.0, "strong bullish bar should score 1.0, got {}", s);
7089 }
7090
7091 #[test]
7092 fn test_candle_score_zero_for_bear_bar() {
7093 let b = make_ohlcv_bar(dec!(108), dec!(110), dec!(99), dec!(100));
7095 let s = OhlcvBar::candle_score(&[b]).unwrap();
7096 assert_eq!(s, 0.0, "bearish bar should score 0.0, got {}", s);
7097 }
7098
7099 #[test]
7102 fn test_bar_speed_none_for_empty() {
7103 assert!(OhlcvBar::bar_speed(&[]).is_none());
7104 }
7105
7106 #[test]
7109 fn test_higher_highs_count_zero_for_single_bar() {
7110 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
7111 assert_eq!(OhlcvBar::higher_highs_count(&[b]), 0);
7112 }
7113
7114 #[test]
7115 fn test_higher_highs_count_correct() {
7116 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
7117 let b2 = make_ohlcv_bar(dec!(105), dec!(120), dec!(103), dec!(115)); let b3 = make_ohlcv_bar(dec!(115), dec!(115), dec!(110), dec!(112)); assert_eq!(OhlcvBar::higher_highs_count(&[b1, b2, b3]), 1);
7120 }
7121
7122 #[test]
7123 fn test_lower_lows_count_correct() {
7124 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
7125 let b2 = make_ohlcv_bar(dec!(105), dec!(112), dec!(85), dec!(108)); let b3 = make_ohlcv_bar(dec!(108), dec!(115), dec!(95), dec!(112)); assert_eq!(OhlcvBar::lower_lows_count(&[b1, b2, b3]), 1);
7128 }
7129
7130 #[test]
7133 fn test_close_minus_open_pct_none_for_empty() {
7134 assert!(OhlcvBar::close_minus_open_pct(&[]).is_none());
7135 }
7136
7137 #[test]
7138 fn test_close_minus_open_pct_positive_for_bull_bar() {
7139 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(98), dec!(110));
7140 let p = OhlcvBar::close_minus_open_pct(&[b]).unwrap();
7141 assert!(p > 0.0, "bullish bar should give positive pct, got {}", p);
7142 }
7143
7144 #[test]
7147 fn test_volume_per_range_none_for_empty() {
7148 assert!(OhlcvBar::volume_per_range(&[]).is_none());
7149 }
7150
7151 #[test]
7152 fn test_volume_per_range_positive_for_valid_bar() {
7153 let mut b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)); b.volume = dec!(100);
7154 let r = OhlcvBar::volume_per_range(&[b]).unwrap();
7155 assert!(r > 0.0, "expected positive volume/range, got {}", r);
7156 }
7157
7158 #[test]
7159 fn test_up_volume_fraction_none_for_empty() {
7160 assert!(OhlcvBar::up_volume_fraction(&[]).is_none());
7161 }
7162
7163 #[test]
7164 fn test_up_volume_fraction_all_up() {
7165 let mut b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(95), dec!(108)); b1.volume = dec!(50);
7167 let mut b2 = make_ohlcv_bar(dec!(105), dec!(115), dec!(100), dec!(112)); b2.volume = dec!(50);
7168 let f = OhlcvBar::up_volume_fraction(&[b1, b2]).unwrap();
7169 assert!((f - 1.0).abs() < 1e-9, "all up bars → fraction=1.0, got {}", f);
7170 }
7171
7172 #[test]
7173 fn test_tail_upper_fraction_none_for_empty() {
7174 assert!(OhlcvBar::tail_upper_fraction(&[]).is_none());
7175 }
7176
7177 #[test]
7178 fn test_tail_upper_fraction_correct() {
7179 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
7181 let f = OhlcvBar::tail_upper_fraction(&[b]).unwrap();
7182 assert!((f - 0.25).abs() < 1e-9, "expected 0.25, got {}", f);
7183 }
7184
7185 #[test]
7186 fn test_tail_lower_fraction_none_for_empty() {
7187 assert!(OhlcvBar::tail_lower_fraction(&[]).is_none());
7188 }
7189
7190 #[test]
7191 fn test_tail_lower_fraction_correct() {
7192 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
7194 let f = OhlcvBar::tail_lower_fraction(&[b]).unwrap();
7195 assert!((f - 0.5).abs() < 1e-9, "expected 0.5, got {}", f);
7196 }
7197
7198 #[test]
7199 fn test_range_std_dev_none_for_single_bar() {
7200 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
7201 assert!(OhlcvBar::range_std_dev(&[b]).is_none());
7202 }
7203
7204 #[test]
7205 fn test_range_std_dev_zero_for_equal_ranges() {
7206 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
7207 let b2 = make_ohlcv_bar(dec!(200), dec!(210), dec!(190), dec!(205));
7208 let sd = OhlcvBar::range_std_dev(&[b1, b2]).unwrap();
7209 assert!(sd.abs() < 1e-9, "equal ranges → std_dev=0, got {}", sd);
7210 }
7211
7212 #[test]
7213 fn test_body_fraction_none_for_empty() {
7214 assert!(OhlcvBar::body_fraction(&[]).is_none());
7215 }
7216
7217 #[test]
7218 fn test_body_fraction_doji_is_zero() {
7219 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
7221 let f = OhlcvBar::body_fraction(&[b]).unwrap();
7222 assert!(f.abs() < 1e-9, "doji → body_fraction=0, got {}", f);
7223 }
7224
7225 #[test]
7226 fn test_bullish_ratio_none_for_empty() {
7227 assert!(OhlcvBar::bullish_ratio(&[]).is_none());
7228 }
7229
7230 #[test]
7231 fn test_bullish_ratio_all_bullish() {
7232 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(95), dec!(108));
7233 let b2 = make_ohlcv_bar(dec!(105), dec!(115), dec!(100), dec!(112));
7234 let r = OhlcvBar::bullish_ratio(&[b1, b2]).unwrap();
7235 assert!((r - 1.0).abs() < 1e-9, "all bullish → ratio=1.0, got {}", r);
7236 }
7237
7238 #[test]
7239 fn test_peak_trough_close_none_for_empty() {
7240 assert!(OhlcvBar::peak_close(&[]).is_none());
7241 assert!(OhlcvBar::trough_close(&[]).is_none());
7242 }
7243
7244 #[test]
7245 fn test_peak_trough_close_correct() {
7246 let bars = vec![
7247 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)),
7248 make_ohlcv_bar(dec!(105), dec!(120), dec!(100), dec!(115)),
7249 make_ohlcv_bar(dec!(110), dec!(115), dec!(95), dec!(98)),
7250 ];
7251 assert_eq!(OhlcvBar::peak_close(&bars).unwrap(), dec!(115));
7252 assert_eq!(OhlcvBar::trough_close(&bars).unwrap(), dec!(98));
7253 }
7254
7255 #[test]
7260 fn test_close_to_range_position_none_for_empty() {
7261 assert!(OhlcvBar::close_to_range_position(&[]).is_none());
7262 }
7263
7264 #[test]
7265 fn test_close_to_range_position_one_when_close_at_high() {
7266 let bar = make_ohlcv_bar(dec!(100), dec!(120), dec!(80), dec!(120));
7267 let r = OhlcvBar::close_to_range_position(&[bar]).unwrap();
7268 assert!((r - 1.0).abs() < 1e-9, "close at high → position=1, got {}", r);
7269 }
7270
7271 #[test]
7272 fn test_close_to_range_position_zero_when_close_at_low() {
7273 let bar = make_ohlcv_bar(dec!(100), dec!(120), dec!(80), dec!(80));
7274 let r = OhlcvBar::close_to_range_position(&[bar]).unwrap();
7275 assert!(r.abs() < 1e-9, "close at low → position=0, got {}", r);
7276 }
7277
7278 #[test]
7281 fn test_volume_oscillator_none_for_insufficient_bars() {
7282 let bars = vec![make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105))];
7283 assert!(OhlcvBar::volume_oscillator(&bars, 1, 3).is_none());
7284 }
7285
7286 #[test]
7287 fn test_volume_oscillator_none_when_short_ge_long() {
7288 let bars: Vec<_> = (0..5)
7289 .map(|_| make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)))
7290 .collect();
7291 assert!(OhlcvBar::volume_oscillator(&bars, 3, 2).is_none());
7292 }
7293
7294 #[test]
7295 fn test_volume_oscillator_zero_for_constant_volume() {
7296 let bars: Vec<_> = (0..5)
7297 .map(|_| make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)))
7298 .collect();
7299 let v = OhlcvBar::volume_oscillator(&bars, 2, 4).unwrap();
7300 assert!(v.abs() < 1e-9, "constant volume → oscillator=0, got {}", v);
7301 }
7302
7303 #[test]
7306 fn test_direction_reversal_count_zero_for_single_bar() {
7307 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
7308 assert_eq!(OhlcvBar::direction_reversal_count(&[bar]), 0);
7309 }
7310
7311 #[test]
7312 fn test_direction_reversal_count_zero_for_all_bullish() {
7313 let bars = vec![
7314 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)),
7315 make_ohlcv_bar(dec!(105), dec!(115), dec!(95), dec!(112)),
7316 ];
7317 assert_eq!(OhlcvBar::direction_reversal_count(&bars), 0);
7318 }
7319
7320 #[test]
7321 fn test_direction_reversal_count_two_for_alternating() {
7322 let bull = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(108));
7323 let bear = make_ohlcv_bar(dec!(108), dec!(112), dec!(95), dec!(102));
7324 let bull2 = make_ohlcv_bar(dec!(102), dec!(115), dec!(98), dec!(110));
7325 let bear2 = make_ohlcv_bar(dec!(110), dec!(115), dec!(100), dec!(104));
7326 assert_eq!(OhlcvBar::direction_reversal_count(&[bull, bear, bull2, bear2]), 3);
7327 }
7328
7329 #[test]
7332 fn test_upper_wick_dominance_fraction_none_for_empty() {
7333 assert!(OhlcvBar::upper_wick_dominance_fraction(&[]).is_none());
7334 }
7335
7336 #[test]
7337 fn test_upper_wick_dominance_fraction_one_when_all_upper() {
7338 let bar = make_ohlcv_bar(dec!(100), dec!(130), dec!(99), dec!(101));
7340 let r = OhlcvBar::upper_wick_dominance_fraction(&[bar]).unwrap();
7341 assert!((r - 1.0).abs() < 1e-9, "all upper dominant → 1.0, got {}", r);
7342 }
7343
7344 #[test]
7347 fn test_avg_open_to_high_ratio_none_for_empty() {
7348 assert!(OhlcvBar::avg_open_to_high_ratio(&[]).is_none());
7349 }
7350
7351 #[test]
7352 fn test_avg_open_to_high_ratio_one_when_open_at_low() {
7353 let bar = make_ohlcv_bar(dec!(80), dec!(120), dec!(80), dec!(100));
7355 let r = OhlcvBar::avg_open_to_high_ratio(&[bar]).unwrap();
7356 assert!((r - 1.0).abs() < 1e-9, "open at low → ratio=1, got {}", r);
7357 }
7358
7359 #[test]
7362 fn test_volume_weighted_range_none_for_empty() {
7363 assert!(OhlcvBar::volume_weighted_range(&[]).is_none());
7364 }
7365
7366 #[test]
7367 fn test_volume_weighted_range_positive() {
7368 let b1 = make_ohlcv_bar(dec!(100), dec!(120), dec!(80), dec!(110));
7369 let b2 = make_ohlcv_bar(dec!(110), dec!(130), dec!(100), dec!(120));
7370 let r = OhlcvBar::volume_weighted_range(&[b1, b2]).unwrap();
7371 assert!(r > 0.0, "should be positive, got {}", r);
7372 }
7373
7374 #[test]
7377 fn test_bar_strength_index_none_for_empty() {
7378 assert!(OhlcvBar::bar_strength_index(&[]).is_none());
7379 }
7380
7381 #[test]
7382 fn test_bar_strength_index_positive_when_closes_near_high() {
7383 let bar = make_ohlcv_bar(dec!(100), dec!(120), dec!(80), dec!(120));
7384 let s = OhlcvBar::bar_strength_index(&[bar]).unwrap();
7385 assert!(s > 0.0, "close at high → positive strength, got {}", s);
7386 }
7387
7388 #[test]
7391 fn test_shadow_to_body_ratio_none_for_empty() {
7392 assert!(OhlcvBar::shadow_to_body_ratio(&[]).is_none());
7393 }
7394
7395 #[test]
7396 fn test_shadow_to_body_ratio_zero_for_marubozu() {
7397 let bar = make_ohlcv_bar(dec!(80), dec!(120), dec!(80), dec!(120));
7399 let r = OhlcvBar::shadow_to_body_ratio(&[bar]).unwrap();
7400 assert!(r.abs() < 1e-9, "marubozu → ratio=0, got {}", r);
7401 }
7402
7403 #[test]
7406 fn test_first_last_close_pct_none_for_empty() {
7407 assert!(OhlcvBar::first_last_close_pct(&[]).is_none());
7408 }
7409
7410 #[test]
7411 fn test_first_last_close_pct_zero_for_same_close() {
7412 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
7413 let r = OhlcvBar::first_last_close_pct(&[bar]).unwrap();
7414 assert!(r.abs() < 1e-9, "same open/close → pct=0, got {}", r);
7415 }
7416
7417 #[test]
7418 fn test_first_last_close_pct_positive_for_rise() {
7419 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
7420 let b2 = make_ohlcv_bar(dec!(100), dec!(120), dec!(95), dec!(110));
7421 let r = OhlcvBar::first_last_close_pct(&[b1, b2]).unwrap();
7422 assert!(r > 0.0, "price rose → positive pct, got {}", r);
7423 }
7424
7425 #[test]
7428 fn test_open_to_close_volatility_none_for_single_bar() {
7429 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
7430 assert!(OhlcvBar::open_to_close_volatility(&[bar]).is_none());
7431 }
7432
7433 #[test]
7434 fn test_open_to_close_volatility_zero_for_identical_bars() {
7435 let bars = vec![
7436 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)),
7437 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)),
7438 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)),
7439 ];
7440 let v = OhlcvBar::open_to_close_volatility(&bars).unwrap();
7441 assert!(v.abs() < 1e-9, "identical bars → volatility=0, got {}", v);
7442 }
7443
7444 #[test]
7447 fn test_close_recovery_ratio_none_for_empty() {
7448 assert!(OhlcvBar::close_recovery_ratio(&[]).is_none());
7449 }
7450
7451 #[test]
7452 fn test_close_recovery_ratio_one_for_close_at_high() {
7453 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(110));
7455 let r = OhlcvBar::close_recovery_ratio(&[b]).unwrap();
7456 assert!((r - 1.0).abs() < 1e-9, "close at high → ratio=1, got {}", r);
7457 }
7458
7459 #[test]
7460 fn test_median_range_none_for_empty() {
7461 assert!(OhlcvBar::median_range(&[]).is_none());
7462 }
7463
7464 #[test]
7465 fn test_median_range_correct_odd() {
7466 let bars = vec![
7467 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)), make_ohlcv_bar(dec!(100), dec!(115), dec!(90), dec!(105)), make_ohlcv_bar(dec!(100), dec!(120), dec!(90), dec!(105)), ];
7471 assert_eq!(OhlcvBar::median_range(&bars).unwrap(), dec!(25));
7472 }
7473
7474 #[test]
7475 fn test_mean_typical_price_none_for_empty() {
7476 assert!(OhlcvBar::mean_typical_price(&[]).is_none());
7477 }
7478
7479 #[test]
7480 fn test_mean_typical_price_correct() {
7481 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
7483 let tp = OhlcvBar::mean_typical_price(&[b]).unwrap();
7484 assert_eq!(tp, b.typical_price());
7485 }
7486
7487 #[test]
7488 fn test_directional_volume_ratio_none_for_empty() {
7489 assert!(OhlcvBar::directional_volume_ratio(&[]).is_none());
7490 }
7491
7492 #[test]
7493 fn test_directional_volume_ratio_one_for_all_bullish() {
7494 let mut b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(108)); b1.volume = dec!(50);
7495 let mut b2 = make_ohlcv_bar(dec!(105), dec!(115), dec!(100), dec!(112)); b2.volume = dec!(50);
7496 let r = OhlcvBar::directional_volume_ratio(&[b1, b2]).unwrap();
7497 assert!((r - 1.0).abs() < 1e-9, "all bullish → ratio=1, got {}", r);
7498 }
7499
7500 #[test]
7501 fn test_inside_bar_fraction_none_for_single_bar() {
7502 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
7503 assert!(OhlcvBar::inside_bar_fraction(&[b]).is_none());
7504 }
7505
7506 #[test]
7507 fn test_body_momentum_empty_is_zero() {
7508 assert_eq!(OhlcvBar::body_momentum(&[]), Decimal::ZERO);
7509 }
7510
7511 #[test]
7512 fn test_body_momentum_bullish_positive() {
7513 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(108));
7514 let m = OhlcvBar::body_momentum(&[b]);
7515 assert!(m > Decimal::ZERO, "bullish bar → positive body momentum");
7516 }
7517
7518 #[test]
7519 fn test_avg_trade_count_none_for_empty() {
7520 assert!(OhlcvBar::avg_trade_count(&[]).is_none());
7521 }
7522
7523 #[test]
7524 fn test_max_trade_count_none_for_empty() {
7525 assert!(OhlcvBar::max_trade_count(&[]).is_none());
7526 }
7527
7528 #[test]
7529 fn test_max_trade_count_returns_max() {
7530 let mut b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)); b1.trade_count = 5;
7531 let mut b2 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)); b2.trade_count = 10;
7532 assert_eq!(OhlcvBar::max_trade_count(&[b1, b2]).unwrap(), 10);
7533 }
7534}