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 pub fn close_to_high_std(bars: &[OhlcvBar]) -> Option<f64> {
2641 use rust_decimal::prelude::ToPrimitive;
2642 if bars.len() < 2 {
2643 return None;
2644 }
2645 let vals: Vec<f64> = bars.iter().filter_map(|b| (b.high - b.close).to_f64()).collect();
2646 if vals.len() < 2 {
2647 return None;
2648 }
2649 let n = vals.len() as f64;
2650 let mean = vals.iter().sum::<f64>() / n;
2651 let variance = vals.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / (n - 1.0);
2652 Some(variance.sqrt())
2653 }
2654
2655 pub fn avg_open_volume_ratio(bars: &[OhlcvBar]) -> Option<f64> {
2661 use rust_decimal::prelude::ToPrimitive;
2662 let vals: Vec<f64> = bars
2663 .iter()
2664 .filter_map(|b| {
2665 if b.open.is_zero() {
2666 return None;
2667 }
2668 (b.volume / b.open).to_f64()
2669 })
2670 .collect();
2671 if vals.is_empty() {
2672 return None;
2673 }
2674 Some(vals.iter().sum::<f64>() / vals.len() as f64)
2675 }
2676
2677 pub fn typical_price_std(bars: &[OhlcvBar]) -> Option<f64> {
2681 use rust_decimal::prelude::ToPrimitive;
2682 if bars.len() < 2 {
2683 return None;
2684 }
2685 let vals: Vec<f64> = bars.iter().filter_map(|b| b.typical_price().to_f64()).collect();
2686 if vals.len() < 2 {
2687 return None;
2688 }
2689 let n = vals.len() as f64;
2690 let mean = vals.iter().sum::<f64>() / n;
2691 let variance = vals.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / (n - 1.0);
2692 Some(variance.sqrt())
2693 }
2694
2695 pub fn vwap_deviation_avg(bars: &[OhlcvBar]) -> Option<f64> {
2701 use rust_decimal::prelude::ToPrimitive;
2702 let vwaps: Vec<f64> = bars
2703 .iter()
2704 .filter_map(|b| b.vwap?.to_f64())
2705 .collect();
2706 if vwaps.is_empty() {
2707 return None;
2708 }
2709 let mean = vwaps.iter().sum::<f64>() / vwaps.len() as f64;
2710 let mad = vwaps.iter().map(|v| (v - mean).abs()).sum::<f64>() / vwaps.len() as f64;
2711 Some(mad)
2712 }
2713
2714 pub fn avg_high_low_ratio(bars: &[OhlcvBar]) -> Option<f64> {
2719 use rust_decimal::prelude::ToPrimitive;
2720 let vals: Vec<f64> = bars
2721 .iter()
2722 .filter_map(|b| {
2723 if b.low.is_zero() {
2724 return None;
2725 }
2726 (b.high / b.low).to_f64()
2727 })
2728 .collect();
2729 if vals.is_empty() {
2730 return None;
2731 }
2732 Some(vals.iter().sum::<f64>() / vals.len() as f64)
2733 }
2734
2735 pub fn gap_fill_fraction(bars: &[OhlcvBar]) -> Option<f64> {
2739 if bars.is_empty() {
2740 return None;
2741 }
2742 let gap_bars = bars.iter().filter(|b| b.is_gap_fill).count();
2743 Some(gap_bars as f64 / bars.len() as f64)
2744 }
2745
2746 pub fn complete_bar_count(bars: &[OhlcvBar]) -> usize {
2748 bars.iter().filter(|b| b.is_complete).count()
2749 }
2750
2751 pub fn min_trade_count(bars: &[OhlcvBar]) -> Option<u64> {
2755 bars.iter().map(|b| b.trade_count).min()
2756 }
2757
2758 pub fn avg_bar_range(bars: &[OhlcvBar]) -> Option<Decimal> {
2762 if bars.is_empty() {
2763 return None;
2764 }
2765 let sum: Decimal = bars.iter().map(|b| b.high - b.low).sum();
2766 Some(sum / Decimal::from(bars.len()))
2767 }
2768
2769 pub fn max_up_move(bars: &[OhlcvBar]) -> Option<Decimal> {
2771 bars.iter().map(|b| (b.close - b.open).max(Decimal::ZERO)).max()
2772 }
2773
2774 pub fn max_down_move(bars: &[OhlcvBar]) -> Option<Decimal> {
2776 bars.iter().map(|b| (b.open - b.close).max(Decimal::ZERO)).max()
2777 }
2778
2779 pub fn avg_close_position(bars: &[OhlcvBar]) -> Option<f64> {
2781 use rust_decimal::prelude::ToPrimitive;
2782 let vals: Vec<f64> = bars
2783 .iter()
2784 .filter_map(|b| {
2785 let range = b.high - b.low;
2786 if range.is_zero() {
2787 return None;
2788 }
2789 let pos = (b.close - b.low).to_f64()? / range.to_f64()?;
2790 Some(pos)
2791 })
2792 .collect();
2793 if vals.is_empty() {
2794 return None;
2795 }
2796 Some(vals.iter().sum::<f64>() / vals.len() as f64)
2797 }
2798
2799 pub fn volume_std(bars: &[OhlcvBar]) -> Option<f64> {
2801 use rust_decimal::prelude::ToPrimitive;
2802 if bars.len() < 2 {
2803 return None;
2804 }
2805 let vols: Vec<f64> = bars.iter().filter_map(|b| b.volume.to_f64()).collect();
2806 let n = vols.len() as f64;
2807 if n < 2.0 {
2808 return None;
2809 }
2810 let mean = vols.iter().sum::<f64>() / n;
2811 let var = vols.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / (n - 1.0);
2812 Some(var.sqrt())
2813 }
2814
2815 pub fn avg_wick_ratio(bars: &[OhlcvBar]) -> Option<f64> {
2817 use rust_decimal::prelude::ToPrimitive;
2818 let vals: Vec<f64> = bars
2819 .iter()
2820 .filter_map(|b| {
2821 let range = b.high - b.low;
2822 if range.is_zero() {
2823 return None;
2824 }
2825 let upper = b.high - b.close.max(b.open);
2826 let lower = b.close.min(b.open) - b.low;
2827 let wick = upper + lower;
2828 let ratio = wick.to_f64()? / range.to_f64()?;
2829 Some(ratio)
2830 })
2831 .collect();
2832 if vals.is_empty() {
2833 return None;
2834 }
2835 Some(vals.iter().sum::<f64>() / vals.len() as f64)
2836 }
2837
2838 pub fn open_gap_mean(bars: &[OhlcvBar]) -> Option<f64> {
2840 use rust_decimal::prelude::ToPrimitive;
2841 if bars.len() < 2 {
2842 return None;
2843 }
2844 let vals: Vec<f64> = bars
2845 .windows(2)
2846 .filter_map(|w| {
2847 let prev_close = w[0].close;
2848 if prev_close.is_zero() {
2849 return None;
2850 }
2851 let gap = (w[1].open - prev_close).abs().to_f64()? / prev_close.to_f64()?;
2852 Some(gap)
2853 })
2854 .collect();
2855 if vals.is_empty() {
2856 return None;
2857 }
2858 Some(vals.iter().sum::<f64>() / vals.len() as f64)
2859 }
2860
2861 pub fn net_directional_move(bars: &[OhlcvBar]) -> Option<f64> {
2863 use rust_decimal::prelude::ToPrimitive;
2864 if bars.is_empty() {
2865 return None;
2866 }
2867 let first_open = bars.first()?.open;
2868 let last_close = bars.last()?.close;
2869 if first_open.is_zero() {
2870 return None;
2871 }
2872 let pct = (last_close - first_open).to_f64()? / first_open.to_f64()?;
2873 Some(pct)
2874 }
2875
2876 pub fn close_above_median_fraction(bars: &[OhlcvBar]) -> Option<f64> {
2884 if bars.is_empty() {
2885 return None;
2886 }
2887 let above = bars.iter().filter(|b| b.close > b.high_low_midpoint()).count();
2888 Some(above as f64 / bars.len() as f64)
2889 }
2890
2891 pub fn avg_range_to_open(bars: &[OhlcvBar]) -> Option<f64> {
2896 use rust_decimal::prelude::ToPrimitive;
2897 let vals: Vec<f64> = bars
2898 .iter()
2899 .filter_map(|b| {
2900 if b.open.is_zero() { return None; }
2901 ((b.high - b.low) / b.open).to_f64()
2902 })
2903 .collect();
2904 if vals.is_empty() {
2905 return None;
2906 }
2907 Some(vals.iter().sum::<f64>() / vals.len() as f64)
2908 }
2909
2910 pub fn close_sum(bars: &[OhlcvBar]) -> Decimal {
2915 bars.iter().map(|b| b.close).sum()
2916 }
2917
2918 pub fn above_avg_volume_count(bars: &[OhlcvBar]) -> usize {
2922 let avg = Self::mean_volume(bars).unwrap_or(Decimal::ZERO);
2923 if avg.is_zero() {
2924 return 0;
2925 }
2926 bars.iter().filter(|b| b.volume > avg).count()
2927 }
2928
2929 pub fn median_close(bars: &[OhlcvBar]) -> Option<Decimal> {
2933 if bars.is_empty() {
2934 return None;
2935 }
2936 let mut closes: Vec<Decimal> = bars.iter().map(|b| b.close).collect();
2937 closes.sort();
2938 let n = closes.len();
2939 if n % 2 == 1 {
2940 Some(closes[n / 2])
2941 } else {
2942 Some((closes[n / 2 - 1] + closes[n / 2]) / Decimal::from(2u64))
2943 }
2944 }
2945
2946 pub fn flat_bar_fraction(bars: &[OhlcvBar]) -> Option<f64> {
2950 if bars.is_empty() {
2951 return None;
2952 }
2953 let flat = bars.iter().filter(|b| b.open == b.close).count();
2954 Some(flat as f64 / bars.len() as f64)
2955 }
2956
2957 pub fn avg_body_to_range(bars: &[OhlcvBar]) -> Option<f64> {
2962 use rust_decimal::prelude::ToPrimitive;
2963 let vals: Vec<f64> = bars
2964 .iter()
2965 .filter_map(|b| {
2966 let r = b.range();
2967 if r.is_zero() { return None; }
2968 (b.body() / r).to_f64()
2969 })
2970 .collect();
2971 if vals.is_empty() {
2972 return None;
2973 }
2974 Some(vals.iter().sum::<f64>() / vals.len() as f64)
2975 }
2976
2977 pub fn max_open_gap(bars: &[OhlcvBar]) -> Option<Decimal> {
2981 if bars.len() < 2 {
2982 return None;
2983 }
2984 bars.windows(2)
2985 .map(|w| (w[1].open - w[0].close).abs())
2986 .max()
2987 }
2988
2989 pub fn volume_trend_slope(bars: &[OhlcvBar]) -> Option<f64> {
2994 use rust_decimal::prelude::ToPrimitive;
2995 let n = bars.len();
2996 if n < 2 {
2997 return None;
2998 }
2999 let n_f = n as f64;
3000 let x_mean = (n_f - 1.0) / 2.0;
3001 let y: Vec<f64> = bars.iter().filter_map(|b| b.volume.to_f64()).collect();
3002 if y.len() < 2 {
3003 return None;
3004 }
3005 let y_mean = y.iter().sum::<f64>() / y.len() as f64;
3006 let num: f64 = y.iter().enumerate().map(|(i, &v)| (i as f64 - x_mean) * (v - y_mean)).sum();
3007 let den: f64 = (0..n).map(|i| (i as f64 - x_mean).powi(2)).sum();
3008 if den == 0.0 { None } else { Some(num / den) }
3009 }
3010
3011 pub fn up_close_fraction(bars: &[OhlcvBar]) -> Option<f64> {
3015 if bars.len() < 2 {
3016 return None;
3017 }
3018 let up = bars.windows(2).filter(|w| w[1].close > w[0].close).count();
3019 Some(up as f64 / (bars.len() - 1) as f64)
3020 }
3021
3022 pub fn avg_upper_shadow_ratio(bars: &[OhlcvBar]) -> Option<f64> {
3027 use rust_decimal::prelude::ToPrimitive;
3028 let vals: Vec<f64> = bars
3029 .iter()
3030 .filter_map(|b| {
3031 let r = b.range();
3032 if r.is_zero() { return None; }
3033 (b.upper_shadow() / r).to_f64()
3034 })
3035 .collect();
3036 if vals.is_empty() {
3037 return None;
3038 }
3039 Some(vals.iter().sum::<f64>() / vals.len() as f64)
3040 }
3041
3042 pub fn avg_lower_shadow_ratio(bars: &[OhlcvBar]) -> Option<f64> {
3046 use rust_decimal::prelude::ToPrimitive;
3047 let vals: Vec<f64> = bars
3048 .iter()
3049 .filter_map(|b| {
3050 let r = b.range();
3051 if r.is_zero() { return None; }
3052 (b.lower_shadow() / r).to_f64()
3053 })
3054 .collect();
3055 if vals.is_empty() {
3056 return None;
3057 }
3058 Some(vals.iter().sum::<f64>() / vals.len() as f64)
3059 }
3060
3061 pub fn close_to_open_range_ratio(bars: &[OhlcvBar]) -> Option<f64> {
3063 use rust_decimal::prelude::ToPrimitive;
3064 let vals: Vec<f64> = bars
3065 .iter()
3066 .filter_map(|b| {
3067 let r = b.range();
3068 if r.is_zero() { return None; }
3069 ((b.close - b.open) / r).to_f64()
3070 })
3071 .collect();
3072 if vals.is_empty() {
3073 return None;
3074 }
3075 Some(vals.iter().sum::<f64>() / vals.len() as f64)
3076 }
3077
3078 pub fn max_high(bars: &[OhlcvBar]) -> Option<Decimal> {
3080 bars.iter().map(|b| b.high).max()
3081 }
3082
3083 pub fn min_low(bars: &[OhlcvBar]) -> Option<Decimal> {
3085 bars.iter().map(|b| b.low).min()
3086 }
3087
3088 pub fn avg_bar_efficiency(bars: &[OhlcvBar]) -> Option<f64> {
3090 use rust_decimal::prelude::ToPrimitive;
3091 let vals: Vec<f64> = bars
3092 .iter()
3093 .filter_map(|b| {
3094 let r = b.range();
3095 if r.is_zero() { return None; }
3096 ((b.close - b.open).abs() / r).to_f64()
3097 })
3098 .collect();
3099 if vals.is_empty() {
3100 return None;
3101 }
3102 Some(vals.iter().sum::<f64>() / vals.len() as f64)
3103 }
3104
3105 pub fn open_range_fraction(bars: &[OhlcvBar]) -> Option<f64> {
3107 if bars.is_empty() {
3108 return None;
3109 }
3110 let count = bars
3111 .iter()
3112 .filter(|b| {
3113 let mid = (b.high + b.low) / Decimal::from(2);
3114 b.open >= mid
3115 })
3116 .count();
3117 Some(count as f64 / bars.len() as f64)
3118 }
3119
3120 pub fn close_skewness(bars: &[OhlcvBar]) -> Option<f64> {
3122 use rust_decimal::prelude::ToPrimitive;
3123 if bars.len() < 3 {
3124 return None;
3125 }
3126 let vals: Vec<f64> = bars.iter().filter_map(|b| b.close.to_f64()).collect();
3127 let n = vals.len() as f64;
3128 let mean = vals.iter().sum::<f64>() / n;
3129 let var = vals.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / n;
3130 let std = var.sqrt();
3131 if std < 1e-12 {
3132 return None;
3133 }
3134 let skew = vals.iter().map(|v| ((v - mean) / std).powi(3)).sum::<f64>() / n;
3135 Some(skew)
3136 }
3137
3138 pub fn volume_above_median_fraction(bars: &[OhlcvBar]) -> Option<f64> {
3140 if bars.is_empty() {
3141 return None;
3142 }
3143 let mut vols: Vec<Decimal> = bars.iter().map(|b| b.volume).collect();
3144 vols.sort();
3145 let mid = vols.len() / 2;
3146 let median = if vols.len() % 2 == 0 {
3147 (vols[mid - 1] + vols[mid]) / Decimal::from(2)
3148 } else {
3149 vols[mid]
3150 };
3151 let count = bars.iter().filter(|b| b.volume > median).count();
3152 Some(count as f64 / bars.len() as f64)
3153 }
3154
3155 pub fn typical_price_sum(bars: &[OhlcvBar]) -> Decimal {
3157 bars.iter()
3158 .map(|b| (b.high + b.low + b.close) / Decimal::from(3))
3159 .sum()
3160 }
3161
3162 pub fn max_body_size(bars: &[OhlcvBar]) -> Option<Decimal> {
3164 bars.iter().map(|b| (b.close - b.open).abs()).max()
3165 }
3166
3167 pub fn min_body_size(bars: &[OhlcvBar]) -> Option<Decimal> {
3169 bars.iter().map(|b| (b.close - b.open).abs()).min()
3170 }
3171
3172 pub fn avg_lower_wick_to_range(bars: &[OhlcvBar]) -> Option<f64> {
3174 use rust_decimal::prelude::ToPrimitive;
3175 let vals: Vec<f64> = bars
3176 .iter()
3177 .filter_map(|b| {
3178 let range = b.high - b.low;
3179 if range.is_zero() {
3180 return None;
3181 }
3182 let lower_wick = b.open.min(b.close) - b.low;
3183 (lower_wick / range).to_f64()
3184 })
3185 .collect();
3186 if vals.is_empty() {
3187 return None;
3188 }
3189 Some(vals.iter().sum::<f64>() / vals.len() as f64)
3190 }
3191
3192 pub fn total_range(bars: &[OhlcvBar]) -> Decimal {
3196 bars.iter().map(|b| b.high - b.low).sum()
3197 }
3198
3199 pub fn close_at_high_fraction(bars: &[OhlcvBar]) -> Option<f64> {
3201 if bars.is_empty() {
3202 return None;
3203 }
3204 let count = bars.iter().filter(|b| b.close == b.high).count();
3205 Some(count as f64 / bars.len() as f64)
3206 }
3207
3208 pub fn close_at_low_fraction(bars: &[OhlcvBar]) -> Option<f64> {
3210 if bars.is_empty() {
3211 return None;
3212 }
3213 let count = bars.iter().filter(|b| b.close == b.low).count();
3214 Some(count as f64 / bars.len() as f64)
3215 }
3216
3217 pub fn avg_high_above_open_ratio(bars: &[OhlcvBar]) -> Option<f64> {
3219 use rust_decimal::prelude::ToPrimitive;
3220 let vals: Vec<f64> = bars
3221 .iter()
3222 .filter_map(|b| {
3223 let range = b.high - b.low;
3224 if range.is_zero() {
3225 return None;
3226 }
3227 ((b.high - b.open) / range).to_f64()
3228 })
3229 .collect();
3230 if vals.is_empty() {
3231 return None;
3232 }
3233 Some(vals.iter().sum::<f64>() / vals.len() as f64)
3234 }
3235
3236 pub fn continuation_bar_count(bars: &[OhlcvBar]) -> usize {
3238 if bars.len() < 2 {
3239 return 0;
3240 }
3241 bars.windows(2)
3242 .filter(|w| w[1].open == w[0].close)
3243 .count()
3244 }
3245
3246 pub fn down_close_volume(bars: &[OhlcvBar]) -> Decimal {
3248 bars.iter()
3249 .filter(|b| b.close < b.open)
3250 .map(|b| b.volume)
3251 .sum()
3252 }
3253
3254 pub fn up_close_volume(bars: &[OhlcvBar]) -> Decimal {
3256 bars.iter()
3257 .filter(|b| b.close > b.open)
3258 .map(|b| b.volume)
3259 .sum()
3260 }
3261
3262 pub fn mean_open(bars: &[OhlcvBar]) -> Option<Decimal> {
3266 if bars.is_empty() {
3267 return None;
3268 }
3269 let sum: Decimal = bars.iter().map(|b| b.open).sum();
3270 Some(sum / Decimal::from(bars.len() as i64))
3271 }
3272
3273 pub fn new_high_count(bars: &[OhlcvBar]) -> usize {
3275 if bars.is_empty() {
3276 return 0;
3277 }
3278 let mut running_max = bars[0].high;
3279 let mut count = 0usize;
3280 for b in bars.iter().skip(1) {
3281 if b.high > running_max {
3282 count += 1;
3283 running_max = b.high;
3284 }
3285 }
3286 count
3287 }
3288
3289 pub fn new_low_count(bars: &[OhlcvBar]) -> usize {
3291 if bars.is_empty() {
3292 return 0;
3293 }
3294 let mut running_min = bars[0].low;
3295 let mut count = 0usize;
3296 for b in bars.iter().skip(1) {
3297 if b.low < running_min {
3298 count += 1;
3299 running_min = b.low;
3300 }
3301 }
3302 count
3303 }
3304
3305 pub fn close_std(bars: &[OhlcvBar]) -> Option<f64> {
3307 use rust_decimal::prelude::ToPrimitive;
3308 if bars.len() < 2 {
3309 return None;
3310 }
3311 let vals: Vec<f64> = bars.iter().filter_map(|b| b.close.to_f64()).collect();
3312 let n = vals.len() as f64;
3313 let mean = vals.iter().sum::<f64>() / n;
3314 let var = vals.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / (n - 1.0);
3315 Some(var.sqrt())
3316 }
3317
3318 pub fn zero_volume_fraction(bars: &[OhlcvBar]) -> Option<f64> {
3320 if bars.is_empty() {
3321 return None;
3322 }
3323 let count = bars.iter().filter(|b| b.volume.is_zero()).count();
3324 Some(count as f64 / bars.len() as f64)
3325 }
3326
3327 pub fn avg_open_to_close(bars: &[OhlcvBar]) -> Option<Decimal> {
3331 if bars.is_empty() {
3332 return None;
3333 }
3334 let sum: Decimal = bars.iter().map(|b| b.close - b.open).sum();
3335 Some(sum / Decimal::from(bars.len() as i64))
3336 }
3337
3338 pub fn max_bar_volume(bars: &[OhlcvBar]) -> Option<Decimal> {
3340 bars.iter().map(|b| b.volume).max()
3341 }
3342
3343 pub fn min_bar_volume(bars: &[OhlcvBar]) -> Option<Decimal> {
3345 bars.iter().map(|b| b.volume).min()
3346 }
3347
3348 pub fn body_to_range_std(bars: &[OhlcvBar]) -> Option<f64> {
3352 use rust_decimal::prelude::ToPrimitive;
3353 let ratios: Vec<f64> = bars
3354 .iter()
3355 .filter(|b| b.high > b.low)
3356 .filter_map(|b| {
3357 let body = (b.close - b.open).abs();
3358 let range = b.high - b.low;
3359 (body / range).to_f64()
3360 })
3361 .collect();
3362 if ratios.len() < 2 {
3363 return None;
3364 }
3365 let n = ratios.len() as f64;
3366 let mean = ratios.iter().sum::<f64>() / n;
3367 let var = ratios.iter().map(|r| (r - mean).powi(2)).sum::<f64>() / (n - 1.0);
3368 Some(var.sqrt())
3369 }
3370
3371 pub fn avg_wick_symmetry(bars: &[OhlcvBar]) -> Option<f64> {
3375 use rust_decimal::prelude::ToPrimitive;
3376 let ratios: Vec<f64> = bars
3377 .iter()
3378 .filter_map(|b| {
3379 let upper = b.high - b.close.max(b.open);
3380 let lower = b.close.min(b.open) - b.low;
3381 if upper.is_zero() && lower.is_zero() {
3382 return None;
3383 }
3384 let lo = upper.min(lower);
3385 let hi = upper.max(lower);
3386 if hi.is_zero() {
3387 return None;
3388 }
3389 (lo / hi).to_f64()
3390 })
3391 .collect();
3392 if ratios.is_empty() {
3393 return None;
3394 }
3395 Some(ratios.iter().sum::<f64>() / ratios.len() as f64)
3396 }
3397
3398 pub fn avg_range_pct_of_open(bars: &[OhlcvBar]) -> Option<f64> {
3404 use rust_decimal::prelude::ToPrimitive;
3405 let vals: Vec<f64> = bars
3406 .iter()
3407 .filter_map(|b| {
3408 if b.open.is_zero() { return None; }
3409 ((b.high - b.low) / b.open).to_f64()
3410 })
3411 .collect();
3412 if vals.is_empty() {
3413 return None;
3414 }
3415 Some(vals.iter().sum::<f64>() / vals.len() as f64)
3416 }
3417
3418 pub fn high_volume_fraction(bars: &[OhlcvBar]) -> Option<f64> {
3422 if bars.is_empty() {
3423 return None;
3424 }
3425 let max_vol = bars.iter().map(|b| b.volume).max()?;
3426 let min_vol = bars.iter().map(|b| b.volume).min()?;
3427 let mid = (max_vol + min_vol) / Decimal::from(2);
3428 if max_vol == min_vol {
3429 return None;
3430 }
3431 let count = bars.iter().filter(|b| b.volume > mid).count();
3432 Some(count as f64 / bars.len() as f64)
3433 }
3434
3435 pub fn close_cluster_count(bars: &[OhlcvBar]) -> usize {
3439 use rust_decimal::prelude::ToPrimitive;
3440 if bars.len() < 2 {
3441 return 0;
3442 }
3443 bars.windows(2)
3444 .filter(|w| {
3445 if w[0].close.is_zero() {
3446 return false;
3447 }
3448 let pct_diff = ((w[1].close - w[0].close) / w[0].close).abs();
3449 pct_diff <= rust_decimal::Decimal::new(1, 2)
3450 })
3451 .count()
3452 }
3453
3454 pub fn mean_vwap(bars: &[OhlcvBar]) -> Option<Decimal> {
3458 let vals: Vec<Decimal> = bars.iter().filter_map(|b| b.vwap).collect();
3459 if vals.is_empty() {
3460 return None;
3461 }
3462 let sum: Decimal = vals.iter().copied().sum();
3463 Some(sum / Decimal::from(vals.len() as i64))
3464 }
3465
3466 pub fn complete_fraction(bars: &[OhlcvBar]) -> Option<f64> {
3470 if bars.is_empty() {
3471 return None;
3472 }
3473 let count = bars.iter().filter(|b| b.is_complete).count();
3474 Some(count as f64 / bars.len() as f64)
3475 }
3476
3477 pub fn total_body_movement(bars: &[OhlcvBar]) -> Decimal {
3479 bars.iter().map(|b| (b.close - b.open).abs()).sum()
3480 }
3481
3482 pub fn open_std(bars: &[OhlcvBar]) -> Option<f64> {
3484 use rust_decimal::prelude::ToPrimitive;
3485 if bars.len() < 2 {
3486 return None;
3487 }
3488 let vals: Vec<f64> = bars.iter().filter_map(|b| b.open.to_f64()).collect();
3489 let n = vals.len() as f64;
3490 let mean = vals.iter().sum::<f64>() / n;
3491 let var = vals.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / (n - 1.0);
3492 Some(var.sqrt())
3493 }
3494
3495 pub fn mean_high_low_ratio(bars: &[OhlcvBar]) -> Option<f64> {
3497 use rust_decimal::prelude::ToPrimitive;
3498 let vals: Vec<f64> = bars
3499 .iter()
3500 .filter(|b| !b.low.is_zero())
3501 .filter_map(|b| (b.high / b.low).to_f64())
3502 .collect();
3503 if vals.is_empty() {
3504 return None;
3505 }
3506 Some(vals.iter().sum::<f64>() / vals.len() as f64)
3507 }
3508
3509 pub fn max_consecutive_up_bars(bars: &[OhlcvBar]) -> usize {
3515 let mut max_run = 0usize;
3516 let mut run = 0usize;
3517 for b in bars {
3518 if b.close > b.open {
3519 run += 1;
3520 if run > max_run {
3521 max_run = run;
3522 }
3523 } else {
3524 run = 0;
3525 }
3526 }
3527 max_run
3528 }
3529
3530 pub fn avg_upper_shadow_fraction(bars: &[OhlcvBar]) -> Option<f64> {
3533 use rust_decimal::prelude::ToPrimitive;
3534 let vals: Vec<f64> = bars
3535 .iter()
3536 .filter(|b| b.high > b.low)
3537 .filter_map(|b| {
3538 let range = b.high - b.low;
3539 let upper = b.high - b.close.max(b.open);
3540 (upper / range).to_f64()
3541 })
3542 .collect();
3543 if vals.is_empty() {
3544 return None;
3545 }
3546 Some(vals.iter().sum::<f64>() / vals.len() as f64)
3547 }
3548
3549 pub fn up_down_bar_ratio(bars: &[OhlcvBar]) -> Option<f64> {
3552 let ups = bars.iter().filter(|b| b.close > b.open).count();
3553 let downs = bars.iter().filter(|b| b.close < b.open).count();
3554 if downs == 0 {
3555 return None;
3556 }
3557 Some(ups as f64 / downs as f64)
3558 }
3559
3560 pub fn close_range_fraction(bars: &[OhlcvBar]) -> Option<f64> {
3566 use rust_decimal::prelude::ToPrimitive;
3567 let half = Decimal::new(5, 1);
3568 let vals: Vec<f64> = bars
3569 .iter()
3570 .filter(|b| b.high > b.low)
3571 .filter_map(|b| {
3572 let r = (b.close - b.low) / (b.high - b.low);
3573 r.to_f64()
3574 })
3575 .collect();
3576 if vals.is_empty() {
3577 return None;
3578 }
3579 let _ = half;
3580 let count = vals.iter().filter(|&&v| v > 0.5).count();
3581 Some(count as f64 / vals.len() as f64)
3582 }
3583
3584 pub fn tail_symmetry(bars: &[OhlcvBar]) -> Option<f64> {
3588 use rust_decimal::prelude::ToPrimitive;
3589 let vals: Vec<f64> = bars
3590 .iter()
3591 .filter(|b| b.high > b.low)
3592 .filter_map(|b| {
3593 let range = (b.high - b.low).to_f64()?;
3594 let upper = (b.high - b.close.max(b.open)).to_f64()?;
3595 let lower = (b.close.min(b.open) - b.low).to_f64()?;
3596 Some(1.0 - (upper - lower).abs() / range)
3597 })
3598 .collect();
3599 if vals.is_empty() {
3600 return None;
3601 }
3602 Some(vals.iter().sum::<f64>() / vals.len() as f64)
3603 }
3604
3605 pub fn bar_trend_strength(bars: &[OhlcvBar]) -> Option<f64> {
3609 if bars.len() < 2 {
3610 return None;
3611 }
3612 let up_count = bars.windows(2).filter(|w| w[1].close > w[0].close).count();
3613 Some(up_count as f64 / (bars.len() - 1) as f64)
3614 }
3615
3616}
3617
3618impl std::fmt::Display for OhlcvBar {
3619 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3620 write!(
3621 f,
3622 "{} {} [{}/{}/{}/{} v={}]",
3623 self.symbol, self.timeframe, self.open, self.high, self.low, self.close, self.volume
3624 )
3625 }
3626}
3627
3628pub struct OhlcvAggregator {
3630 symbol: String,
3631 timeframe: Timeframe,
3632 current_bar: Option<OhlcvBar>,
3633 last_bar: Option<OhlcvBar>,
3635 emit_empty_bars: bool,
3639 bars_emitted: u64,
3641 price_volume_sum: Decimal,
3643 total_volume: Decimal,
3645 peak_volume: Option<Decimal>,
3647 min_volume: Option<Decimal>,
3649}
3650
3651impl OhlcvAggregator {
3652 pub fn new(symbol: impl Into<String>, timeframe: Timeframe) -> Result<Self, StreamError> {
3657 let tf_dur = timeframe.duration_ms();
3658 if tf_dur == 0 {
3659 return Err(StreamError::ConfigError {
3660 reason: "OhlcvAggregator timeframe duration must be > 0".into(),
3661 });
3662 }
3663 Ok(Self {
3664 symbol: symbol.into(),
3665 timeframe,
3666 current_bar: None,
3667 last_bar: None,
3668 emit_empty_bars: false,
3669 bars_emitted: 0,
3670 price_volume_sum: Decimal::ZERO,
3671 total_volume: Decimal::ZERO,
3672 peak_volume: None,
3673 min_volume: None,
3674 })
3675 }
3676
3677 pub fn with_emit_empty_bars(mut self, enabled: bool) -> Self {
3679 self.emit_empty_bars = enabled;
3680 self
3681 }
3682
3683 #[must_use = "completed bars are returned; ignoring them loses bar data"]
3692 #[inline]
3693 pub fn feed(&mut self, tick: &NormalizedTick) -> Result<Vec<OhlcvBar>, StreamError> {
3694 if tick.symbol != self.symbol {
3695 return Err(StreamError::AggregationError {
3696 reason: format!(
3697 "tick symbol '{}' does not match aggregator '{}'",
3698 tick.symbol, self.symbol
3699 ),
3700 });
3701 }
3702
3703 let tick_ts = tick.exchange_ts_ms.unwrap_or(tick.received_at_ms);
3705 let bar_start = self.timeframe.bar_start_ms(tick_ts);
3706 let mut emitted: Vec<OhlcvBar> = Vec::new();
3707
3708 let bar_window_changed = self
3710 .current_bar
3711 .as_ref()
3712 .map_or(false, |b| b.bar_start_ms != bar_start);
3713
3714 if bar_window_changed {
3715 let mut completed = self.current_bar.take().unwrap_or_else(|| unreachable!());
3717 completed.is_complete = true;
3718 let prev_close = completed.close;
3719 let prev_start = completed.bar_start_ms;
3720 emitted.push(completed);
3721
3722 if self.emit_empty_bars {
3724 let dur = self.timeframe.duration_ms();
3725 let mut gap_start = prev_start + dur;
3726 while gap_start < bar_start {
3727 emitted.push(OhlcvBar {
3728 symbol: self.symbol.clone(),
3729 timeframe: self.timeframe,
3730 bar_start_ms: gap_start,
3731 open: prev_close,
3732 high: prev_close,
3733 low: prev_close,
3734 close: prev_close,
3735 volume: Decimal::ZERO,
3736 trade_count: 0,
3737 is_complete: true,
3738 is_gap_fill: true,
3739 vwap: None,
3740 });
3741 gap_start += dur;
3742 }
3743 }
3744 }
3745
3746 let tick_value = tick.value();
3748 if self.current_bar.is_some() {
3749 self.price_volume_sum += tick_value;
3750 } else {
3751 self.price_volume_sum = tick_value;
3752 }
3753
3754 match &mut self.current_bar {
3755 Some(bar) => {
3756 if tick.price > bar.high {
3757 bar.high = tick.price;
3758 }
3759 if tick.price < bar.low {
3760 bar.low = tick.price;
3761 }
3762 bar.close = tick.price;
3763 bar.volume += tick.quantity;
3764 bar.trade_count += 1;
3765 bar.vwap = if bar.volume.is_zero() {
3766 None
3767 } else {
3768 Some(self.price_volume_sum / bar.volume)
3769 };
3770 }
3771 None => {
3772 self.current_bar = Some(OhlcvBar {
3773 symbol: self.symbol.clone(),
3774 timeframe: self.timeframe,
3775 bar_start_ms: bar_start,
3776 open: tick.price,
3777 high: tick.price,
3778 low: tick.price,
3779 close: tick.price,
3780 volume: tick.quantity,
3781 trade_count: 1,
3782 is_complete: false,
3783 is_gap_fill: false,
3784 vwap: Some(tick.price), });
3786 }
3787 }
3788 self.bars_emitted += emitted.len() as u64;
3789 for b in &emitted {
3790 self.total_volume += b.volume;
3791 self.peak_volume = Some(match self.peak_volume {
3792 Some(prev) => prev.max(b.volume),
3793 None => b.volume,
3794 });
3795 self.min_volume = Some(match self.min_volume {
3796 Some(prev) => prev.min(b.volume),
3797 None => b.volume,
3798 });
3799 }
3800 if let Some(b) = emitted.last() {
3801 self.last_bar = Some(b.clone());
3802 }
3803 Ok(emitted)
3804 }
3805
3806 pub fn current_bar(&self) -> Option<&OhlcvBar> {
3808 self.current_bar.as_ref()
3809 }
3810
3811 #[must_use = "the flushed bar is returned; ignoring it loses the partial bar"]
3813 pub fn flush(&mut self) -> Option<OhlcvBar> {
3814 let mut bar = self.current_bar.take()?;
3815 bar.is_complete = true;
3816 self.bars_emitted += 1;
3817 self.total_volume += bar.volume;
3818 self.peak_volume = Some(match self.peak_volume {
3819 Some(prev) => prev.max(bar.volume),
3820 None => bar.volume,
3821 });
3822 self.min_volume = Some(match self.min_volume {
3823 Some(prev) => prev.min(bar.volume),
3824 None => bar.volume,
3825 });
3826 self.last_bar = Some(bar.clone());
3827 Some(bar)
3828 }
3829
3830 pub fn last_bar(&self) -> Option<&OhlcvBar> {
3835 self.last_bar.as_ref()
3836 }
3837
3838 pub fn bar_count(&self) -> u64 {
3840 self.bars_emitted
3841 }
3842
3843 pub fn reset(&mut self) {
3848 self.current_bar = None;
3849 self.last_bar = None;
3850 self.bars_emitted = 0;
3851 self.price_volume_sum = Decimal::ZERO;
3852 self.total_volume = Decimal::ZERO;
3853 self.peak_volume = None;
3854 self.min_volume = None;
3855 }
3856
3857 pub fn total_volume(&self) -> Decimal {
3862 self.total_volume
3863 }
3864
3865 pub fn peak_volume(&self) -> Option<Decimal> {
3870 self.peak_volume
3871 }
3872
3873 pub fn min_volume(&self) -> Option<Decimal> {
3878 self.min_volume
3879 }
3880
3881 pub fn volume_range(&self) -> Option<(Decimal, Decimal)> {
3886 Some((self.min_volume?, self.peak_volume?))
3887 }
3888
3889 pub fn average_volume(&self) -> Option<Decimal> {
3893 if self.bars_emitted == 0 {
3894 return None;
3895 }
3896 Some(self.total_volume / Decimal::from(self.bars_emitted))
3897 }
3898
3899 pub fn symbol(&self) -> &str {
3901 &self.symbol
3902 }
3903
3904 pub fn timeframe(&self) -> Timeframe {
3906 self.timeframe
3907 }
3908
3909 pub fn window_progress(&self, now_ms: u64) -> Option<f64> {
3915 let bar = self.current_bar.as_ref()?;
3916 let elapsed = now_ms.saturating_sub(bar.bar_start_ms);
3917 let duration = self.timeframe.duration_ms();
3918 let progress = elapsed as f64 / duration as f64;
3919 Some(progress.clamp(0.0, 1.0))
3920 }
3921
3922 pub fn is_active(&self) -> bool {
3925 self.current_bar.is_some()
3926 }
3927
3928 pub fn vwap_current(&self) -> Option<Decimal> {
3933 let bar = self.current_bar.as_ref()?;
3934 if bar.volume.is_zero() {
3935 return None;
3936 }
3937 Some(self.price_volume_sum / bar.volume)
3938 }
3939}
3940
3941#[cfg(test)]
3942#[allow(deprecated)]
3943mod tests {
3944 use super::*;
3945 use crate::tick::{Exchange, NormalizedTick, TradeSide};
3946 use rust_decimal_macros::dec;
3947
3948 fn make_tick(symbol: &str, price: Decimal, qty: Decimal, ts_ms: u64) -> NormalizedTick {
3949 NormalizedTick {
3950 exchange: Exchange::Binance,
3951 symbol: symbol.to_string(),
3952 price,
3953 quantity: qty,
3954 side: Some(TradeSide::Buy),
3955 trade_id: None,
3956 exchange_ts_ms: None,
3957 received_at_ms: ts_ms,
3958 }
3959 }
3960
3961 fn make_tick_with_exchange_ts(
3962 symbol: &str,
3963 price: Decimal,
3964 qty: Decimal,
3965 exchange_ts_ms: u64,
3966 received_at_ms: u64,
3967 ) -> NormalizedTick {
3968 NormalizedTick {
3969 exchange: Exchange::Binance,
3970 symbol: symbol.to_string(),
3971 price,
3972 quantity: qty,
3973 side: Some(TradeSide::Buy),
3974 trade_id: None,
3975 exchange_ts_ms: Some(exchange_ts_ms),
3976 received_at_ms,
3977 }
3978 }
3979
3980 fn agg(symbol: &str, tf: Timeframe) -> OhlcvAggregator {
3981 OhlcvAggregator::new(symbol, tf).unwrap()
3982 }
3983
3984 #[test]
3985 fn test_timeframe_seconds_duration_ms() {
3986 assert_eq!(Timeframe::Seconds(30).duration_ms(), 30_000);
3987 }
3988
3989 #[test]
3990 fn test_timeframe_minutes_duration_ms() {
3991 assert_eq!(Timeframe::Minutes(5).duration_ms(), 300_000);
3992 }
3993
3994 #[test]
3995 fn test_timeframe_hours_duration_ms() {
3996 assert_eq!(Timeframe::Hours(1).duration_ms(), 3_600_000);
3997 }
3998
3999 #[test]
4000 fn test_timeframe_bar_start_ms_aligns() {
4001 let tf = Timeframe::Minutes(1);
4002 let ts = 61_500; assert_eq!(tf.bar_start_ms(ts), 60_000);
4004 }
4005
4006 #[test]
4007 fn test_timeframe_display() {
4008 assert_eq!(Timeframe::Seconds(30).to_string(), "30s");
4009 assert_eq!(Timeframe::Minutes(5).to_string(), "5m");
4010 assert_eq!(Timeframe::Hours(4).to_string(), "4h");
4011 }
4012
4013 #[test]
4014 fn test_timeframe_ord_seconds_lt_minutes() {
4015 assert!(Timeframe::Seconds(30) < Timeframe::Minutes(1));
4016 }
4017
4018 #[test]
4019 fn test_timeframe_ord_minutes_lt_hours() {
4020 assert!(Timeframe::Minutes(59) < Timeframe::Hours(1));
4021 }
4022
4023 #[test]
4024 fn test_timeframe_ord_same_duration_equal() {
4025 assert_eq!(Timeframe::Seconds(60), Timeframe::Seconds(60));
4026 assert_eq!(
4027 Timeframe::Seconds(3600).cmp(&Timeframe::Hours(1)),
4028 std::cmp::Ordering::Equal
4029 );
4030 }
4031
4032 #[test]
4033 fn test_timeframe_ord_sort() {
4034 let mut tfs = vec![
4035 Timeframe::Hours(1),
4036 Timeframe::Seconds(30),
4037 Timeframe::Minutes(5),
4038 ];
4039 tfs.sort();
4040 assert_eq!(tfs[0], Timeframe::Seconds(30));
4041 assert_eq!(tfs[1], Timeframe::Minutes(5));
4042 assert_eq!(tfs[2], Timeframe::Hours(1));
4043 }
4044
4045 #[test]
4046 fn test_ohlcv_aggregator_first_tick_sets_ohlcv() {
4047 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4048 let tick = make_tick("BTC-USD", dec!(50000), dec!(1), 60_000);
4049 let result = agg.feed(&tick).unwrap();
4050 assert!(result.is_empty()); let bar = agg.current_bar().unwrap();
4052 assert_eq!(bar.open, dec!(50000));
4053 assert_eq!(bar.high, dec!(50000));
4054 assert_eq!(bar.low, dec!(50000));
4055 assert_eq!(bar.close, dec!(50000));
4056 assert_eq!(bar.volume, dec!(1));
4057 assert_eq!(bar.trade_count, 1);
4058 }
4059
4060 #[test]
4061 fn test_ohlcv_aggregator_high_low_tracking() {
4062 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4063 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1), 60_000))
4064 .unwrap();
4065 agg.feed(&make_tick("BTC-USD", dec!(51000), dec!(1), 60_100))
4066 .unwrap();
4067 agg.feed(&make_tick("BTC-USD", dec!(49500), dec!(1), 60_200))
4068 .unwrap();
4069 let bar = agg.current_bar().unwrap();
4070 assert_eq!(bar.high, dec!(51000));
4071 assert_eq!(bar.low, dec!(49500));
4072 assert_eq!(bar.close, dec!(49500));
4073 assert_eq!(bar.trade_count, 3);
4074 }
4075
4076 #[test]
4077 fn test_ohlcv_aggregator_bar_completes_on_new_window() {
4078 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4079 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1), 60_000))
4080 .unwrap();
4081 agg.feed(&make_tick("BTC-USD", dec!(50100), dec!(2), 60_500))
4082 .unwrap();
4083 let mut bars = agg
4085 .feed(&make_tick("BTC-USD", dec!(50200), dec!(1), 120_000))
4086 .unwrap();
4087 assert_eq!(bars.len(), 1);
4088 let bar = bars.remove(0);
4089 assert!(bar.is_complete);
4090 assert_eq!(bar.open, dec!(50000));
4091 assert_eq!(bar.close, dec!(50100));
4092 assert_eq!(bar.volume, dec!(3));
4093 assert_eq!(bar.bar_start_ms, 60_000);
4094 }
4095
4096 #[test]
4097 fn test_ohlcv_aggregator_new_bar_started_after_completion() {
4098 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4099 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1), 60_000))
4100 .unwrap();
4101 agg.feed(&make_tick("BTC-USD", dec!(50200), dec!(1), 120_000))
4102 .unwrap();
4103 let bar = agg.current_bar().unwrap();
4104 assert_eq!(bar.open, dec!(50200));
4105 assert_eq!(bar.bar_start_ms, 120_000);
4106 }
4107
4108 #[test]
4109 fn test_ohlcv_aggregator_flush_marks_complete() {
4110 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4111 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1), 60_000))
4112 .unwrap();
4113 let flushed = agg.flush().unwrap();
4114 assert!(flushed.is_complete);
4115 assert!(agg.current_bar().is_none());
4116 }
4117
4118 #[test]
4119 fn test_ohlcv_aggregator_flush_empty_returns_none() {
4120 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4121 assert!(agg.flush().is_none());
4122 }
4123
4124 #[test]
4125 fn test_ohlcv_aggregator_wrong_symbol_returns_error() {
4126 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4127 let tick = make_tick("ETH-USD", dec!(3000), dec!(1), 60_000);
4128 let result = agg.feed(&tick);
4129 assert!(matches!(result, Err(StreamError::AggregationError { .. })));
4130 }
4131
4132 #[test]
4133 fn test_ohlcv_aggregator_volume_accumulates() {
4134 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4135 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1.5), 60_000))
4136 .unwrap();
4137 agg.feed(&make_tick("BTC-USD", dec!(50100), dec!(2.5), 60_100))
4138 .unwrap();
4139 let bar = agg.current_bar().unwrap();
4140 assert_eq!(bar.volume, dec!(4));
4141 }
4142
4143 #[test]
4144 fn test_ohlcv_bar_symbol_and_timeframe() {
4145 let mut agg = agg("BTC-USD", Timeframe::Minutes(5));
4146 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1), 300_000))
4147 .unwrap();
4148 let bar = agg.current_bar().unwrap();
4149 assert_eq!(bar.symbol, "BTC-USD");
4150 assert_eq!(bar.timeframe, Timeframe::Minutes(5));
4151 }
4152
4153 #[test]
4154 fn test_ohlcv_aggregator_symbol_accessor() {
4155 let agg = agg("ETH-USD", Timeframe::Hours(1));
4156 assert_eq!(agg.symbol(), "ETH-USD");
4157 assert_eq!(agg.timeframe(), Timeframe::Hours(1));
4158 }
4159
4160 #[test]
4161 fn test_bar_aligned_by_exchange_ts_not_received_ts() {
4162 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4166 let tick = make_tick_with_exchange_ts("BTC-USD", dec!(50000), dec!(1), 60_500, 120_100);
4167 agg.feed(&tick).unwrap();
4168 let bar = agg.current_bar().unwrap();
4169 assert_eq!(bar.bar_start_ms, 60_000, "bar should use exchange_ts_ms");
4170 }
4171
4172 #[test]
4173 fn test_bar_falls_back_to_received_ts_when_no_exchange_ts() {
4174 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4175 let tick = make_tick("BTC-USD", dec!(50000), dec!(1), 75_000);
4176 agg.feed(&tick).unwrap();
4177 let bar = agg.current_bar().unwrap();
4178 assert_eq!(bar.bar_start_ms, 60_000);
4179 }
4180
4181 #[test]
4184 fn test_emit_empty_bars_no_gap_no_empties() {
4185 let mut agg = OhlcvAggregator::new("BTC-USD", Timeframe::Minutes(1))
4187 .unwrap()
4188 .with_emit_empty_bars(true);
4189 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1), 60_000))
4190 .unwrap();
4191 let bars = agg
4192 .feed(&make_tick("BTC-USD", dec!(50100), dec!(1), 120_000))
4193 .unwrap();
4194 assert_eq!(bars.len(), 1);
4196 assert_eq!(bars[0].bar_start_ms, 60_000);
4197 assert_eq!(bars[0].volume, dec!(1));
4198 }
4199
4200 #[test]
4201 fn test_emit_empty_bars_two_skipped_windows() {
4202 let mut agg = OhlcvAggregator::new("BTC-USD", Timeframe::Minutes(1))
4205 .unwrap()
4206 .with_emit_empty_bars(true);
4207 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1), 60_000))
4208 .unwrap();
4209 let bars = agg
4210 .feed(&make_tick("BTC-USD", dec!(51000), dec!(1), 240_000))
4211 .unwrap();
4212 assert_eq!(bars.len(), 3);
4214 assert_eq!(bars[0].bar_start_ms, 60_000);
4215 assert!(!bars[0].volume.is_zero()); assert_eq!(bars[1].bar_start_ms, 120_000);
4217 assert!(bars[1].volume.is_zero()); assert_eq!(bars[1].trade_count, 0);
4219 assert_eq!(bars[1].open, dec!(50000)); assert_eq!(bars[2].bar_start_ms, 180_000);
4221 assert!(bars[2].volume.is_zero()); }
4223
4224 #[test]
4225 fn test_emit_empty_bars_disabled_no_empties_on_gap() {
4226 let mut agg = OhlcvAggregator::new("BTC-USD", Timeframe::Minutes(1))
4227 .unwrap()
4228 .with_emit_empty_bars(false);
4229 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1), 60_000))
4230 .unwrap();
4231 let bars = agg
4232 .feed(&make_tick("BTC-USD", dec!(51000), dec!(1), 240_000))
4233 .unwrap();
4234 assert_eq!(bars.len(), 1); }
4236
4237 #[test]
4238 fn test_emit_empty_bars_is_complete_true() {
4239 let mut agg = OhlcvAggregator::new("BTC-USD", Timeframe::Minutes(1))
4240 .unwrap()
4241 .with_emit_empty_bars(true);
4242 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1), 60_000))
4243 .unwrap();
4244 let bars = agg
4245 .feed(&make_tick("BTC-USD", dec!(51000), dec!(1), 240_000))
4246 .unwrap();
4247 for bar in &bars {
4248 assert!(bar.is_complete, "all emitted bars must be marked complete");
4249 }
4250 }
4251
4252 #[test]
4253 fn test_ohlcv_bar_display() {
4254 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4255 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1), 60_000))
4256 .unwrap();
4257 let bar = agg.current_bar().unwrap();
4258 let s = bar.to_string();
4259 assert!(s.contains("BTC-USD"));
4260 assert!(s.contains("1m"));
4261 assert!(s.contains("50000"));
4262 }
4263
4264 #[test]
4265 fn test_bar_count_increments_on_feed() {
4266 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4267 assert_eq!(agg.bar_count(), 0);
4268 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1), 60_000))
4269 .unwrap();
4270 agg.feed(&make_tick("BTC-USD", dec!(50100), dec!(1), 120_000))
4271 .unwrap();
4272 assert_eq!(agg.bar_count(), 1);
4273 }
4274
4275 #[test]
4276 fn test_bar_count_increments_on_flush() {
4277 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4278 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1), 60_000))
4279 .unwrap();
4280 agg.flush().unwrap();
4281 assert_eq!(agg.bar_count(), 1);
4282 }
4283
4284 #[test]
4285 fn test_ohlcv_bar_range() {
4286 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4287 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1), 60_000))
4288 .unwrap();
4289 agg.feed(&make_tick("BTC-USD", dec!(51000), dec!(1), 60_100))
4290 .unwrap();
4291 agg.feed(&make_tick("BTC-USD", dec!(49500), dec!(1), 60_200))
4292 .unwrap();
4293 let bar = agg.current_bar().unwrap();
4294 assert_eq!(bar.range(), dec!(1500)); }
4296
4297 #[test]
4298 fn test_ohlcv_bar_body_bullish() {
4299 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4300 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1), 60_000))
4301 .unwrap();
4302 agg.feed(&make_tick("BTC-USD", dec!(50500), dec!(1), 60_100))
4303 .unwrap();
4304 let bar = agg.current_bar().unwrap();
4305 assert_eq!(bar.body(), dec!(500));
4307 }
4308
4309 #[test]
4310 fn test_ohlcv_bar_body_bearish() {
4311 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4312 agg.feed(&make_tick("BTC-USD", dec!(50500), dec!(1), 60_000))
4313 .unwrap();
4314 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1), 60_100))
4315 .unwrap();
4316 let bar = agg.current_bar().unwrap();
4317 assert_eq!(bar.body(), dec!(500));
4319 }
4320
4321 #[test]
4322 fn test_aggregator_reset_clears_bar_and_count() {
4323 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4324 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1), 60_000))
4325 .unwrap();
4326 agg.feed(&make_tick("BTC-USD", dec!(50100), dec!(1), 120_000))
4327 .unwrap();
4328 assert_eq!(agg.bar_count(), 1);
4329 assert!(agg.current_bar().is_some());
4330 agg.reset();
4331 assert_eq!(agg.bar_count(), 0);
4332 assert!(agg.current_bar().is_none());
4333 }
4334
4335 #[test]
4336 fn test_ohlcv_bar_is_bullish_when_close_gt_open() {
4337 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4338 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1), 60_000))
4339 .unwrap();
4340 agg.feed(&make_tick("BTC-USD", dec!(51000), dec!(1), 60_100))
4341 .unwrap();
4342 let bar = agg.current_bar().unwrap();
4343 assert!(bar.is_bullish());
4344 assert!(!bar.is_bearish());
4345 }
4346
4347 #[test]
4348 fn test_ohlcv_bar_is_bearish_when_close_lt_open() {
4349 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4350 agg.feed(&make_tick("BTC-USD", dec!(51000), dec!(1), 60_000))
4351 .unwrap();
4352 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1), 60_100))
4353 .unwrap();
4354 let bar = agg.current_bar().unwrap();
4355 assert!(bar.is_bearish());
4356 assert!(!bar.is_bullish());
4357 }
4358
4359 #[test]
4360 fn test_ohlcv_bar_neither_bullish_nor_bearish_on_equal_open_close() {
4361 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4362 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1), 60_000))
4363 .unwrap();
4364 let bar = agg.current_bar().unwrap();
4366 assert!(!bar.is_bullish());
4367 assert!(!bar.is_bearish());
4368 }
4369
4370 #[test]
4371 fn test_ohlcv_bar_vwap_single_tick_equals_price() {
4372 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4373 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(2), 60_000))
4374 .unwrap();
4375 let bar = agg.current_bar().unwrap();
4376 assert_eq!(bar.vwap, Some(dec!(50000)));
4377 }
4378
4379 #[test]
4380 fn test_ohlcv_bar_vwap_two_equal_price_ticks() {
4381 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4382 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1), 60_000))
4383 .unwrap();
4384 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(3), 60_100))
4385 .unwrap();
4386 let bar = agg.current_bar().unwrap();
4387 assert_eq!(bar.vwap, Some(dec!(50000)));
4389 }
4390
4391 #[test]
4392 fn test_ohlcv_bar_vwap_two_different_price_ticks() {
4393 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4394 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1), 60_000))
4395 .unwrap();
4396 agg.feed(&make_tick("BTC-USD", dec!(51000), dec!(1), 60_100))
4397 .unwrap();
4398 let bar = agg.current_bar().unwrap();
4399 assert_eq!(bar.vwap, Some(dec!(50500)));
4401 }
4402
4403 #[test]
4404 fn test_ohlcv_bar_vwap_gap_fill_is_none() {
4405 let mut agg = OhlcvAggregator::new("BTC-USD", Timeframe::Minutes(1))
4406 .unwrap()
4407 .with_emit_empty_bars(true);
4408 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1), 60_000))
4409 .unwrap();
4410 let bars = agg
4411 .feed(&make_tick("BTC-USD", dec!(51000), dec!(1), 240_000))
4412 .unwrap();
4413 assert!(bars[0].vwap.is_some());
4415 assert!(bars[1].vwap.is_none());
4416 assert!(bars[2].vwap.is_none());
4417 }
4418
4419 #[test]
4420 fn test_aggregator_reset_allows_fresh_start() {
4421 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4422 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1), 60_000))
4423 .unwrap();
4424 agg.reset();
4425 agg.feed(&make_tick("BTC-USD", dec!(99999), dec!(2), 60_000))
4426 .unwrap();
4427 let bar = agg.current_bar().unwrap();
4428 assert_eq!(bar.open, dec!(99999));
4429 }
4430
4431 #[test]
4434 fn test_from_duration_ms_hours() {
4435 assert_eq!(Timeframe::from_duration_ms(3_600_000), Some(Timeframe::Hours(1)));
4436 assert_eq!(Timeframe::from_duration_ms(7_200_000), Some(Timeframe::Hours(2)));
4437 }
4438
4439 #[test]
4440 fn test_from_duration_ms_minutes() {
4441 assert_eq!(Timeframe::from_duration_ms(300_000), Some(Timeframe::Minutes(5)));
4442 assert_eq!(Timeframe::from_duration_ms(60_000), Some(Timeframe::Minutes(1)));
4443 }
4444
4445 #[test]
4446 fn test_from_duration_ms_seconds() {
4447 assert_eq!(Timeframe::from_duration_ms(15_000), Some(Timeframe::Seconds(15)));
4448 assert_eq!(Timeframe::from_duration_ms(1_000), Some(Timeframe::Seconds(1)));
4449 }
4450
4451 #[test]
4452 fn test_from_duration_ms_zero_returns_none() {
4453 assert_eq!(Timeframe::from_duration_ms(0), None);
4454 }
4455
4456 #[test]
4457 fn test_from_duration_ms_non_whole_second_returns_none() {
4458 assert_eq!(Timeframe::from_duration_ms(1_500), None);
4459 }
4460
4461 #[test]
4462 fn test_from_duration_ms_roundtrip() {
4463 for tf in [Timeframe::Seconds(30), Timeframe::Minutes(5), Timeframe::Hours(4)] {
4464 assert_eq!(Timeframe::from_duration_ms(tf.duration_ms()), Some(tf));
4465 }
4466 }
4467
4468 #[test]
4471 fn test_is_doji_exact_zero_body() {
4472 let bar = OhlcvBar {
4473 symbol: "X".into(), timeframe: Timeframe::Minutes(1),
4474 bar_start_ms: 0, open: dec!(100), high: dec!(105),
4475 low: dec!(95), close: dec!(100),
4476 volume: dec!(1), trade_count: 1, is_complete: true,
4477 is_gap_fill: false, vwap: None,
4478 };
4479 assert!(bar.is_doji(Decimal::ZERO));
4480 }
4481
4482 #[test]
4483 fn test_is_doji_small_epsilon() {
4484 let bar = OhlcvBar {
4485 symbol: "X".into(), timeframe: Timeframe::Minutes(1),
4486 bar_start_ms: 0, open: dec!(100), high: dec!(105),
4487 low: dec!(95), close: dec!(100.005),
4488 volume: dec!(1), trade_count: 1, is_complete: true,
4489 is_gap_fill: false, vwap: None,
4490 };
4491 assert!(bar.is_doji(dec!(0.01)));
4492 assert!(!bar.is_doji(Decimal::ZERO));
4493 }
4494
4495 #[test]
4496 fn test_wick_upper_bullish() {
4497 let bar = OhlcvBar {
4499 symbol: "X".into(), timeframe: Timeframe::Minutes(1),
4500 bar_start_ms: 0, open: dec!(100), high: dec!(107),
4501 low: dec!(98), close: dec!(104),
4502 volume: dec!(1), trade_count: 1, is_complete: true,
4503 is_gap_fill: false, vwap: None,
4504 };
4505 assert_eq!(bar.wick_upper(), dec!(3));
4506 }
4507
4508 #[test]
4509 fn test_wick_lower_bearish() {
4510 let bar = OhlcvBar {
4512 symbol: "X".into(), timeframe: Timeframe::Minutes(1),
4513 bar_start_ms: 0, open: dec!(104), high: dec!(107),
4514 low: dec!(97), close: dec!(100),
4515 volume: dec!(1), trade_count: 1, is_complete: true,
4516 is_gap_fill: false, vwap: None,
4517 };
4518 assert_eq!(bar.wick_lower(), dec!(3));
4519 }
4520
4521 #[test]
4524 fn test_window_progress_none_when_no_bar() {
4525 let agg = agg("BTC-USD", Timeframe::Minutes(1));
4526 assert!(agg.window_progress(60_000).is_none());
4527 }
4528
4529 #[test]
4530 fn test_window_progress_at_start_is_zero() {
4531 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4532 agg.feed(&make_tick("BTC-USD", dec!(100), dec!(1), 60_000)).unwrap();
4534 assert_eq!(agg.window_progress(60_000), Some(0.0));
4535 }
4536
4537 #[test]
4538 fn test_window_progress_midpoint() {
4539 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4540 agg.feed(&make_tick("BTC-USD", dec!(100), dec!(1), 60_000)).unwrap();
4541 let progress = agg.window_progress(90_000).unwrap();
4543 assert!((progress - 0.5).abs() < 1e-9);
4544 }
4545
4546 #[test]
4547 fn test_window_progress_clamps_at_one() {
4548 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4549 agg.feed(&make_tick("BTC-USD", dec!(100), dec!(1), 60_000)).unwrap();
4550 assert_eq!(agg.window_progress(150_000), Some(1.0));
4552 }
4553
4554 #[test]
4557 fn test_price_change_bullish_is_positive() {
4558 let bar = make_bar(dec!(100), dec!(110), dec!(98), dec!(105));
4559 assert_eq!(bar.price_change(), dec!(5));
4560 }
4561
4562 #[test]
4563 fn test_price_change_bearish_is_negative() {
4564 let bar = make_bar(dec!(105), dec!(110), dec!(98), dec!(100));
4565 assert_eq!(bar.price_change(), dec!(-5));
4566 }
4567
4568 #[test]
4569 fn test_price_change_doji_is_zero() {
4570 let bar = make_bar(dec!(100), dec!(102), dec!(98), dec!(100));
4571 assert_eq!(bar.price_change(), dec!(0));
4572 }
4573
4574 #[test]
4577 fn test_total_volume_zero_before_completion() {
4578 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4579 agg.feed(&make_tick("BTC-USD", dec!(100), dec!(2), 60_000)).unwrap();
4580 assert_eq!(agg.total_volume(), dec!(0));
4582 }
4583
4584 #[test]
4585 fn test_total_volume_accumulates_across_bars() {
4586 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4587 agg.feed(&make_tick("BTC-USD", dec!(100), dec!(2), 60_000)).unwrap();
4589 agg.feed(&make_tick("BTC-USD", dec!(101), dec!(3), 120_000)).unwrap();
4591 assert_eq!(agg.total_volume(), dec!(2));
4593 agg.feed(&make_tick("BTC-USD", dec!(102), dec!(5), 180_000)).unwrap();
4595 assert_eq!(agg.total_volume(), dec!(5)); }
4597
4598 #[test]
4599 fn test_total_volume_reset_clears() {
4600 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4601 agg.feed(&make_tick("BTC-USD", dec!(100), dec!(2), 60_000)).unwrap();
4602 agg.feed(&make_tick("BTC-USD", dec!(101), dec!(3), 120_000)).unwrap();
4603 agg.reset();
4604 assert_eq!(agg.total_volume(), dec!(0));
4605 }
4606
4607 fn make_bar(open: Decimal, high: Decimal, low: Decimal, close: Decimal) -> OhlcvBar {
4610 OhlcvBar {
4611 symbol: "X".into(),
4612 timeframe: Timeframe::Minutes(1),
4613 bar_start_ms: 0,
4614 open,
4615 high,
4616 low,
4617 close,
4618 volume: dec!(1),
4619 trade_count: 1,
4620 is_complete: true,
4621 is_gap_fill: false,
4622 vwap: None,
4623 }
4624 }
4625
4626 #[test]
4627 fn test_typical_price() {
4628 let bar = make_bar(dec!(9), dec!(12), dec!(8), dec!(10));
4630 assert_eq!(bar.typical_price(), dec!(10));
4631 }
4632
4633 #[test]
4634 fn test_median_price() {
4635 let bar = make_bar(dec!(9), dec!(12), dec!(8), dec!(10));
4637 assert_eq!(bar.median_price(), dec!(10));
4638 }
4639
4640 #[test]
4641 fn test_typical_price_differs_from_median() {
4642 let bar = make_bar(dec!(8), dec!(10), dec!(6), dec!(10));
4644 assert_eq!(bar.median_price(), dec!(8));
4645 assert!(bar.typical_price() > bar.median_price());
4646 }
4647
4648 #[test]
4649 fn test_close_location_value_at_high() {
4650 let bar = make_bar(dec!(100), dec!(110), dec!(90), dec!(110));
4652 let clv = bar.close_location_value().unwrap();
4653 assert!((clv - 1.0).abs() < 1e-9, "expected 1.0 got {clv}");
4654 }
4655
4656 #[test]
4657 fn test_close_location_value_at_low() {
4658 let bar = make_bar(dec!(100), dec!(110), dec!(90), dec!(90));
4660 let clv = bar.close_location_value().unwrap();
4661 assert!((clv + 1.0).abs() < 1e-9, "expected -1.0 got {clv}");
4662 }
4663
4664 #[test]
4665 fn test_close_location_value_midpoint_is_zero() {
4666 let bar = make_bar(dec!(100), dec!(110), dec!(90), dec!(100));
4668 let clv = bar.close_location_value().unwrap();
4669 assert!(clv.abs() < 1e-9, "expected 0.0 got {clv}");
4670 }
4671
4672 #[test]
4673 fn test_close_location_value_zero_range_returns_none() {
4674 let bar = make_bar(dec!(100), dec!(100), dec!(100), dec!(100));
4675 assert!(bar.close_location_value().is_none());
4676 }
4677
4678 #[test]
4679 fn test_body_direction_bullish() {
4680 let bar = make_bar(dec!(90), dec!(110), dec!(85), dec!(105));
4681 assert_eq!(bar.body_direction(), BarDirection::Bullish);
4682 }
4683
4684 #[test]
4685 fn test_body_direction_bearish() {
4686 let bar = make_bar(dec!(105), dec!(110), dec!(85), dec!(90));
4687 assert_eq!(bar.body_direction(), BarDirection::Bearish);
4688 }
4689
4690 #[test]
4691 fn test_body_direction_neutral() {
4692 let bar = make_bar(dec!(100), dec!(110), dec!(85), dec!(100));
4693 assert_eq!(bar.body_direction(), BarDirection::Neutral);
4694 }
4695
4696 #[test]
4699 fn test_last_bar_none_before_completion() {
4700 let agg = agg("BTC-USD", Timeframe::Minutes(1));
4701 assert!(agg.last_bar().is_none());
4702 }
4703
4704 #[test]
4705 fn test_last_bar_set_after_bar_completion() {
4706 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4707 agg.feed(&make_tick("BTC-USD", dec!(100), dec!(1), 60_000)).unwrap();
4709 agg.feed(&make_tick("BTC-USD", dec!(200), dec!(1), 120_000)).unwrap();
4711 let last = agg.last_bar().unwrap();
4712 assert!(last.is_complete);
4713 assert_eq!(last.close, dec!(100));
4714 }
4715
4716 #[test]
4717 fn test_last_bar_set_after_flush() {
4718 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4719 agg.feed(&make_tick("BTC-USD", dec!(50), dec!(1), 60_000)).unwrap();
4720 let flushed = agg.flush().unwrap();
4721 assert_eq!(agg.last_bar().unwrap().close, flushed.close);
4722 }
4723
4724 #[test]
4725 fn test_last_bar_cleared_on_reset() {
4726 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4727 agg.feed(&make_tick("BTC-USD", dec!(100), dec!(1), 60_000)).unwrap();
4728 agg.feed(&make_tick("BTC-USD", dec!(200), dec!(1), 120_000)).unwrap();
4729 assert!(agg.last_bar().is_some());
4730 agg.reset();
4731 assert!(agg.last_bar().is_none());
4732 }
4733
4734 #[test]
4737 fn test_weighted_close_basic() {
4738 let bar = make_bar(dec!(9), dec!(12), dec!(8), dec!(10));
4740 assert_eq!(bar.weighted_close(), dec!(10));
4741 }
4742
4743 #[test]
4744 fn test_weighted_close_weights_close_more_than_typical() {
4745 let bar = make_bar(dec!(50), dec!(100), dec!(0), dec!(80));
4747 assert_eq!(bar.weighted_close(), dec!(65));
4748 }
4749
4750 #[test]
4751 fn test_price_change_pct_bullish() {
4752 let bar = make_bar(dec!(100), dec!(115), dec!(95), dec!(110));
4754 let pct = bar.price_change_pct().unwrap();
4755 assert!((pct - 10.0).abs() < 1e-9, "expected 10.0 got {pct}");
4756 }
4757
4758 #[test]
4759 fn test_price_change_pct_bearish() {
4760 let bar = make_bar(dec!(200), dec!(210), dec!(175), dec!(180));
4762 let pct = bar.price_change_pct().unwrap();
4763 assert!((pct - (-10.0)).abs() < 1e-9, "expected -10.0 got {pct}");
4764 }
4765
4766 #[test]
4767 fn test_price_change_pct_zero_open_returns_none() {
4768 let bar = make_bar(dec!(0), dec!(5), dec!(0), dec!(3));
4769 assert!(bar.price_change_pct().is_none());
4770 }
4771
4772 #[test]
4773 fn test_wick_ratio_all_wicks() {
4774 let bar = make_bar(dec!(5), dec!(10), dec!(0), dec!(5));
4776 let r = bar.wick_ratio().unwrap();
4777 assert!((r - 1.0).abs() < 1e-9, "expected 1.0 got {r}");
4778 }
4779
4780 #[test]
4781 fn test_wick_ratio_no_wicks() {
4782 let bar = make_bar(dec!(0), dec!(10), dec!(0), dec!(10));
4784 let r = bar.wick_ratio().unwrap();
4785 assert!((r - 0.0).abs() < 1e-9, "expected 0.0 got {r}");
4786 }
4787
4788 #[test]
4789 fn test_wick_ratio_zero_range_returns_none() {
4790 let bar = make_bar(dec!(5), dec!(5), dec!(5), dec!(5));
4792 assert!(bar.wick_ratio().is_none());
4793 }
4794
4795 #[test]
4798 fn test_body_ratio_no_wicks_is_one() {
4799 let bar = make_bar(dec!(0), dec!(10), dec!(0), dec!(10));
4801 let r = bar.body_ratio().unwrap();
4802 assert!((r - 1.0).abs() < 1e-9);
4803 }
4804
4805 #[test]
4806 fn test_body_ratio_all_wicks_is_zero() {
4807 let bar = make_bar(dec!(5), dec!(10), dec!(0), dec!(5));
4809 let r = bar.body_ratio().unwrap();
4810 assert!((r - 0.0).abs() < 1e-9);
4811 }
4812
4813 #[test]
4814 fn test_body_ratio_zero_range_returns_none() {
4815 let bar = make_bar(dec!(5), dec!(5), dec!(5), dec!(5));
4816 assert!(bar.body_ratio().is_none());
4817 }
4818
4819 #[test]
4820 fn test_body_ratio_plus_wick_ratio_equals_one() {
4821 let bar = make_bar(dec!(4), dec!(10), dec!(0), dec!(8));
4823 let body = bar.body_ratio().unwrap();
4824 let wick = bar.wick_ratio().unwrap();
4825 assert!((body + wick - 1.0).abs() < 1e-9);
4826 }
4827
4828 #[test]
4831 fn test_average_volume_none_before_bars() {
4832 let agg = agg("BTC-USD", Timeframe::Minutes(1));
4833 assert!(agg.average_volume().is_none());
4834 }
4835
4836 #[test]
4837 fn test_average_volume_one_bar() {
4838 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4839 agg.feed(&make_tick("BTC-USD", dec!(100), dec!(4), 60_000)).unwrap();
4840 agg.feed(&make_tick("BTC-USD", dec!(101), dec!(1), 120_000)).unwrap();
4841 assert_eq!(agg.average_volume(), Some(dec!(4)));
4843 }
4844
4845 #[test]
4846 fn test_average_volume_two_bars() {
4847 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4848 agg.feed(&make_tick("BTC-USD", dec!(100), dec!(4), 60_000)).unwrap();
4849 agg.feed(&make_tick("BTC-USD", dec!(101), dec!(6), 120_000)).unwrap();
4850 agg.feed(&make_tick("BTC-USD", dec!(102), dec!(1), 180_000)).unwrap();
4851 assert_eq!(agg.average_volume(), Some(dec!(5)));
4853 }
4854
4855 #[test]
4858 fn test_true_range_no_gap() {
4859 let bar = make_bar(dec!(9), dec!(12), dec!(8), dec!(11));
4861 assert_eq!(bar.true_range(dec!(10)), dec!(4));
4862 }
4863
4864 #[test]
4865 fn test_true_range_gap_up() {
4866 let bar = make_bar(dec!(12), dec!(15), dec!(12), dec!(13));
4868 assert_eq!(bar.true_range(dec!(10)), dec!(5));
4869 }
4870
4871 #[test]
4872 fn test_true_range_gap_down() {
4873 let bar = make_bar(dec!(7), dec!(8), dec!(5), dec!(6));
4875 assert_eq!(bar.true_range(dec!(12)), dec!(7));
4876 }
4877
4878 #[test]
4879 fn test_inside_bar_true_when_contained() {
4880 let prev = make_bar(dec!(9), dec!(15), dec!(5), dec!(12));
4881 let curr = make_bar(dec!(10), dec!(14), dec!(6), dec!(11));
4882 assert!(curr.is_inside_bar(&prev));
4883 }
4884
4885 #[test]
4886 fn test_inside_bar_false_when_not_contained() {
4887 let prev = make_bar(dec!(9), dec!(15), dec!(5), dec!(12));
4888 let curr = make_bar(dec!(10), dec!(16), dec!(6), dec!(11));
4889 assert!(!curr.is_inside_bar(&prev));
4890 }
4891
4892 #[test]
4893 fn test_outside_bar_true_when_engulfing() {
4894 let prev = make_bar(dec!(9), dec!(12), dec!(8), dec!(11));
4895 let curr = make_bar(dec!(10), dec!(14), dec!(6), dec!(11));
4896 assert!(curr.outside_bar(&prev));
4897 }
4898
4899 #[test]
4900 fn test_outside_bar_false_when_not_engulfing() {
4901 let prev = make_bar(dec!(9), dec!(12), dec!(8), dec!(11));
4902 let curr = make_bar(dec!(10), dec!(11), dec!(9), dec!(10));
4903 assert!(!curr.outside_bar(&prev));
4904 }
4905
4906 #[test]
4909 fn test_is_hammer_classic() {
4910 let bar = make_bar(dec!(9), dec!(10), dec!(0), dec!(9));
4913 assert!(bar.is_hammer());
4914 }
4915
4916 #[test]
4917 fn test_is_hammer_false_large_upper_wick() {
4918 let bar = make_bar(dec!(5), dec!(10), dec!(0), dec!(5));
4920 assert!(!bar.is_hammer());
4921 }
4922
4923 #[test]
4924 fn test_is_hammer_false_zero_range() {
4925 let bar = make_bar(dec!(5), dec!(5), dec!(5), dec!(5));
4926 assert!(!bar.is_hammer());
4927 }
4928
4929 #[test]
4932 fn test_peak_volume_none_before_completion() {
4933 let agg = agg("BTC-USD", Timeframe::Minutes(1));
4934 assert!(agg.peak_volume().is_none());
4935 }
4936
4937 #[test]
4938 fn test_peak_volume_tracks_maximum() {
4939 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4940 agg.feed(&make_tick("BTC-USD", dec!(100), dec!(3), 60_000)).unwrap();
4942 agg.feed(&make_tick("BTC-USD", dec!(101), dec!(10), 120_000)).unwrap();
4944 assert_eq!(agg.peak_volume(), Some(dec!(3)));
4945 agg.feed(&make_tick("BTC-USD", dec!(102), dec!(1), 180_000)).unwrap();
4947 assert_eq!(agg.peak_volume(), Some(dec!(10)));
4948 }
4949
4950 #[test]
4951 fn test_peak_volume_reset_clears() {
4952 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4953 agg.feed(&make_tick("BTC-USD", dec!(100), dec!(5), 60_000)).unwrap();
4954 agg.feed(&make_tick("BTC-USD", dec!(101), dec!(1), 120_000)).unwrap();
4955 agg.reset();
4956 assert!(agg.peak_volume().is_none());
4957 }
4958
4959 #[test]
4960 fn test_peak_volume_via_flush() {
4961 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4962 agg.feed(&make_tick("BTC-USD", dec!(100), dec!(7), 60_000)).unwrap();
4963 agg.flush();
4964 assert_eq!(agg.peak_volume(), Some(dec!(7)));
4965 }
4966
4967 #[test]
4970 fn test_is_shooting_star_classic() {
4971 let bar = make_bar(dec!(1), dec!(10), dec!(0), dec!(1));
4974 assert!(bar.is_shooting_star());
4975 }
4976
4977 #[test]
4978 fn test_is_shooting_star_false_large_lower_wick() {
4979 let bar = make_bar(dec!(5), dec!(10), dec!(0), dec!(5));
4981 assert!(!bar.is_shooting_star());
4982 }
4983
4984 #[test]
4985 fn test_is_shooting_star_false_zero_range() {
4986 let bar = make_bar(dec!(5), dec!(5), dec!(5), dec!(5));
4987 assert!(!bar.is_shooting_star());
4988 }
4989
4990 #[test]
4991 fn test_hammer_and_shooting_star_are_mutually_exclusive_for_typical_bars() {
4992 let hammer = make_bar(dec!(9), dec!(10), dec!(0), dec!(9));
4994 let star = make_bar(dec!(1), dec!(10), dec!(0), dec!(1));
4996 assert!(hammer.is_hammer() && !hammer.is_shooting_star());
4997 assert!(star.is_shooting_star() && !star.is_hammer());
4998 }
4999
5000 #[test]
5003 fn test_min_volume_none_before_completion() {
5004 let agg = agg("BTC-USD", Timeframe::Minutes(1));
5005 assert!(agg.min_volume().is_none());
5006 }
5007
5008 #[test]
5009 fn test_min_volume_tracks_minimum() {
5010 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
5011 agg.feed(&make_tick("BTC-USD", dec!(100), dec!(10), 60_000)).unwrap();
5013 agg.feed(&make_tick("BTC-USD", dec!(101), dec!(1), 120_000)).unwrap();
5014 assert_eq!(agg.min_volume(), Some(dec!(10)));
5015 agg.feed(&make_tick("BTC-USD", dec!(102), dec!(5), 180_000)).unwrap();
5017 assert_eq!(agg.min_volume(), Some(dec!(1)));
5018 }
5019
5020 #[test]
5021 fn test_min_volume_reset_clears() {
5022 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
5023 agg.feed(&make_tick("BTC-USD", dec!(100), dec!(5), 60_000)).unwrap();
5024 agg.feed(&make_tick("BTC-USD", dec!(101), dec!(1), 120_000)).unwrap();
5025 agg.reset();
5026 assert!(agg.min_volume().is_none());
5027 }
5028
5029 #[test]
5032 fn test_is_gap_up_true() {
5033 let prev = make_bar(dec!(5), dec!(10), dec!(4), dec!(8));
5034 let curr = make_bar(dec!(9), dec!(12), dec!(8), dec!(11)); assert!(curr.is_gap_up(&prev));
5036 }
5037
5038 #[test]
5039 fn test_is_gap_up_false_when_equal() {
5040 let prev = make_bar(dec!(5), dec!(10), dec!(4), dec!(8));
5041 let curr = make_bar(dec!(8), dec!(12), dec!(7), dec!(11)); assert!(!curr.is_gap_up(&prev));
5043 }
5044
5045 #[test]
5046 fn test_is_gap_down_true() {
5047 let prev = make_bar(dec!(5), dec!(10), dec!(4), dec!(8));
5048 let curr = make_bar(dec!(7), dec!(8), dec!(6), dec!(7)); assert!(curr.is_gap_down(&prev));
5050 }
5051
5052 #[test]
5053 fn test_is_gap_down_false_when_equal() {
5054 let prev = make_bar(dec!(5), dec!(10), dec!(4), dec!(8));
5055 let curr = make_bar(dec!(8), dec!(9), dec!(7), dec!(8)); assert!(!curr.is_gap_down(&prev));
5057 }
5058
5059 #[test]
5062 fn test_volume_range_none_before_completion() {
5063 let agg = agg("BTC-USD", Timeframe::Minutes(1));
5064 assert!(agg.volume_range().is_none());
5065 }
5066
5067 #[test]
5068 fn test_volume_range_after_two_bars() {
5069 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
5070 agg.feed(&make_tick("BTC-USD", dec!(100), dec!(3), 60_000)).unwrap();
5071 agg.feed(&make_tick("BTC-USD", dec!(101), dec!(10), 120_000)).unwrap();
5072 agg.feed(&make_tick("BTC-USD", dec!(102), dec!(1), 180_000)).unwrap();
5073 assert_eq!(agg.volume_range(), Some((dec!(3), dec!(10))));
5075 }
5076
5077 fn make_ohlcv_bar(open: Decimal, high: Decimal, low: Decimal, close: Decimal) -> OhlcvBar {
5080 OhlcvBar {
5081 symbol: "X".into(),
5082 timeframe: Timeframe::Minutes(1),
5083 open,
5084 high,
5085 low,
5086 close,
5087 volume: dec!(1),
5088 bar_start_ms: 0,
5089 trade_count: 1,
5090 is_complete: false,
5091 is_gap_fill: false,
5092 vwap: None,
5093 }
5094 }
5095
5096 #[test]
5097 fn test_body_to_range_ratio_bullish_full_body() {
5098 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(100), dec!(110));
5100 assert_eq!(bar.body_to_range_ratio(), Some(dec!(1)));
5101 }
5102
5103 #[test]
5104 fn test_body_to_range_ratio_doji_like() {
5105 let bar = make_ohlcv_bar(dec!(100), dec!(102), dec!(98), dec!(100));
5107 assert_eq!(bar.body_to_range_ratio(), Some(dec!(0)));
5108 }
5109
5110 #[test]
5111 fn test_body_to_range_ratio_none_when_range_zero() {
5112 let bar = make_ohlcv_bar(dec!(100), dec!(100), dec!(100), dec!(100));
5113 assert!(bar.body_to_range_ratio().is_none());
5114 }
5115
5116 #[test]
5119 fn test_is_active_false_before_any_ticks() {
5120 let agg = agg("BTC-USD", Timeframe::Minutes(1));
5121 assert!(!agg.is_active());
5122 }
5123
5124 #[test]
5125 fn test_is_active_true_after_first_tick() {
5126 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
5127 agg.feed(&make_tick("BTC-USD", dec!(100), dec!(1), 1_000)).unwrap();
5128 assert!(agg.is_active());
5129 }
5130
5131 #[test]
5132 fn test_is_active_false_after_flush() {
5133 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
5134 agg.feed(&make_tick("BTC-USD", dec!(100), dec!(1), 1_000)).unwrap();
5135 agg.flush();
5136 assert!(!agg.is_active());
5137 }
5138
5139 #[test]
5142 fn test_is_long_upper_wick_true_when_upper_wick_dominates() {
5143 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(100), dec!(101));
5145 assert!(bar.is_long_upper_wick());
5146 }
5147
5148 #[test]
5149 fn test_is_long_upper_wick_false_for_full_body() {
5150 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(100), dec!(110));
5152 assert!(!bar.is_long_upper_wick());
5153 }
5154
5155 #[test]
5156 fn test_is_long_upper_wick_false_when_equal() {
5157 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(100), dec!(105));
5159 assert!(!bar.is_long_upper_wick());
5160 }
5161
5162 #[test]
5165 fn test_price_change_abs_bullish_bar() {
5166 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(100), dec!(108));
5167 assert_eq!(bar.price_change_abs(), dec!(8));
5168 }
5169
5170 #[test]
5171 fn test_price_change_abs_bearish_bar() {
5172 let bar = make_ohlcv_bar(dec!(110), dec!(110), dec!(100), dec!(102));
5173 assert_eq!(bar.price_change_abs(), dec!(8));
5174 }
5175
5176 #[test]
5177 fn test_price_change_abs_doji_zero() {
5178 let bar = make_ohlcv_bar(dec!(100), dec!(105), dec!(95), dec!(100));
5179 assert_eq!(bar.price_change_abs(), dec!(0));
5180 }
5181
5182 #[test]
5185 fn test_vwap_current_none_before_any_ticks() {
5186 let agg = agg("BTC-USD", Timeframe::Minutes(1));
5187 assert!(agg.vwap_current().is_none());
5188 }
5189
5190 #[test]
5191 fn test_vwap_current_equals_price_for_single_tick() {
5192 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
5193 agg.feed(&make_tick("BTC-USD", dec!(200), dec!(5), 1_000)).unwrap();
5194 assert_eq!(agg.vwap_current(), Some(dec!(200)));
5196 }
5197
5198 #[test]
5199 fn test_vwap_current_weighted_average() {
5200 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
5201 agg.feed(&make_tick("BTC-USD", dec!(100), dec!(1), 1_000)).unwrap();
5202 agg.feed(&make_tick("BTC-USD", dec!(200), dec!(3), 2_000)).unwrap();
5203 assert_eq!(agg.vwap_current(), Some(dec!(175)));
5205 }
5206
5207 fn bar(o: i64, h: i64, l: i64, c: i64) -> OhlcvBar {
5210 OhlcvBar {
5211 symbol: "X".into(),
5212 timeframe: Timeframe::Minutes(1),
5213 open: Decimal::from(o),
5214 high: Decimal::from(h),
5215 low: Decimal::from(l),
5216 close: Decimal::from(c),
5217 volume: Decimal::ZERO,
5218 bar_start_ms: 0,
5219 trade_count: 0,
5220 is_complete: false,
5221 is_gap_fill: false,
5222 vwap: None,
5223 }
5224 }
5225
5226 #[test]
5227 fn test_upper_shadow_equals_wick_upper() {
5228 let b = bar(100, 120, 90, 110);
5229 assert_eq!(b.upper_shadow(), b.wick_upper());
5230 assert_eq!(b.upper_shadow(), Decimal::from(10)); }
5232
5233 #[test]
5234 fn test_lower_shadow_equals_wick_lower() {
5235 let b = bar(100, 120, 90, 110);
5236 assert_eq!(b.lower_shadow(), b.wick_lower());
5237 assert_eq!(b.lower_shadow(), Decimal::from(10)); }
5239
5240 #[test]
5241 fn test_is_spinning_top_true_when_small_body_large_wicks() {
5242 let b = bar(100, 130, 80, 110);
5247 assert!(b.is_spinning_top(dec!(0.3)));
5248 }
5249
5250 #[test]
5251 fn test_is_spinning_top_false_when_body_too_large() {
5252 let b = bar(80, 130, 80, 120);
5254 assert!(!b.is_spinning_top(dec!(0.3)));
5255 }
5256
5257 #[test]
5258 fn test_is_spinning_top_false_when_zero_range() {
5259 let b = bar(100, 100, 100, 100);
5260 assert!(!b.is_spinning_top(dec!(0.3)));
5261 }
5262
5263 #[test]
5264 fn test_hlc3_equals_typical_price() {
5265 let b = bar(100, 120, 80, 110);
5266 assert_eq!(b.hlc3(), b.typical_price());
5267 assert_eq!(b.hlc3(), (Decimal::from(120) + Decimal::from(80) + Decimal::from(110)) / Decimal::from(3));
5269 }
5270
5271 #[test]
5274 fn test_is_bearish_true_when_close_below_open() {
5275 let bar = make_ohlcv_bar(dec!(110), dec!(115), dec!(100), dec!(105));
5276 assert!(bar.is_bearish());
5277 }
5278
5279 #[test]
5280 fn test_is_bearish_false_when_close_above_open() {
5281 let bar = make_ohlcv_bar(dec!(100), dec!(115), dec!(95), dec!(110));
5282 assert!(!bar.is_bearish());
5283 }
5284
5285 #[test]
5286 fn test_is_bearish_false_when_doji() {
5287 let bar = make_ohlcv_bar(dec!(100), dec!(105), dec!(95), dec!(100));
5288 assert!(!bar.is_bearish());
5289 }
5290
5291 #[test]
5294 fn test_wick_ratio_zero_for_full_body_no_wicks() {
5295 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(100), dec!(110));
5297 let ratio = bar.wick_ratio().unwrap();
5298 assert!(ratio.abs() < 1e-10);
5299 }
5300
5301 #[test]
5302 fn test_wick_ratio_one_for_pure_wick_doji() {
5303 let bar = make_ohlcv_bar(dec!(105), dec!(110), dec!(100), dec!(105));
5305 let ratio = bar.wick_ratio().unwrap();
5306 assert!((ratio - 1.0).abs() < 1e-10);
5307 }
5308
5309 #[test]
5310 fn test_wick_ratio_none_for_zero_range_bar() {
5311 let bar = make_ohlcv_bar(dec!(100), dec!(100), dec!(100), dec!(100));
5312 assert!(bar.wick_ratio().is_none());
5313 }
5314
5315 #[test]
5318 fn test_is_bullish_true_when_close_above_open() {
5319 let bar = make_ohlcv_bar(dec!(100), dec!(115), dec!(95), dec!(110));
5320 assert!(bar.is_bullish());
5321 }
5322
5323 #[test]
5324 fn test_is_bullish_false_when_close_below_open() {
5325 let bar = make_ohlcv_bar(dec!(110), dec!(115), dec!(100), dec!(105));
5326 assert!(!bar.is_bullish());
5327 }
5328
5329 #[test]
5330 fn test_is_bullish_false_when_doji() {
5331 let bar = make_ohlcv_bar(dec!(100), dec!(105), dec!(95), dec!(100));
5332 assert!(!bar.is_bullish());
5333 }
5334
5335 #[test]
5338 fn test_bar_duration_ms_one_minute() {
5339 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
5340 assert_eq!(bar.bar_duration_ms(), 60_000);
5341 }
5342
5343 #[test]
5344 fn test_bar_duration_ms_consistent_with_timeframe() {
5345 let mut bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
5346 bar.timeframe = Timeframe::Hours(1);
5347 assert_eq!(bar.bar_duration_ms(), 3_600_000);
5348 }
5349
5350 #[test]
5351 fn test_bar_duration_ms_seconds_timeframe() {
5352 let mut bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
5353 bar.timeframe = Timeframe::Seconds(30);
5354 assert_eq!(bar.bar_duration_ms(), 30_000);
5355 }
5356
5357 #[test]
5360 fn test_ohlc4_equals_average_of_all_four_prices() {
5361 let b = bar(100, 120, 80, 110);
5362 let expected = (Decimal::from(100) + Decimal::from(120) + Decimal::from(80) + Decimal::from(110))
5364 / Decimal::from(4);
5365 assert_eq!(b.ohlc4(), expected);
5366 }
5367
5368 #[test]
5369 fn test_is_marubozu_true_when_no_wicks() {
5370 let b = bar(100, 110, 100, 110);
5372 assert!(b.is_marubozu());
5373 }
5374
5375 #[test]
5376 fn test_is_marubozu_false_when_has_upper_wick() {
5377 let b = bar(100, 115, 100, 110);
5378 assert!(!b.is_marubozu());
5379 }
5380
5381 #[test]
5382 fn test_is_marubozu_false_when_has_lower_wick() {
5383 let b = bar(100, 110, 95, 110);
5384 assert!(!b.is_marubozu());
5385 }
5386
5387 #[test]
5390 fn test_is_harami_true_when_body_inside_prev_body() {
5391 let prev = bar(98, 115, 90, 108); let curr = bar(100, 110, 95, 105); assert!(curr.is_harami(&prev));
5394 }
5395
5396 #[test]
5397 fn test_is_harami_false_when_body_engulfs_prev() {
5398 let prev = bar(100, 110, 95, 105); let curr = bar(98, 115, 90, 108); assert!(!curr.is_harami(&prev));
5401 }
5402
5403 #[test]
5404 fn test_is_harami_false_when_bodies_equal() {
5405 let prev = bar(100, 110, 90, 105);
5406 let curr = bar(100, 110, 90, 105); assert!(!curr.is_harami(&prev));
5408 }
5409
5410 #[test]
5411 fn test_tail_length_upper_wick_longer() {
5412 let b = bar(100, 120, 95, 105);
5414 assert_eq!(b.tail_length(), Decimal::from(15));
5415 }
5416
5417 #[test]
5418 fn test_tail_length_lower_wick_longer() {
5419 let b = bar(105, 110, 80, 100);
5421 assert_eq!(b.tail_length(), Decimal::from(20));
5422 }
5423
5424 #[test]
5425 fn test_tail_length_zero_for_marubozu() {
5426 let b = bar(100, 110, 100, 110);
5428 assert!(b.tail_length().is_zero());
5429 }
5430
5431 #[test]
5434 fn test_is_inside_bar_true_when_range_within_prev() {
5435 let prev = bar(90, 120, 80, 110); let curr = bar(95, 115, 85, 100); assert!(curr.is_inside_bar(&prev));
5438 }
5439
5440 #[test]
5441 fn test_is_inside_bar_false_when_high_exceeds_prev_high() {
5442 let prev = bar(90, 110, 80, 100); let curr = bar(95, 112, 85, 100); assert!(!curr.is_inside_bar(&prev));
5445 }
5446
5447 #[test]
5448 fn test_is_inside_bar_false_when_equal_range() {
5449 let prev = bar(90, 110, 80, 100);
5450 let curr = bar(90, 110, 80, 100); assert!(!curr.is_inside_bar(&prev));
5452 }
5453
5454 #[test]
5455 fn test_bar_type_bullish() {
5456 let b = bar(100, 110, 90, 105); assert_eq!(b.bar_type(), "bullish");
5458 }
5459
5460 #[test]
5461 fn test_bar_type_bearish() {
5462 let b = bar(105, 110, 90, 100); assert_eq!(b.bar_type(), "bearish");
5464 }
5465
5466 #[test]
5467 fn test_bar_type_doji() {
5468 let b = bar(100, 110, 90, 100); assert_eq!(b.bar_type(), "doji");
5470 }
5471
5472 #[test]
5475 fn test_body_pct_none_for_zero_range() {
5476 let b = bar(100, 100, 100, 100);
5477 assert!(b.body_pct().is_none());
5478 }
5479
5480 #[test]
5481 fn test_body_pct_100_for_marubozu() {
5482 let b = bar(100, 110, 100, 110);
5484 assert_eq!(b.body_pct().unwrap(), Decimal::ONE_HUNDRED);
5485 }
5486
5487 #[test]
5488 fn test_body_pct_50_for_half_body() {
5489 let b = bar(100, 110, 100, 105);
5491 assert_eq!(b.body_pct().unwrap(), Decimal::from(50));
5492 }
5493
5494 #[test]
5495 fn test_is_bullish_hammer_true_for_classic_hammer() {
5496 let b = bar(108, 110, 100, 109);
5499 assert!(b.is_bullish_hammer());
5500 }
5501
5502 #[test]
5503 fn test_is_bullish_hammer_false_when_lower_wick_not_long_enough() {
5504 let b = bar(100, 110, 98, 108);
5506 assert!(!b.is_bullish_hammer());
5507 }
5508
5509 #[test]
5510 fn test_is_bullish_hammer_false_for_doji() {
5511 let b = bar(100, 110, 90, 100); assert!(!b.is_bullish_hammer());
5513 }
5514
5515 #[test]
5517 fn test_is_marubozu_true_when_full_body() {
5518 let b = bar(100, 110, 100, 110);
5520 assert!(b.is_marubozu());
5521 }
5522
5523 #[test]
5524 fn test_is_marubozu_false_when_large_wicks() {
5525 let b = bar(100, 120, 80, 110);
5527 assert!(!b.is_marubozu());
5528 }
5529
5530 #[test]
5531 fn test_is_marubozu_true_for_zero_range_flat_bar() {
5532 let b = bar(100, 100, 100, 100);
5534 assert!(b.is_marubozu());
5535 }
5536
5537 #[test]
5539 fn test_upper_wick_pct_zero_when_no_upper_wick() {
5540 let b = bar(100, 110, 90, 110);
5542 let pct = b.upper_wick_pct().unwrap();
5543 assert!(pct.is_zero(), "expected 0, got {pct}");
5544 }
5545
5546 #[test]
5547 fn test_upper_wick_pct_50_when_half_range() {
5548 let b = bar(100, 120, 100, 110);
5550 let pct = b.upper_wick_pct().unwrap();
5551 assert_eq!(pct, dec!(50));
5552 }
5553
5554 #[test]
5555 fn test_upper_wick_pct_none_for_zero_range() {
5556 let b = bar(100, 100, 100, 100);
5557 assert!(b.upper_wick_pct().is_none());
5558 }
5559
5560 #[test]
5562 fn test_lower_wick_pct_zero_when_no_lower_wick() {
5563 let b = bar(100, 110, 100, 105);
5565 let pct = b.lower_wick_pct().unwrap();
5566 assert!(pct.is_zero(), "expected 0, got {pct}");
5567 }
5568
5569 #[test]
5570 fn test_lower_wick_pct_50_when_half_range() {
5571 let b = bar(110, 120, 100, 115);
5573 let pct = b.lower_wick_pct().unwrap();
5574 assert_eq!(pct, dec!(50));
5575 }
5576
5577 #[test]
5578 fn test_lower_wick_pct_none_for_zero_range() {
5579 let b = bar(100, 100, 100, 100);
5580 assert!(b.lower_wick_pct().is_none());
5581 }
5582
5583 #[test]
5585 fn test_is_bearish_engulfing_true_for_bearish_engulf() {
5586 let prev = bar(100, 115, 95, 110); let curr = bar(112, 115, 88, 90); assert!(curr.is_bearish_engulfing(&prev));
5589 }
5590
5591 #[test]
5592 fn test_is_bearish_engulfing_false_for_bullish_engulf() {
5593 let prev = bar(110, 115, 95, 100); let curr = bar(98, 120, 95, 115); assert!(!curr.is_bearish_engulfing(&prev));
5596 }
5597
5598 #[test]
5599 fn test_is_engulfing_true_when_body_contains_prev_body() {
5600 let prev = bar(100, 110, 95, 105); let curr = bar(98, 115, 95, 108); assert!(curr.is_engulfing(&prev));
5603 }
5604
5605 #[test]
5606 fn test_is_engulfing_false_when_only_partial_overlap() {
5607 let prev = bar(100, 115, 90, 112); let curr = bar(101, 115, 90, 113); assert!(!curr.is_engulfing(&prev));
5610 }
5611
5612 #[test]
5613 fn test_is_engulfing_false_for_equal_bodies() {
5614 let prev = bar(100, 110, 90, 108);
5615 let curr = bar(100, 110, 90, 108); assert!(!curr.is_engulfing(&prev));
5617 }
5618
5619 #[test]
5622 fn test_has_upper_wick_true_when_high_above_max_oc() {
5623 let bar = make_ohlcv_bar(dec!(100), dec!(115), dec!(100), dec!(110));
5625 assert!(bar.has_upper_wick());
5626 }
5627
5628 #[test]
5629 fn test_has_upper_wick_false_for_full_body() {
5630 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(100), dec!(110));
5632 assert!(!bar.has_upper_wick());
5633 }
5634
5635 #[test]
5636 fn test_has_lower_wick_true_when_low_below_min_oc() {
5637 let bar = make_ohlcv_bar(dec!(105), dec!(110), dec!(100), dec!(110));
5639 assert!(bar.has_lower_wick());
5640 }
5641
5642 #[test]
5643 fn test_has_lower_wick_false_for_full_body() {
5644 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(100), dec!(110));
5646 assert!(!bar.has_lower_wick());
5647 }
5648
5649 #[test]
5652 fn test_is_gravestone_doji_true() {
5653 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(100), dec!(100));
5655 assert!(bar.is_gravestone_doji(dec!(0)));
5656 }
5657
5658 #[test]
5659 fn test_is_gravestone_doji_false_when_close_above_low() {
5660 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(99), dec!(105));
5662 assert!(!bar.is_gravestone_doji(dec!(1)));
5663 }
5664
5665 #[test]
5668 fn test_is_dragonfly_doji_true() {
5669 let bar = make_ohlcv_bar(dec!(110), dec!(110), dec!(100), dec!(110));
5671 assert!(bar.is_dragonfly_doji(dec!(0)));
5672 }
5673
5674 #[test]
5675 fn test_is_dragonfly_doji_false_when_close_below_high() {
5676 let bar = make_ohlcv_bar(dec!(105), dec!(110), dec!(100), dec!(105));
5678 assert!(!bar.is_dragonfly_doji(dec!(1)));
5679 }
5680
5681 #[test]
5684 fn test_is_flat_true() {
5685 let bar = make_ohlcv_bar(dec!(100), dec!(100), dec!(100), dec!(100));
5686 assert!(bar.is_flat());
5687 }
5688
5689 #[test]
5690 fn test_is_flat_false_when_range_exists() {
5691 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
5692 assert!(!bar.is_flat());
5693 }
5694
5695 #[test]
5696 fn test_close_to_high_ratio_normal() {
5697 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(110));
5698 let r = bar.close_to_high_ratio().unwrap();
5700 assert!((r - 1.0).abs() < 1e-9);
5701 }
5702
5703 #[test]
5704 fn test_close_to_high_ratio_none_when_high_zero() {
5705 let bar = make_ohlcv_bar(dec!(0), dec!(0), dec!(0), dec!(0));
5706 assert!(bar.close_to_high_ratio().is_none());
5707 }
5708
5709 #[test]
5710 fn test_close_open_ratio_normal() {
5711 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(110));
5713 let r = bar.close_open_ratio().unwrap();
5714 assert!((r - 1.1).abs() < 1e-9);
5715 }
5716
5717 #[test]
5718 fn test_close_open_ratio_none_when_open_zero() {
5719 let bar = make_ohlcv_bar(dec!(0), dec!(10), dec!(0), dec!(5));
5720 assert!(bar.close_open_ratio().is_none());
5721 }
5722
5723 #[test]
5726 fn test_true_range_simple_hl_dominates() {
5727 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
5729 assert_eq!(bar.true_range_with_prev(dec!(100)), dec!(20));
5730 }
5731
5732 #[test]
5733 fn test_true_range_gap_up_dominates() {
5734 let bar = make_ohlcv_bar(dec!(91), dec!(100), dec!(90), dec!(95));
5736 assert_eq!(bar.true_range_with_prev(dec!(80)), dec!(20));
5737 }
5738
5739 #[test]
5740 fn test_true_range_gap_down_dominates() {
5741 let bar = make_ohlcv_bar(dec!(98), dec!(100), dec!(95), dec!(97));
5743 assert_eq!(bar.true_range_with_prev(dec!(120)), dec!(25));
5744 }
5745
5746 #[test]
5749 fn test_is_outside_bar_true() {
5750 let prev = make_ohlcv_bar(dec!(100), dec!(105), dec!(95), dec!(100));
5751 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
5752 assert!(bar.is_outside_bar(&prev));
5753 }
5754
5755 #[test]
5756 fn test_is_outside_bar_false_when_inside() {
5757 let prev = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
5758 let bar = make_ohlcv_bar(dec!(100), dec!(105), dec!(95), dec!(100));
5759 assert!(!bar.is_outside_bar(&prev));
5760 }
5761
5762 #[test]
5763 fn test_high_low_midpoint_correct() {
5764 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
5765 assert_eq!(bar.high_low_midpoint(), dec!(100));
5767 }
5768
5769 #[test]
5770 fn test_high_low_midpoint_uneven() {
5771 let bar = make_ohlcv_bar(dec!(100), dec!(111), dec!(90), dec!(100));
5772 assert_eq!(bar.high_low_midpoint(), dec!(100.5));
5774 }
5775
5776 #[test]
5779 fn test_gap_up_true() {
5780 let prev = make_ohlcv_bar(dec!(95), dec!(100), dec!(90), dec!(98));
5781 let bar = make_ohlcv_bar(dec!(102), dec!(110), dec!(101), dec!(108));
5782 assert!(bar.gap_up(&prev));
5783 }
5784
5785 #[test]
5786 fn test_gap_up_false_when_no_gap() {
5787 let prev = make_ohlcv_bar(dec!(95), dec!(100), dec!(90), dec!(98));
5788 let bar = make_ohlcv_bar(dec!(99), dec!(105), dec!(98), dec!(104));
5789 assert!(!bar.gap_up(&prev));
5790 }
5791
5792 #[test]
5793 fn test_gap_down_true() {
5794 let prev = make_ohlcv_bar(dec!(95), dec!(100), dec!(90), dec!(92));
5795 let bar = make_ohlcv_bar(dec!(88), dec!(89), dec!(85), dec!(86));
5796 assert!(bar.gap_down(&prev));
5797 }
5798
5799 #[test]
5800 fn test_gap_down_false_when_no_gap() {
5801 let prev = make_ohlcv_bar(dec!(95), dec!(100), dec!(90), dec!(92));
5802 let bar = make_ohlcv_bar(dec!(91), dec!(95), dec!(89), dec!(93));
5803 assert!(!bar.gap_down(&prev));
5804 }
5805
5806 #[test]
5809 fn test_range_pct_correct() {
5810 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
5812 let pct = bar.range_pct().unwrap();
5813 assert!((pct - 20.0).abs() < 1e-9);
5814 }
5815
5816 #[test]
5817 fn test_range_pct_none_when_open_zero() {
5818 let bar = make_ohlcv_bar(dec!(0), dec!(10), dec!(0), dec!(5));
5819 assert!(bar.range_pct().is_none());
5820 }
5821
5822 #[test]
5823 fn test_range_pct_zero_for_flat_bar() {
5824 let bar = make_ohlcv_bar(dec!(100), dec!(100), dec!(100), dec!(100));
5826 let pct = bar.range_pct().unwrap();
5827 assert_eq!(pct, 0.0);
5828 }
5829
5830 #[test]
5833 fn test_body_size_bullish_bar() {
5834 let bar = make_ohlcv_bar(dec!(100), dec!(115), dec!(95), dec!(110));
5836 assert_eq!(bar.body_size(), dec!(10));
5837 }
5838
5839 #[test]
5840 fn test_body_size_bearish_bar() {
5841 let bar = make_ohlcv_bar(dec!(110), dec!(115), dec!(95), dec!(100));
5843 assert_eq!(bar.body_size(), dec!(10));
5844 }
5845
5846 #[test]
5847 fn test_body_size_doji() {
5848 let bar = make_ohlcv_bar(dec!(100), dec!(105), dec!(95), dec!(100));
5850 assert_eq!(bar.body_size(), dec!(0));
5851 }
5852
5853 #[test]
5856 fn test_volume_delta_positive_when_increasing() {
5857 let mut prev = make_ohlcv_bar(dec!(100), dec!(105), dec!(95), dec!(102));
5858 prev.volume = dec!(1000);
5859 let mut bar = make_ohlcv_bar(dec!(102), dec!(110), dec!(98), dec!(108));
5860 bar.volume = dec!(1500);
5861 assert_eq!(bar.volume_delta(&prev), dec!(500));
5862 }
5863
5864 #[test]
5865 fn test_volume_delta_negative_when_decreasing() {
5866 let mut prev = make_ohlcv_bar(dec!(100), dec!(105), dec!(95), dec!(102));
5867 prev.volume = dec!(1500);
5868 let mut bar = make_ohlcv_bar(dec!(102), dec!(110), dec!(98), dec!(108));
5869 bar.volume = dec!(1000);
5870 assert_eq!(bar.volume_delta(&prev), dec!(-500));
5871 }
5872
5873 #[test]
5874 fn test_is_consolidating_true_when_small_range() {
5875 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));
5878 }
5879
5880 #[test]
5881 fn test_is_consolidating_false_when_large_range() {
5882 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));
5885 }
5886
5887 #[test]
5890 fn test_relative_volume_correct() {
5891 let bar = make_ohlcv_bar(dec!(100), dec!(105), dec!(95), dec!(103));
5892 let rv = bar.relative_volume(dec!(2)).unwrap();
5894 assert!((rv - 0.5).abs() < 1e-9);
5895 }
5896
5897 #[test]
5898 fn test_relative_volume_none_when_avg_zero() {
5899 let bar = make_ohlcv_bar(dec!(100), dec!(105), dec!(95), dec!(103));
5900 assert!(bar.relative_volume(dec!(0)).is_none());
5901 }
5902
5903 #[test]
5904 fn test_intraday_reversal_true_for_bullish_then_bearish() {
5905 let prev = make_ohlcv_bar(dec!(100), dec!(108), dec!(99), dec!(105));
5907 let bar = make_ohlcv_bar(dec!(105), dec!(107), dec!(97), dec!(98));
5909 assert!(bar.intraday_reversal(&prev));
5910 }
5911
5912 #[test]
5913 fn test_intraday_reversal_false_for_continuation() {
5914 let prev = make_ohlcv_bar(dec!(100), dec!(108), dec!(99), dec!(105));
5916 let bar = make_ohlcv_bar(dec!(104), dec!(115), dec!(103), dec!(113));
5917 assert!(!bar.intraday_reversal(&prev));
5918 }
5919
5920 #[test]
5923 fn test_price_at_pct_zero_returns_low() {
5924 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
5925 assert_eq!(bar.price_at_pct(0.0), dec!(90));
5926 }
5927
5928 #[test]
5929 fn test_price_at_pct_one_returns_high() {
5930 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
5931 assert_eq!(bar.price_at_pct(1.0), dec!(110));
5932 }
5933
5934 #[test]
5935 fn test_price_at_pct_half_returns_midpoint() {
5936 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
5937 assert_eq!(bar.price_at_pct(0.5), dec!(100));
5939 }
5940
5941 #[test]
5942 fn test_price_at_pct_clamped_above_one() {
5943 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
5944 assert_eq!(bar.price_at_pct(2.0), dec!(110));
5945 }
5946
5947 #[test]
5950 fn test_average_true_range_none_when_fewer_than_two_bars() {
5951 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
5952 assert!(OhlcvBar::average_true_range(&[bar]).is_none());
5953 assert!(OhlcvBar::average_true_range(&[]).is_none());
5954 }
5955
5956 #[test]
5957 fn test_average_true_range_two_bars_no_gap() {
5958 let bar1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
5961 let bar2 = make_ohlcv_bar(dec!(105), dec!(115), dec!(95), dec!(110));
5962 let atr = OhlcvBar::average_true_range(&[bar1, bar2]).unwrap();
5963 assert_eq!(atr, dec!(20)); }
5965
5966 #[test]
5967 fn test_average_true_range_three_bars_mean() {
5968 let bar1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
5972 let bar2 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
5973 let bar3 = make_ohlcv_bar(dec!(110), dec!(120), dec!(100), dec!(115));
5974 let atr = OhlcvBar::average_true_range(&[bar1, bar2, bar3]).unwrap();
5975 assert_eq!(atr, dec!(20));
5976 }
5977
5978 #[test]
5981 fn test_average_body_none_when_empty() {
5982 assert!(OhlcvBar::average_body(&[]).is_none());
5983 }
5984
5985 #[test]
5986 fn test_average_body_single_bar() {
5987 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(108));
5988 assert_eq!(OhlcvBar::average_body(&[bar]), Some(dec!(8)));
5990 }
5991
5992 #[test]
5993 fn test_average_body_multiple_bars() {
5994 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();
5998 assert_eq!(avg, dec!(40) / dec!(3));
6000 }
6001
6002 #[test]
6005 fn test_bullish_count_zero_for_empty_slice() {
6006 assert_eq!(OhlcvBar::bullish_count(&[]), 0);
6007 }
6008
6009 #[test]
6010 fn test_bullish_count_all_bullish() {
6011 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);
6014 }
6015
6016 #[test]
6017 fn test_bearish_count_correct() {
6018 let bull = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(108));
6019 let bear = make_ohlcv_bar(dec!(108), dec!(110), dec!(90), dec!(95));
6020 let doji = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
6021 assert_eq!(OhlcvBar::bearish_count(&[bull, bear, doji]), 1);
6022 }
6023
6024 #[test]
6025 fn test_win_rate_none_when_empty() {
6026 assert!(OhlcvBar::win_rate(&[]).is_none());
6027 }
6028
6029 #[test]
6030 fn test_win_rate_all_bullish() {
6031 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(108));
6032 let b2 = make_ohlcv_bar(dec!(108), dec!(115), dec!(105), dec!(112));
6033 let wr = OhlcvBar::win_rate(&[b1, b2]).unwrap();
6034 assert!((wr - 1.0).abs() < 1e-9);
6035 }
6036
6037 #[test]
6038 fn test_win_rate_half_and_half() {
6039 let bull = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(108));
6040 let bear = make_ohlcv_bar(dec!(108), dec!(110), dec!(90), dec!(95));
6041 let wr = OhlcvBar::win_rate(&[bull, bear]).unwrap();
6042 assert!((wr - 0.5).abs() < 1e-9);
6043 }
6044
6045 #[test]
6048 fn test_bullish_streak_zero_for_empty_slice() {
6049 assert_eq!(OhlcvBar::bullish_streak(&[]), 0);
6050 }
6051
6052 #[test]
6053 fn test_bullish_streak_zero_when_last_bar_bearish() {
6054 let bull = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(108));
6055 let bear = make_ohlcv_bar(dec!(108), dec!(110), dec!(90), dec!(95));
6056 assert_eq!(OhlcvBar::bullish_streak(&[bull, bear]), 0);
6057 }
6058
6059 #[test]
6060 fn test_bullish_streak_counts_consecutive_tail() {
6061 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);
6065 }
6066
6067 #[test]
6068 fn test_bearish_streak_counts_consecutive_tail() {
6069 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);
6073 }
6074
6075 #[test]
6078 fn test_max_drawdown_none_when_fewer_than_2_bars() {
6079 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
6080 assert!(OhlcvBar::max_drawdown(&[bar]).is_none());
6081 assert!(OhlcvBar::max_drawdown(&[]).is_none());
6082 }
6083
6084 #[test]
6085 fn test_max_drawdown_zero_when_monotone_increasing() {
6086 let b1 = make_ohlcv_bar(dec!(100), dec!(105), dec!(98), dec!(100));
6087 let b2 = make_ohlcv_bar(dec!(100), dec!(110), dec!(99), dec!(105));
6088 let b3 = make_ohlcv_bar(dec!(105), dec!(115), dec!(104), dec!(110));
6089 let dd = OhlcvBar::max_drawdown(&[b1, b2, b3]).unwrap();
6090 assert_eq!(dd, 0.0);
6091 }
6092
6093 #[test]
6094 fn test_max_drawdown_correct_after_peak_then_drop() {
6095 let b1 = make_ohlcv_bar(dec!(100), dec!(102), dec!(98), dec!(100));
6097 let b2 = make_ohlcv_bar(dec!(100), dec!(125), dec!(99), dec!(120));
6098 let b3 = make_ohlcv_bar(dec!(120), dec!(121), dec!(88), dec!(90));
6099 let dd = OhlcvBar::max_drawdown(&[b1, b2, b3]).unwrap();
6100 assert!((dd - 0.25).abs() < 1e-9, "expected 0.25, got {dd}");
6101 }
6102
6103 #[test]
6106 fn test_mean_volume_none_when_empty() {
6107 assert!(OhlcvBar::mean_volume(&[]).is_none());
6108 }
6109
6110 #[test]
6111 fn test_mean_volume_single_bar() {
6112 let mut bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
6113 bar.volume = dec!(200);
6114 assert_eq!(OhlcvBar::mean_volume(&[bar]), Some(dec!(200)));
6115 }
6116
6117 #[test]
6118 fn test_mean_volume_multiple_bars() {
6119 let mut b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
6120 b1.volume = dec!(100);
6121 let mut b2 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
6122 b2.volume = dec!(200);
6123 let mut b3 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
6124 b3.volume = dec!(300);
6125 assert_eq!(OhlcvBar::mean_volume(&[b1, b2, b3]), Some(dec!(200)));
6126 }
6127
6128 #[test]
6131 fn test_vwap_deviation_none_when_vwap_not_set() {
6132 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
6133 assert!(bar.vwap_deviation().is_none());
6134 }
6135
6136 #[test]
6137 fn test_vwap_deviation_zero_when_close_equals_vwap() {
6138 let mut bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
6139 bar.vwap = Some(dec!(100));
6140 assert_eq!(bar.vwap_deviation(), Some(0.0));
6141 }
6142
6143 #[test]
6144 fn test_vwap_deviation_correct_value() {
6145 let mut bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(110));
6146 bar.vwap = Some(dec!(100));
6147 let dev = bar.vwap_deviation().unwrap();
6149 assert!((dev - 0.1).abs() < 1e-10);
6150 }
6151
6152 #[test]
6155 fn test_high_close_ratio_none_when_high_zero() {
6156 let bar = OhlcvBar {
6157 symbol: "X".into(),
6158 timeframe: Timeframe::Minutes(1),
6159 open: dec!(0),
6160 high: dec!(0),
6161 low: dec!(0),
6162 close: dec!(0),
6163 volume: dec!(1),
6164 bar_start_ms: 0,
6165 trade_count: 1,
6166 is_complete: false,
6167 is_gap_fill: false,
6168 vwap: None,
6169 };
6170 assert!(bar.high_close_ratio().is_none());
6171 }
6172
6173 #[test]
6174 fn test_high_close_ratio_one_when_close_equals_high() {
6175 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(110));
6176 let ratio = bar.high_close_ratio().unwrap();
6177 assert!((ratio - 1.0).abs() < 1e-10);
6178 }
6179
6180 #[test]
6181 fn test_high_close_ratio_less_than_one_when_close_below_high() {
6182 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(99));
6183 let ratio = bar.high_close_ratio().unwrap();
6184 assert!(ratio < 1.0);
6185 }
6186
6187 #[test]
6190 fn test_lower_shadow_pct_none_when_range_zero() {
6191 let bar = make_ohlcv_bar(dec!(100), dec!(100), dec!(100), dec!(100));
6192 assert!(bar.lower_shadow_pct().is_none());
6193 }
6194
6195 #[test]
6196 fn test_lower_shadow_pct_zero_when_no_lower_shadow() {
6197 let bar = make_ohlcv_bar(dec!(90), dec!(110), dec!(90), dec!(110));
6199 let pct = bar.lower_shadow_pct().unwrap();
6200 assert!(pct.abs() < 1e-10);
6201 }
6202
6203 #[test]
6204 fn test_lower_shadow_pct_correct_value() {
6205 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
6207 let pct = bar.lower_shadow_pct().unwrap();
6208 assert!((pct - 0.5).abs() < 1e-10);
6209 }
6210
6211 #[test]
6214 fn test_open_close_ratio_none_when_open_zero() {
6215 let bar = make_ohlcv_bar(dec!(0), dec!(10), dec!(0), dec!(5));
6216 assert!(bar.open_close_ratio().is_none());
6217 }
6218
6219 #[test]
6220 fn test_open_close_ratio_one_when_flat() {
6221 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
6222 let ratio = bar.open_close_ratio().unwrap();
6223 assert!((ratio - 1.0).abs() < 1e-10);
6224 }
6225
6226 #[test]
6227 fn test_open_close_ratio_above_one_for_bullish_bar() {
6228 let bar = make_ohlcv_bar(dec!(100), dec!(115), dec!(95), dec!(110));
6229 let ratio = bar.open_close_ratio().unwrap();
6230 assert!(ratio > 1.0);
6231 }
6232
6233 #[test]
6236 fn test_is_wide_range_bar_true_when_range_exceeds_threshold() {
6237 let bar = make_ohlcv_bar(dec!(100), dec!(120), dec!(95), dec!(110)); assert!(bar.is_wide_range_bar(dec!(20)));
6239 }
6240
6241 #[test]
6242 fn test_is_wide_range_bar_false_when_range_equals_threshold() {
6243 let bar = make_ohlcv_bar(dec!(100), dec!(120), dec!(100), dec!(110)); assert!(!bar.is_wide_range_bar(dec!(20)));
6245 }
6246
6247 #[test]
6250 fn test_close_to_low_ratio_none_when_range_zero() {
6251 let bar = make_ohlcv_bar(dec!(100), dec!(100), dec!(100), dec!(100));
6252 assert!(bar.close_to_low_ratio().is_none());
6253 }
6254
6255 #[test]
6256 fn test_close_to_low_ratio_one_when_closed_at_high() {
6257 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(110));
6258 let ratio = bar.close_to_low_ratio().unwrap();
6259 assert!((ratio - 1.0).abs() < 1e-10);
6260 }
6261
6262 #[test]
6263 fn test_close_to_low_ratio_zero_when_closed_at_low() {
6264 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(90));
6265 let ratio = bar.close_to_low_ratio().unwrap();
6266 assert!(ratio.abs() < 1e-10);
6267 }
6268
6269 #[test]
6270 fn test_close_to_low_ratio_half_at_midpoint() {
6271 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
6272 let ratio = bar.close_to_low_ratio().unwrap();
6274 assert!((ratio - 0.5).abs() < 1e-10);
6275 }
6276
6277 #[test]
6280 fn test_volume_per_trade_none_when_trade_count_zero() {
6281 let mut bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
6282 bar.trade_count = 0;
6283 assert!(bar.volume_per_trade().is_none());
6284 }
6285
6286 #[test]
6287 fn test_volume_per_trade_correct_value() {
6288 let mut bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
6289 bar.volume = dec!(500);
6290 bar.trade_count = 5;
6291 assert_eq!(bar.volume_per_trade(), Some(dec!(100)));
6292 }
6293
6294 #[test]
6297 fn test_price_range_overlap_true_when_ranges_overlap() {
6298 let a = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
6299 let b = make_ohlcv_bar(dec!(105), dec!(120), dec!(95), dec!(110));
6300 assert!(a.price_range_overlap(&b));
6301 }
6302
6303 #[test]
6304 fn test_price_range_overlap_false_when_no_overlap() {
6305 let a = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
6306 let b = make_ohlcv_bar(dec!(120), dec!(130), dec!(115), dec!(125));
6307 assert!(!a.price_range_overlap(&b));
6308 }
6309
6310 #[test]
6311 fn test_price_range_overlap_true_at_exact_touch() {
6312 let a = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
6313 let b = make_ohlcv_bar(dec!(115), dec!(125), dec!(110), dec!(120));
6314 assert!(a.price_range_overlap(&b));
6315 }
6316
6317 #[test]
6320 fn test_bar_height_pct_none_when_open_zero() {
6321 let bar = make_ohlcv_bar(dec!(0), dec!(10), dec!(0), dec!(5));
6322 assert!(bar.bar_height_pct().is_none());
6323 }
6324
6325 #[test]
6326 fn test_bar_height_pct_correct_value() {
6327 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100)); let pct = bar.bar_height_pct().unwrap();
6330 assert!((pct - 0.2).abs() < 1e-10);
6331 }
6332
6333 #[test]
6336 fn test_is_bullish_engulfing_true_for_valid_pattern() {
6337 let prev = make_ohlcv_bar(dec!(110), dec!(115), dec!(95), dec!(100));
6339 let bar = make_ohlcv_bar(dec!(98), dec!(115), dec!(95), dec!(112));
6340 assert!(bar.is_bullish_engulfing(&prev));
6341 }
6342
6343 #[test]
6344 fn test_is_bullish_engulfing_false_when_bearish() {
6345 let prev = make_ohlcv_bar(dec!(110), dec!(115), dec!(95), dec!(100));
6346 let bar = make_ohlcv_bar(dec!(108), dec!(115), dec!(95), dec!(95));
6347 assert!(!bar.is_bullish_engulfing(&prev));
6348 }
6349
6350 #[test]
6353 fn test_close_gap_positive_for_gap_up() {
6354 let prev = make_ohlcv_bar(dec!(100), dec!(105), dec!(95), dec!(102));
6355 let bar = make_ohlcv_bar(dec!(106), dec!(110), dec!(103), dec!(108)); assert_eq!(bar.close_gap(&prev), dec!(4));
6357 }
6358
6359 #[test]
6360 fn test_close_gap_negative_for_gap_down() {
6361 let prev = make_ohlcv_bar(dec!(100), dec!(105), dec!(95), dec!(102));
6362 let bar = make_ohlcv_bar(dec!(98), dec!(100), dec!(95), dec!(97)); assert_eq!(bar.close_gap(&prev), dec!(-4));
6364 }
6365
6366 #[test]
6367 fn test_close_gap_zero_when_no_gap() {
6368 let prev = make_ohlcv_bar(dec!(100), dec!(105), dec!(95), dec!(102));
6369 let bar = make_ohlcv_bar(dec!(102), dec!(110), dec!(100), dec!(108));
6370 assert_eq!(bar.close_gap(&prev), dec!(0));
6371 }
6372
6373 #[test]
6376 fn test_close_above_midpoint_true_when_above_mid() {
6377 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
6379 assert!(bar.close_above_midpoint());
6380 }
6381
6382 #[test]
6383 fn test_close_above_midpoint_false_when_at_mid() {
6384 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100)); assert!(!bar.close_above_midpoint());
6386 }
6387
6388 #[test]
6391 fn test_close_momentum_positive_when_rising() {
6392 let prev = make_ohlcv_bar(dec!(100), dec!(105), dec!(95), dec!(100));
6393 let bar = make_ohlcv_bar(dec!(100), dec!(115), dec!(98), dec!(110));
6394 assert_eq!(bar.close_momentum(&prev), dec!(10));
6395 }
6396
6397 #[test]
6398 fn test_close_momentum_zero_when_unchanged() {
6399 let prev = make_ohlcv_bar(dec!(100), dec!(105), dec!(95), dec!(100));
6400 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(95), dec!(100));
6401 assert_eq!(bar.close_momentum(&prev), dec!(0));
6402 }
6403
6404 #[test]
6407 fn test_bar_range_correct() {
6408 let bar = make_ohlcv_bar(dec!(100), dec!(120), dec!(90), dec!(110));
6409 assert_eq!(bar.bar_range(), dec!(30));
6410 }
6411
6412 #[test]
6415 fn test_linear_regression_slope_none_for_single_bar() {
6416 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
6417 assert!(OhlcvBar::linear_regression_slope(&[bar]).is_none());
6418 }
6419
6420 #[test]
6421 fn test_linear_regression_slope_positive_for_rising_closes() {
6422 let bars = vec![
6423 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100)),
6424 make_ohlcv_bar(dec!(100), dec!(115), dec!(95), dec!(110)),
6425 make_ohlcv_bar(dec!(110), dec!(120), dec!(105), dec!(120)),
6426 ];
6427 let slope = OhlcvBar::linear_regression_slope(&bars).unwrap();
6428 assert!(slope > 0.0, "slope should be positive for rising closes");
6429 }
6430
6431 #[test]
6432 fn test_linear_regression_slope_negative_for_falling_closes() {
6433 let bars = vec![
6434 make_ohlcv_bar(dec!(120), dec!(125), dec!(115), dec!(120)),
6435 make_ohlcv_bar(dec!(120), dec!(115), dec!(105), dec!(110)),
6436 make_ohlcv_bar(dec!(110), dec!(108), dec!(95), dec!(100)),
6437 ];
6438 let slope = OhlcvBar::linear_regression_slope(&bars).unwrap();
6439 assert!(slope < 0.0, "slope should be negative for falling closes");
6440 }
6441
6442 #[test]
6443 fn test_linear_regression_slope_near_zero_for_flat_closes() {
6444 let bars = vec![
6445 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100)),
6446 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100)),
6447 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100)),
6448 ];
6449 let slope = OhlcvBar::linear_regression_slope(&bars).unwrap();
6450 assert!(slope.abs() < 1e-10, "slope should be ~0 for identical closes");
6451 }
6452
6453 #[test]
6456 fn test_volume_slope_none_for_single_bar() {
6457 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
6458 assert!(OhlcvBar::volume_slope(&[bar]).is_none());
6459 }
6460
6461 #[test]
6462 fn test_volume_slope_positive_for_rising_volume() {
6463 let make_bar_with_vol = |v: u64| {
6464 let mut b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
6465 b.volume = Decimal::from(v);
6466 b
6467 };
6468 let bars = vec![make_bar_with_vol(100), make_bar_with_vol(200), make_bar_with_vol(300)];
6469 assert!(OhlcvBar::volume_slope(&bars).unwrap() > 0.0);
6470 }
6471
6472 #[test]
6475 fn test_highest_close_none_for_empty_slice() {
6476 assert!(OhlcvBar::highest_close(&[]).is_none());
6477 }
6478
6479 #[test]
6480 fn test_highest_close_returns_max_close() {
6481 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
6482 let b2 = make_ohlcv_bar(dec!(100), dec!(120), dec!(95), dec!(115));
6483 let b3 = make_ohlcv_bar(dec!(100), dec!(108), dec!(90), dec!(102));
6484 assert_eq!(OhlcvBar::highest_close(&[b1, b2, b3]), Some(dec!(115)));
6485 }
6486
6487 #[test]
6488 fn test_lowest_close_none_for_empty_slice() {
6489 assert!(OhlcvBar::lowest_close(&[]).is_none());
6490 }
6491
6492 #[test]
6493 fn test_lowest_close_returns_min_close() {
6494 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
6495 let b2 = make_ohlcv_bar(dec!(100), dec!(120), dec!(95), dec!(115));
6496 let b3 = make_ohlcv_bar(dec!(100), dec!(108), dec!(90), dec!(102));
6497 assert_eq!(OhlcvBar::lowest_close(&[b1, b2, b3]), Some(dec!(102)));
6498 }
6499
6500 #[test]
6503 fn test_close_range_none_for_empty_slice() {
6504 assert!(OhlcvBar::close_range(&[]).is_none());
6505 }
6506
6507 #[test]
6508 fn test_close_range_correct() {
6509 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(102));
6510 let b2 = make_ohlcv_bar(dec!(100), dec!(120), dec!(95), dec!(115));
6511 assert_eq!(OhlcvBar::close_range(&[b1, b2]), Some(dec!(13)));
6513 }
6514
6515 #[test]
6516 fn test_momentum_none_for_insufficient_bars() {
6517 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
6518 assert!(OhlcvBar::momentum(&[bar], 1).is_none());
6519 }
6520
6521 #[test]
6522 fn test_momentum_positive_for_rising_close() {
6523 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
6524 let b2 = make_ohlcv_bar(dec!(100), dec!(115), dec!(95), dec!(110));
6525 let mom = OhlcvBar::momentum(&[b1, b2], 1).unwrap();
6527 assert!((mom - 0.1).abs() < 1e-10);
6528 }
6529
6530 #[test]
6531 fn test_momentum_negative_for_falling_close() {
6532 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(110));
6533 let b2 = make_ohlcv_bar(dec!(100), dec!(108), dec!(88), dec!(99));
6534 let mom = OhlcvBar::momentum(&[b1, b2], 1).unwrap();
6536 assert!(mom < 0.0);
6537 }
6538
6539 #[test]
6542 fn test_mean_close_none_for_empty_slice() {
6543 assert!(OhlcvBar::mean_close(&[]).is_none());
6544 }
6545
6546 #[test]
6547 fn test_mean_close_single_bar() {
6548 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
6549 assert_eq!(OhlcvBar::mean_close(&[bar]), Some(dec!(105)));
6550 }
6551
6552 #[test]
6553 fn test_mean_close_multiple_bars() {
6554 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
6555 let b2 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(110));
6556 let b3 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(120));
6557 assert_eq!(OhlcvBar::mean_close(&[b1, b2, b3]), Some(dec!(110)));
6559 }
6560
6561 #[test]
6564 fn test_close_std_dev_none_for_single_bar() {
6565 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
6566 assert!(OhlcvBar::close_std_dev(&[bar]).is_none());
6567 }
6568
6569 #[test]
6570 fn test_close_std_dev_zero_for_identical_closes() {
6571 let bars = vec![
6572 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100)),
6573 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100)),
6574 ];
6575 let sd = OhlcvBar::close_std_dev(&bars).unwrap();
6576 assert!(sd.abs() < 1e-10, "std_dev should be ~0 for identical closes");
6577 }
6578
6579 #[test]
6580 fn test_close_std_dev_positive_for_varied_closes() {
6581 let bars = vec![
6582 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(90)),
6583 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(110)),
6584 ];
6585 assert!(OhlcvBar::close_std_dev(&bars).unwrap() > 0.0);
6586 }
6587
6588 #[test]
6591 fn test_price_efficiency_ratio_none_for_single_bar() {
6592 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
6593 assert!(OhlcvBar::price_efficiency_ratio(&[bar]).is_none());
6594 }
6595
6596 #[test]
6597 fn test_price_efficiency_ratio_one_for_trending_price() {
6598 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
6600 let b2 = make_ohlcv_bar(dec!(110), dec!(120), dec!(100), dec!(110));
6601 let b3 = make_ohlcv_bar(dec!(120), dec!(130), dec!(110), dec!(120));
6602 let ratio = OhlcvBar::price_efficiency_ratio(&[b1, b2, b3]).unwrap();
6604 assert!(ratio > 0.0 && ratio <= 1.0);
6605 }
6606
6607 #[test]
6608 fn test_price_efficiency_ratio_none_for_zero_total_range() {
6609 let bars = vec![
6611 make_ohlcv_bar(dec!(100), dec!(100), dec!(100), dec!(100)),
6612 make_ohlcv_bar(dec!(100), dec!(100), dec!(100), dec!(100)),
6613 ];
6614 assert!(OhlcvBar::price_efficiency_ratio(&bars).is_none());
6615 }
6616
6617 #[test]
6620 fn test_clv_plus_one_when_close_at_high() {
6621 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(110));
6623 let clv = bar.close_location_value().unwrap();
6624 assert!((clv - 1.0).abs() < 1e-10, "CLV should be 1.0 when close == high, got {clv}");
6625 }
6626
6627 #[test]
6628 fn test_clv_minus_one_when_close_at_low() {
6629 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(90));
6630 let clv = bar.close_location_value().unwrap();
6631 assert!((clv + 1.0).abs() < 1e-10, "CLV should be -1.0 when close == low, got {clv}");
6632 }
6633
6634 #[test]
6635 fn test_clv_zero_when_close_at_midpoint() {
6636 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
6637 let clv = bar.close_location_value().unwrap();
6638 assert!(clv.abs() < 1e-10, "CLV should be 0 at midpoint, got {clv}");
6639 }
6640
6641 #[test]
6642 fn test_clv_none_for_zero_range_bar() {
6643 let bar = make_ohlcv_bar(dec!(100), dec!(100), dec!(100), dec!(100));
6644 assert!(bar.close_location_value().is_none());
6645 }
6646
6647 #[test]
6648 fn test_mean_clv_none_for_empty_slice() {
6649 assert!(OhlcvBar::mean_clv(&[]).is_none());
6650 }
6651
6652 #[test]
6653 fn test_mean_clv_positive_for_bullish_closes() {
6654 let bars = vec![
6655 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(108)), make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(106)), ];
6658 let clv = OhlcvBar::mean_clv(&bars).unwrap();
6659 assert!(clv > 0.0, "mean CLV should be positive when closes are near highs");
6660 }
6661
6662 #[test]
6663 fn test_mean_range_none_for_empty_slice() {
6664 assert!(OhlcvBar::mean_range(&[]).is_none());
6665 }
6666
6667 #[test]
6668 fn test_mean_range_single_bar() {
6669 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
6670 assert_eq!(OhlcvBar::mean_range(&[bar]), Some(dec!(20)));
6671 }
6672
6673 #[test]
6674 fn test_mean_range_multiple_bars() {
6675 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)));
6678 }
6679
6680 #[test]
6681 fn test_close_z_score_none_for_empty_slice() {
6682 assert!(OhlcvBar::close_z_score(&[], dec!(100)).is_none());
6683 }
6684
6685 #[test]
6686 fn test_close_z_score_of_mean_is_zero() {
6687 let bars = vec![
6688 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100)),
6689 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100)),
6690 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(110)),
6691 ];
6692 let mean = (dec!(100) + dec!(100) + dec!(110)) / dec!(3);
6694 let z = OhlcvBar::close_z_score(&bars, mean).unwrap();
6695 assert!(z.abs() < 1e-6);
6696 }
6697
6698 #[test]
6699 fn test_close_z_score_positive_above_mean() {
6700 let bars = vec![
6701 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100)),
6702 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100)),
6703 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(110)),
6704 ];
6705 let z = OhlcvBar::close_z_score(&bars, dec!(120)).unwrap();
6706 assert!(z > 0.0);
6707 }
6708
6709 #[test]
6710 fn test_bollinger_band_width_none_for_empty_slice() {
6711 assert!(OhlcvBar::bollinger_band_width(&[]).is_none());
6712 }
6713
6714 #[test]
6715 fn test_bollinger_band_width_zero_for_identical_closes() {
6716 let bars = vec![
6717 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100)),
6718 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100)),
6719 ];
6720 assert_eq!(OhlcvBar::bollinger_band_width(&bars), Some(0.0));
6721 }
6722
6723 #[test]
6724 fn test_bollinger_band_width_positive_for_varying_closes() {
6725 let bars = vec![
6726 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(90)),
6727 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100)),
6728 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(110)),
6729 ];
6730 let bw = OhlcvBar::bollinger_band_width(&bars).unwrap();
6731 assert!(bw > 0.0);
6732 }
6733
6734 #[test]
6735 fn test_up_down_ratio_none_for_no_bearish_bars() {
6736 let bars = vec![
6737 make_ohlcv_bar(dec!(90), dec!(110), dec!(85), dec!(105)), ];
6739 assert!(OhlcvBar::up_down_ratio(&bars).is_none());
6740 }
6741
6742 #[test]
6743 fn test_up_down_ratio_two_to_one() {
6744 let bull = make_ohlcv_bar(dec!(90), dec!(110), dec!(85), dec!(105));
6745 let bear = make_ohlcv_bar(dec!(110), dec!(115), dec!(85), dec!(95));
6746 let bars = vec![bull.clone(), bull, bear];
6747 let ratio = OhlcvBar::up_down_ratio(&bars).unwrap();
6748 assert!((ratio - 2.0).abs() < 1e-9);
6749 }
6750
6751 #[test]
6754 fn test_volume_weighted_close_none_for_empty_slice() {
6755 assert!(OhlcvBar::volume_weighted_close(&[]).is_none());
6756 }
6757
6758 #[test]
6759 fn test_volume_weighted_close_single_bar() {
6760 let mut bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
6761 bar.volume = dec!(10);
6762 assert_eq!(OhlcvBar::volume_weighted_close(&[bar]), Some(dec!(105)));
6763 }
6764
6765 #[test]
6766 fn test_volume_weighted_close_weights_by_volume() {
6767 let mut b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
6768 b1.volume = dec!(1);
6769 let mut b2 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(200));
6770 b2.volume = dec!(3);
6771 assert_eq!(OhlcvBar::volume_weighted_close(&[b1, b2]), Some(dec!(175)));
6773 }
6774
6775 #[test]
6778 fn test_rolling_return_none_for_empty_slice() {
6779 assert!(OhlcvBar::rolling_return(&[]).is_none());
6780 }
6781
6782 #[test]
6783 fn test_rolling_return_none_for_single_bar() {
6784 assert!(OhlcvBar::rolling_return(&[make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105))]).is_none());
6785 }
6786
6787 #[test]
6788 fn test_rolling_return_positive_when_close_rises() {
6789 let b1 = make_ohlcv_bar(dec!(100), dec!(105), dec!(95), dec!(100));
6790 let b2 = make_ohlcv_bar(dec!(100), dec!(115), dec!(95), dec!(110));
6791 let ret = OhlcvBar::rolling_return(&[b1, b2]).unwrap();
6792 assert!((ret - 0.1).abs() < 1e-9);
6793 }
6794
6795 #[test]
6798 fn test_average_high_none_for_empty_slice() {
6799 assert!(OhlcvBar::average_high(&[]).is_none());
6800 }
6801
6802 #[test]
6803 fn test_average_high_single_bar() {
6804 let bar = make_ohlcv_bar(dec!(100), dec!(120), dec!(90), dec!(105));
6805 assert_eq!(OhlcvBar::average_high(&[bar]), Some(dec!(120)));
6806 }
6807
6808 #[test]
6809 fn test_average_high_multiple_bars() {
6810 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
6811 let b2 = make_ohlcv_bar(dec!(100), dec!(130), dec!(90), dec!(105));
6812 assert_eq!(OhlcvBar::average_high(&[b1, b2]), Some(dec!(120)));
6813 }
6814
6815 #[test]
6816 fn test_average_low_none_for_empty_slice() {
6817 assert!(OhlcvBar::average_low(&[]).is_none());
6818 }
6819
6820 #[test]
6821 fn test_average_low_single_bar() {
6822 let bar = make_ohlcv_bar(dec!(100), dec!(120), dec!(80), dec!(105));
6823 assert_eq!(OhlcvBar::average_low(&[bar]), Some(dec!(80)));
6824 }
6825
6826 #[test]
6827 fn test_average_low_multiple_bars() {
6828 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(80), dec!(105));
6829 let b2 = make_ohlcv_bar(dec!(100), dec!(130), dec!(60), dec!(105));
6830 assert_eq!(OhlcvBar::average_low(&[b1, b2]), Some(dec!(70)));
6831 }
6832
6833 #[test]
6836 fn test_min_body_none_for_empty_slice() {
6837 assert!(OhlcvBar::min_body(&[]).is_none());
6838 }
6839
6840 #[test]
6841 fn test_min_body_returns_smallest_body() {
6842 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)));
6845 }
6846
6847 #[test]
6848 fn test_max_body_none_for_empty_slice() {
6849 assert!(OhlcvBar::max_body(&[]).is_none());
6850 }
6851
6852 #[test]
6853 fn test_max_body_returns_largest_body() {
6854 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)));
6857 }
6858
6859 #[test]
6862 fn test_atr_pct_none_for_single_bar() {
6863 assert!(OhlcvBar::atr_pct(&[make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105))]).is_none());
6864 }
6865
6866 #[test]
6867 fn test_atr_pct_positive_for_normal_bars() {
6868 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
6869 let b2 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
6870 let pct = OhlcvBar::atr_pct(&[b1, b2]).unwrap();
6871 assert!(pct > 0.0);
6872 }
6873
6874 #[test]
6877 fn test_breakout_count_zero_for_empty_slice() {
6878 assert_eq!(OhlcvBar::breakout_count(&[]), 0);
6879 }
6880
6881 #[test]
6882 fn test_breakout_count_zero_for_single_bar() {
6883 assert_eq!(OhlcvBar::breakout_count(&[make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105))]), 0);
6884 }
6885
6886 #[test]
6887 fn test_breakout_count_detects_close_above_prev_high() {
6888 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(108));
6890 let b2 = make_ohlcv_bar(dec!(108), dec!(120), dec!(105), dec!(115));
6891 assert_eq!(OhlcvBar::breakout_count(&[b1, b2]), 1);
6892 }
6893
6894 #[test]
6895 fn test_breakout_count_zero_when_close_at_prev_high() {
6896 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(108));
6898 let b2 = make_ohlcv_bar(dec!(108), dec!(115), dec!(105), dec!(110));
6899 assert_eq!(OhlcvBar::breakout_count(&[b1, b2]), 0);
6900 }
6901
6902 #[test]
6905 fn test_doji_count_zero_for_empty_slice() {
6906 assert_eq!(OhlcvBar::doji_count(&[], dec!(0.001)), 0);
6907 }
6908
6909 #[test]
6910 fn test_doji_count_detects_doji_bars() {
6911 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);
6914 }
6915
6916 #[test]
6919 fn test_channel_width_none_for_empty_slice() {
6920 assert!(OhlcvBar::channel_width(&[]).is_none());
6921 }
6922
6923 #[test]
6924 fn test_channel_width_correct() {
6925 let b1 = make_ohlcv_bar(dec!(100), dec!(120), dec!(90), dec!(105));
6926 let b2 = make_ohlcv_bar(dec!(100), dec!(115), dec!(80), dec!(100));
6927 assert_eq!(OhlcvBar::channel_width(&[b1, b2]), Some(dec!(40)));
6929 }
6930
6931 #[test]
6934 fn test_sma_none_for_zero_period() {
6935 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
6936 assert!(OhlcvBar::sma(&[bar], 0).is_none());
6937 }
6938
6939 #[test]
6940 fn test_sma_none_when_fewer_bars_than_period() {
6941 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
6942 assert!(OhlcvBar::sma(&[bar], 3).is_none());
6943 }
6944
6945 #[test]
6946 fn test_sma_correct_for_last_n_bars() {
6947 let bars = vec![
6948 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100)),
6949 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(110)),
6950 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(120)),
6951 ];
6952 assert_eq!(OhlcvBar::sma(&bars, 3), Some(dec!(110)));
6954 }
6955
6956 #[test]
6959 fn test_mean_wick_ratio_none_for_empty_slice() {
6960 assert!(OhlcvBar::mean_wick_ratio(&[]).is_none());
6961 }
6962
6963 #[test]
6964 fn test_mean_wick_ratio_in_range_zero_to_one() {
6965 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
6966 let b2 = make_ohlcv_bar(dec!(100), dec!(115), dec!(85), dec!(100));
6967 let ratio = OhlcvBar::mean_wick_ratio(&[b1, b2]).unwrap();
6968 assert!(ratio >= 0.0 && ratio <= 1.0);
6969 }
6970
6971 #[test]
6974 fn test_bullish_volume_zero_for_empty_slice() {
6975 assert_eq!(OhlcvBar::bullish_volume(&[]), dec!(0));
6976 }
6977
6978 #[test]
6979 fn test_bullish_volume_sums_bullish_bars() {
6980 let mut bull = make_ohlcv_bar(dec!(90), dec!(110), dec!(85), dec!(105));
6981 bull.volume = dec!(100);
6982 let mut bear = make_ohlcv_bar(dec!(110), dec!(115), dec!(85), dec!(95));
6983 bear.volume = dec!(50);
6984 assert_eq!(OhlcvBar::bullish_volume(&[bull, bear]), dec!(100));
6985 }
6986
6987 #[test]
6988 fn test_bearish_volume_zero_for_empty_slice() {
6989 assert_eq!(OhlcvBar::bearish_volume(&[]), dec!(0));
6990 }
6991
6992 #[test]
6993 fn test_bearish_volume_sums_bearish_bars() {
6994 let mut bull = make_ohlcv_bar(dec!(90), dec!(110), dec!(85), dec!(105));
6995 bull.volume = dec!(100);
6996 let mut bear = make_ohlcv_bar(dec!(110), dec!(115), dec!(85), dec!(95));
6997 bear.volume = dec!(50);
6998 assert_eq!(OhlcvBar::bearish_volume(&[bull, bear]), dec!(50));
6999 }
7000
7001 #[test]
7004 fn test_close_above_mid_count_zero_for_empty_slice() {
7005 assert_eq!(OhlcvBar::close_above_mid_count(&[]), 0);
7006 }
7007
7008 #[test]
7009 fn test_close_above_mid_count_correct() {
7010 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);
7014 }
7015
7016 #[test]
7019 fn test_ema_none_for_empty_slice() {
7020 assert!(OhlcvBar::ema(&[], 0.5).is_none());
7021 }
7022
7023 #[test]
7024 fn test_ema_single_bar_equals_close() {
7025 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
7026 let e = OhlcvBar::ema(&[bar], 0.5).unwrap();
7027 assert!((e - 105.0).abs() < 1e-9);
7028 }
7029
7030 #[test]
7031 fn test_ema_alpha_one_equals_last_close() {
7032 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
7033 let b2 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(200));
7034 let e = OhlcvBar::ema(&[b1, b2], 1.0).unwrap();
7035 assert!((e - 200.0).abs() < 1e-9);
7036 }
7037
7038 #[test]
7041 fn test_highest_open_none_for_empty_slice() {
7042 assert!(OhlcvBar::highest_open(&[]).is_none());
7043 }
7044
7045 #[test]
7046 fn test_highest_open_returns_max() {
7047 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
7048 let b2 = make_ohlcv_bar(dec!(130), dec!(140), dec!(120), dec!(135));
7049 assert_eq!(OhlcvBar::highest_open(&[b1, b2]), Some(dec!(130)));
7050 }
7051
7052 #[test]
7053 fn test_lowest_open_none_for_empty_slice() {
7054 assert!(OhlcvBar::lowest_open(&[]).is_none());
7055 }
7056
7057 #[test]
7058 fn test_lowest_open_returns_min() {
7059 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
7060 let b2 = make_ohlcv_bar(dec!(130), dec!(140), dec!(120), dec!(135));
7061 assert_eq!(OhlcvBar::lowest_open(&[b1, b2]), Some(dec!(100)));
7062 }
7063
7064 #[test]
7067 fn test_rising_close_count_zero_for_empty_slice() {
7068 assert_eq!(OhlcvBar::rising_close_count(&[]), 0);
7069 }
7070
7071 #[test]
7072 fn test_rising_close_count_zero_for_single_bar() {
7073 assert_eq!(OhlcvBar::rising_close_count(&[make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105))]), 0);
7074 }
7075
7076 #[test]
7077 fn test_rising_close_count_correct() {
7078 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
7079 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);
7083 }
7084
7085 #[test]
7088 fn test_mean_body_ratio_none_for_empty_slice() {
7089 assert!(OhlcvBar::mean_body_ratio(&[]).is_none());
7090 }
7091
7092 #[test]
7093 fn test_mean_body_ratio_in_range_zero_to_one() {
7094 let b1 = make_ohlcv_bar(dec!(100), dec!(120), dec!(80), dec!(110));
7095 let b2 = make_ohlcv_bar(dec!(100), dec!(120), dec!(80), dec!(100));
7096 let ratio = OhlcvBar::mean_body_ratio(&[b1, b2]).unwrap();
7097 assert!(ratio >= 0.0 && ratio <= 1.0);
7098 }
7099
7100 #[test]
7103 fn test_volume_std_dev_none_for_single_bar() {
7104 let mut b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
7105 b.volume = dec!(100);
7106 assert!(OhlcvBar::volume_std_dev(&[b]).is_none());
7107 }
7108
7109 #[test]
7110 fn test_volume_std_dev_zero_for_identical_volumes() {
7111 let mut b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)); b1.volume = dec!(50);
7112 let mut b2 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)); b2.volume = dec!(50);
7113 assert_eq!(OhlcvBar::volume_std_dev(&[b1, b2]), Some(0.0));
7114 }
7115
7116 #[test]
7117 fn test_volume_std_dev_positive_for_varied_volumes() {
7118 let mut b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)); b1.volume = dec!(10);
7119 let mut b2 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)); b2.volume = dec!(100);
7120 let std = OhlcvBar::volume_std_dev(&[b1, b2]).unwrap();
7121 assert!(std > 0.0);
7122 }
7123
7124 #[test]
7127 fn test_max_volume_bar_none_for_empty_slice() {
7128 assert!(OhlcvBar::max_volume_bar(&[]).is_none());
7129 }
7130
7131 #[test]
7132 fn test_max_volume_bar_returns_highest_volume() {
7133 let mut b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)); b1.volume = dec!(10);
7134 let mut b2 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)); b2.volume = dec!(100);
7135 let bars = [b1, b2];
7136 let bar = OhlcvBar::max_volume_bar(&bars).unwrap();
7137 assert_eq!(bar.volume, dec!(100));
7138 }
7139
7140 #[test]
7141 fn test_min_volume_bar_returns_lowest_volume() {
7142 let mut b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)); b1.volume = dec!(10);
7143 let mut b2 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)); b2.volume = dec!(100);
7144 let bars = [b1, b2];
7145 let bar = OhlcvBar::min_volume_bar(&bars).unwrap();
7146 assert_eq!(bar.volume, dec!(10));
7147 }
7148
7149 #[test]
7152 fn test_gap_sum_zero_for_empty_slice() {
7153 assert_eq!(OhlcvBar::gap_sum(&[]), dec!(0));
7154 }
7155
7156 #[test]
7157 fn test_gap_sum_zero_for_single_bar() {
7158 assert_eq!(OhlcvBar::gap_sum(&[make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105))]), dec!(0));
7159 }
7160
7161 #[test]
7162 fn test_gap_sum_positive_for_gap_up_sequence() {
7163 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
7165 let b2 = make_ohlcv_bar(dec!(110), dec!(120), dec!(105), dec!(115));
7166 assert_eq!(OhlcvBar::gap_sum(&[b1, b2]), dec!(10));
7167 }
7168
7169 #[test]
7170 fn test_gap_sum_negative_for_gap_down_sequence() {
7171 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
7173 let b2 = make_ohlcv_bar(dec!(90), dec!(95), dec!(80), dec!(85));
7174 assert_eq!(OhlcvBar::gap_sum(&[b1, b2]), dec!(-10));
7175 }
7176
7177 #[test]
7180 fn test_three_white_soldiers_false_for_fewer_than_3_bars() {
7181 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
7182 assert!(!OhlcvBar::three_white_soldiers(&[b]));
7183 }
7184
7185 #[test]
7186 fn test_three_white_soldiers_true_for_classic_pattern() {
7187 let b1 = make_ohlcv_bar(dec!(100), dec!(115), dec!(98), dec!(112));
7188 let b2 = make_ohlcv_bar(dec!(112), dec!(128), dec!(110), dec!(125));
7189 let b3 = make_ohlcv_bar(dec!(125), dec!(142), dec!(123), dec!(140));
7190 assert!(OhlcvBar::three_white_soldiers(&[b1, b2, b3]));
7191 }
7192
7193 #[test]
7194 fn test_three_white_soldiers_false_for_bearish_bar_in_sequence() {
7195 let b1 = make_ohlcv_bar(dec!(100), dec!(115), dec!(98), dec!(112));
7197 let b2 = make_ohlcv_bar(dec!(115), dec!(120), dec!(105), dec!(108));
7198 let b3 = make_ohlcv_bar(dec!(108), dec!(130), dec!(106), dec!(128));
7199 assert!(!OhlcvBar::three_white_soldiers(&[b1, b2, b3]));
7200 }
7201
7202 #[test]
7205 fn test_three_black_crows_false_for_fewer_than_3_bars() {
7206 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(95));
7207 assert!(!OhlcvBar::three_black_crows(&[b]));
7208 }
7209
7210 #[test]
7211 fn test_three_black_crows_true_for_classic_pattern() {
7212 let b1 = make_ohlcv_bar(dec!(140), dec!(142), dec!(110), dec!(112));
7213 let b2 = make_ohlcv_bar(dec!(112), dec!(114), dec!(95), dec!(97));
7214 let b3 = make_ohlcv_bar(dec!(97), dec!(99), dec!(80), dec!(82));
7215 assert!(OhlcvBar::three_black_crows(&[b1, b2, b3]));
7216 }
7217
7218 #[test]
7219 fn test_three_black_crows_false_for_bullish_bar_in_sequence() {
7220 let b1 = make_ohlcv_bar(dec!(140), dec!(142), dec!(110), dec!(112));
7222 let b2 = make_ohlcv_bar(dec!(108), dec!(120), dec!(106), dec!(118));
7223 let b3 = make_ohlcv_bar(dec!(115), dec!(116), dec!(90), dec!(92));
7224 assert!(!OhlcvBar::three_black_crows(&[b1, b2, b3]));
7225 }
7226
7227 #[test]
7230 fn test_is_gap_bar_true_when_open_differs_from_prev_close() {
7231 let bar = make_ohlcv_bar(dec!(105), dec!(115), dec!(100), dec!(110));
7232 assert!(OhlcvBar::is_gap_bar(&bar, dec!(100)));
7233 }
7234
7235 #[test]
7236 fn test_is_gap_bar_false_when_open_equals_prev_close() {
7237 let bar = make_ohlcv_bar(dec!(100), dec!(115), dec!(98), dec!(110));
7238 assert!(!OhlcvBar::is_gap_bar(&bar, dec!(100)));
7239 }
7240
7241 #[test]
7244 fn test_gap_bars_count_zero_for_empty_slice() {
7245 assert_eq!(OhlcvBar::gap_bars_count(&[]), 0);
7246 }
7247
7248 #[test]
7249 fn test_gap_bars_count_zero_when_no_gaps() {
7250 let b1 = make_ohlcv_bar(dec!(90), dec!(105), dec!(88), dec!(100));
7252 let b2 = make_ohlcv_bar(dec!(100), dec!(115), dec!(98), dec!(110));
7253 assert_eq!(OhlcvBar::gap_bars_count(&[b1, b2]), 0);
7254 }
7255
7256 #[test]
7257 fn test_gap_bars_count_counts_all_gaps() {
7258 let b1 = make_ohlcv_bar(dec!(90), dec!(105), dec!(88), dec!(100));
7259 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);
7263 }
7264
7265 #[test]
7268 fn test_inside_bar_true_when_range_inside_prior_v2() {
7269 let prior = make_ohlcv_bar(dec!(100), dec!(120), dec!(80), dec!(110));
7270 let bar = make_ohlcv_bar(dec!(105), dec!(115), dec!(90), dec!(108));
7271 assert!(bar.inside_bar(&prior));
7272 }
7273
7274 #[test]
7275 fn test_inside_bar_false_when_high_exceeds_prior_v2() {
7276 let prior = make_ohlcv_bar(dec!(100), dec!(120), dec!(80), dec!(110));
7277 let bar = make_ohlcv_bar(dec!(105), dec!(125), dec!(90), dec!(118));
7278 assert!(!bar.inside_bar(&prior));
7279 }
7280
7281 #[test]
7282 fn test_outside_bar_true_when_range_engulfs_prior_v2() {
7283 let prior = make_ohlcv_bar(dec!(100), dec!(115), dec!(90), dec!(108));
7284 let bar = make_ohlcv_bar(dec!(95), dec!(120), dec!(85), dec!(112));
7285 assert!(bar.outside_bar(&prior));
7286 }
7287
7288 #[test]
7289 fn test_outside_bar_false_when_range_is_inside_v2() {
7290 let prior = make_ohlcv_bar(dec!(100), dec!(120), dec!(80), dec!(110));
7291 let bar = make_ohlcv_bar(dec!(105), dec!(115), dec!(90), dec!(108));
7292 assert!(!bar.outside_bar(&prior));
7293 }
7294
7295 #[test]
7298 fn test_bar_efficiency_none_for_zero_range_bar() {
7299 let bar = make_ohlcv_bar(dec!(100), dec!(100), dec!(100), dec!(100));
7300 assert!(OhlcvBar::bar_efficiency(&bar).is_none());
7301 }
7302
7303 #[test]
7304 fn test_bar_efficiency_one_for_full_trend_bar() {
7305 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(100), dec!(110));
7307 let eff = OhlcvBar::bar_efficiency(&bar).unwrap();
7308 assert!((eff - 1.0).abs() < 1e-9);
7309 }
7310
7311 #[test]
7312 fn test_bar_efficiency_between_zero_and_one() {
7313 let bar = make_ohlcv_bar(dec!(100), dec!(115), dec!(95), dec!(108));
7314 let eff = OhlcvBar::bar_efficiency(&bar).unwrap();
7315 assert!(eff >= 0.0 && eff <= 1.0);
7316 }
7317
7318 #[test]
7321 fn test_wicks_sum_zero_for_empty_slice() {
7322 assert_eq!(OhlcvBar::wicks_sum(&[]), dec!(0));
7323 }
7324
7325 #[test]
7326 fn test_wicks_sum_correct_for_doji_like_bar() {
7327 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
7329 assert_eq!(OhlcvBar::wicks_sum(&[bar]), dec!(20));
7330 }
7331
7332 #[test]
7335 fn test_avg_close_to_high_none_for_empty_slice() {
7336 assert!(OhlcvBar::avg_close_to_high(&[]).is_none());
7337 }
7338
7339 #[test]
7340 fn test_avg_close_to_high_correct_for_two_bars() {
7341 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(95), dec!(105));
7343 let b2 = make_ohlcv_bar(dec!(110), dec!(120), dec!(108), dec!(115));
7344 let avg = OhlcvBar::avg_close_to_high(&[b1, b2]).unwrap();
7345 assert!((avg - 5.0).abs() < 1e-9);
7346 }
7347
7348 #[test]
7351 fn test_avg_range_r65_none_for_empty() {
7352 assert!(OhlcvBar::avg_range(&[]).is_none());
7353 }
7354
7355 #[test]
7356 fn test_avg_range_r65_correct() {
7357 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
7358 let b2 = make_ohlcv_bar(dec!(105), dec!(120), dec!(100), dec!(115));
7359 let avg = OhlcvBar::avg_range(&[b1, b2]).unwrap();
7360 assert!((avg - 20.0).abs() < 1e-9);
7361 }
7362
7363 #[test]
7366 fn test_max_close_r65_none_empty() {
7367 assert!(OhlcvBar::max_close(&[]).is_none());
7368 }
7369
7370 #[test]
7371 fn test_max_close_r65_highest() {
7372 let b1 = make_ohlcv_bar(dec!(100), dec!(115), dec!(95), dec!(110));
7373 let b2 = make_ohlcv_bar(dec!(110), dec!(130), dec!(108), dec!(125));
7374 let b3 = make_ohlcv_bar(dec!(115), dec!(120), dec!(112), dec!(118));
7375 assert_eq!(OhlcvBar::max_close(&[b1, b2, b3]), Some(dec!(125)));
7376 }
7377
7378 #[test]
7379 fn test_min_close_r65_lowest() {
7380 let b1 = make_ohlcv_bar(dec!(100), dec!(115), dec!(95), dec!(110));
7381 let b2 = make_ohlcv_bar(dec!(110), dec!(130), dec!(108), dec!(125));
7382 let b3 = make_ohlcv_bar(dec!(90), dec!(105), dec!(88), dec!(95));
7383 assert_eq!(OhlcvBar::min_close(&[b1, b2, b3]), Some(dec!(95)));
7384 }
7385
7386 #[test]
7389 fn test_trend_strength_r65_none_single() {
7390 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
7391 assert!(OhlcvBar::trend_strength(&[b]).is_none());
7392 }
7393
7394 #[test]
7395 fn test_trend_strength_r65_one_bullish() {
7396 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(95), dec!(105));
7397 let b2 = make_ohlcv_bar(dec!(105), dec!(120), dec!(103), dec!(115));
7398 let b3 = make_ohlcv_bar(dec!(115), dec!(130), dec!(113), dec!(128));
7399 let s = OhlcvBar::trend_strength(&[b1, b2, b3]).unwrap();
7400 assert!((s - 1.0).abs() < 1e-9);
7401 }
7402
7403 #[test]
7404 fn test_trend_strength_r65_zero_bearish() {
7405 let b1 = make_ohlcv_bar(dec!(128), dec!(130), dec!(113), dec!(128));
7406 let b2 = make_ohlcv_bar(dec!(115), dec!(120), dec!(103), dec!(110));
7407 let b3 = make_ohlcv_bar(dec!(105), dec!(110), dec!(95), dec!(100));
7408 let s = OhlcvBar::trend_strength(&[b1, b2, b3]).unwrap();
7409 assert!((s - 0.0).abs() < 1e-9);
7410 }
7411
7412 #[test]
7415 fn test_net_change_none_for_empty() {
7416 assert!(OhlcvBar::net_change(&[]).is_none());
7417 }
7418
7419 #[test]
7420 fn test_net_change_positive_for_bullish_bar() {
7421 let b = make_ohlcv_bar(dec!(100), dec!(115), dec!(98), dec!(112));
7422 assert_eq!(OhlcvBar::net_change(&[b]), Some(dec!(12)));
7423 }
7424
7425 #[test]
7426 fn test_net_change_negative_for_bearish_bar() {
7427 let b = make_ohlcv_bar(dec!(110), dec!(112), dec!(95), dec!(100));
7428 assert_eq!(OhlcvBar::net_change(&[b]), Some(dec!(-10)));
7429 }
7430
7431 #[test]
7434 fn test_open_to_close_pct_none_for_empty() {
7435 assert!(OhlcvBar::open_to_close_pct(&[]).is_none());
7436 }
7437
7438 #[test]
7439 fn test_open_to_close_pct_correct() {
7440 let b = make_ohlcv_bar(dec!(100), dec!(115), dec!(98), dec!(110));
7442 let pct = OhlcvBar::open_to_close_pct(&[b]).unwrap();
7443 assert!((pct - 10.0).abs() < 1e-9);
7444 }
7445
7446 #[test]
7449 fn test_high_to_low_pct_none_for_empty() {
7450 assert!(OhlcvBar::high_to_low_pct(&[]).is_none());
7451 }
7452
7453 #[test]
7454 fn test_high_to_low_pct_correct() {
7455 let b = make_ohlcv_bar(dec!(150), dec!(200), dec!(100), dec!(160));
7457 let pct = OhlcvBar::high_to_low_pct(&[b]).unwrap();
7458 assert!((pct - 50.0).abs() < 1e-9);
7459 }
7460
7461 #[test]
7464 fn test_consecutive_highs_zero_for_single_bar() {
7465 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
7466 assert_eq!(OhlcvBar::consecutive_highs(&[b]), 0);
7467 }
7468
7469 #[test]
7470 fn test_consecutive_highs_counts_trailing_highs() {
7471 let b1 = make_ohlcv_bar(dec!(95), dec!(110), dec!(90), dec!(105));
7473 let b2 = make_ohlcv_bar(dec!(105), dec!(120), dec!(103), dec!(115));
7474 let b3 = make_ohlcv_bar(dec!(115), dec!(130), dec!(113), dec!(125));
7475 assert_eq!(OhlcvBar::consecutive_highs(&[b1, b2, b3]), 2);
7476 }
7477
7478 #[test]
7479 fn test_consecutive_lows_counts_trailing_lows() {
7480 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
7482 let b2 = make_ohlcv_bar(dec!(95), dec!(108), dec!(80), dec!(100));
7483 let b3 = make_ohlcv_bar(dec!(90), dec!(102), dec!(70), dec!(95));
7484 assert_eq!(OhlcvBar::consecutive_lows(&[b1, b2, b3]), 2);
7485 }
7486
7487 #[test]
7490 fn test_volume_change_pct_none_for_single_bar() {
7491 let mut b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
7492 b.volume = dec!(100);
7493 assert!(OhlcvBar::volume_change_pct(&[b]).is_none());
7494 }
7495
7496 #[test]
7497 fn test_volume_change_pct_correct() {
7498 let mut b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)); b1.volume = dec!(100);
7500 let mut b2 = make_ohlcv_bar(dec!(105), dec!(115), dec!(103), dec!(110)); b2.volume = dec!(150);
7501 let pct = OhlcvBar::volume_change_pct(&[b1, b2]).unwrap();
7502 assert!((pct - 50.0).abs() < 1e-9);
7503 }
7504
7505 #[test]
7508 fn test_clv_r67_plus_one_at_high() {
7509 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(110));
7511 let clv = b.close_location_value().unwrap();
7512 assert!((clv - 1.0).abs() < 1e-9);
7513 }
7514
7515 #[test]
7516 fn test_clv_r67_minus_one_at_low() {
7517 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(90));
7519 let clv = b.close_location_value().unwrap();
7520 assert!((clv - (-1.0)).abs() < 1e-9);
7521 }
7522
7523 #[test]
7524 fn test_clv_r67_none_for_zero_range() {
7525 let b = make_ohlcv_bar(dec!(100), dec!(100), dec!(100), dec!(100));
7526 assert!(b.close_location_value().is_none());
7527 }
7528
7529 #[test]
7532 fn test_body_pct_r67_none_for_zero_range() {
7533 let b = make_ohlcv_bar(dec!(100), dec!(100), dec!(100), dec!(100));
7534 assert!(b.body_pct().is_none());
7535 }
7536
7537 #[test]
7538 fn test_body_pct_r67_100_for_full_body() {
7539 let b = make_ohlcv_bar(dec!(90), dec!(110), dec!(90), dec!(110));
7541 assert_eq!(b.body_pct(), Some(dec!(100)));
7542 }
7543
7544 #[test]
7547 fn test_bullish_count_r67_correct() {
7548 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);
7552 }
7553
7554 #[test]
7555 fn test_bearish_count_r67_correct() {
7556 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);
7559 }
7560
7561 #[test]
7564 fn test_open_gap_pct_none_for_single_bar() {
7565 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
7566 assert!(OhlcvBar::open_gap_pct(&[b]).is_none());
7567 }
7568
7569 #[test]
7570 fn test_open_gap_pct_positive_for_gap_up() {
7571 let b1 = make_ohlcv_bar(dec!(90), dec!(105), dec!(88), dec!(100));
7573 let b2 = make_ohlcv_bar(dec!(105), dec!(115), dec!(103), dec!(110));
7574 let pct = OhlcvBar::open_gap_pct(&[b1, b2]).unwrap();
7575 assert!((pct - 5.0).abs() < 1e-9);
7576 }
7577
7578 #[test]
7581 fn test_volume_cumulative_zero_for_empty() {
7582 assert_eq!(OhlcvBar::volume_cumulative(&[]), dec!(0));
7583 }
7584
7585 #[test]
7586 fn test_volume_cumulative_sums_all_volumes() {
7587 let mut b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)); b1.volume = dec!(100);
7588 let mut b2 = make_ohlcv_bar(dec!(105), dec!(115), dec!(103), dec!(110)); b2.volume = dec!(200);
7589 assert_eq!(OhlcvBar::volume_cumulative(&[b1, b2]), dec!(300));
7590 }
7591
7592 #[test]
7595 fn test_price_position_none_for_empty() {
7596 assert!(OhlcvBar::price_position(&[]).is_none());
7597 }
7598
7599 #[test]
7600 fn test_price_position_one_when_close_at_highest() {
7601 let b1 = make_ohlcv_bar(dec!(85), dec!(100), dec!(80), dec!(95));
7603 let b2 = make_ohlcv_bar(dec!(110), dec!(120), dec!(100), dec!(120));
7604 let pos = OhlcvBar::price_position(&[b1, b2]).unwrap();
7605 assert!((pos - 1.0).abs() < 1e-9);
7606 }
7607
7608 #[test]
7611 fn test_close_above_open_count_zero_for_empty() {
7612 assert_eq!(OhlcvBar::close_above_open_count(&[]), 0);
7613 }
7614
7615 #[test]
7616 fn test_close_above_open_count_correct() {
7617 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(95), dec!(108));
7619 let b2 = make_ohlcv_bar(dec!(110), dec!(115), dec!(100), dec!(102));
7620 assert_eq!(OhlcvBar::close_above_open_count(&[b1, b2]), 1);
7621 }
7622
7623 #[test]
7626 fn test_volume_price_correlation_none_for_single_bar() {
7627 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
7628 assert!(OhlcvBar::volume_price_correlation(&[b]).is_none());
7629 }
7630
7631 #[test]
7632 fn test_volume_price_correlation_positive_for_comoving() {
7633 let mut b1 = make_ohlcv_bar(dec!(100), dec!(105), dec!(98), dec!(102)); b1.volume = dec!(100);
7635 let mut b2 = make_ohlcv_bar(dec!(102), dec!(110), dec!(100), dec!(108)); b2.volume = dec!(200);
7636 let corr = OhlcvBar::volume_price_correlation(&[b1, b2]).unwrap();
7637 assert!(corr > 0.0, "expected positive correlation, got {}", corr);
7638 }
7639
7640 #[test]
7643 fn test_body_consistency_none_for_empty() {
7644 assert!(OhlcvBar::body_consistency(&[]).is_none());
7645 }
7646
7647 #[test]
7648 fn test_body_consistency_one_for_all_big_bodies() {
7649 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(100), dec!(108));
7651 let b2 = make_ohlcv_bar(dec!(110), dec!(120), dec!(110), dec!(118));
7652 let r = OhlcvBar::body_consistency(&[b1, b2]).unwrap();
7653 assert!((r - 1.0).abs() < 1e-9, "expected 1.0, got {}", r);
7654 }
7655
7656 #[test]
7659 fn test_close_volatility_ratio_none_for_single_bar() {
7660 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
7661 assert!(OhlcvBar::close_volatility_ratio(&[b]).is_none());
7662 }
7663
7664 #[test]
7665 fn test_close_volatility_ratio_positive_for_varied_closes() {
7666 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
7667 let b2 = make_ohlcv_bar(dec!(110), dec!(120), dec!(105), dec!(115));
7668 let r = OhlcvBar::close_volatility_ratio(&[b1, b2]).unwrap();
7669 assert!(r > 0.0, "expected positive ratio, got {}", r);
7670 }
7671
7672 #[test]
7673 fn test_close_volatility_ratio_zero_for_identical_closes() {
7674 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
7675 let b2 = make_ohlcv_bar(dec!(100), dec!(112), dec!(88), dec!(105));
7676 let r = OhlcvBar::close_volatility_ratio(&[b1, b2]).unwrap();
7677 assert!((r - 0.0).abs() < 1e-9, "expected 0.0 for identical closes, got {}", r);
7678 }
7679
7680 #[test]
7683 fn test_is_trending_up_false_for_empty() {
7684 assert!(!OhlcvBar::is_trending_up(&[], 3));
7685 }
7686
7687 #[test]
7688 fn test_is_trending_up_false_for_n_less_than_2() {
7689 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
7690 assert!(!OhlcvBar::is_trending_up(&[b], 1));
7691 }
7692
7693 #[test]
7694 fn test_is_trending_up_true_for_rising_closes() {
7695 let b1 = make_ohlcv_bar(dec!(100), dec!(105), dec!(98), dec!(102));
7696 let b2 = make_ohlcv_bar(dec!(102), dec!(110), dec!(100), dec!(107));
7697 let b3 = make_ohlcv_bar(dec!(107), dec!(115), dec!(105), dec!(112));
7698 assert!(OhlcvBar::is_trending_up(&[b1, b2, b3], 3));
7699 }
7700
7701 #[test]
7702 fn test_is_trending_down_true_for_falling_closes() {
7703 let b1 = make_ohlcv_bar(dec!(112), dec!(115), dec!(105), dec!(110));
7704 let b2 = make_ohlcv_bar(dec!(110), dec!(112), dec!(100), dec!(105));
7705 let b3 = make_ohlcv_bar(dec!(105), dec!(108), dec!(95), dec!(98));
7706 assert!(OhlcvBar::is_trending_down(&[b1, b2, b3], 3));
7707 }
7708
7709 #[test]
7712 fn test_volume_acceleration_none_for_single_bar() {
7713 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
7714 assert!(OhlcvBar::volume_acceleration(&[b]).is_none());
7715 }
7716
7717 #[test]
7718 fn test_volume_acceleration_positive_when_volume_rises() {
7719 let mut b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)); b1.volume = dec!(100);
7720 let mut b2 = make_ohlcv_bar(dec!(105), dec!(115), dec!(103), dec!(110)); b2.volume = dec!(150);
7721 let acc = OhlcvBar::volume_acceleration(&[b1, b2]).unwrap();
7722 assert!(acc > 0.0, "volume rose so acceleration should be positive, got {}", acc);
7723 }
7724
7725 #[test]
7728 fn test_wick_body_ratio_none_for_empty() {
7729 assert!(OhlcvBar::wick_body_ratio(&[]).is_none());
7730 }
7731
7732 #[test]
7733 fn test_wick_body_ratio_none_for_doji_bar() {
7734 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
7736 assert!(OhlcvBar::wick_body_ratio(&[b]).is_none());
7737 }
7738
7739 #[test]
7740 fn test_wick_body_ratio_positive_for_wicked_bar() {
7741 let b = make_ohlcv_bar(dec!(100), dec!(115), dec!(95), dec!(105));
7743 let r = OhlcvBar::wick_body_ratio(&[b]).unwrap();
7744 assert!(r > 0.0, "expected positive wick/body ratio, got {}", r);
7745 }
7746
7747 #[test]
7750 fn test_close_momentum_score_none_for_empty() {
7751 assert!(OhlcvBar::close_momentum_score(&[]).is_none());
7752 }
7753
7754 #[test]
7755 fn test_close_momentum_score_half_for_symmetric() {
7756 let b1 = make_ohlcv_bar(dec!(88), dec!(95), dec!(85), dec!(90));
7758 let b2 = make_ohlcv_bar(dec!(108), dec!(115), dec!(105), dec!(110));
7759 let score = OhlcvBar::close_momentum_score(&[b1, b2]).unwrap();
7760 assert!((score - 0.5).abs() < 1e-9, "expected 0.5, got {}", score);
7761 }
7762
7763 #[test]
7766 fn test_range_expansion_count_zero_for_single_bar() {
7767 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
7768 assert_eq!(OhlcvBar::range_expansion_count(&[b]), 0);
7769 }
7770
7771 #[test]
7772 fn test_range_expansion_count_correct() {
7773 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
7775 let b2 = make_ohlcv_bar(dec!(105), dec!(120), dec!(90), dec!(110));
7776 assert_eq!(OhlcvBar::range_expansion_count(&[b1, b2]), 1);
7777 }
7778
7779 #[test]
7782 fn test_gap_count_zero_for_single_bar() {
7783 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
7784 assert_eq!(OhlcvBar::gap_count(&[b]), 0);
7785 }
7786
7787 #[test]
7788 fn test_gap_count_detects_gap() {
7789 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
7790 let b2 = make_ohlcv_bar(dec!(108), dec!(115), dec!(106), dec!(112));
7792 assert_eq!(OhlcvBar::gap_count(&[b1, b2]), 1);
7793 }
7794
7795 #[test]
7796 fn test_gap_count_zero_when_open_equals_close() {
7797 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
7798 let b2 = make_ohlcv_bar(dec!(105), dec!(115), dec!(103), dec!(112));
7800 assert_eq!(OhlcvBar::gap_count(&[b1, b2]), 0);
7801 }
7802
7803 #[test]
7806 fn test_avg_wick_size_none_for_empty() {
7807 assert!(OhlcvBar::avg_wick_size(&[]).is_none());
7808 }
7809
7810 #[test]
7811 fn test_avg_wick_size_correct() {
7812 let b = make_ohlcv_bar(dec!(100), dec!(115), dec!(95), dec!(105));
7815 let ws = OhlcvBar::avg_wick_size(&[b]).unwrap();
7816 assert!((ws - 15.0).abs() < 1e-6, "expected 15.0, got {}", ws);
7817 }
7818
7819 #[test]
7822 fn test_mean_volume_ratio_empty_for_empty_slice() {
7823 assert!(OhlcvBar::mean_volume_ratio(&[]).is_empty());
7824 }
7825
7826 #[test]
7827 fn test_mean_volume_ratio_sums_to_n_times_mean() {
7828 let mut b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)); b1.volume = dec!(100);
7829 let mut b2 = make_ohlcv_bar(dec!(105), dec!(115), dec!(103), dec!(110)); b2.volume = dec!(300);
7830 let ratios = OhlcvBar::mean_volume_ratio(&[b1, b2]);
7832 assert_eq!(ratios.len(), 2);
7833 let r0 = ratios[0].unwrap();
7834 let r1 = ratios[1].unwrap();
7835 assert!((r0 - 0.5).abs() < 1e-6, "expected 0.5, got {}", r0);
7836 assert!((r1 - 1.5).abs() < 1e-6, "expected 1.5, got {}", r1);
7837 }
7838
7839 #[test]
7842 fn test_price_compression_ratio_none_for_zero_range() {
7843 let b = make_ohlcv_bar(dec!(100), dec!(100), dec!(100), dec!(100));
7845 assert!(OhlcvBar::price_compression_ratio(&[b]).is_none());
7846 }
7847
7848 #[test]
7849 fn test_price_compression_ratio_in_range() {
7850 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(108));
7851 let r = OhlcvBar::price_compression_ratio(&[b]).unwrap();
7852 assert!(r >= 0.0 && r <= 1.0, "expected value in [0,1], got {}", r);
7853 }
7854
7855 #[test]
7858 fn test_open_close_spread_none_for_empty() {
7859 assert!(OhlcvBar::open_close_spread(&[]).is_none());
7860 }
7861
7862 #[test]
7863 fn test_open_close_spread_zero_for_doji() {
7864 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
7865 let s = OhlcvBar::open_close_spread(&[b]).unwrap();
7866 assert!((s - 0.0).abs() < 1e-9, "doji should have spread=0, got {}", s);
7867 }
7868
7869 #[test]
7870 fn test_open_close_spread_positive_for_directional_bar() {
7871 let b = make_ohlcv_bar(dec!(100), dec!(115), dec!(98), dec!(110));
7872 let s = OhlcvBar::open_close_spread(&[b]).unwrap();
7873 assert!(s > 0.0, "directional bar should have positive spread, got {}", s);
7874 }
7875
7876 #[test]
7879 fn test_close_above_high_ma_zero_for_too_few_bars() {
7880 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
7881 assert_eq!(OhlcvBar::close_above_high_ma(&[b], 2), 0);
7882 }
7883
7884 #[test]
7885 fn test_close_above_high_ma_detects_breakout() {
7886 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
7888 let b2 = make_ohlcv_bar(dec!(110), dec!(120), dec!(108), dec!(118));
7889 assert_eq!(OhlcvBar::close_above_high_ma(&[b1, b2], 2), 1);
7890 }
7891
7892 #[test]
7895 fn test_max_consecutive_gains_zero_for_single_bar() {
7896 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
7897 assert_eq!(OhlcvBar::max_consecutive_gains(&[b]), 0);
7898 }
7899
7900 #[test]
7901 fn test_max_consecutive_gains_correct() {
7902 let b1 = make_ohlcv_bar(dec!(98), dec!(102), dec!(96), dec!(100));
7904 let b2 = make_ohlcv_bar(dec!(100), dec!(108), dec!(99), dec!(105));
7905 let b3 = make_ohlcv_bar(dec!(105), dec!(112), dec!(104), dec!(110));
7906 let b4 = make_ohlcv_bar(dec!(110), dec!(111), dec!(105), dec!(108));
7907 let b5 = make_ohlcv_bar(dec!(108), dec!(116), dec!(107), dec!(115));
7908 assert_eq!(OhlcvBar::max_consecutive_gains(&[b1, b2, b3, b4, b5]), 2);
7909 }
7910
7911 #[test]
7912 fn test_max_consecutive_losses_correct() {
7913 let b1 = make_ohlcv_bar(dec!(112), dec!(115), dec!(108), dec!(110));
7915 let b2 = make_ohlcv_bar(dec!(110), dec!(112), dec!(103), dec!(105));
7916 let b3 = make_ohlcv_bar(dec!(105), dec!(108), dec!(98), dec!(100));
7917 let b4 = make_ohlcv_bar(dec!(100), dec!(112), dec!(98), dec!(108));
7918 assert_eq!(OhlcvBar::max_consecutive_losses(&[b1, b2, b3, b4]), 2);
7919 }
7920
7921 #[test]
7924 fn test_price_path_length_none_for_single_bar() {
7925 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
7926 assert!(OhlcvBar::price_path_length(&[b]).is_none());
7927 }
7928
7929 #[test]
7930 fn test_price_path_length_correct() {
7931 let b1 = make_ohlcv_bar(dec!(98), dec!(102), dec!(96), dec!(100));
7933 let b2 = make_ohlcv_bar(dec!(100), dec!(112), dec!(99), dec!(110));
7934 let b3 = make_ohlcv_bar(dec!(110), dec!(112), dec!(103), dec!(105));
7935 let len = OhlcvBar::price_path_length(&[b1, b2, b3]).unwrap();
7936 assert!((len - 15.0).abs() < 1e-6, "expected 15.0, got {}", len);
7937 }
7938
7939 #[test]
7942 fn test_close_reversion_count_zero_for_single_bar() {
7943 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
7944 assert_eq!(OhlcvBar::close_reversion_count(&[b]), 0);
7945 }
7946
7947 #[test]
7948 fn test_close_reversion_count_returns_usize() {
7949 let b1 = make_ohlcv_bar(dec!(98), dec!(102), dec!(96), dec!(100));
7950 let b2 = make_ohlcv_bar(dec!(100), dec!(112), dec!(99), dec!(110));
7951 let b3 = make_ohlcv_bar(dec!(110), dec!(112), dec!(103), dec!(105));
7952 let _ = OhlcvBar::close_reversion_count(&[b1, b2, b3]);
7954 }
7955
7956 #[test]
7959 fn test_atr_ratio_none_for_single_bar() {
7960 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
7961 assert!(OhlcvBar::atr_ratio(&[b]).is_none());
7962 }
7963
7964 #[test]
7965 fn test_atr_ratio_positive_for_valid_bars() {
7966 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
7967 let b2 = make_ohlcv_bar(dec!(105), dec!(115), dec!(103), dec!(110));
7968 let r = OhlcvBar::atr_ratio(&[b1, b2]).unwrap();
7969 assert!(r > 0.0, "expected positive ATR ratio, got {}", r);
7970 }
7971
7972 #[test]
7975 fn test_volume_trend_strength_none_for_single_bar() {
7976 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
7977 assert!(OhlcvBar::volume_trend_strength(&[b]).is_none());
7978 }
7979
7980 #[test]
7981 fn test_volume_trend_strength_positive_for_rising_volume() {
7982 let mut b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)); b1.volume = dec!(100);
7983 let mut b2 = make_ohlcv_bar(dec!(105), dec!(115), dec!(103), dec!(110)); b2.volume = dec!(200);
7984 let mut b3 = make_ohlcv_bar(dec!(110), dec!(120), dec!(108), dec!(115)); b3.volume = dec!(300);
7985 let s = OhlcvBar::volume_trend_strength(&[b1, b2, b3]).unwrap();
7986 assert!(s > 0.0, "rising volume should give positive strength, got {}", s);
7987 }
7988
7989 #[test]
7992 fn test_high_close_spread_none_for_empty() {
7993 assert!(OhlcvBar::high_close_spread(&[]).is_none());
7994 }
7995
7996 #[test]
7997 fn test_high_close_spread_zero_when_close_equals_high() {
7998 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(110));
8000 let s = OhlcvBar::high_close_spread(&[b]).unwrap();
8001 assert!((s - 0.0).abs() < 1e-9, "expected 0.0, got {}", s);
8002 }
8003
8004 #[test]
8005 fn test_high_close_spread_positive_for_wicked_bar() {
8006 let b = make_ohlcv_bar(dec!(100), dec!(115), dec!(95), dec!(105));
8007 let s = OhlcvBar::high_close_spread(&[b]).unwrap();
8008 assert!(s > 0.0, "expected positive spread, got {}", s);
8009 }
8010
8011 #[test]
8014 fn test_open_range_none_for_empty() {
8015 assert!(OhlcvBar::open_range(&[]).is_none());
8016 }
8017
8018 #[test]
8019 fn test_open_range_zero_for_doji() {
8020 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
8021 let r = OhlcvBar::open_range(&[b]).unwrap();
8022 assert!((r - 0.0).abs() < 1e-9, "doji should have open_range=0, got {}", r);
8023 }
8024
8025 #[test]
8026 fn test_open_range_positive_for_directional() {
8027 let b = make_ohlcv_bar(dec!(100), dec!(115), dec!(98), dec!(110));
8028 let r = OhlcvBar::open_range(&[b]).unwrap();
8029 assert!(r > 0.0, "directional bar should have positive open_range, got {}", r);
8030 }
8031
8032 #[test]
8035 fn test_normalized_close_none_for_single_bar() {
8036 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8037 assert!(OhlcvBar::normalized_close(&[b]).is_none());
8038 }
8039
8040 #[test]
8041 fn test_normalized_close_one_when_last_close_is_max() {
8042 let b1 = make_ohlcv_bar(dec!(98), dec!(105), dec!(96), dec!(100));
8043 let b2 = make_ohlcv_bar(dec!(100), dec!(115), dec!(99), dec!(110));
8044 let nc = OhlcvBar::normalized_close(&[b1, b2]).unwrap();
8045 assert!((nc - 1.0).abs() < 1e-9, "last close = max should give 1.0, got {}", nc);
8046 }
8047
8048 #[test]
8049 fn test_normalized_close_zero_when_last_close_is_min() {
8050 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(90));
8051 let b2 = make_ohlcv_bar(dec!(90), dec!(105), dec!(88), dec!(100));
8052 let nc = OhlcvBar::normalized_close(&[b1, b2]).unwrap();
8055 assert!(nc >= 0.0 && nc <= 1.0, "normalized close should be in [0,1], got {}", nc);
8056 }
8057
8058 #[test]
8061 fn test_candle_score_none_for_empty() {
8062 assert!(OhlcvBar::candle_score(&[]).is_none());
8063 }
8064
8065 #[test]
8066 fn test_candle_score_one_for_strong_bull_bar() {
8067 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(99), dec!(108));
8069 let s = OhlcvBar::candle_score(&[b]).unwrap();
8070 assert_eq!(s, 1.0, "strong bullish bar should score 1.0, got {}", s);
8071 }
8072
8073 #[test]
8074 fn test_candle_score_zero_for_bear_bar() {
8075 let b = make_ohlcv_bar(dec!(108), dec!(110), dec!(99), dec!(100));
8077 let s = OhlcvBar::candle_score(&[b]).unwrap();
8078 assert_eq!(s, 0.0, "bearish bar should score 0.0, got {}", s);
8079 }
8080
8081 #[test]
8084 fn test_bar_speed_none_for_empty() {
8085 assert!(OhlcvBar::bar_speed(&[]).is_none());
8086 }
8087
8088 #[test]
8091 fn test_higher_highs_count_zero_for_single_bar() {
8092 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8093 assert_eq!(OhlcvBar::higher_highs_count(&[b]), 0);
8094 }
8095
8096 #[test]
8097 fn test_higher_highs_count_correct() {
8098 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8099 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);
8102 }
8103
8104 #[test]
8105 fn test_lower_lows_count_correct() {
8106 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8107 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);
8110 }
8111
8112 #[test]
8115 fn test_close_minus_open_pct_none_for_empty() {
8116 assert!(OhlcvBar::close_minus_open_pct(&[]).is_none());
8117 }
8118
8119 #[test]
8120 fn test_close_minus_open_pct_positive_for_bull_bar() {
8121 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(98), dec!(110));
8122 let p = OhlcvBar::close_minus_open_pct(&[b]).unwrap();
8123 assert!(p > 0.0, "bullish bar should give positive pct, got {}", p);
8124 }
8125
8126 #[test]
8129 fn test_volume_per_range_none_for_empty() {
8130 assert!(OhlcvBar::volume_per_range(&[]).is_none());
8131 }
8132
8133 #[test]
8134 fn test_volume_per_range_positive_for_valid_bar() {
8135 let mut b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)); b.volume = dec!(100);
8136 let r = OhlcvBar::volume_per_range(&[b]).unwrap();
8137 assert!(r > 0.0, "expected positive volume/range, got {}", r);
8138 }
8139
8140 #[test]
8141 fn test_up_volume_fraction_none_for_empty() {
8142 assert!(OhlcvBar::up_volume_fraction(&[]).is_none());
8143 }
8144
8145 #[test]
8146 fn test_up_volume_fraction_all_up() {
8147 let mut b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(95), dec!(108)); b1.volume = dec!(50);
8149 let mut b2 = make_ohlcv_bar(dec!(105), dec!(115), dec!(100), dec!(112)); b2.volume = dec!(50);
8150 let f = OhlcvBar::up_volume_fraction(&[b1, b2]).unwrap();
8151 assert!((f - 1.0).abs() < 1e-9, "all up bars → fraction=1.0, got {}", f);
8152 }
8153
8154 #[test]
8155 fn test_tail_upper_fraction_none_for_empty() {
8156 assert!(OhlcvBar::tail_upper_fraction(&[]).is_none());
8157 }
8158
8159 #[test]
8160 fn test_tail_upper_fraction_correct() {
8161 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8163 let f = OhlcvBar::tail_upper_fraction(&[b]).unwrap();
8164 assert!((f - 0.25).abs() < 1e-9, "expected 0.25, got {}", f);
8165 }
8166
8167 #[test]
8168 fn test_tail_lower_fraction_none_for_empty() {
8169 assert!(OhlcvBar::tail_lower_fraction(&[]).is_none());
8170 }
8171
8172 #[test]
8173 fn test_tail_lower_fraction_correct() {
8174 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8176 let f = OhlcvBar::tail_lower_fraction(&[b]).unwrap();
8177 assert!((f - 0.5).abs() < 1e-9, "expected 0.5, got {}", f);
8178 }
8179
8180 #[test]
8181 fn test_range_std_dev_none_for_single_bar() {
8182 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8183 assert!(OhlcvBar::range_std_dev(&[b]).is_none());
8184 }
8185
8186 #[test]
8187 fn test_range_std_dev_zero_for_equal_ranges() {
8188 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8189 let b2 = make_ohlcv_bar(dec!(200), dec!(210), dec!(190), dec!(205));
8190 let sd = OhlcvBar::range_std_dev(&[b1, b2]).unwrap();
8191 assert!(sd.abs() < 1e-9, "equal ranges → std_dev=0, got {}", sd);
8192 }
8193
8194 #[test]
8195 fn test_body_fraction_none_for_empty() {
8196 assert!(OhlcvBar::body_fraction(&[]).is_none());
8197 }
8198
8199 #[test]
8200 fn test_body_fraction_doji_is_zero() {
8201 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
8203 let f = OhlcvBar::body_fraction(&[b]).unwrap();
8204 assert!(f.abs() < 1e-9, "doji → body_fraction=0, got {}", f);
8205 }
8206
8207 #[test]
8208 fn test_bullish_ratio_none_for_empty() {
8209 assert!(OhlcvBar::bullish_ratio(&[]).is_none());
8210 }
8211
8212 #[test]
8213 fn test_bullish_ratio_all_bullish() {
8214 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(95), dec!(108));
8215 let b2 = make_ohlcv_bar(dec!(105), dec!(115), dec!(100), dec!(112));
8216 let r = OhlcvBar::bullish_ratio(&[b1, b2]).unwrap();
8217 assert!((r - 1.0).abs() < 1e-9, "all bullish → ratio=1.0, got {}", r);
8218 }
8219
8220 #[test]
8221 fn test_peak_trough_close_none_for_empty() {
8222 assert!(OhlcvBar::peak_close(&[]).is_none());
8223 assert!(OhlcvBar::trough_close(&[]).is_none());
8224 }
8225
8226 #[test]
8227 fn test_peak_trough_close_correct() {
8228 let bars = vec![
8229 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)),
8230 make_ohlcv_bar(dec!(105), dec!(120), dec!(100), dec!(115)),
8231 make_ohlcv_bar(dec!(110), dec!(115), dec!(95), dec!(98)),
8232 ];
8233 assert_eq!(OhlcvBar::peak_close(&bars).unwrap(), dec!(115));
8234 assert_eq!(OhlcvBar::trough_close(&bars).unwrap(), dec!(98));
8235 }
8236
8237 #[test]
8242 fn test_close_to_range_position_none_for_empty() {
8243 assert!(OhlcvBar::close_to_range_position(&[]).is_none());
8244 }
8245
8246 #[test]
8247 fn test_close_to_range_position_one_when_close_at_high() {
8248 let bar = make_ohlcv_bar(dec!(100), dec!(120), dec!(80), dec!(120));
8249 let r = OhlcvBar::close_to_range_position(&[bar]).unwrap();
8250 assert!((r - 1.0).abs() < 1e-9, "close at high → position=1, got {}", r);
8251 }
8252
8253 #[test]
8254 fn test_close_to_range_position_zero_when_close_at_low() {
8255 let bar = make_ohlcv_bar(dec!(100), dec!(120), dec!(80), dec!(80));
8256 let r = OhlcvBar::close_to_range_position(&[bar]).unwrap();
8257 assert!(r.abs() < 1e-9, "close at low → position=0, got {}", r);
8258 }
8259
8260 #[test]
8263 fn test_volume_oscillator_none_for_insufficient_bars() {
8264 let bars = vec![make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105))];
8265 assert!(OhlcvBar::volume_oscillator(&bars, 1, 3).is_none());
8266 }
8267
8268 #[test]
8269 fn test_volume_oscillator_none_when_short_ge_long() {
8270 let bars: Vec<_> = (0..5)
8271 .map(|_| make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)))
8272 .collect();
8273 assert!(OhlcvBar::volume_oscillator(&bars, 3, 2).is_none());
8274 }
8275
8276 #[test]
8277 fn test_volume_oscillator_zero_for_constant_volume() {
8278 let bars: Vec<_> = (0..5)
8279 .map(|_| make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)))
8280 .collect();
8281 let v = OhlcvBar::volume_oscillator(&bars, 2, 4).unwrap();
8282 assert!(v.abs() < 1e-9, "constant volume → oscillator=0, got {}", v);
8283 }
8284
8285 #[test]
8288 fn test_direction_reversal_count_zero_for_single_bar() {
8289 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8290 assert_eq!(OhlcvBar::direction_reversal_count(&[bar]), 0);
8291 }
8292
8293 #[test]
8294 fn test_direction_reversal_count_zero_for_all_bullish() {
8295 let bars = vec![
8296 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)),
8297 make_ohlcv_bar(dec!(105), dec!(115), dec!(95), dec!(112)),
8298 ];
8299 assert_eq!(OhlcvBar::direction_reversal_count(&bars), 0);
8300 }
8301
8302 #[test]
8303 fn test_direction_reversal_count_two_for_alternating() {
8304 let bull = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(108));
8305 let bear = make_ohlcv_bar(dec!(108), dec!(112), dec!(95), dec!(102));
8306 let bull2 = make_ohlcv_bar(dec!(102), dec!(115), dec!(98), dec!(110));
8307 let bear2 = make_ohlcv_bar(dec!(110), dec!(115), dec!(100), dec!(104));
8308 assert_eq!(OhlcvBar::direction_reversal_count(&[bull, bear, bull2, bear2]), 3);
8309 }
8310
8311 #[test]
8314 fn test_upper_wick_dominance_fraction_none_for_empty() {
8315 assert!(OhlcvBar::upper_wick_dominance_fraction(&[]).is_none());
8316 }
8317
8318 #[test]
8319 fn test_upper_wick_dominance_fraction_one_when_all_upper() {
8320 let bar = make_ohlcv_bar(dec!(100), dec!(130), dec!(99), dec!(101));
8322 let r = OhlcvBar::upper_wick_dominance_fraction(&[bar]).unwrap();
8323 assert!((r - 1.0).abs() < 1e-9, "all upper dominant → 1.0, got {}", r);
8324 }
8325
8326 #[test]
8329 fn test_avg_open_to_high_ratio_none_for_empty() {
8330 assert!(OhlcvBar::avg_open_to_high_ratio(&[]).is_none());
8331 }
8332
8333 #[test]
8334 fn test_avg_open_to_high_ratio_one_when_open_at_low() {
8335 let bar = make_ohlcv_bar(dec!(80), dec!(120), dec!(80), dec!(100));
8337 let r = OhlcvBar::avg_open_to_high_ratio(&[bar]).unwrap();
8338 assert!((r - 1.0).abs() < 1e-9, "open at low → ratio=1, got {}", r);
8339 }
8340
8341 #[test]
8344 fn test_volume_weighted_range_none_for_empty() {
8345 assert!(OhlcvBar::volume_weighted_range(&[]).is_none());
8346 }
8347
8348 #[test]
8349 fn test_volume_weighted_range_positive() {
8350 let b1 = make_ohlcv_bar(dec!(100), dec!(120), dec!(80), dec!(110));
8351 let b2 = make_ohlcv_bar(dec!(110), dec!(130), dec!(100), dec!(120));
8352 let r = OhlcvBar::volume_weighted_range(&[b1, b2]).unwrap();
8353 assert!(r > 0.0, "should be positive, got {}", r);
8354 }
8355
8356 #[test]
8359 fn test_bar_strength_index_none_for_empty() {
8360 assert!(OhlcvBar::bar_strength_index(&[]).is_none());
8361 }
8362
8363 #[test]
8364 fn test_bar_strength_index_positive_when_closes_near_high() {
8365 let bar = make_ohlcv_bar(dec!(100), dec!(120), dec!(80), dec!(120));
8366 let s = OhlcvBar::bar_strength_index(&[bar]).unwrap();
8367 assert!(s > 0.0, "close at high → positive strength, got {}", s);
8368 }
8369
8370 #[test]
8373 fn test_shadow_to_body_ratio_none_for_empty() {
8374 assert!(OhlcvBar::shadow_to_body_ratio(&[]).is_none());
8375 }
8376
8377 #[test]
8378 fn test_shadow_to_body_ratio_zero_for_marubozu() {
8379 let bar = make_ohlcv_bar(dec!(80), dec!(120), dec!(80), dec!(120));
8381 let r = OhlcvBar::shadow_to_body_ratio(&[bar]).unwrap();
8382 assert!(r.abs() < 1e-9, "marubozu → ratio=0, got {}", r);
8383 }
8384
8385 #[test]
8388 fn test_first_last_close_pct_none_for_empty() {
8389 assert!(OhlcvBar::first_last_close_pct(&[]).is_none());
8390 }
8391
8392 #[test]
8393 fn test_first_last_close_pct_zero_for_same_close() {
8394 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
8395 let r = OhlcvBar::first_last_close_pct(&[bar]).unwrap();
8396 assert!(r.abs() < 1e-9, "same open/close → pct=0, got {}", r);
8397 }
8398
8399 #[test]
8400 fn test_first_last_close_pct_positive_for_rise() {
8401 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
8402 let b2 = make_ohlcv_bar(dec!(100), dec!(120), dec!(95), dec!(110));
8403 let r = OhlcvBar::first_last_close_pct(&[b1, b2]).unwrap();
8404 assert!(r > 0.0, "price rose → positive pct, got {}", r);
8405 }
8406
8407 #[test]
8410 fn test_open_to_close_volatility_none_for_single_bar() {
8411 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8412 assert!(OhlcvBar::open_to_close_volatility(&[bar]).is_none());
8413 }
8414
8415 #[test]
8416 fn test_open_to_close_volatility_zero_for_identical_bars() {
8417 let bars = vec![
8418 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)),
8419 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)),
8420 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)),
8421 ];
8422 let v = OhlcvBar::open_to_close_volatility(&bars).unwrap();
8423 assert!(v.abs() < 1e-9, "identical bars → volatility=0, got {}", v);
8424 }
8425
8426 #[test]
8429 fn test_close_recovery_ratio_none_for_empty() {
8430 assert!(OhlcvBar::close_recovery_ratio(&[]).is_none());
8431 }
8432
8433 #[test]
8434 fn test_close_recovery_ratio_one_for_close_at_high() {
8435 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(110));
8437 let r = OhlcvBar::close_recovery_ratio(&[b]).unwrap();
8438 assert!((r - 1.0).abs() < 1e-9, "close at high → ratio=1, got {}", r);
8439 }
8440
8441 #[test]
8442 fn test_median_range_none_for_empty() {
8443 assert!(OhlcvBar::median_range(&[]).is_none());
8444 }
8445
8446 #[test]
8447 fn test_median_range_correct_odd() {
8448 let bars = vec![
8449 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)), ];
8453 assert_eq!(OhlcvBar::median_range(&bars).unwrap(), dec!(25));
8454 }
8455
8456 #[test]
8457 fn test_mean_typical_price_none_for_empty() {
8458 assert!(OhlcvBar::mean_typical_price(&[]).is_none());
8459 }
8460
8461 #[test]
8462 fn test_mean_typical_price_correct() {
8463 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8465 let expected = b.typical_price();
8466 let b2 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8467 let tp = OhlcvBar::mean_typical_price(&[b2]).unwrap();
8468 assert_eq!(tp, expected);
8469 }
8470
8471 #[test]
8472 fn test_directional_volume_ratio_none_for_empty() {
8473 assert!(OhlcvBar::directional_volume_ratio(&[]).is_none());
8474 }
8475
8476 #[test]
8477 fn test_directional_volume_ratio_one_for_all_bullish() {
8478 let mut b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(108)); b1.volume = dec!(50);
8479 let mut b2 = make_ohlcv_bar(dec!(105), dec!(115), dec!(100), dec!(112)); b2.volume = dec!(50);
8480 let r = OhlcvBar::directional_volume_ratio(&[b1, b2]).unwrap();
8481 assert!((r - 1.0).abs() < 1e-9, "all bullish → ratio=1, got {}", r);
8482 }
8483
8484 #[test]
8485 fn test_inside_bar_fraction_none_for_single_bar() {
8486 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8487 assert!(OhlcvBar::inside_bar_fraction(&[b]).is_none());
8488 }
8489
8490 #[test]
8491 fn test_body_momentum_empty_is_zero() {
8492 assert_eq!(OhlcvBar::body_momentum(&[]), Decimal::ZERO);
8493 }
8494
8495 #[test]
8496 fn test_body_momentum_bullish_positive() {
8497 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(108));
8498 let m = OhlcvBar::body_momentum(&[b]);
8499 assert!(m > Decimal::ZERO, "bullish bar → positive body momentum");
8500 }
8501
8502 #[test]
8503 fn test_avg_trade_count_none_for_empty() {
8504 assert!(OhlcvBar::avg_trade_count(&[]).is_none());
8505 }
8506
8507 #[test]
8508 fn test_max_trade_count_none_for_empty() {
8509 assert!(OhlcvBar::max_trade_count(&[]).is_none());
8510 }
8511
8512 #[test]
8513 fn test_max_trade_count_returns_max() {
8514 let mut b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)); b1.trade_count = 5;
8515 let mut b2 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)); b2.trade_count = 10;
8516 assert_eq!(OhlcvBar::max_trade_count(&[b1, b2]).unwrap(), 10);
8517 }
8518
8519 #[test]
8522 fn test_close_to_high_std_none_for_single_bar() {
8523 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8524 assert!(OhlcvBar::close_to_high_std(&[b]).is_none());
8525 }
8526
8527 #[test]
8528 fn test_close_to_high_std_zero_for_identical_bars() {
8529 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8530 let b2 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8531 let sd = OhlcvBar::close_to_high_std(&[b1, b2]).unwrap();
8532 assert!(sd.abs() < 1e-9, "identical bars → std=0, got {}", sd);
8533 }
8534
8535 #[test]
8536 fn test_avg_open_volume_ratio_none_for_empty() {
8537 assert!(OhlcvBar::avg_open_volume_ratio(&[]).is_none());
8538 }
8539
8540 #[test]
8541 fn test_typical_price_std_none_for_single_bar() {
8542 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8543 assert!(OhlcvBar::typical_price_std(&[b]).is_none());
8544 }
8545
8546 #[test]
8547 fn test_typical_price_std_zero_for_identical_bars() {
8548 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8549 let b2 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8550 let sd = OhlcvBar::typical_price_std(&[b1, b2]).unwrap();
8551 assert!(sd.abs() < 1e-9, "identical bars → std=0, got {}", sd);
8552 }
8553
8554 #[test]
8555 fn test_vwap_deviation_avg_none_for_empty() {
8556 assert!(OhlcvBar::vwap_deviation_avg(&[]).is_none());
8557 }
8558
8559 #[test]
8560 fn test_avg_high_low_ratio_none_for_empty() {
8561 assert!(OhlcvBar::avg_high_low_ratio(&[]).is_none());
8562 }
8563
8564 #[test]
8565 fn test_avg_high_low_ratio_one_for_doji() {
8566 let b = make_ohlcv_bar(dec!(100), dec!(100), dec!(100), dec!(100));
8568 let r = OhlcvBar::avg_high_low_ratio(&[b]).unwrap();
8569 assert!((r - 1.0).abs() < 1e-9, "high==low → ratio=1, got {}", r);
8570 }
8571
8572 #[test]
8573 fn test_gap_fill_fraction_none_for_empty() {
8574 assert!(OhlcvBar::gap_fill_fraction(&[]).is_none());
8575 }
8576
8577 #[test]
8578 fn test_gap_fill_fraction_zero_for_no_gaps() {
8579 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8580 let f = OhlcvBar::gap_fill_fraction(&[b]).unwrap();
8581 assert!(f.abs() < 1e-9, "no gap fills → fraction=0, got {}", f);
8582 }
8583
8584 #[test]
8585 fn test_complete_bar_count_zero_for_incomplete() {
8586 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8587 assert_eq!(OhlcvBar::complete_bar_count(&[b]), 0);
8588 }
8589
8590 #[test]
8591 fn test_min_trade_count_none_for_empty() {
8592 assert!(OhlcvBar::min_trade_count(&[]).is_none());
8593 }
8594
8595 #[test]
8596 fn test_min_trade_count_returns_min() {
8597 let mut b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)); b1.trade_count = 5;
8598 let mut b2 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)); b2.trade_count = 2;
8599 assert_eq!(OhlcvBar::min_trade_count(&[b1, b2]).unwrap(), 2);
8600 }
8601
8602 #[test]
8605 fn test_avg_bar_range_none_for_empty() {
8606 assert!(OhlcvBar::avg_bar_range(&[]).is_none());
8607 }
8608
8609 #[test]
8610 fn test_avg_bar_range_correct_value() {
8611 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)); let b2 = make_ohlcv_bar(dec!(100), dec!(115), dec!(95), dec!(105)); let r = OhlcvBar::avg_bar_range(&[b1, b2]).unwrap();
8614 assert_eq!(r, dec!(20));
8615 }
8616
8617 #[test]
8618 fn test_max_up_move_none_for_empty() {
8619 assert!(OhlcvBar::max_up_move(&[]).is_none());
8620 }
8621
8622 #[test]
8623 fn test_max_up_move_largest_bullish_body() {
8624 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(108)); let b2 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)); assert_eq!(OhlcvBar::max_up_move(&[b1, b2]).unwrap(), dec!(8));
8627 }
8628
8629 #[test]
8630 fn test_max_down_move_none_for_empty() {
8631 assert!(OhlcvBar::max_down_move(&[]).is_none());
8632 }
8633
8634 #[test]
8635 fn test_max_down_move_largest_bearish_body() {
8636 let b1 = make_ohlcv_bar(dec!(108), dec!(115), dec!(85), dec!(100)); let b2 = make_ohlcv_bar(dec!(103), dec!(110), dec!(90), dec!(100)); assert_eq!(OhlcvBar::max_down_move(&[b1, b2]).unwrap(), dec!(8));
8639 }
8640
8641 #[test]
8642 fn test_avg_close_position_none_for_doji_only() {
8643 let b = make_ohlcv_bar(dec!(100), dec!(100), dec!(100), dec!(100)); assert!(OhlcvBar::avg_close_position(&[b]).is_none());
8645 }
8646
8647 #[test]
8648 fn test_avg_close_position_one_for_close_at_high() {
8649 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(100), dec!(110));
8650 let pos = OhlcvBar::avg_close_position(&[b]).unwrap();
8651 assert!((pos - 1.0).abs() < 1e-9, "close at high → position=1, got {}", pos);
8652 }
8653
8654 #[test]
8655 fn test_volume_std_none_for_single_bar() {
8656 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8657 assert!(OhlcvBar::volume_std(&[b]).is_none());
8658 }
8659
8660 #[test]
8661 fn test_volume_std_zero_for_equal_volumes() {
8662 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8663 let b2 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8664 let s = OhlcvBar::volume_std(&[b1, b2]).unwrap();
8665 assert!(s.abs() < 1e-9, "equal volumes → std=0, got {}", s);
8666 }
8667
8668 #[test]
8669 fn test_avg_wick_ratio_none_for_doji_only() {
8670 let b = make_ohlcv_bar(dec!(100), dec!(100), dec!(100), dec!(100));
8671 assert!(OhlcvBar::avg_wick_ratio(&[b]).is_none());
8672 }
8673
8674 #[test]
8675 fn test_avg_wick_ratio_in_range() {
8676 let b = make_ohlcv_bar(dec!(100), dec!(115), dec!(85), dec!(105));
8677 let r = OhlcvBar::avg_wick_ratio(&[b]).unwrap();
8678 assert!(r >= 0.0 && r <= 1.0, "wick ratio should be in [0,1], got {}", r);
8679 }
8680
8681 #[test]
8682 fn test_open_gap_mean_none_for_single_bar() {
8683 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8684 assert!(OhlcvBar::open_gap_mean(&[b]).is_none());
8685 }
8686
8687 #[test]
8688 fn test_open_gap_mean_zero_for_no_gap() {
8689 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8690 let mut b2 = make_ohlcv_bar(dec!(105), dec!(115), dec!(95), dec!(110));
8691 b2.open = dec!(105); let g = OhlcvBar::open_gap_mean(&[b1, b2]).unwrap();
8693 assert!(g.abs() < 1e-9, "no gap → mean=0, got {}", g);
8694 }
8695
8696 #[test]
8697 fn test_net_directional_move_none_for_empty() {
8698 assert!(OhlcvBar::net_directional_move(&[]).is_none());
8699 }
8700
8701 #[test]
8702 fn test_net_directional_move_positive_for_rising_close() {
8703 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8704 let b2 = make_ohlcv_bar(dec!(105), dec!(120), dec!(100), dec!(115));
8705 let m = OhlcvBar::net_directional_move(&[b1, b2]).unwrap();
8706 assert!(m > 0.0, "rising bar sequence → positive move, got {}", m);
8707 }
8708
8709 #[test]
8712 fn test_close_above_median_fraction_none_for_empty() {
8713 assert!(OhlcvBar::close_above_median_fraction(&[]).is_none());
8714 }
8715
8716 #[test]
8717 fn test_close_above_median_fraction_half_for_symmetric() {
8718 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(95));
8719 let b2 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8720 let b3 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(95));
8721 let b4 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(108));
8722 let f = OhlcvBar::close_above_median_fraction(&[b1, b2, b3, b4]).unwrap();
8723 assert!(f >= 0.0 && f <= 1.0, "fraction in [0,1], got {}", f);
8724 }
8725
8726 #[test]
8727 fn test_avg_range_to_open_none_for_empty() {
8728 assert!(OhlcvBar::avg_range_to_open(&[]).is_none());
8729 }
8730
8731 #[test]
8732 fn test_close_sum_zero_for_empty() {
8733 assert_eq!(OhlcvBar::close_sum(&[]), dec!(0));
8734 }
8735
8736 #[test]
8737 fn test_close_sum_sums_all_closes() {
8738 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8739 let b2 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(107));
8740 assert_eq!(OhlcvBar::close_sum(&[b1, b2]), dec!(212));
8741 }
8742
8743 #[test]
8744 fn test_above_avg_volume_count_zero_for_empty() {
8745 assert_eq!(OhlcvBar::above_avg_volume_count(&[]), 0);
8746 }
8747
8748 #[test]
8749 fn test_median_close_none_for_empty() {
8750 assert!(OhlcvBar::median_close(&[]).is_none());
8751 }
8752
8753 #[test]
8754 fn test_median_close_correct_for_sorted() {
8755 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
8756 let b2 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8757 let b3 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(110));
8758 let m = OhlcvBar::median_close(&[b1, b2, b3]).unwrap();
8759 assert_eq!(m, dec!(105));
8760 }
8761
8762 #[test]
8763 fn test_flat_bar_fraction_none_for_empty() {
8764 assert!(OhlcvBar::flat_bar_fraction(&[]).is_none());
8765 }
8766
8767 #[test]
8768 fn test_avg_body_to_range_none_for_empty() {
8769 assert!(OhlcvBar::avg_body_to_range(&[]).is_none());
8770 }
8771
8772 #[test]
8773 fn test_avg_body_to_range_in_range() {
8774 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8775 let r = OhlcvBar::avg_body_to_range(&[b]).unwrap();
8776 assert!(r >= 0.0 && r <= 1.0, "body-to-range in [0,1], got {}", r);
8777 }
8778
8779 #[test]
8780 fn test_max_open_gap_none_for_single_bar() {
8781 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8782 assert!(OhlcvBar::max_open_gap(&[b]).is_none());
8783 }
8784
8785 #[test]
8786 fn test_volume_trend_slope_none_for_single_bar() {
8787 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8788 assert!(OhlcvBar::volume_trend_slope(&[b]).is_none());
8789 }
8790
8791 #[test]
8792 fn test_up_close_fraction_none_for_empty() {
8793 assert!(OhlcvBar::up_close_fraction(&[]).is_none());
8794 }
8795
8796 #[test]
8797 fn test_avg_upper_shadow_ratio_none_for_doji_only() {
8798 let b = make_ohlcv_bar(dec!(100), dec!(100), dec!(100), dec!(100));
8799 assert!(OhlcvBar::avg_upper_shadow_ratio(&[b]).is_none());
8800 }
8801
8802 #[test]
8805 fn test_avg_lower_shadow_ratio_none_for_empty() {
8806 assert!(OhlcvBar::avg_lower_shadow_ratio(&[]).is_none());
8807 }
8808
8809 #[test]
8810 fn test_avg_lower_shadow_ratio_in_range() {
8811 let b = make_ohlcv_bar(dec!(100), dec!(115), dec!(85), dec!(105));
8812 let r = OhlcvBar::avg_lower_shadow_ratio(&[b]).unwrap();
8813 assert!(r >= 0.0 && r <= 1.0, "lower shadow ratio in [0,1], got {}", r);
8814 }
8815
8816 #[test]
8817 fn test_close_to_open_range_ratio_none_for_doji_only() {
8818 let b = make_ohlcv_bar(dec!(100), dec!(100), dec!(100), dec!(100));
8819 assert!(OhlcvBar::close_to_open_range_ratio(&[b]).is_none());
8820 }
8821
8822 #[test]
8823 fn test_max_high_none_for_empty() {
8824 assert!(OhlcvBar::max_high(&[]).is_none());
8825 }
8826
8827 #[test]
8828 fn test_max_high_returns_maximum() {
8829 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8830 let b2 = make_ohlcv_bar(dec!(100), dec!(120), dec!(90), dec!(110));
8831 assert_eq!(OhlcvBar::max_high(&[b1, b2]).unwrap(), dec!(120));
8832 }
8833
8834 #[test]
8835 fn test_min_low_none_for_empty() {
8836 assert!(OhlcvBar::min_low(&[]).is_none());
8837 }
8838
8839 #[test]
8840 fn test_min_low_returns_minimum() {
8841 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(85), dec!(105));
8842 let b2 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8843 assert_eq!(OhlcvBar::min_low(&[b1, b2]).unwrap(), dec!(85));
8844 }
8845
8846 #[test]
8847 fn test_avg_bar_efficiency_none_for_doji_only() {
8848 let b = make_ohlcv_bar(dec!(100), dec!(100), dec!(100), dec!(100));
8849 assert!(OhlcvBar::avg_bar_efficiency(&[b]).is_none());
8850 }
8851
8852 #[test]
8853 fn test_avg_bar_efficiency_one_for_full_body_bar() {
8854 let b = make_ohlcv_bar(dec!(90), dec!(110), dec!(90), dec!(110));
8855 let e = OhlcvBar::avg_bar_efficiency(&[b]).unwrap();
8856 assert!((e - 1.0).abs() < 1e-9, "full body → efficiency=1, got {}", e);
8857 }
8858
8859 #[test]
8860 fn test_open_range_fraction_none_for_empty() {
8861 assert!(OhlcvBar::open_range_fraction(&[]).is_none());
8862 }
8863
8864 #[test]
8865 fn test_open_range_fraction_in_range() {
8866 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8867 let f = OhlcvBar::open_range_fraction(&[b]).unwrap();
8868 assert!(f >= 0.0 && f <= 1.0, "fraction in [0,1], got {}", f);
8869 }
8870
8871 #[test]
8872 fn test_close_skewness_none_for_two_bars() {
8873 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8874 let b2 = make_ohlcv_bar(dec!(105), dec!(115), dec!(95), dec!(110));
8875 assert!(OhlcvBar::close_skewness(&[b1, b2]).is_none());
8876 }
8877
8878 #[test]
8879 fn test_close_skewness_returns_value_for_three_bars() {
8880 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
8881 let b2 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8882 let b3 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(200));
8883 let s = OhlcvBar::close_skewness(&[b1, b2, b3]);
8884 assert!(s.is_some(), "skewness should be computed for 3 bars");
8885 }
8886
8887 #[test]
8888 fn test_volume_above_median_fraction_none_for_empty() {
8889 assert!(OhlcvBar::volume_above_median_fraction(&[]).is_none());
8890 }
8891
8892 #[test]
8893 fn test_volume_above_median_fraction_in_range() {
8894 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8895 let b2 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8896 let f = OhlcvBar::volume_above_median_fraction(&[b1, b2]).unwrap();
8897 assert!(f >= 0.0 && f <= 1.0, "fraction in [0,1], got {}", f);
8898 }
8899
8900 #[test]
8901 fn test_typical_price_sum_zero_for_empty() {
8902 assert_eq!(OhlcvBar::typical_price_sum(&[]), dec!(0));
8903 }
8904
8905 #[test]
8906 fn test_typical_price_sum_correct_value() {
8907 let b = make_ohlcv_bar(dec!(100), dec!(120), dec!(80), dec!(100));
8908 assert_eq!(OhlcvBar::typical_price_sum(&[b]), dec!(100));
8910 }
8911
8912 #[test]
8913 fn test_max_body_size_none_for_empty() {
8914 assert!(OhlcvBar::max_body_size(&[]).is_none());
8915 }
8916
8917 #[test]
8918 fn test_max_body_size_correct_value() {
8919 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(108));
8920 let b2 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(103));
8921 assert_eq!(OhlcvBar::max_body_size(&[b1, b2]).unwrap(), dec!(8));
8922 }
8923
8924 #[test]
8925 fn test_min_body_size_none_for_empty() {
8926 assert!(OhlcvBar::min_body_size(&[]).is_none());
8927 }
8928
8929 #[test]
8930 fn test_avg_lower_wick_to_range_none_for_empty() {
8931 assert!(OhlcvBar::avg_lower_wick_to_range(&[]).is_none());
8932 }
8933
8934 #[test]
8935 fn test_avg_lower_wick_to_range_zero_for_open_at_low() {
8936 let b = make_ohlcv_bar(dec!(90), dec!(110), dec!(90), dec!(105));
8938 let r = OhlcvBar::avg_lower_wick_to_range(&[b]).unwrap();
8939 assert!(r.abs() < 1e-9, "open=low → lower wick=0, got {}", r);
8940 }
8941
8942 #[test]
8945 fn test_total_range_zero_for_empty() {
8946 assert_eq!(OhlcvBar::total_range(&[]), dec!(0));
8947 }
8948
8949 #[test]
8950 fn test_total_range_sum_of_ranges() {
8951 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)); let b2 = make_ohlcv_bar(dec!(100), dec!(115), dec!(95), dec!(110)); assert_eq!(OhlcvBar::total_range(&[b1, b2]), dec!(40));
8954 }
8955
8956 #[test]
8957 fn test_close_at_high_fraction_none_for_empty() {
8958 assert!(OhlcvBar::close_at_high_fraction(&[]).is_none());
8959 }
8960
8961 #[test]
8962 fn test_close_at_high_fraction_one_when_all_close_at_high() {
8963 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(110));
8964 let f = OhlcvBar::close_at_high_fraction(&[b]).unwrap();
8965 assert!((f - 1.0).abs() < 1e-9, "close=high → fraction=1, got {}", f);
8966 }
8967
8968 #[test]
8969 fn test_close_at_low_fraction_none_for_empty() {
8970 assert!(OhlcvBar::close_at_low_fraction(&[]).is_none());
8971 }
8972
8973 #[test]
8974 fn test_close_at_low_fraction_one_when_all_close_at_low() {
8975 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(90));
8976 let f = OhlcvBar::close_at_low_fraction(&[b]).unwrap();
8977 assert!((f - 1.0).abs() < 1e-9, "close=low → fraction=1, got {}", f);
8978 }
8979
8980 #[test]
8981 fn test_avg_high_above_open_ratio_none_for_empty() {
8982 assert!(OhlcvBar::avg_high_above_open_ratio(&[]).is_none());
8983 }
8984
8985 #[test]
8986 fn test_avg_high_above_open_ratio_in_range() {
8987 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8988 let r = OhlcvBar::avg_high_above_open_ratio(&[b]).unwrap();
8989 assert!(r >= 0.0 && r <= 1.0, "ratio in [0,1], got {}", r);
8990 }
8991
8992 #[test]
8993 fn test_continuation_bar_count_zero_for_single() {
8994 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8995 assert_eq!(OhlcvBar::continuation_bar_count(&[b]), 0);
8996 }
8997
8998 #[test]
8999 fn test_down_close_volume_zero_for_all_up_close() {
9000 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)); assert_eq!(OhlcvBar::down_close_volume(&[b]), dec!(0));
9002 }
9003
9004 #[test]
9005 fn test_up_close_volume_zero_for_all_down_close() {
9006 let b = make_ohlcv_bar(dec!(105), dec!(110), dec!(90), dec!(100)); assert_eq!(OhlcvBar::up_close_volume(&[b]), dec!(0));
9008 }
9009
9010 #[test]
9013 fn test_mean_open_none_for_empty() {
9014 assert!(OhlcvBar::mean_open(&[]).is_none());
9015 }
9016
9017 #[test]
9018 fn test_mean_open_correct() {
9019 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
9020 let b2 = make_ohlcv_bar(dec!(200), dec!(210), dec!(190), dec!(205));
9021 assert_eq!(OhlcvBar::mean_open(&[b1, b2]).unwrap(), dec!(150));
9022 }
9023
9024 #[test]
9025 fn test_new_high_count_zero_for_single() {
9026 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
9027 assert_eq!(OhlcvBar::new_high_count(&[b]), 0);
9028 }
9029
9030 #[test]
9031 fn test_new_high_count_correct() {
9032 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
9033 let b2 = make_ohlcv_bar(dec!(100), dec!(120), dec!(90), dec!(115));
9034 let b3 = make_ohlcv_bar(dec!(100), dec!(115), dec!(90), dec!(110));
9035 assert_eq!(OhlcvBar::new_high_count(&[b1, b2, b3]), 1);
9036 }
9037
9038 #[test]
9039 fn test_new_low_count_zero_for_single() {
9040 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
9041 assert_eq!(OhlcvBar::new_low_count(&[b]), 0);
9042 }
9043
9044 #[test]
9045 fn test_close_std_none_for_single() {
9046 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
9047 assert!(OhlcvBar::close_std(&[b]).is_none());
9048 }
9049
9050 #[test]
9051 fn test_close_std_zero_for_constant_close() {
9052 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
9053 let b2 = make_ohlcv_bar(dec!(101), dec!(111), dec!(91), dec!(105));
9054 let s = OhlcvBar::close_std(&[b1, b2]).unwrap();
9055 assert!(s.abs() < 1e-9, "constant close → std=0, got {}", s);
9056 }
9057
9058 #[test]
9059 fn test_zero_volume_fraction_none_for_empty() {
9060 assert!(OhlcvBar::zero_volume_fraction(&[]).is_none());
9061 }
9062
9063 #[test]
9064 fn test_zero_volume_fraction_zero_when_no_zero_volume() {
9065 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
9066 let f = OhlcvBar::zero_volume_fraction(&[b]).unwrap();
9067 assert!(f.abs() < 1e-9, "bar has volume → zero_vol_fraction=0, got {}", f);
9068 }
9069
9070 #[test]
9073 fn test_avg_open_to_close_none_for_empty() {
9074 assert!(OhlcvBar::avg_open_to_close(&[]).is_none());
9075 }
9076
9077 #[test]
9078 fn test_avg_open_to_close_positive_when_all_bullish() {
9079 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)); let r = OhlcvBar::avg_open_to_close(&[b]).unwrap();
9081 assert!(r > dec!(0), "bullish bar → avg_open_to_close > 0, got {}", r);
9082 }
9083
9084 #[test]
9085 fn test_avg_open_to_close_zero_for_doji() {
9086 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100)); let r = OhlcvBar::avg_open_to_close(&[b]).unwrap();
9088 assert_eq!(r, dec!(0));
9089 }
9090
9091 #[test]
9092 fn test_max_bar_volume_none_for_empty() {
9093 assert!(OhlcvBar::max_bar_volume(&[]).is_none());
9094 }
9095
9096 #[test]
9097 fn test_max_bar_volume_selects_largest() {
9098 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
9099 let b2 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
9100 let vol = OhlcvBar::max_bar_volume(&[b1, b2]).unwrap();
9103 assert!(vol > dec!(0));
9104 }
9105
9106 #[test]
9107 fn test_min_bar_volume_none_for_empty() {
9108 assert!(OhlcvBar::min_bar_volume(&[]).is_none());
9109 }
9110
9111 #[test]
9112 fn test_body_to_range_std_none_for_single() {
9113 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
9114 assert!(OhlcvBar::body_to_range_std(&[b]).is_none());
9115 }
9116
9117 #[test]
9118 fn test_body_to_range_std_nonneg_for_varied_bars() {
9119 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
9120 let b2 = make_ohlcv_bar(dec!(100), dec!(120), dec!(80), dec!(100));
9121 let s = OhlcvBar::body_to_range_std(&[b1, b2]).unwrap();
9122 assert!(s >= 0.0, "std dev should be non-negative, got {}", s);
9123 }
9124
9125 #[test]
9126 fn test_avg_wick_symmetry_none_for_empty() {
9127 assert!(OhlcvBar::avg_wick_symmetry(&[]).is_none());
9128 }
9129
9130 #[test]
9131 fn test_avg_wick_symmetry_in_range() {
9132 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
9133 let s = OhlcvBar::avg_wick_symmetry(&[b]).unwrap();
9135 assert!(s >= 0.0 && s <= 1.0, "symmetry in [0,1], got {}", s);
9136 }
9137
9138 #[test]
9141 fn test_avg_range_pct_of_open_none_for_empty() {
9142 assert!(OhlcvBar::avg_range_pct_of_open(&[]).is_none());
9143 }
9144
9145 #[test]
9146 fn test_avg_range_pct_of_open_correct() {
9147 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
9149 let r = OhlcvBar::avg_range_pct_of_open(&[b]).unwrap();
9150 assert!((r - 0.2).abs() < 1e-9, "range/open = 0.2, got {}", r);
9151 }
9152
9153 #[test]
9154 fn test_high_volume_fraction_none_for_empty() {
9155 assert!(OhlcvBar::high_volume_fraction(&[]).is_none());
9156 }
9157
9158 #[test]
9159 fn test_close_cluster_count_zero_for_single_bar() {
9160 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
9161 assert_eq!(OhlcvBar::close_cluster_count(&[b]), 0);
9162 }
9163
9164 #[test]
9165 fn test_mean_vwap_none_for_bars_without_vwap() {
9166 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
9167 assert!(OhlcvBar::mean_vwap(&[b]).is_none());
9168 }
9169
9170 #[test]
9171 fn test_complete_fraction_none_for_empty() {
9172 assert!(OhlcvBar::complete_fraction(&[]).is_none());
9173 }
9174
9175 #[test]
9176 fn test_complete_fraction_zero_when_none_complete() {
9177 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
9178 let f = OhlcvBar::complete_fraction(&[b]).unwrap();
9180 assert!(f.abs() < 1e-9, "no complete bars → fraction=0, got {}", f);
9181 }
9182
9183 #[test]
9184 fn test_total_body_movement_zero_for_empty() {
9185 assert_eq!(OhlcvBar::total_body_movement(&[]), rust_decimal::Decimal::ZERO);
9186 }
9187
9188 #[test]
9189 fn test_total_body_movement_correct() {
9190 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)); let b2 = make_ohlcv_bar(dec!(110), dec!(120), dec!(100), dec!(100)); assert_eq!(OhlcvBar::total_body_movement(&[b1, b2]), dec!(15));
9193 }
9194
9195 #[test]
9196 fn test_open_std_none_for_single() {
9197 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
9198 assert!(OhlcvBar::open_std(&[b]).is_none());
9199 }
9200
9201 #[test]
9202 fn test_open_std_zero_for_constant_open() {
9203 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
9204 let b2 = make_ohlcv_bar(dec!(100), dec!(120), dec!(80), dec!(110));
9205 let s = OhlcvBar::open_std(&[b1, b2]).unwrap();
9206 assert!(s.abs() < 1e-9, "constant open → std=0, got {}", s);
9207 }
9208
9209 #[test]
9210 fn test_mean_high_low_ratio_none_for_empty() {
9211 assert!(OhlcvBar::mean_high_low_ratio(&[]).is_none());
9212 }
9213
9214 #[test]
9215 fn test_mean_high_low_ratio_above_one() {
9216 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
9217 let r = OhlcvBar::mean_high_low_ratio(&[b]).unwrap();
9218 assert!(r > 1.0, "high > low → ratio > 1, got {}", r);
9219 }
9220
9221 #[test]
9224 fn test_max_consecutive_up_bars_zero_for_empty() {
9225 assert_eq!(OhlcvBar::max_consecutive_up_bars(&[]), 0);
9226 }
9227
9228 #[test]
9229 fn test_max_consecutive_up_bars_zero_for_all_down() {
9230 let b = make_ohlcv_bar(dec!(105), dec!(110), dec!(90), dec!(100)); assert_eq!(OhlcvBar::max_consecutive_up_bars(&[b]), 0);
9232 }
9233
9234 #[test]
9235 fn test_max_consecutive_up_bars_correct_run() {
9236 let up = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)); let dn = make_ohlcv_bar(dec!(105), dec!(115), dec!(90), dec!(100)); let up2 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(108)); let up3 = make_ohlcv_bar(dec!(108), dec!(120), dec!(100), dec!(115)); assert_eq!(OhlcvBar::max_consecutive_up_bars(&[up, dn, up2, up3]), 2);
9241 }
9242
9243 #[test]
9244 fn test_avg_upper_shadow_fraction_none_for_empty() {
9245 assert!(OhlcvBar::avg_upper_shadow_fraction(&[]).is_none());
9246 }
9247
9248 #[test]
9249 fn test_avg_upper_shadow_fraction_in_range() {
9250 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100)); let f = OhlcvBar::avg_upper_shadow_fraction(&[b]).unwrap();
9252 assert!(f >= 0.0 && f <= 1.0, "fraction in [0,1], got {}", f);
9253 }
9254
9255 #[test]
9256 fn test_up_down_bar_ratio_none_for_no_down_bars() {
9257 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
9259 assert!(OhlcvBar::up_down_bar_ratio(&[b]).is_none());
9260 }
9261
9262 #[test]
9263 fn test_up_down_bar_ratio_one_for_balanced() {
9264 let up_bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
9265 let dn_bar = make_ohlcv_bar(dec!(105), dec!(110), dec!(90), dec!(100));
9266 let r = OhlcvBar::up_down_bar_ratio(&[up_bar, dn_bar]).unwrap();
9267 assert!((r - 1.0).abs() < 1e-9, "1 up / 1 down → 1.0, got {}", r);
9268 }
9269
9270 #[test]
9273 fn test_close_range_fraction_none_for_empty() {
9274 assert!(OhlcvBar::close_range_fraction(&[]).is_none());
9275 }
9276
9277 #[test]
9278 fn test_close_range_fraction_one_for_close_at_high() {
9279 let b = make_ohlcv_bar(dec!(90), dec!(110), dec!(90), dec!(110));
9281 let f = OhlcvBar::close_range_fraction(&[b]).unwrap();
9282 assert!((f - 1.0).abs() < 1e-9, "close=high → 1.0, got {}", f);
9283 }
9284
9285 #[test]
9286 fn test_close_range_fraction_zero_for_close_at_low() {
9287 let b = make_ohlcv_bar(dec!(110), dec!(120), dec!(90), dec!(90));
9289 let f = OhlcvBar::close_range_fraction(&[b]).unwrap();
9290 assert!((f - 0.0).abs() < 1e-9, "close=low → 0.0, got {}", f);
9291 }
9292
9293 #[test]
9294 fn test_tail_symmetry_none_for_empty() {
9295 assert!(OhlcvBar::tail_symmetry(&[]).is_none());
9296 }
9297
9298 #[test]
9299 fn test_tail_symmetry_one_for_symmetric_bar() {
9300 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100)); let s = OhlcvBar::tail_symmetry(&[b]).unwrap();
9303 assert!((s - 1.0).abs() < 1e-9, "symmetric bar → 1.0, got {}", s);
9304 }
9305
9306 #[test]
9307 fn test_bar_trend_strength_none_for_single_bar() {
9308 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
9309 assert!(OhlcvBar::bar_trend_strength(&[b]).is_none());
9310 }
9311
9312 #[test]
9313 fn test_bar_trend_strength_one_for_monotone_up() {
9314 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
9315 let b2 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
9316 let b3 = make_ohlcv_bar(dec!(105), dec!(115), dec!(95), dec!(110));
9317 let s = OhlcvBar::bar_trend_strength(&[b1, b2, b3]).unwrap();
9319 assert!((s - 1.0).abs() < 1e-9, "monotone up → 1.0, got {}", s);
9320 }
9321}