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 pub fn gap_up_count(bars: &[OhlcvBar]) -> usize {
3622 if bars.len() < 2 {
3623 return 0;
3624 }
3625 bars.windows(2).filter(|w| w[1].open > w[0].close).count()
3626 }
3627
3628 pub fn gap_down_count(bars: &[OhlcvBar]) -> usize {
3632 if bars.len() < 2 {
3633 return 0;
3634 }
3635 bars.windows(2).filter(|w| w[1].open < w[0].close).count()
3636 }
3637
3638 pub fn mean_bar_efficiency(bars: &[OhlcvBar]) -> Option<f64> {
3642 use rust_decimal::prelude::ToPrimitive;
3643 let vals: Vec<f64> = bars
3644 .iter()
3645 .filter(|b| b.high > b.low)
3646 .filter_map(|b| {
3647 let body = (b.close - b.open).abs();
3648 let range = b.high - b.low;
3649 (body / range).to_f64()
3650 })
3651 .collect();
3652 if vals.is_empty() {
3653 return None;
3654 }
3655 Some(vals.iter().sum::<f64>() / vals.len() as f64)
3656 }
3657
3658}
3659
3660impl std::fmt::Display for OhlcvBar {
3661 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3662 write!(
3663 f,
3664 "{} {} [{}/{}/{}/{} v={}]",
3665 self.symbol, self.timeframe, self.open, self.high, self.low, self.close, self.volume
3666 )
3667 }
3668}
3669
3670pub struct OhlcvAggregator {
3672 symbol: String,
3673 timeframe: Timeframe,
3674 current_bar: Option<OhlcvBar>,
3675 last_bar: Option<OhlcvBar>,
3677 emit_empty_bars: bool,
3681 bars_emitted: u64,
3683 price_volume_sum: Decimal,
3685 total_volume: Decimal,
3687 peak_volume: Option<Decimal>,
3689 min_volume: Option<Decimal>,
3691}
3692
3693impl OhlcvAggregator {
3694 pub fn new(symbol: impl Into<String>, timeframe: Timeframe) -> Result<Self, StreamError> {
3699 let tf_dur = timeframe.duration_ms();
3700 if tf_dur == 0 {
3701 return Err(StreamError::ConfigError {
3702 reason: "OhlcvAggregator timeframe duration must be > 0".into(),
3703 });
3704 }
3705 Ok(Self {
3706 symbol: symbol.into(),
3707 timeframe,
3708 current_bar: None,
3709 last_bar: None,
3710 emit_empty_bars: false,
3711 bars_emitted: 0,
3712 price_volume_sum: Decimal::ZERO,
3713 total_volume: Decimal::ZERO,
3714 peak_volume: None,
3715 min_volume: None,
3716 })
3717 }
3718
3719 pub fn with_emit_empty_bars(mut self, enabled: bool) -> Self {
3721 self.emit_empty_bars = enabled;
3722 self
3723 }
3724
3725 #[must_use = "completed bars are returned; ignoring them loses bar data"]
3734 #[inline]
3735 pub fn feed(&mut self, tick: &NormalizedTick) -> Result<Vec<OhlcvBar>, StreamError> {
3736 if tick.symbol != self.symbol {
3737 return Err(StreamError::AggregationError {
3738 reason: format!(
3739 "tick symbol '{}' does not match aggregator '{}'",
3740 tick.symbol, self.symbol
3741 ),
3742 });
3743 }
3744
3745 let tick_ts = tick.exchange_ts_ms.unwrap_or(tick.received_at_ms);
3747 let bar_start = self.timeframe.bar_start_ms(tick_ts);
3748 let mut emitted: Vec<OhlcvBar> = Vec::new();
3749
3750 let bar_window_changed = self
3752 .current_bar
3753 .as_ref()
3754 .map_or(false, |b| b.bar_start_ms != bar_start);
3755
3756 if bar_window_changed {
3757 let mut completed = self.current_bar.take().unwrap_or_else(|| unreachable!());
3759 completed.is_complete = true;
3760 let prev_close = completed.close;
3761 let prev_start = completed.bar_start_ms;
3762 emitted.push(completed);
3763
3764 if self.emit_empty_bars {
3766 let dur = self.timeframe.duration_ms();
3767 let mut gap_start = prev_start + dur;
3768 while gap_start < bar_start {
3769 emitted.push(OhlcvBar {
3770 symbol: self.symbol.clone(),
3771 timeframe: self.timeframe,
3772 bar_start_ms: gap_start,
3773 open: prev_close,
3774 high: prev_close,
3775 low: prev_close,
3776 close: prev_close,
3777 volume: Decimal::ZERO,
3778 trade_count: 0,
3779 is_complete: true,
3780 is_gap_fill: true,
3781 vwap: None,
3782 });
3783 gap_start += dur;
3784 }
3785 }
3786 }
3787
3788 let tick_value = tick.value();
3790 if self.current_bar.is_some() {
3791 self.price_volume_sum += tick_value;
3792 } else {
3793 self.price_volume_sum = tick_value;
3794 }
3795
3796 match &mut self.current_bar {
3797 Some(bar) => {
3798 if tick.price > bar.high {
3799 bar.high = tick.price;
3800 }
3801 if tick.price < bar.low {
3802 bar.low = tick.price;
3803 }
3804 bar.close = tick.price;
3805 bar.volume += tick.quantity;
3806 bar.trade_count += 1;
3807 bar.vwap = if bar.volume.is_zero() {
3808 None
3809 } else {
3810 Some(self.price_volume_sum / bar.volume)
3811 };
3812 }
3813 None => {
3814 self.current_bar = Some(OhlcvBar {
3815 symbol: self.symbol.clone(),
3816 timeframe: self.timeframe,
3817 bar_start_ms: bar_start,
3818 open: tick.price,
3819 high: tick.price,
3820 low: tick.price,
3821 close: tick.price,
3822 volume: tick.quantity,
3823 trade_count: 1,
3824 is_complete: false,
3825 is_gap_fill: false,
3826 vwap: Some(tick.price), });
3828 }
3829 }
3830 self.bars_emitted += emitted.len() as u64;
3831 for b in &emitted {
3832 self.total_volume += b.volume;
3833 self.peak_volume = Some(match self.peak_volume {
3834 Some(prev) => prev.max(b.volume),
3835 None => b.volume,
3836 });
3837 self.min_volume = Some(match self.min_volume {
3838 Some(prev) => prev.min(b.volume),
3839 None => b.volume,
3840 });
3841 }
3842 if let Some(b) = emitted.last() {
3843 self.last_bar = Some(b.clone());
3844 }
3845 Ok(emitted)
3846 }
3847
3848 pub fn current_bar(&self) -> Option<&OhlcvBar> {
3850 self.current_bar.as_ref()
3851 }
3852
3853 #[must_use = "the flushed bar is returned; ignoring it loses the partial bar"]
3855 pub fn flush(&mut self) -> Option<OhlcvBar> {
3856 let mut bar = self.current_bar.take()?;
3857 bar.is_complete = true;
3858 self.bars_emitted += 1;
3859 self.total_volume += bar.volume;
3860 self.peak_volume = Some(match self.peak_volume {
3861 Some(prev) => prev.max(bar.volume),
3862 None => bar.volume,
3863 });
3864 self.min_volume = Some(match self.min_volume {
3865 Some(prev) => prev.min(bar.volume),
3866 None => bar.volume,
3867 });
3868 self.last_bar = Some(bar.clone());
3869 Some(bar)
3870 }
3871
3872 pub fn last_bar(&self) -> Option<&OhlcvBar> {
3877 self.last_bar.as_ref()
3878 }
3879
3880 pub fn bar_count(&self) -> u64 {
3882 self.bars_emitted
3883 }
3884
3885 pub fn reset(&mut self) {
3890 self.current_bar = None;
3891 self.last_bar = None;
3892 self.bars_emitted = 0;
3893 self.price_volume_sum = Decimal::ZERO;
3894 self.total_volume = Decimal::ZERO;
3895 self.peak_volume = None;
3896 self.min_volume = None;
3897 }
3898
3899 pub fn total_volume(&self) -> Decimal {
3904 self.total_volume
3905 }
3906
3907 pub fn peak_volume(&self) -> Option<Decimal> {
3912 self.peak_volume
3913 }
3914
3915 pub fn min_volume(&self) -> Option<Decimal> {
3920 self.min_volume
3921 }
3922
3923 pub fn volume_range(&self) -> Option<(Decimal, Decimal)> {
3928 Some((self.min_volume?, self.peak_volume?))
3929 }
3930
3931 pub fn average_volume(&self) -> Option<Decimal> {
3935 if self.bars_emitted == 0 {
3936 return None;
3937 }
3938 Some(self.total_volume / Decimal::from(self.bars_emitted))
3939 }
3940
3941 pub fn symbol(&self) -> &str {
3943 &self.symbol
3944 }
3945
3946 pub fn timeframe(&self) -> Timeframe {
3948 self.timeframe
3949 }
3950
3951 pub fn window_progress(&self, now_ms: u64) -> Option<f64> {
3957 let bar = self.current_bar.as_ref()?;
3958 let elapsed = now_ms.saturating_sub(bar.bar_start_ms);
3959 let duration = self.timeframe.duration_ms();
3960 let progress = elapsed as f64 / duration as f64;
3961 Some(progress.clamp(0.0, 1.0))
3962 }
3963
3964 pub fn is_active(&self) -> bool {
3967 self.current_bar.is_some()
3968 }
3969
3970 pub fn vwap_current(&self) -> Option<Decimal> {
3975 let bar = self.current_bar.as_ref()?;
3976 if bar.volume.is_zero() {
3977 return None;
3978 }
3979 Some(self.price_volume_sum / bar.volume)
3980 }
3981}
3982
3983#[cfg(test)]
3984#[allow(deprecated)]
3985mod tests {
3986 use super::*;
3987 use crate::tick::{Exchange, NormalizedTick, TradeSide};
3988 use rust_decimal_macros::dec;
3989
3990 fn make_tick(symbol: &str, price: Decimal, qty: Decimal, ts_ms: u64) -> NormalizedTick {
3991 NormalizedTick {
3992 exchange: Exchange::Binance,
3993 symbol: symbol.to_string(),
3994 price,
3995 quantity: qty,
3996 side: Some(TradeSide::Buy),
3997 trade_id: None,
3998 exchange_ts_ms: None,
3999 received_at_ms: ts_ms,
4000 }
4001 }
4002
4003 fn make_tick_with_exchange_ts(
4004 symbol: &str,
4005 price: Decimal,
4006 qty: Decimal,
4007 exchange_ts_ms: u64,
4008 received_at_ms: u64,
4009 ) -> NormalizedTick {
4010 NormalizedTick {
4011 exchange: Exchange::Binance,
4012 symbol: symbol.to_string(),
4013 price,
4014 quantity: qty,
4015 side: Some(TradeSide::Buy),
4016 trade_id: None,
4017 exchange_ts_ms: Some(exchange_ts_ms),
4018 received_at_ms,
4019 }
4020 }
4021
4022 fn agg(symbol: &str, tf: Timeframe) -> OhlcvAggregator {
4023 OhlcvAggregator::new(symbol, tf).unwrap()
4024 }
4025
4026 #[test]
4027 fn test_timeframe_seconds_duration_ms() {
4028 assert_eq!(Timeframe::Seconds(30).duration_ms(), 30_000);
4029 }
4030
4031 #[test]
4032 fn test_timeframe_minutes_duration_ms() {
4033 assert_eq!(Timeframe::Minutes(5).duration_ms(), 300_000);
4034 }
4035
4036 #[test]
4037 fn test_timeframe_hours_duration_ms() {
4038 assert_eq!(Timeframe::Hours(1).duration_ms(), 3_600_000);
4039 }
4040
4041 #[test]
4042 fn test_timeframe_bar_start_ms_aligns() {
4043 let tf = Timeframe::Minutes(1);
4044 let ts = 61_500; assert_eq!(tf.bar_start_ms(ts), 60_000);
4046 }
4047
4048 #[test]
4049 fn test_timeframe_display() {
4050 assert_eq!(Timeframe::Seconds(30).to_string(), "30s");
4051 assert_eq!(Timeframe::Minutes(5).to_string(), "5m");
4052 assert_eq!(Timeframe::Hours(4).to_string(), "4h");
4053 }
4054
4055 #[test]
4056 fn test_timeframe_ord_seconds_lt_minutes() {
4057 assert!(Timeframe::Seconds(30) < Timeframe::Minutes(1));
4058 }
4059
4060 #[test]
4061 fn test_timeframe_ord_minutes_lt_hours() {
4062 assert!(Timeframe::Minutes(59) < Timeframe::Hours(1));
4063 }
4064
4065 #[test]
4066 fn test_timeframe_ord_same_duration_equal() {
4067 assert_eq!(Timeframe::Seconds(60), Timeframe::Seconds(60));
4068 assert_eq!(
4069 Timeframe::Seconds(3600).cmp(&Timeframe::Hours(1)),
4070 std::cmp::Ordering::Equal
4071 );
4072 }
4073
4074 #[test]
4075 fn test_timeframe_ord_sort() {
4076 let mut tfs = vec![
4077 Timeframe::Hours(1),
4078 Timeframe::Seconds(30),
4079 Timeframe::Minutes(5),
4080 ];
4081 tfs.sort();
4082 assert_eq!(tfs[0], Timeframe::Seconds(30));
4083 assert_eq!(tfs[1], Timeframe::Minutes(5));
4084 assert_eq!(tfs[2], Timeframe::Hours(1));
4085 }
4086
4087 #[test]
4088 fn test_ohlcv_aggregator_first_tick_sets_ohlcv() {
4089 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4090 let tick = make_tick("BTC-USD", dec!(50000), dec!(1), 60_000);
4091 let result = agg.feed(&tick).unwrap();
4092 assert!(result.is_empty()); let bar = agg.current_bar().unwrap();
4094 assert_eq!(bar.open, dec!(50000));
4095 assert_eq!(bar.high, dec!(50000));
4096 assert_eq!(bar.low, dec!(50000));
4097 assert_eq!(bar.close, dec!(50000));
4098 assert_eq!(bar.volume, dec!(1));
4099 assert_eq!(bar.trade_count, 1);
4100 }
4101
4102 #[test]
4103 fn test_ohlcv_aggregator_high_low_tracking() {
4104 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4105 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1), 60_000))
4106 .unwrap();
4107 agg.feed(&make_tick("BTC-USD", dec!(51000), dec!(1), 60_100))
4108 .unwrap();
4109 agg.feed(&make_tick("BTC-USD", dec!(49500), dec!(1), 60_200))
4110 .unwrap();
4111 let bar = agg.current_bar().unwrap();
4112 assert_eq!(bar.high, dec!(51000));
4113 assert_eq!(bar.low, dec!(49500));
4114 assert_eq!(bar.close, dec!(49500));
4115 assert_eq!(bar.trade_count, 3);
4116 }
4117
4118 #[test]
4119 fn test_ohlcv_aggregator_bar_completes_on_new_window() {
4120 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4121 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1), 60_000))
4122 .unwrap();
4123 agg.feed(&make_tick("BTC-USD", dec!(50100), dec!(2), 60_500))
4124 .unwrap();
4125 let mut bars = agg
4127 .feed(&make_tick("BTC-USD", dec!(50200), dec!(1), 120_000))
4128 .unwrap();
4129 assert_eq!(bars.len(), 1);
4130 let bar = bars.remove(0);
4131 assert!(bar.is_complete);
4132 assert_eq!(bar.open, dec!(50000));
4133 assert_eq!(bar.close, dec!(50100));
4134 assert_eq!(bar.volume, dec!(3));
4135 assert_eq!(bar.bar_start_ms, 60_000);
4136 }
4137
4138 #[test]
4139 fn test_ohlcv_aggregator_new_bar_started_after_completion() {
4140 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4141 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1), 60_000))
4142 .unwrap();
4143 agg.feed(&make_tick("BTC-USD", dec!(50200), dec!(1), 120_000))
4144 .unwrap();
4145 let bar = agg.current_bar().unwrap();
4146 assert_eq!(bar.open, dec!(50200));
4147 assert_eq!(bar.bar_start_ms, 120_000);
4148 }
4149
4150 #[test]
4151 fn test_ohlcv_aggregator_flush_marks_complete() {
4152 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4153 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1), 60_000))
4154 .unwrap();
4155 let flushed = agg.flush().unwrap();
4156 assert!(flushed.is_complete);
4157 assert!(agg.current_bar().is_none());
4158 }
4159
4160 #[test]
4161 fn test_ohlcv_aggregator_flush_empty_returns_none() {
4162 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4163 assert!(agg.flush().is_none());
4164 }
4165
4166 #[test]
4167 fn test_ohlcv_aggregator_wrong_symbol_returns_error() {
4168 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4169 let tick = make_tick("ETH-USD", dec!(3000), dec!(1), 60_000);
4170 let result = agg.feed(&tick);
4171 assert!(matches!(result, Err(StreamError::AggregationError { .. })));
4172 }
4173
4174 #[test]
4175 fn test_ohlcv_aggregator_volume_accumulates() {
4176 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4177 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1.5), 60_000))
4178 .unwrap();
4179 agg.feed(&make_tick("BTC-USD", dec!(50100), dec!(2.5), 60_100))
4180 .unwrap();
4181 let bar = agg.current_bar().unwrap();
4182 assert_eq!(bar.volume, dec!(4));
4183 }
4184
4185 #[test]
4186 fn test_ohlcv_bar_symbol_and_timeframe() {
4187 let mut agg = agg("BTC-USD", Timeframe::Minutes(5));
4188 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1), 300_000))
4189 .unwrap();
4190 let bar = agg.current_bar().unwrap();
4191 assert_eq!(bar.symbol, "BTC-USD");
4192 assert_eq!(bar.timeframe, Timeframe::Minutes(5));
4193 }
4194
4195 #[test]
4196 fn test_ohlcv_aggregator_symbol_accessor() {
4197 let agg = agg("ETH-USD", Timeframe::Hours(1));
4198 assert_eq!(agg.symbol(), "ETH-USD");
4199 assert_eq!(agg.timeframe(), Timeframe::Hours(1));
4200 }
4201
4202 #[test]
4203 fn test_bar_aligned_by_exchange_ts_not_received_ts() {
4204 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4208 let tick = make_tick_with_exchange_ts("BTC-USD", dec!(50000), dec!(1), 60_500, 120_100);
4209 agg.feed(&tick).unwrap();
4210 let bar = agg.current_bar().unwrap();
4211 assert_eq!(bar.bar_start_ms, 60_000, "bar should use exchange_ts_ms");
4212 }
4213
4214 #[test]
4215 fn test_bar_falls_back_to_received_ts_when_no_exchange_ts() {
4216 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4217 let tick = make_tick("BTC-USD", dec!(50000), dec!(1), 75_000);
4218 agg.feed(&tick).unwrap();
4219 let bar = agg.current_bar().unwrap();
4220 assert_eq!(bar.bar_start_ms, 60_000);
4221 }
4222
4223 #[test]
4226 fn test_emit_empty_bars_no_gap_no_empties() {
4227 let mut agg = OhlcvAggregator::new("BTC-USD", Timeframe::Minutes(1))
4229 .unwrap()
4230 .with_emit_empty_bars(true);
4231 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1), 60_000))
4232 .unwrap();
4233 let bars = agg
4234 .feed(&make_tick("BTC-USD", dec!(50100), dec!(1), 120_000))
4235 .unwrap();
4236 assert_eq!(bars.len(), 1);
4238 assert_eq!(bars[0].bar_start_ms, 60_000);
4239 assert_eq!(bars[0].volume, dec!(1));
4240 }
4241
4242 #[test]
4243 fn test_emit_empty_bars_two_skipped_windows() {
4244 let mut agg = OhlcvAggregator::new("BTC-USD", Timeframe::Minutes(1))
4247 .unwrap()
4248 .with_emit_empty_bars(true);
4249 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1), 60_000))
4250 .unwrap();
4251 let bars = agg
4252 .feed(&make_tick("BTC-USD", dec!(51000), dec!(1), 240_000))
4253 .unwrap();
4254 assert_eq!(bars.len(), 3);
4256 assert_eq!(bars[0].bar_start_ms, 60_000);
4257 assert!(!bars[0].volume.is_zero()); assert_eq!(bars[1].bar_start_ms, 120_000);
4259 assert!(bars[1].volume.is_zero()); assert_eq!(bars[1].trade_count, 0);
4261 assert_eq!(bars[1].open, dec!(50000)); assert_eq!(bars[2].bar_start_ms, 180_000);
4263 assert!(bars[2].volume.is_zero()); }
4265
4266 #[test]
4267 fn test_emit_empty_bars_disabled_no_empties_on_gap() {
4268 let mut agg = OhlcvAggregator::new("BTC-USD", Timeframe::Minutes(1))
4269 .unwrap()
4270 .with_emit_empty_bars(false);
4271 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1), 60_000))
4272 .unwrap();
4273 let bars = agg
4274 .feed(&make_tick("BTC-USD", dec!(51000), dec!(1), 240_000))
4275 .unwrap();
4276 assert_eq!(bars.len(), 1); }
4278
4279 #[test]
4280 fn test_emit_empty_bars_is_complete_true() {
4281 let mut agg = OhlcvAggregator::new("BTC-USD", Timeframe::Minutes(1))
4282 .unwrap()
4283 .with_emit_empty_bars(true);
4284 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1), 60_000))
4285 .unwrap();
4286 let bars = agg
4287 .feed(&make_tick("BTC-USD", dec!(51000), dec!(1), 240_000))
4288 .unwrap();
4289 for bar in &bars {
4290 assert!(bar.is_complete, "all emitted bars must be marked complete");
4291 }
4292 }
4293
4294 #[test]
4295 fn test_ohlcv_bar_display() {
4296 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4297 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1), 60_000))
4298 .unwrap();
4299 let bar = agg.current_bar().unwrap();
4300 let s = bar.to_string();
4301 assert!(s.contains("BTC-USD"));
4302 assert!(s.contains("1m"));
4303 assert!(s.contains("50000"));
4304 }
4305
4306 #[test]
4307 fn test_bar_count_increments_on_feed() {
4308 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4309 assert_eq!(agg.bar_count(), 0);
4310 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1), 60_000))
4311 .unwrap();
4312 agg.feed(&make_tick("BTC-USD", dec!(50100), dec!(1), 120_000))
4313 .unwrap();
4314 assert_eq!(agg.bar_count(), 1);
4315 }
4316
4317 #[test]
4318 fn test_bar_count_increments_on_flush() {
4319 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4320 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1), 60_000))
4321 .unwrap();
4322 agg.flush().unwrap();
4323 assert_eq!(agg.bar_count(), 1);
4324 }
4325
4326 #[test]
4327 fn test_ohlcv_bar_range() {
4328 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4329 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1), 60_000))
4330 .unwrap();
4331 agg.feed(&make_tick("BTC-USD", dec!(51000), dec!(1), 60_100))
4332 .unwrap();
4333 agg.feed(&make_tick("BTC-USD", dec!(49500), dec!(1), 60_200))
4334 .unwrap();
4335 let bar = agg.current_bar().unwrap();
4336 assert_eq!(bar.range(), dec!(1500)); }
4338
4339 #[test]
4340 fn test_ohlcv_bar_body_bullish() {
4341 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4342 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1), 60_000))
4343 .unwrap();
4344 agg.feed(&make_tick("BTC-USD", dec!(50500), dec!(1), 60_100))
4345 .unwrap();
4346 let bar = agg.current_bar().unwrap();
4347 assert_eq!(bar.body(), dec!(500));
4349 }
4350
4351 #[test]
4352 fn test_ohlcv_bar_body_bearish() {
4353 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4354 agg.feed(&make_tick("BTC-USD", dec!(50500), dec!(1), 60_000))
4355 .unwrap();
4356 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1), 60_100))
4357 .unwrap();
4358 let bar = agg.current_bar().unwrap();
4359 assert_eq!(bar.body(), dec!(500));
4361 }
4362
4363 #[test]
4364 fn test_aggregator_reset_clears_bar_and_count() {
4365 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4366 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1), 60_000))
4367 .unwrap();
4368 agg.feed(&make_tick("BTC-USD", dec!(50100), dec!(1), 120_000))
4369 .unwrap();
4370 assert_eq!(agg.bar_count(), 1);
4371 assert!(agg.current_bar().is_some());
4372 agg.reset();
4373 assert_eq!(agg.bar_count(), 0);
4374 assert!(agg.current_bar().is_none());
4375 }
4376
4377 #[test]
4378 fn test_ohlcv_bar_is_bullish_when_close_gt_open() {
4379 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4380 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1), 60_000))
4381 .unwrap();
4382 agg.feed(&make_tick("BTC-USD", dec!(51000), dec!(1), 60_100))
4383 .unwrap();
4384 let bar = agg.current_bar().unwrap();
4385 assert!(bar.is_bullish());
4386 assert!(!bar.is_bearish());
4387 }
4388
4389 #[test]
4390 fn test_ohlcv_bar_is_bearish_when_close_lt_open() {
4391 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4392 agg.feed(&make_tick("BTC-USD", dec!(51000), dec!(1), 60_000))
4393 .unwrap();
4394 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1), 60_100))
4395 .unwrap();
4396 let bar = agg.current_bar().unwrap();
4397 assert!(bar.is_bearish());
4398 assert!(!bar.is_bullish());
4399 }
4400
4401 #[test]
4402 fn test_ohlcv_bar_neither_bullish_nor_bearish_on_equal_open_close() {
4403 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4404 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1), 60_000))
4405 .unwrap();
4406 let bar = agg.current_bar().unwrap();
4408 assert!(!bar.is_bullish());
4409 assert!(!bar.is_bearish());
4410 }
4411
4412 #[test]
4413 fn test_ohlcv_bar_vwap_single_tick_equals_price() {
4414 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4415 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(2), 60_000))
4416 .unwrap();
4417 let bar = agg.current_bar().unwrap();
4418 assert_eq!(bar.vwap, Some(dec!(50000)));
4419 }
4420
4421 #[test]
4422 fn test_ohlcv_bar_vwap_two_equal_price_ticks() {
4423 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4424 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1), 60_000))
4425 .unwrap();
4426 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(3), 60_100))
4427 .unwrap();
4428 let bar = agg.current_bar().unwrap();
4429 assert_eq!(bar.vwap, Some(dec!(50000)));
4431 }
4432
4433 #[test]
4434 fn test_ohlcv_bar_vwap_two_different_price_ticks() {
4435 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4436 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1), 60_000))
4437 .unwrap();
4438 agg.feed(&make_tick("BTC-USD", dec!(51000), dec!(1), 60_100))
4439 .unwrap();
4440 let bar = agg.current_bar().unwrap();
4441 assert_eq!(bar.vwap, Some(dec!(50500)));
4443 }
4444
4445 #[test]
4446 fn test_ohlcv_bar_vwap_gap_fill_is_none() {
4447 let mut agg = OhlcvAggregator::new("BTC-USD", Timeframe::Minutes(1))
4448 .unwrap()
4449 .with_emit_empty_bars(true);
4450 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1), 60_000))
4451 .unwrap();
4452 let bars = agg
4453 .feed(&make_tick("BTC-USD", dec!(51000), dec!(1), 240_000))
4454 .unwrap();
4455 assert!(bars[0].vwap.is_some());
4457 assert!(bars[1].vwap.is_none());
4458 assert!(bars[2].vwap.is_none());
4459 }
4460
4461 #[test]
4462 fn test_aggregator_reset_allows_fresh_start() {
4463 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4464 agg.feed(&make_tick("BTC-USD", dec!(50000), dec!(1), 60_000))
4465 .unwrap();
4466 agg.reset();
4467 agg.feed(&make_tick("BTC-USD", dec!(99999), dec!(2), 60_000))
4468 .unwrap();
4469 let bar = agg.current_bar().unwrap();
4470 assert_eq!(bar.open, dec!(99999));
4471 }
4472
4473 #[test]
4476 fn test_from_duration_ms_hours() {
4477 assert_eq!(Timeframe::from_duration_ms(3_600_000), Some(Timeframe::Hours(1)));
4478 assert_eq!(Timeframe::from_duration_ms(7_200_000), Some(Timeframe::Hours(2)));
4479 }
4480
4481 #[test]
4482 fn test_from_duration_ms_minutes() {
4483 assert_eq!(Timeframe::from_duration_ms(300_000), Some(Timeframe::Minutes(5)));
4484 assert_eq!(Timeframe::from_duration_ms(60_000), Some(Timeframe::Minutes(1)));
4485 }
4486
4487 #[test]
4488 fn test_from_duration_ms_seconds() {
4489 assert_eq!(Timeframe::from_duration_ms(15_000), Some(Timeframe::Seconds(15)));
4490 assert_eq!(Timeframe::from_duration_ms(1_000), Some(Timeframe::Seconds(1)));
4491 }
4492
4493 #[test]
4494 fn test_from_duration_ms_zero_returns_none() {
4495 assert_eq!(Timeframe::from_duration_ms(0), None);
4496 }
4497
4498 #[test]
4499 fn test_from_duration_ms_non_whole_second_returns_none() {
4500 assert_eq!(Timeframe::from_duration_ms(1_500), None);
4501 }
4502
4503 #[test]
4504 fn test_from_duration_ms_roundtrip() {
4505 for tf in [Timeframe::Seconds(30), Timeframe::Minutes(5), Timeframe::Hours(4)] {
4506 assert_eq!(Timeframe::from_duration_ms(tf.duration_ms()), Some(tf));
4507 }
4508 }
4509
4510 #[test]
4513 fn test_is_doji_exact_zero_body() {
4514 let bar = OhlcvBar {
4515 symbol: "X".into(), timeframe: Timeframe::Minutes(1),
4516 bar_start_ms: 0, open: dec!(100), high: dec!(105),
4517 low: dec!(95), close: dec!(100),
4518 volume: dec!(1), trade_count: 1, is_complete: true,
4519 is_gap_fill: false, vwap: None,
4520 };
4521 assert!(bar.is_doji(Decimal::ZERO));
4522 }
4523
4524 #[test]
4525 fn test_is_doji_small_epsilon() {
4526 let bar = OhlcvBar {
4527 symbol: "X".into(), timeframe: Timeframe::Minutes(1),
4528 bar_start_ms: 0, open: dec!(100), high: dec!(105),
4529 low: dec!(95), close: dec!(100.005),
4530 volume: dec!(1), trade_count: 1, is_complete: true,
4531 is_gap_fill: false, vwap: None,
4532 };
4533 assert!(bar.is_doji(dec!(0.01)));
4534 assert!(!bar.is_doji(Decimal::ZERO));
4535 }
4536
4537 #[test]
4538 fn test_wick_upper_bullish() {
4539 let bar = OhlcvBar {
4541 symbol: "X".into(), timeframe: Timeframe::Minutes(1),
4542 bar_start_ms: 0, open: dec!(100), high: dec!(107),
4543 low: dec!(98), close: dec!(104),
4544 volume: dec!(1), trade_count: 1, is_complete: true,
4545 is_gap_fill: false, vwap: None,
4546 };
4547 assert_eq!(bar.wick_upper(), dec!(3));
4548 }
4549
4550 #[test]
4551 fn test_wick_lower_bearish() {
4552 let bar = OhlcvBar {
4554 symbol: "X".into(), timeframe: Timeframe::Minutes(1),
4555 bar_start_ms: 0, open: dec!(104), high: dec!(107),
4556 low: dec!(97), close: dec!(100),
4557 volume: dec!(1), trade_count: 1, is_complete: true,
4558 is_gap_fill: false, vwap: None,
4559 };
4560 assert_eq!(bar.wick_lower(), dec!(3));
4561 }
4562
4563 #[test]
4566 fn test_window_progress_none_when_no_bar() {
4567 let agg = agg("BTC-USD", Timeframe::Minutes(1));
4568 assert!(agg.window_progress(60_000).is_none());
4569 }
4570
4571 #[test]
4572 fn test_window_progress_at_start_is_zero() {
4573 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4574 agg.feed(&make_tick("BTC-USD", dec!(100), dec!(1), 60_000)).unwrap();
4576 assert_eq!(agg.window_progress(60_000), Some(0.0));
4577 }
4578
4579 #[test]
4580 fn test_window_progress_midpoint() {
4581 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4582 agg.feed(&make_tick("BTC-USD", dec!(100), dec!(1), 60_000)).unwrap();
4583 let progress = agg.window_progress(90_000).unwrap();
4585 assert!((progress - 0.5).abs() < 1e-9);
4586 }
4587
4588 #[test]
4589 fn test_window_progress_clamps_at_one() {
4590 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4591 agg.feed(&make_tick("BTC-USD", dec!(100), dec!(1), 60_000)).unwrap();
4592 assert_eq!(agg.window_progress(150_000), Some(1.0));
4594 }
4595
4596 #[test]
4599 fn test_price_change_bullish_is_positive() {
4600 let bar = make_bar(dec!(100), dec!(110), dec!(98), dec!(105));
4601 assert_eq!(bar.price_change(), dec!(5));
4602 }
4603
4604 #[test]
4605 fn test_price_change_bearish_is_negative() {
4606 let bar = make_bar(dec!(105), dec!(110), dec!(98), dec!(100));
4607 assert_eq!(bar.price_change(), dec!(-5));
4608 }
4609
4610 #[test]
4611 fn test_price_change_doji_is_zero() {
4612 let bar = make_bar(dec!(100), dec!(102), dec!(98), dec!(100));
4613 assert_eq!(bar.price_change(), dec!(0));
4614 }
4615
4616 #[test]
4619 fn test_total_volume_zero_before_completion() {
4620 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4621 agg.feed(&make_tick("BTC-USD", dec!(100), dec!(2), 60_000)).unwrap();
4622 assert_eq!(agg.total_volume(), dec!(0));
4624 }
4625
4626 #[test]
4627 fn test_total_volume_accumulates_across_bars() {
4628 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4629 agg.feed(&make_tick("BTC-USD", dec!(100), dec!(2), 60_000)).unwrap();
4631 agg.feed(&make_tick("BTC-USD", dec!(101), dec!(3), 120_000)).unwrap();
4633 assert_eq!(agg.total_volume(), dec!(2));
4635 agg.feed(&make_tick("BTC-USD", dec!(102), dec!(5), 180_000)).unwrap();
4637 assert_eq!(agg.total_volume(), dec!(5)); }
4639
4640 #[test]
4641 fn test_total_volume_reset_clears() {
4642 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4643 agg.feed(&make_tick("BTC-USD", dec!(100), dec!(2), 60_000)).unwrap();
4644 agg.feed(&make_tick("BTC-USD", dec!(101), dec!(3), 120_000)).unwrap();
4645 agg.reset();
4646 assert_eq!(agg.total_volume(), dec!(0));
4647 }
4648
4649 fn make_bar(open: Decimal, high: Decimal, low: Decimal, close: Decimal) -> OhlcvBar {
4652 OhlcvBar {
4653 symbol: "X".into(),
4654 timeframe: Timeframe::Minutes(1),
4655 bar_start_ms: 0,
4656 open,
4657 high,
4658 low,
4659 close,
4660 volume: dec!(1),
4661 trade_count: 1,
4662 is_complete: true,
4663 is_gap_fill: false,
4664 vwap: None,
4665 }
4666 }
4667
4668 #[test]
4669 fn test_typical_price() {
4670 let bar = make_bar(dec!(9), dec!(12), dec!(8), dec!(10));
4672 assert_eq!(bar.typical_price(), dec!(10));
4673 }
4674
4675 #[test]
4676 fn test_median_price() {
4677 let bar = make_bar(dec!(9), dec!(12), dec!(8), dec!(10));
4679 assert_eq!(bar.median_price(), dec!(10));
4680 }
4681
4682 #[test]
4683 fn test_typical_price_differs_from_median() {
4684 let bar = make_bar(dec!(8), dec!(10), dec!(6), dec!(10));
4686 assert_eq!(bar.median_price(), dec!(8));
4687 assert!(bar.typical_price() > bar.median_price());
4688 }
4689
4690 #[test]
4691 fn test_close_location_value_at_high() {
4692 let bar = make_bar(dec!(100), dec!(110), dec!(90), dec!(110));
4694 let clv = bar.close_location_value().unwrap();
4695 assert!((clv - 1.0).abs() < 1e-9, "expected 1.0 got {clv}");
4696 }
4697
4698 #[test]
4699 fn test_close_location_value_at_low() {
4700 let bar = make_bar(dec!(100), dec!(110), dec!(90), dec!(90));
4702 let clv = bar.close_location_value().unwrap();
4703 assert!((clv + 1.0).abs() < 1e-9, "expected -1.0 got {clv}");
4704 }
4705
4706 #[test]
4707 fn test_close_location_value_midpoint_is_zero() {
4708 let bar = make_bar(dec!(100), dec!(110), dec!(90), dec!(100));
4710 let clv = bar.close_location_value().unwrap();
4711 assert!(clv.abs() < 1e-9, "expected 0.0 got {clv}");
4712 }
4713
4714 #[test]
4715 fn test_close_location_value_zero_range_returns_none() {
4716 let bar = make_bar(dec!(100), dec!(100), dec!(100), dec!(100));
4717 assert!(bar.close_location_value().is_none());
4718 }
4719
4720 #[test]
4721 fn test_body_direction_bullish() {
4722 let bar = make_bar(dec!(90), dec!(110), dec!(85), dec!(105));
4723 assert_eq!(bar.body_direction(), BarDirection::Bullish);
4724 }
4725
4726 #[test]
4727 fn test_body_direction_bearish() {
4728 let bar = make_bar(dec!(105), dec!(110), dec!(85), dec!(90));
4729 assert_eq!(bar.body_direction(), BarDirection::Bearish);
4730 }
4731
4732 #[test]
4733 fn test_body_direction_neutral() {
4734 let bar = make_bar(dec!(100), dec!(110), dec!(85), dec!(100));
4735 assert_eq!(bar.body_direction(), BarDirection::Neutral);
4736 }
4737
4738 #[test]
4741 fn test_last_bar_none_before_completion() {
4742 let agg = agg("BTC-USD", Timeframe::Minutes(1));
4743 assert!(agg.last_bar().is_none());
4744 }
4745
4746 #[test]
4747 fn test_last_bar_set_after_bar_completion() {
4748 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4749 agg.feed(&make_tick("BTC-USD", dec!(100), dec!(1), 60_000)).unwrap();
4751 agg.feed(&make_tick("BTC-USD", dec!(200), dec!(1), 120_000)).unwrap();
4753 let last = agg.last_bar().unwrap();
4754 assert!(last.is_complete);
4755 assert_eq!(last.close, dec!(100));
4756 }
4757
4758 #[test]
4759 fn test_last_bar_set_after_flush() {
4760 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4761 agg.feed(&make_tick("BTC-USD", dec!(50), dec!(1), 60_000)).unwrap();
4762 let flushed = agg.flush().unwrap();
4763 assert_eq!(agg.last_bar().unwrap().close, flushed.close);
4764 }
4765
4766 #[test]
4767 fn test_last_bar_cleared_on_reset() {
4768 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4769 agg.feed(&make_tick("BTC-USD", dec!(100), dec!(1), 60_000)).unwrap();
4770 agg.feed(&make_tick("BTC-USD", dec!(200), dec!(1), 120_000)).unwrap();
4771 assert!(agg.last_bar().is_some());
4772 agg.reset();
4773 assert!(agg.last_bar().is_none());
4774 }
4775
4776 #[test]
4779 fn test_weighted_close_basic() {
4780 let bar = make_bar(dec!(9), dec!(12), dec!(8), dec!(10));
4782 assert_eq!(bar.weighted_close(), dec!(10));
4783 }
4784
4785 #[test]
4786 fn test_weighted_close_weights_close_more_than_typical() {
4787 let bar = make_bar(dec!(50), dec!(100), dec!(0), dec!(80));
4789 assert_eq!(bar.weighted_close(), dec!(65));
4790 }
4791
4792 #[test]
4793 fn test_price_change_pct_bullish() {
4794 let bar = make_bar(dec!(100), dec!(115), dec!(95), dec!(110));
4796 let pct = bar.price_change_pct().unwrap();
4797 assert!((pct - 10.0).abs() < 1e-9, "expected 10.0 got {pct}");
4798 }
4799
4800 #[test]
4801 fn test_price_change_pct_bearish() {
4802 let bar = make_bar(dec!(200), dec!(210), dec!(175), dec!(180));
4804 let pct = bar.price_change_pct().unwrap();
4805 assert!((pct - (-10.0)).abs() < 1e-9, "expected -10.0 got {pct}");
4806 }
4807
4808 #[test]
4809 fn test_price_change_pct_zero_open_returns_none() {
4810 let bar = make_bar(dec!(0), dec!(5), dec!(0), dec!(3));
4811 assert!(bar.price_change_pct().is_none());
4812 }
4813
4814 #[test]
4815 fn test_wick_ratio_all_wicks() {
4816 let bar = make_bar(dec!(5), dec!(10), dec!(0), dec!(5));
4818 let r = bar.wick_ratio().unwrap();
4819 assert!((r - 1.0).abs() < 1e-9, "expected 1.0 got {r}");
4820 }
4821
4822 #[test]
4823 fn test_wick_ratio_no_wicks() {
4824 let bar = make_bar(dec!(0), dec!(10), dec!(0), dec!(10));
4826 let r = bar.wick_ratio().unwrap();
4827 assert!((r - 0.0).abs() < 1e-9, "expected 0.0 got {r}");
4828 }
4829
4830 #[test]
4831 fn test_wick_ratio_zero_range_returns_none() {
4832 let bar = make_bar(dec!(5), dec!(5), dec!(5), dec!(5));
4834 assert!(bar.wick_ratio().is_none());
4835 }
4836
4837 #[test]
4840 fn test_body_ratio_no_wicks_is_one() {
4841 let bar = make_bar(dec!(0), dec!(10), dec!(0), dec!(10));
4843 let r = bar.body_ratio().unwrap();
4844 assert!((r - 1.0).abs() < 1e-9);
4845 }
4846
4847 #[test]
4848 fn test_body_ratio_all_wicks_is_zero() {
4849 let bar = make_bar(dec!(5), dec!(10), dec!(0), dec!(5));
4851 let r = bar.body_ratio().unwrap();
4852 assert!((r - 0.0).abs() < 1e-9);
4853 }
4854
4855 #[test]
4856 fn test_body_ratio_zero_range_returns_none() {
4857 let bar = make_bar(dec!(5), dec!(5), dec!(5), dec!(5));
4858 assert!(bar.body_ratio().is_none());
4859 }
4860
4861 #[test]
4862 fn test_body_ratio_plus_wick_ratio_equals_one() {
4863 let bar = make_bar(dec!(4), dec!(10), dec!(0), dec!(8));
4865 let body = bar.body_ratio().unwrap();
4866 let wick = bar.wick_ratio().unwrap();
4867 assert!((body + wick - 1.0).abs() < 1e-9);
4868 }
4869
4870 #[test]
4873 fn test_average_volume_none_before_bars() {
4874 let agg = agg("BTC-USD", Timeframe::Minutes(1));
4875 assert!(agg.average_volume().is_none());
4876 }
4877
4878 #[test]
4879 fn test_average_volume_one_bar() {
4880 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4881 agg.feed(&make_tick("BTC-USD", dec!(100), dec!(4), 60_000)).unwrap();
4882 agg.feed(&make_tick("BTC-USD", dec!(101), dec!(1), 120_000)).unwrap();
4883 assert_eq!(agg.average_volume(), Some(dec!(4)));
4885 }
4886
4887 #[test]
4888 fn test_average_volume_two_bars() {
4889 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4890 agg.feed(&make_tick("BTC-USD", dec!(100), dec!(4), 60_000)).unwrap();
4891 agg.feed(&make_tick("BTC-USD", dec!(101), dec!(6), 120_000)).unwrap();
4892 agg.feed(&make_tick("BTC-USD", dec!(102), dec!(1), 180_000)).unwrap();
4893 assert_eq!(agg.average_volume(), Some(dec!(5)));
4895 }
4896
4897 #[test]
4900 fn test_true_range_no_gap() {
4901 let bar = make_bar(dec!(9), dec!(12), dec!(8), dec!(11));
4903 assert_eq!(bar.true_range(dec!(10)), dec!(4));
4904 }
4905
4906 #[test]
4907 fn test_true_range_gap_up() {
4908 let bar = make_bar(dec!(12), dec!(15), dec!(12), dec!(13));
4910 assert_eq!(bar.true_range(dec!(10)), dec!(5));
4911 }
4912
4913 #[test]
4914 fn test_true_range_gap_down() {
4915 let bar = make_bar(dec!(7), dec!(8), dec!(5), dec!(6));
4917 assert_eq!(bar.true_range(dec!(12)), dec!(7));
4918 }
4919
4920 #[test]
4921 fn test_inside_bar_true_when_contained() {
4922 let prev = make_bar(dec!(9), dec!(15), dec!(5), dec!(12));
4923 let curr = make_bar(dec!(10), dec!(14), dec!(6), dec!(11));
4924 assert!(curr.is_inside_bar(&prev));
4925 }
4926
4927 #[test]
4928 fn test_inside_bar_false_when_not_contained() {
4929 let prev = make_bar(dec!(9), dec!(15), dec!(5), dec!(12));
4930 let curr = make_bar(dec!(10), dec!(16), dec!(6), dec!(11));
4931 assert!(!curr.is_inside_bar(&prev));
4932 }
4933
4934 #[test]
4935 fn test_outside_bar_true_when_engulfing() {
4936 let prev = make_bar(dec!(9), dec!(12), dec!(8), dec!(11));
4937 let curr = make_bar(dec!(10), dec!(14), dec!(6), dec!(11));
4938 assert!(curr.outside_bar(&prev));
4939 }
4940
4941 #[test]
4942 fn test_outside_bar_false_when_not_engulfing() {
4943 let prev = make_bar(dec!(9), dec!(12), dec!(8), dec!(11));
4944 let curr = make_bar(dec!(10), dec!(11), dec!(9), dec!(10));
4945 assert!(!curr.outside_bar(&prev));
4946 }
4947
4948 #[test]
4951 fn test_is_hammer_classic() {
4952 let bar = make_bar(dec!(9), dec!(10), dec!(0), dec!(9));
4955 assert!(bar.is_hammer());
4956 }
4957
4958 #[test]
4959 fn test_is_hammer_false_large_upper_wick() {
4960 let bar = make_bar(dec!(5), dec!(10), dec!(0), dec!(5));
4962 assert!(!bar.is_hammer());
4963 }
4964
4965 #[test]
4966 fn test_is_hammer_false_zero_range() {
4967 let bar = make_bar(dec!(5), dec!(5), dec!(5), dec!(5));
4968 assert!(!bar.is_hammer());
4969 }
4970
4971 #[test]
4974 fn test_peak_volume_none_before_completion() {
4975 let agg = agg("BTC-USD", Timeframe::Minutes(1));
4976 assert!(agg.peak_volume().is_none());
4977 }
4978
4979 #[test]
4980 fn test_peak_volume_tracks_maximum() {
4981 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4982 agg.feed(&make_tick("BTC-USD", dec!(100), dec!(3), 60_000)).unwrap();
4984 agg.feed(&make_tick("BTC-USD", dec!(101), dec!(10), 120_000)).unwrap();
4986 assert_eq!(agg.peak_volume(), Some(dec!(3)));
4987 agg.feed(&make_tick("BTC-USD", dec!(102), dec!(1), 180_000)).unwrap();
4989 assert_eq!(agg.peak_volume(), Some(dec!(10)));
4990 }
4991
4992 #[test]
4993 fn test_peak_volume_reset_clears() {
4994 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
4995 agg.feed(&make_tick("BTC-USD", dec!(100), dec!(5), 60_000)).unwrap();
4996 agg.feed(&make_tick("BTC-USD", dec!(101), dec!(1), 120_000)).unwrap();
4997 agg.reset();
4998 assert!(agg.peak_volume().is_none());
4999 }
5000
5001 #[test]
5002 fn test_peak_volume_via_flush() {
5003 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
5004 agg.feed(&make_tick("BTC-USD", dec!(100), dec!(7), 60_000)).unwrap();
5005 agg.flush();
5006 assert_eq!(agg.peak_volume(), Some(dec!(7)));
5007 }
5008
5009 #[test]
5012 fn test_is_shooting_star_classic() {
5013 let bar = make_bar(dec!(1), dec!(10), dec!(0), dec!(1));
5016 assert!(bar.is_shooting_star());
5017 }
5018
5019 #[test]
5020 fn test_is_shooting_star_false_large_lower_wick() {
5021 let bar = make_bar(dec!(5), dec!(10), dec!(0), dec!(5));
5023 assert!(!bar.is_shooting_star());
5024 }
5025
5026 #[test]
5027 fn test_is_shooting_star_false_zero_range() {
5028 let bar = make_bar(dec!(5), dec!(5), dec!(5), dec!(5));
5029 assert!(!bar.is_shooting_star());
5030 }
5031
5032 #[test]
5033 fn test_hammer_and_shooting_star_are_mutually_exclusive_for_typical_bars() {
5034 let hammer = make_bar(dec!(9), dec!(10), dec!(0), dec!(9));
5036 let star = make_bar(dec!(1), dec!(10), dec!(0), dec!(1));
5038 assert!(hammer.is_hammer() && !hammer.is_shooting_star());
5039 assert!(star.is_shooting_star() && !star.is_hammer());
5040 }
5041
5042 #[test]
5045 fn test_min_volume_none_before_completion() {
5046 let agg = agg("BTC-USD", Timeframe::Minutes(1));
5047 assert!(agg.min_volume().is_none());
5048 }
5049
5050 #[test]
5051 fn test_min_volume_tracks_minimum() {
5052 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
5053 agg.feed(&make_tick("BTC-USD", dec!(100), dec!(10), 60_000)).unwrap();
5055 agg.feed(&make_tick("BTC-USD", dec!(101), dec!(1), 120_000)).unwrap();
5056 assert_eq!(agg.min_volume(), Some(dec!(10)));
5057 agg.feed(&make_tick("BTC-USD", dec!(102), dec!(5), 180_000)).unwrap();
5059 assert_eq!(agg.min_volume(), Some(dec!(1)));
5060 }
5061
5062 #[test]
5063 fn test_min_volume_reset_clears() {
5064 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
5065 agg.feed(&make_tick("BTC-USD", dec!(100), dec!(5), 60_000)).unwrap();
5066 agg.feed(&make_tick("BTC-USD", dec!(101), dec!(1), 120_000)).unwrap();
5067 agg.reset();
5068 assert!(agg.min_volume().is_none());
5069 }
5070
5071 #[test]
5074 fn test_is_gap_up_true() {
5075 let prev = make_bar(dec!(5), dec!(10), dec!(4), dec!(8));
5076 let curr = make_bar(dec!(9), dec!(12), dec!(8), dec!(11)); assert!(curr.is_gap_up(&prev));
5078 }
5079
5080 #[test]
5081 fn test_is_gap_up_false_when_equal() {
5082 let prev = make_bar(dec!(5), dec!(10), dec!(4), dec!(8));
5083 let curr = make_bar(dec!(8), dec!(12), dec!(7), dec!(11)); assert!(!curr.is_gap_up(&prev));
5085 }
5086
5087 #[test]
5088 fn test_is_gap_down_true() {
5089 let prev = make_bar(dec!(5), dec!(10), dec!(4), dec!(8));
5090 let curr = make_bar(dec!(7), dec!(8), dec!(6), dec!(7)); assert!(curr.is_gap_down(&prev));
5092 }
5093
5094 #[test]
5095 fn test_is_gap_down_false_when_equal() {
5096 let prev = make_bar(dec!(5), dec!(10), dec!(4), dec!(8));
5097 let curr = make_bar(dec!(8), dec!(9), dec!(7), dec!(8)); assert!(!curr.is_gap_down(&prev));
5099 }
5100
5101 #[test]
5104 fn test_volume_range_none_before_completion() {
5105 let agg = agg("BTC-USD", Timeframe::Minutes(1));
5106 assert!(agg.volume_range().is_none());
5107 }
5108
5109 #[test]
5110 fn test_volume_range_after_two_bars() {
5111 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
5112 agg.feed(&make_tick("BTC-USD", dec!(100), dec!(3), 60_000)).unwrap();
5113 agg.feed(&make_tick("BTC-USD", dec!(101), dec!(10), 120_000)).unwrap();
5114 agg.feed(&make_tick("BTC-USD", dec!(102), dec!(1), 180_000)).unwrap();
5115 assert_eq!(agg.volume_range(), Some((dec!(3), dec!(10))));
5117 }
5118
5119 fn make_ohlcv_bar(open: Decimal, high: Decimal, low: Decimal, close: Decimal) -> OhlcvBar {
5122 OhlcvBar {
5123 symbol: "X".into(),
5124 timeframe: Timeframe::Minutes(1),
5125 open,
5126 high,
5127 low,
5128 close,
5129 volume: dec!(1),
5130 bar_start_ms: 0,
5131 trade_count: 1,
5132 is_complete: false,
5133 is_gap_fill: false,
5134 vwap: None,
5135 }
5136 }
5137
5138 #[test]
5139 fn test_body_to_range_ratio_bullish_full_body() {
5140 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(100), dec!(110));
5142 assert_eq!(bar.body_to_range_ratio(), Some(dec!(1)));
5143 }
5144
5145 #[test]
5146 fn test_body_to_range_ratio_doji_like() {
5147 let bar = make_ohlcv_bar(dec!(100), dec!(102), dec!(98), dec!(100));
5149 assert_eq!(bar.body_to_range_ratio(), Some(dec!(0)));
5150 }
5151
5152 #[test]
5153 fn test_body_to_range_ratio_none_when_range_zero() {
5154 let bar = make_ohlcv_bar(dec!(100), dec!(100), dec!(100), dec!(100));
5155 assert!(bar.body_to_range_ratio().is_none());
5156 }
5157
5158 #[test]
5161 fn test_is_active_false_before_any_ticks() {
5162 let agg = agg("BTC-USD", Timeframe::Minutes(1));
5163 assert!(!agg.is_active());
5164 }
5165
5166 #[test]
5167 fn test_is_active_true_after_first_tick() {
5168 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
5169 agg.feed(&make_tick("BTC-USD", dec!(100), dec!(1), 1_000)).unwrap();
5170 assert!(agg.is_active());
5171 }
5172
5173 #[test]
5174 fn test_is_active_false_after_flush() {
5175 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
5176 agg.feed(&make_tick("BTC-USD", dec!(100), dec!(1), 1_000)).unwrap();
5177 agg.flush();
5178 assert!(!agg.is_active());
5179 }
5180
5181 #[test]
5184 fn test_is_long_upper_wick_true_when_upper_wick_dominates() {
5185 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(100), dec!(101));
5187 assert!(bar.is_long_upper_wick());
5188 }
5189
5190 #[test]
5191 fn test_is_long_upper_wick_false_for_full_body() {
5192 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(100), dec!(110));
5194 assert!(!bar.is_long_upper_wick());
5195 }
5196
5197 #[test]
5198 fn test_is_long_upper_wick_false_when_equal() {
5199 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(100), dec!(105));
5201 assert!(!bar.is_long_upper_wick());
5202 }
5203
5204 #[test]
5207 fn test_price_change_abs_bullish_bar() {
5208 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(100), dec!(108));
5209 assert_eq!(bar.price_change_abs(), dec!(8));
5210 }
5211
5212 #[test]
5213 fn test_price_change_abs_bearish_bar() {
5214 let bar = make_ohlcv_bar(dec!(110), dec!(110), dec!(100), dec!(102));
5215 assert_eq!(bar.price_change_abs(), dec!(8));
5216 }
5217
5218 #[test]
5219 fn test_price_change_abs_doji_zero() {
5220 let bar = make_ohlcv_bar(dec!(100), dec!(105), dec!(95), dec!(100));
5221 assert_eq!(bar.price_change_abs(), dec!(0));
5222 }
5223
5224 #[test]
5227 fn test_vwap_current_none_before_any_ticks() {
5228 let agg = agg("BTC-USD", Timeframe::Minutes(1));
5229 assert!(agg.vwap_current().is_none());
5230 }
5231
5232 #[test]
5233 fn test_vwap_current_equals_price_for_single_tick() {
5234 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
5235 agg.feed(&make_tick("BTC-USD", dec!(200), dec!(5), 1_000)).unwrap();
5236 assert_eq!(agg.vwap_current(), Some(dec!(200)));
5238 }
5239
5240 #[test]
5241 fn test_vwap_current_weighted_average() {
5242 let mut agg = agg("BTC-USD", Timeframe::Minutes(1));
5243 agg.feed(&make_tick("BTC-USD", dec!(100), dec!(1), 1_000)).unwrap();
5244 agg.feed(&make_tick("BTC-USD", dec!(200), dec!(3), 2_000)).unwrap();
5245 assert_eq!(agg.vwap_current(), Some(dec!(175)));
5247 }
5248
5249 fn bar(o: i64, h: i64, l: i64, c: i64) -> OhlcvBar {
5252 OhlcvBar {
5253 symbol: "X".into(),
5254 timeframe: Timeframe::Minutes(1),
5255 open: Decimal::from(o),
5256 high: Decimal::from(h),
5257 low: Decimal::from(l),
5258 close: Decimal::from(c),
5259 volume: Decimal::ZERO,
5260 bar_start_ms: 0,
5261 trade_count: 0,
5262 is_complete: false,
5263 is_gap_fill: false,
5264 vwap: None,
5265 }
5266 }
5267
5268 #[test]
5269 fn test_upper_shadow_equals_wick_upper() {
5270 let b = bar(100, 120, 90, 110);
5271 assert_eq!(b.upper_shadow(), b.wick_upper());
5272 assert_eq!(b.upper_shadow(), Decimal::from(10)); }
5274
5275 #[test]
5276 fn test_lower_shadow_equals_wick_lower() {
5277 let b = bar(100, 120, 90, 110);
5278 assert_eq!(b.lower_shadow(), b.wick_lower());
5279 assert_eq!(b.lower_shadow(), Decimal::from(10)); }
5281
5282 #[test]
5283 fn test_is_spinning_top_true_when_small_body_large_wicks() {
5284 let b = bar(100, 130, 80, 110);
5289 assert!(b.is_spinning_top(dec!(0.3)));
5290 }
5291
5292 #[test]
5293 fn test_is_spinning_top_false_when_body_too_large() {
5294 let b = bar(80, 130, 80, 120);
5296 assert!(!b.is_spinning_top(dec!(0.3)));
5297 }
5298
5299 #[test]
5300 fn test_is_spinning_top_false_when_zero_range() {
5301 let b = bar(100, 100, 100, 100);
5302 assert!(!b.is_spinning_top(dec!(0.3)));
5303 }
5304
5305 #[test]
5306 fn test_hlc3_equals_typical_price() {
5307 let b = bar(100, 120, 80, 110);
5308 assert_eq!(b.hlc3(), b.typical_price());
5309 assert_eq!(b.hlc3(), (Decimal::from(120) + Decimal::from(80) + Decimal::from(110)) / Decimal::from(3));
5311 }
5312
5313 #[test]
5316 fn test_is_bearish_true_when_close_below_open() {
5317 let bar = make_ohlcv_bar(dec!(110), dec!(115), dec!(100), dec!(105));
5318 assert!(bar.is_bearish());
5319 }
5320
5321 #[test]
5322 fn test_is_bearish_false_when_close_above_open() {
5323 let bar = make_ohlcv_bar(dec!(100), dec!(115), dec!(95), dec!(110));
5324 assert!(!bar.is_bearish());
5325 }
5326
5327 #[test]
5328 fn test_is_bearish_false_when_doji() {
5329 let bar = make_ohlcv_bar(dec!(100), dec!(105), dec!(95), dec!(100));
5330 assert!(!bar.is_bearish());
5331 }
5332
5333 #[test]
5336 fn test_wick_ratio_zero_for_full_body_no_wicks() {
5337 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(100), dec!(110));
5339 let ratio = bar.wick_ratio().unwrap();
5340 assert!(ratio.abs() < 1e-10);
5341 }
5342
5343 #[test]
5344 fn test_wick_ratio_one_for_pure_wick_doji() {
5345 let bar = make_ohlcv_bar(dec!(105), dec!(110), dec!(100), dec!(105));
5347 let ratio = bar.wick_ratio().unwrap();
5348 assert!((ratio - 1.0).abs() < 1e-10);
5349 }
5350
5351 #[test]
5352 fn test_wick_ratio_none_for_zero_range_bar() {
5353 let bar = make_ohlcv_bar(dec!(100), dec!(100), dec!(100), dec!(100));
5354 assert!(bar.wick_ratio().is_none());
5355 }
5356
5357 #[test]
5360 fn test_is_bullish_true_when_close_above_open() {
5361 let bar = make_ohlcv_bar(dec!(100), dec!(115), dec!(95), dec!(110));
5362 assert!(bar.is_bullish());
5363 }
5364
5365 #[test]
5366 fn test_is_bullish_false_when_close_below_open() {
5367 let bar = make_ohlcv_bar(dec!(110), dec!(115), dec!(100), dec!(105));
5368 assert!(!bar.is_bullish());
5369 }
5370
5371 #[test]
5372 fn test_is_bullish_false_when_doji() {
5373 let bar = make_ohlcv_bar(dec!(100), dec!(105), dec!(95), dec!(100));
5374 assert!(!bar.is_bullish());
5375 }
5376
5377 #[test]
5380 fn test_bar_duration_ms_one_minute() {
5381 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
5382 assert_eq!(bar.bar_duration_ms(), 60_000);
5383 }
5384
5385 #[test]
5386 fn test_bar_duration_ms_consistent_with_timeframe() {
5387 let mut bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
5388 bar.timeframe = Timeframe::Hours(1);
5389 assert_eq!(bar.bar_duration_ms(), 3_600_000);
5390 }
5391
5392 #[test]
5393 fn test_bar_duration_ms_seconds_timeframe() {
5394 let mut bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
5395 bar.timeframe = Timeframe::Seconds(30);
5396 assert_eq!(bar.bar_duration_ms(), 30_000);
5397 }
5398
5399 #[test]
5402 fn test_ohlc4_equals_average_of_all_four_prices() {
5403 let b = bar(100, 120, 80, 110);
5404 let expected = (Decimal::from(100) + Decimal::from(120) + Decimal::from(80) + Decimal::from(110))
5406 / Decimal::from(4);
5407 assert_eq!(b.ohlc4(), expected);
5408 }
5409
5410 #[test]
5411 fn test_is_marubozu_true_when_no_wicks() {
5412 let b = bar(100, 110, 100, 110);
5414 assert!(b.is_marubozu());
5415 }
5416
5417 #[test]
5418 fn test_is_marubozu_false_when_has_upper_wick() {
5419 let b = bar(100, 115, 100, 110);
5420 assert!(!b.is_marubozu());
5421 }
5422
5423 #[test]
5424 fn test_is_marubozu_false_when_has_lower_wick() {
5425 let b = bar(100, 110, 95, 110);
5426 assert!(!b.is_marubozu());
5427 }
5428
5429 #[test]
5432 fn test_is_harami_true_when_body_inside_prev_body() {
5433 let prev = bar(98, 115, 90, 108); let curr = bar(100, 110, 95, 105); assert!(curr.is_harami(&prev));
5436 }
5437
5438 #[test]
5439 fn test_is_harami_false_when_body_engulfs_prev() {
5440 let prev = bar(100, 110, 95, 105); let curr = bar(98, 115, 90, 108); assert!(!curr.is_harami(&prev));
5443 }
5444
5445 #[test]
5446 fn test_is_harami_false_when_bodies_equal() {
5447 let prev = bar(100, 110, 90, 105);
5448 let curr = bar(100, 110, 90, 105); assert!(!curr.is_harami(&prev));
5450 }
5451
5452 #[test]
5453 fn test_tail_length_upper_wick_longer() {
5454 let b = bar(100, 120, 95, 105);
5456 assert_eq!(b.tail_length(), Decimal::from(15));
5457 }
5458
5459 #[test]
5460 fn test_tail_length_lower_wick_longer() {
5461 let b = bar(105, 110, 80, 100);
5463 assert_eq!(b.tail_length(), Decimal::from(20));
5464 }
5465
5466 #[test]
5467 fn test_tail_length_zero_for_marubozu() {
5468 let b = bar(100, 110, 100, 110);
5470 assert!(b.tail_length().is_zero());
5471 }
5472
5473 #[test]
5476 fn test_is_inside_bar_true_when_range_within_prev() {
5477 let prev = bar(90, 120, 80, 110); let curr = bar(95, 115, 85, 100); assert!(curr.is_inside_bar(&prev));
5480 }
5481
5482 #[test]
5483 fn test_is_inside_bar_false_when_high_exceeds_prev_high() {
5484 let prev = bar(90, 110, 80, 100); let curr = bar(95, 112, 85, 100); assert!(!curr.is_inside_bar(&prev));
5487 }
5488
5489 #[test]
5490 fn test_is_inside_bar_false_when_equal_range() {
5491 let prev = bar(90, 110, 80, 100);
5492 let curr = bar(90, 110, 80, 100); assert!(!curr.is_inside_bar(&prev));
5494 }
5495
5496 #[test]
5497 fn test_bar_type_bullish() {
5498 let b = bar(100, 110, 90, 105); assert_eq!(b.bar_type(), "bullish");
5500 }
5501
5502 #[test]
5503 fn test_bar_type_bearish() {
5504 let b = bar(105, 110, 90, 100); assert_eq!(b.bar_type(), "bearish");
5506 }
5507
5508 #[test]
5509 fn test_bar_type_doji() {
5510 let b = bar(100, 110, 90, 100); assert_eq!(b.bar_type(), "doji");
5512 }
5513
5514 #[test]
5517 fn test_body_pct_none_for_zero_range() {
5518 let b = bar(100, 100, 100, 100);
5519 assert!(b.body_pct().is_none());
5520 }
5521
5522 #[test]
5523 fn test_body_pct_100_for_marubozu() {
5524 let b = bar(100, 110, 100, 110);
5526 assert_eq!(b.body_pct().unwrap(), Decimal::ONE_HUNDRED);
5527 }
5528
5529 #[test]
5530 fn test_body_pct_50_for_half_body() {
5531 let b = bar(100, 110, 100, 105);
5533 assert_eq!(b.body_pct().unwrap(), Decimal::from(50));
5534 }
5535
5536 #[test]
5537 fn test_is_bullish_hammer_true_for_classic_hammer() {
5538 let b = bar(108, 110, 100, 109);
5541 assert!(b.is_bullish_hammer());
5542 }
5543
5544 #[test]
5545 fn test_is_bullish_hammer_false_when_lower_wick_not_long_enough() {
5546 let b = bar(100, 110, 98, 108);
5548 assert!(!b.is_bullish_hammer());
5549 }
5550
5551 #[test]
5552 fn test_is_bullish_hammer_false_for_doji() {
5553 let b = bar(100, 110, 90, 100); assert!(!b.is_bullish_hammer());
5555 }
5556
5557 #[test]
5559 fn test_is_marubozu_true_when_full_body() {
5560 let b = bar(100, 110, 100, 110);
5562 assert!(b.is_marubozu());
5563 }
5564
5565 #[test]
5566 fn test_is_marubozu_false_when_large_wicks() {
5567 let b = bar(100, 120, 80, 110);
5569 assert!(!b.is_marubozu());
5570 }
5571
5572 #[test]
5573 fn test_is_marubozu_true_for_zero_range_flat_bar() {
5574 let b = bar(100, 100, 100, 100);
5576 assert!(b.is_marubozu());
5577 }
5578
5579 #[test]
5581 fn test_upper_wick_pct_zero_when_no_upper_wick() {
5582 let b = bar(100, 110, 90, 110);
5584 let pct = b.upper_wick_pct().unwrap();
5585 assert!(pct.is_zero(), "expected 0, got {pct}");
5586 }
5587
5588 #[test]
5589 fn test_upper_wick_pct_50_when_half_range() {
5590 let b = bar(100, 120, 100, 110);
5592 let pct = b.upper_wick_pct().unwrap();
5593 assert_eq!(pct, dec!(50));
5594 }
5595
5596 #[test]
5597 fn test_upper_wick_pct_none_for_zero_range() {
5598 let b = bar(100, 100, 100, 100);
5599 assert!(b.upper_wick_pct().is_none());
5600 }
5601
5602 #[test]
5604 fn test_lower_wick_pct_zero_when_no_lower_wick() {
5605 let b = bar(100, 110, 100, 105);
5607 let pct = b.lower_wick_pct().unwrap();
5608 assert!(pct.is_zero(), "expected 0, got {pct}");
5609 }
5610
5611 #[test]
5612 fn test_lower_wick_pct_50_when_half_range() {
5613 let b = bar(110, 120, 100, 115);
5615 let pct = b.lower_wick_pct().unwrap();
5616 assert_eq!(pct, dec!(50));
5617 }
5618
5619 #[test]
5620 fn test_lower_wick_pct_none_for_zero_range() {
5621 let b = bar(100, 100, 100, 100);
5622 assert!(b.lower_wick_pct().is_none());
5623 }
5624
5625 #[test]
5627 fn test_is_bearish_engulfing_true_for_bearish_engulf() {
5628 let prev = bar(100, 115, 95, 110); let curr = bar(112, 115, 88, 90); assert!(curr.is_bearish_engulfing(&prev));
5631 }
5632
5633 #[test]
5634 fn test_is_bearish_engulfing_false_for_bullish_engulf() {
5635 let prev = bar(110, 115, 95, 100); let curr = bar(98, 120, 95, 115); assert!(!curr.is_bearish_engulfing(&prev));
5638 }
5639
5640 #[test]
5641 fn test_is_engulfing_true_when_body_contains_prev_body() {
5642 let prev = bar(100, 110, 95, 105); let curr = bar(98, 115, 95, 108); assert!(curr.is_engulfing(&prev));
5645 }
5646
5647 #[test]
5648 fn test_is_engulfing_false_when_only_partial_overlap() {
5649 let prev = bar(100, 115, 90, 112); let curr = bar(101, 115, 90, 113); assert!(!curr.is_engulfing(&prev));
5652 }
5653
5654 #[test]
5655 fn test_is_engulfing_false_for_equal_bodies() {
5656 let prev = bar(100, 110, 90, 108);
5657 let curr = bar(100, 110, 90, 108); assert!(!curr.is_engulfing(&prev));
5659 }
5660
5661 #[test]
5664 fn test_has_upper_wick_true_when_high_above_max_oc() {
5665 let bar = make_ohlcv_bar(dec!(100), dec!(115), dec!(100), dec!(110));
5667 assert!(bar.has_upper_wick());
5668 }
5669
5670 #[test]
5671 fn test_has_upper_wick_false_for_full_body() {
5672 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(100), dec!(110));
5674 assert!(!bar.has_upper_wick());
5675 }
5676
5677 #[test]
5678 fn test_has_lower_wick_true_when_low_below_min_oc() {
5679 let bar = make_ohlcv_bar(dec!(105), dec!(110), dec!(100), dec!(110));
5681 assert!(bar.has_lower_wick());
5682 }
5683
5684 #[test]
5685 fn test_has_lower_wick_false_for_full_body() {
5686 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(100), dec!(110));
5688 assert!(!bar.has_lower_wick());
5689 }
5690
5691 #[test]
5694 fn test_is_gravestone_doji_true() {
5695 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(100), dec!(100));
5697 assert!(bar.is_gravestone_doji(dec!(0)));
5698 }
5699
5700 #[test]
5701 fn test_is_gravestone_doji_false_when_close_above_low() {
5702 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(99), dec!(105));
5704 assert!(!bar.is_gravestone_doji(dec!(1)));
5705 }
5706
5707 #[test]
5710 fn test_is_dragonfly_doji_true() {
5711 let bar = make_ohlcv_bar(dec!(110), dec!(110), dec!(100), dec!(110));
5713 assert!(bar.is_dragonfly_doji(dec!(0)));
5714 }
5715
5716 #[test]
5717 fn test_is_dragonfly_doji_false_when_close_below_high() {
5718 let bar = make_ohlcv_bar(dec!(105), dec!(110), dec!(100), dec!(105));
5720 assert!(!bar.is_dragonfly_doji(dec!(1)));
5721 }
5722
5723 #[test]
5726 fn test_is_flat_true() {
5727 let bar = make_ohlcv_bar(dec!(100), dec!(100), dec!(100), dec!(100));
5728 assert!(bar.is_flat());
5729 }
5730
5731 #[test]
5732 fn test_is_flat_false_when_range_exists() {
5733 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
5734 assert!(!bar.is_flat());
5735 }
5736
5737 #[test]
5738 fn test_close_to_high_ratio_normal() {
5739 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(110));
5740 let r = bar.close_to_high_ratio().unwrap();
5742 assert!((r - 1.0).abs() < 1e-9);
5743 }
5744
5745 #[test]
5746 fn test_close_to_high_ratio_none_when_high_zero() {
5747 let bar = make_ohlcv_bar(dec!(0), dec!(0), dec!(0), dec!(0));
5748 assert!(bar.close_to_high_ratio().is_none());
5749 }
5750
5751 #[test]
5752 fn test_close_open_ratio_normal() {
5753 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(110));
5755 let r = bar.close_open_ratio().unwrap();
5756 assert!((r - 1.1).abs() < 1e-9);
5757 }
5758
5759 #[test]
5760 fn test_close_open_ratio_none_when_open_zero() {
5761 let bar = make_ohlcv_bar(dec!(0), dec!(10), dec!(0), dec!(5));
5762 assert!(bar.close_open_ratio().is_none());
5763 }
5764
5765 #[test]
5768 fn test_true_range_simple_hl_dominates() {
5769 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
5771 assert_eq!(bar.true_range_with_prev(dec!(100)), dec!(20));
5772 }
5773
5774 #[test]
5775 fn test_true_range_gap_up_dominates() {
5776 let bar = make_ohlcv_bar(dec!(91), dec!(100), dec!(90), dec!(95));
5778 assert_eq!(bar.true_range_with_prev(dec!(80)), dec!(20));
5779 }
5780
5781 #[test]
5782 fn test_true_range_gap_down_dominates() {
5783 let bar = make_ohlcv_bar(dec!(98), dec!(100), dec!(95), dec!(97));
5785 assert_eq!(bar.true_range_with_prev(dec!(120)), dec!(25));
5786 }
5787
5788 #[test]
5791 fn test_is_outside_bar_true() {
5792 let prev = make_ohlcv_bar(dec!(100), dec!(105), dec!(95), dec!(100));
5793 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
5794 assert!(bar.is_outside_bar(&prev));
5795 }
5796
5797 #[test]
5798 fn test_is_outside_bar_false_when_inside() {
5799 let prev = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
5800 let bar = make_ohlcv_bar(dec!(100), dec!(105), dec!(95), dec!(100));
5801 assert!(!bar.is_outside_bar(&prev));
5802 }
5803
5804 #[test]
5805 fn test_high_low_midpoint_correct() {
5806 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
5807 assert_eq!(bar.high_low_midpoint(), dec!(100));
5809 }
5810
5811 #[test]
5812 fn test_high_low_midpoint_uneven() {
5813 let bar = make_ohlcv_bar(dec!(100), dec!(111), dec!(90), dec!(100));
5814 assert_eq!(bar.high_low_midpoint(), dec!(100.5));
5816 }
5817
5818 #[test]
5821 fn test_gap_up_true() {
5822 let prev = make_ohlcv_bar(dec!(95), dec!(100), dec!(90), dec!(98));
5823 let bar = make_ohlcv_bar(dec!(102), dec!(110), dec!(101), dec!(108));
5824 assert!(bar.gap_up(&prev));
5825 }
5826
5827 #[test]
5828 fn test_gap_up_false_when_no_gap() {
5829 let prev = make_ohlcv_bar(dec!(95), dec!(100), dec!(90), dec!(98));
5830 let bar = make_ohlcv_bar(dec!(99), dec!(105), dec!(98), dec!(104));
5831 assert!(!bar.gap_up(&prev));
5832 }
5833
5834 #[test]
5835 fn test_gap_down_true() {
5836 let prev = make_ohlcv_bar(dec!(95), dec!(100), dec!(90), dec!(92));
5837 let bar = make_ohlcv_bar(dec!(88), dec!(89), dec!(85), dec!(86));
5838 assert!(bar.gap_down(&prev));
5839 }
5840
5841 #[test]
5842 fn test_gap_down_false_when_no_gap() {
5843 let prev = make_ohlcv_bar(dec!(95), dec!(100), dec!(90), dec!(92));
5844 let bar = make_ohlcv_bar(dec!(91), dec!(95), dec!(89), dec!(93));
5845 assert!(!bar.gap_down(&prev));
5846 }
5847
5848 #[test]
5851 fn test_range_pct_correct() {
5852 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
5854 let pct = bar.range_pct().unwrap();
5855 assert!((pct - 20.0).abs() < 1e-9);
5856 }
5857
5858 #[test]
5859 fn test_range_pct_none_when_open_zero() {
5860 let bar = make_ohlcv_bar(dec!(0), dec!(10), dec!(0), dec!(5));
5861 assert!(bar.range_pct().is_none());
5862 }
5863
5864 #[test]
5865 fn test_range_pct_zero_for_flat_bar() {
5866 let bar = make_ohlcv_bar(dec!(100), dec!(100), dec!(100), dec!(100));
5868 let pct = bar.range_pct().unwrap();
5869 assert_eq!(pct, 0.0);
5870 }
5871
5872 #[test]
5875 fn test_body_size_bullish_bar() {
5876 let bar = make_ohlcv_bar(dec!(100), dec!(115), dec!(95), dec!(110));
5878 assert_eq!(bar.body_size(), dec!(10));
5879 }
5880
5881 #[test]
5882 fn test_body_size_bearish_bar() {
5883 let bar = make_ohlcv_bar(dec!(110), dec!(115), dec!(95), dec!(100));
5885 assert_eq!(bar.body_size(), dec!(10));
5886 }
5887
5888 #[test]
5889 fn test_body_size_doji() {
5890 let bar = make_ohlcv_bar(dec!(100), dec!(105), dec!(95), dec!(100));
5892 assert_eq!(bar.body_size(), dec!(0));
5893 }
5894
5895 #[test]
5898 fn test_volume_delta_positive_when_increasing() {
5899 let mut prev = make_ohlcv_bar(dec!(100), dec!(105), dec!(95), dec!(102));
5900 prev.volume = dec!(1000);
5901 let mut bar = make_ohlcv_bar(dec!(102), dec!(110), dec!(98), dec!(108));
5902 bar.volume = dec!(1500);
5903 assert_eq!(bar.volume_delta(&prev), dec!(500));
5904 }
5905
5906 #[test]
5907 fn test_volume_delta_negative_when_decreasing() {
5908 let mut prev = make_ohlcv_bar(dec!(100), dec!(105), dec!(95), dec!(102));
5909 prev.volume = dec!(1500);
5910 let mut bar = make_ohlcv_bar(dec!(102), dec!(110), dec!(98), dec!(108));
5911 bar.volume = dec!(1000);
5912 assert_eq!(bar.volume_delta(&prev), dec!(-500));
5913 }
5914
5915 #[test]
5916 fn test_is_consolidating_true_when_small_range() {
5917 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));
5920 }
5921
5922 #[test]
5923 fn test_is_consolidating_false_when_large_range() {
5924 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));
5927 }
5928
5929 #[test]
5932 fn test_relative_volume_correct() {
5933 let bar = make_ohlcv_bar(dec!(100), dec!(105), dec!(95), dec!(103));
5934 let rv = bar.relative_volume(dec!(2)).unwrap();
5936 assert!((rv - 0.5).abs() < 1e-9);
5937 }
5938
5939 #[test]
5940 fn test_relative_volume_none_when_avg_zero() {
5941 let bar = make_ohlcv_bar(dec!(100), dec!(105), dec!(95), dec!(103));
5942 assert!(bar.relative_volume(dec!(0)).is_none());
5943 }
5944
5945 #[test]
5946 fn test_intraday_reversal_true_for_bullish_then_bearish() {
5947 let prev = make_ohlcv_bar(dec!(100), dec!(108), dec!(99), dec!(105));
5949 let bar = make_ohlcv_bar(dec!(105), dec!(107), dec!(97), dec!(98));
5951 assert!(bar.intraday_reversal(&prev));
5952 }
5953
5954 #[test]
5955 fn test_intraday_reversal_false_for_continuation() {
5956 let prev = make_ohlcv_bar(dec!(100), dec!(108), dec!(99), dec!(105));
5958 let bar = make_ohlcv_bar(dec!(104), dec!(115), dec!(103), dec!(113));
5959 assert!(!bar.intraday_reversal(&prev));
5960 }
5961
5962 #[test]
5965 fn test_price_at_pct_zero_returns_low() {
5966 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
5967 assert_eq!(bar.price_at_pct(0.0), dec!(90));
5968 }
5969
5970 #[test]
5971 fn test_price_at_pct_one_returns_high() {
5972 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
5973 assert_eq!(bar.price_at_pct(1.0), dec!(110));
5974 }
5975
5976 #[test]
5977 fn test_price_at_pct_half_returns_midpoint() {
5978 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
5979 assert_eq!(bar.price_at_pct(0.5), dec!(100));
5981 }
5982
5983 #[test]
5984 fn test_price_at_pct_clamped_above_one() {
5985 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
5986 assert_eq!(bar.price_at_pct(2.0), dec!(110));
5987 }
5988
5989 #[test]
5992 fn test_average_true_range_none_when_fewer_than_two_bars() {
5993 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
5994 assert!(OhlcvBar::average_true_range(&[bar]).is_none());
5995 assert!(OhlcvBar::average_true_range(&[]).is_none());
5996 }
5997
5998 #[test]
5999 fn test_average_true_range_two_bars_no_gap() {
6000 let bar1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
6003 let bar2 = make_ohlcv_bar(dec!(105), dec!(115), dec!(95), dec!(110));
6004 let atr = OhlcvBar::average_true_range(&[bar1, bar2]).unwrap();
6005 assert_eq!(atr, dec!(20)); }
6007
6008 #[test]
6009 fn test_average_true_range_three_bars_mean() {
6010 let bar1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
6014 let bar2 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
6015 let bar3 = make_ohlcv_bar(dec!(110), dec!(120), dec!(100), dec!(115));
6016 let atr = OhlcvBar::average_true_range(&[bar1, bar2, bar3]).unwrap();
6017 assert_eq!(atr, dec!(20));
6018 }
6019
6020 #[test]
6023 fn test_average_body_none_when_empty() {
6024 assert!(OhlcvBar::average_body(&[]).is_none());
6025 }
6026
6027 #[test]
6028 fn test_average_body_single_bar() {
6029 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(108));
6030 assert_eq!(OhlcvBar::average_body(&[bar]), Some(dec!(8)));
6032 }
6033
6034 #[test]
6035 fn test_average_body_multiple_bars() {
6036 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();
6040 assert_eq!(avg, dec!(40) / dec!(3));
6042 }
6043
6044 #[test]
6047 fn test_bullish_count_zero_for_empty_slice() {
6048 assert_eq!(OhlcvBar::bullish_count(&[]), 0);
6049 }
6050
6051 #[test]
6052 fn test_bullish_count_all_bullish() {
6053 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);
6056 }
6057
6058 #[test]
6059 fn test_bearish_count_correct() {
6060 let bull = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(108));
6061 let bear = make_ohlcv_bar(dec!(108), dec!(110), dec!(90), dec!(95));
6062 let doji = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
6063 assert_eq!(OhlcvBar::bearish_count(&[bull, bear, doji]), 1);
6064 }
6065
6066 #[test]
6067 fn test_win_rate_none_when_empty() {
6068 assert!(OhlcvBar::win_rate(&[]).is_none());
6069 }
6070
6071 #[test]
6072 fn test_win_rate_all_bullish() {
6073 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(108));
6074 let b2 = make_ohlcv_bar(dec!(108), dec!(115), dec!(105), dec!(112));
6075 let wr = OhlcvBar::win_rate(&[b1, b2]).unwrap();
6076 assert!((wr - 1.0).abs() < 1e-9);
6077 }
6078
6079 #[test]
6080 fn test_win_rate_half_and_half() {
6081 let bull = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(108));
6082 let bear = make_ohlcv_bar(dec!(108), dec!(110), dec!(90), dec!(95));
6083 let wr = OhlcvBar::win_rate(&[bull, bear]).unwrap();
6084 assert!((wr - 0.5).abs() < 1e-9);
6085 }
6086
6087 #[test]
6090 fn test_bullish_streak_zero_for_empty_slice() {
6091 assert_eq!(OhlcvBar::bullish_streak(&[]), 0);
6092 }
6093
6094 #[test]
6095 fn test_bullish_streak_zero_when_last_bar_bearish() {
6096 let bull = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(108));
6097 let bear = make_ohlcv_bar(dec!(108), dec!(110), dec!(90), dec!(95));
6098 assert_eq!(OhlcvBar::bullish_streak(&[bull, bear]), 0);
6099 }
6100
6101 #[test]
6102 fn test_bullish_streak_counts_consecutive_tail() {
6103 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);
6107 }
6108
6109 #[test]
6110 fn test_bearish_streak_counts_consecutive_tail() {
6111 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);
6115 }
6116
6117 #[test]
6120 fn test_max_drawdown_none_when_fewer_than_2_bars() {
6121 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
6122 assert!(OhlcvBar::max_drawdown(&[bar]).is_none());
6123 assert!(OhlcvBar::max_drawdown(&[]).is_none());
6124 }
6125
6126 #[test]
6127 fn test_max_drawdown_zero_when_monotone_increasing() {
6128 let b1 = make_ohlcv_bar(dec!(100), dec!(105), dec!(98), dec!(100));
6129 let b2 = make_ohlcv_bar(dec!(100), dec!(110), dec!(99), dec!(105));
6130 let b3 = make_ohlcv_bar(dec!(105), dec!(115), dec!(104), dec!(110));
6131 let dd = OhlcvBar::max_drawdown(&[b1, b2, b3]).unwrap();
6132 assert_eq!(dd, 0.0);
6133 }
6134
6135 #[test]
6136 fn test_max_drawdown_correct_after_peak_then_drop() {
6137 let b1 = make_ohlcv_bar(dec!(100), dec!(102), dec!(98), dec!(100));
6139 let b2 = make_ohlcv_bar(dec!(100), dec!(125), dec!(99), dec!(120));
6140 let b3 = make_ohlcv_bar(dec!(120), dec!(121), dec!(88), dec!(90));
6141 let dd = OhlcvBar::max_drawdown(&[b1, b2, b3]).unwrap();
6142 assert!((dd - 0.25).abs() < 1e-9, "expected 0.25, got {dd}");
6143 }
6144
6145 #[test]
6148 fn test_mean_volume_none_when_empty() {
6149 assert!(OhlcvBar::mean_volume(&[]).is_none());
6150 }
6151
6152 #[test]
6153 fn test_mean_volume_single_bar() {
6154 let mut bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
6155 bar.volume = dec!(200);
6156 assert_eq!(OhlcvBar::mean_volume(&[bar]), Some(dec!(200)));
6157 }
6158
6159 #[test]
6160 fn test_mean_volume_multiple_bars() {
6161 let mut b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
6162 b1.volume = dec!(100);
6163 let mut b2 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
6164 b2.volume = dec!(200);
6165 let mut b3 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
6166 b3.volume = dec!(300);
6167 assert_eq!(OhlcvBar::mean_volume(&[b1, b2, b3]), Some(dec!(200)));
6168 }
6169
6170 #[test]
6173 fn test_vwap_deviation_none_when_vwap_not_set() {
6174 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
6175 assert!(bar.vwap_deviation().is_none());
6176 }
6177
6178 #[test]
6179 fn test_vwap_deviation_zero_when_close_equals_vwap() {
6180 let mut bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
6181 bar.vwap = Some(dec!(100));
6182 assert_eq!(bar.vwap_deviation(), Some(0.0));
6183 }
6184
6185 #[test]
6186 fn test_vwap_deviation_correct_value() {
6187 let mut bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(110));
6188 bar.vwap = Some(dec!(100));
6189 let dev = bar.vwap_deviation().unwrap();
6191 assert!((dev - 0.1).abs() < 1e-10);
6192 }
6193
6194 #[test]
6197 fn test_high_close_ratio_none_when_high_zero() {
6198 let bar = OhlcvBar {
6199 symbol: "X".into(),
6200 timeframe: Timeframe::Minutes(1),
6201 open: dec!(0),
6202 high: dec!(0),
6203 low: dec!(0),
6204 close: dec!(0),
6205 volume: dec!(1),
6206 bar_start_ms: 0,
6207 trade_count: 1,
6208 is_complete: false,
6209 is_gap_fill: false,
6210 vwap: None,
6211 };
6212 assert!(bar.high_close_ratio().is_none());
6213 }
6214
6215 #[test]
6216 fn test_high_close_ratio_one_when_close_equals_high() {
6217 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(110));
6218 let ratio = bar.high_close_ratio().unwrap();
6219 assert!((ratio - 1.0).abs() < 1e-10);
6220 }
6221
6222 #[test]
6223 fn test_high_close_ratio_less_than_one_when_close_below_high() {
6224 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(99));
6225 let ratio = bar.high_close_ratio().unwrap();
6226 assert!(ratio < 1.0);
6227 }
6228
6229 #[test]
6232 fn test_lower_shadow_pct_none_when_range_zero() {
6233 let bar = make_ohlcv_bar(dec!(100), dec!(100), dec!(100), dec!(100));
6234 assert!(bar.lower_shadow_pct().is_none());
6235 }
6236
6237 #[test]
6238 fn test_lower_shadow_pct_zero_when_no_lower_shadow() {
6239 let bar = make_ohlcv_bar(dec!(90), dec!(110), dec!(90), dec!(110));
6241 let pct = bar.lower_shadow_pct().unwrap();
6242 assert!(pct.abs() < 1e-10);
6243 }
6244
6245 #[test]
6246 fn test_lower_shadow_pct_correct_value() {
6247 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
6249 let pct = bar.lower_shadow_pct().unwrap();
6250 assert!((pct - 0.5).abs() < 1e-10);
6251 }
6252
6253 #[test]
6256 fn test_open_close_ratio_none_when_open_zero() {
6257 let bar = make_ohlcv_bar(dec!(0), dec!(10), dec!(0), dec!(5));
6258 assert!(bar.open_close_ratio().is_none());
6259 }
6260
6261 #[test]
6262 fn test_open_close_ratio_one_when_flat() {
6263 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
6264 let ratio = bar.open_close_ratio().unwrap();
6265 assert!((ratio - 1.0).abs() < 1e-10);
6266 }
6267
6268 #[test]
6269 fn test_open_close_ratio_above_one_for_bullish_bar() {
6270 let bar = make_ohlcv_bar(dec!(100), dec!(115), dec!(95), dec!(110));
6271 let ratio = bar.open_close_ratio().unwrap();
6272 assert!(ratio > 1.0);
6273 }
6274
6275 #[test]
6278 fn test_is_wide_range_bar_true_when_range_exceeds_threshold() {
6279 let bar = make_ohlcv_bar(dec!(100), dec!(120), dec!(95), dec!(110)); assert!(bar.is_wide_range_bar(dec!(20)));
6281 }
6282
6283 #[test]
6284 fn test_is_wide_range_bar_false_when_range_equals_threshold() {
6285 let bar = make_ohlcv_bar(dec!(100), dec!(120), dec!(100), dec!(110)); assert!(!bar.is_wide_range_bar(dec!(20)));
6287 }
6288
6289 #[test]
6292 fn test_close_to_low_ratio_none_when_range_zero() {
6293 let bar = make_ohlcv_bar(dec!(100), dec!(100), dec!(100), dec!(100));
6294 assert!(bar.close_to_low_ratio().is_none());
6295 }
6296
6297 #[test]
6298 fn test_close_to_low_ratio_one_when_closed_at_high() {
6299 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(110));
6300 let ratio = bar.close_to_low_ratio().unwrap();
6301 assert!((ratio - 1.0).abs() < 1e-10);
6302 }
6303
6304 #[test]
6305 fn test_close_to_low_ratio_zero_when_closed_at_low() {
6306 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(90));
6307 let ratio = bar.close_to_low_ratio().unwrap();
6308 assert!(ratio.abs() < 1e-10);
6309 }
6310
6311 #[test]
6312 fn test_close_to_low_ratio_half_at_midpoint() {
6313 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
6314 let ratio = bar.close_to_low_ratio().unwrap();
6316 assert!((ratio - 0.5).abs() < 1e-10);
6317 }
6318
6319 #[test]
6322 fn test_volume_per_trade_none_when_trade_count_zero() {
6323 let mut bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
6324 bar.trade_count = 0;
6325 assert!(bar.volume_per_trade().is_none());
6326 }
6327
6328 #[test]
6329 fn test_volume_per_trade_correct_value() {
6330 let mut bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
6331 bar.volume = dec!(500);
6332 bar.trade_count = 5;
6333 assert_eq!(bar.volume_per_trade(), Some(dec!(100)));
6334 }
6335
6336 #[test]
6339 fn test_price_range_overlap_true_when_ranges_overlap() {
6340 let a = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
6341 let b = make_ohlcv_bar(dec!(105), dec!(120), dec!(95), dec!(110));
6342 assert!(a.price_range_overlap(&b));
6343 }
6344
6345 #[test]
6346 fn test_price_range_overlap_false_when_no_overlap() {
6347 let a = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
6348 let b = make_ohlcv_bar(dec!(120), dec!(130), dec!(115), dec!(125));
6349 assert!(!a.price_range_overlap(&b));
6350 }
6351
6352 #[test]
6353 fn test_price_range_overlap_true_at_exact_touch() {
6354 let a = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
6355 let b = make_ohlcv_bar(dec!(115), dec!(125), dec!(110), dec!(120));
6356 assert!(a.price_range_overlap(&b));
6357 }
6358
6359 #[test]
6362 fn test_bar_height_pct_none_when_open_zero() {
6363 let bar = make_ohlcv_bar(dec!(0), dec!(10), dec!(0), dec!(5));
6364 assert!(bar.bar_height_pct().is_none());
6365 }
6366
6367 #[test]
6368 fn test_bar_height_pct_correct_value() {
6369 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100)); let pct = bar.bar_height_pct().unwrap();
6372 assert!((pct - 0.2).abs() < 1e-10);
6373 }
6374
6375 #[test]
6378 fn test_is_bullish_engulfing_true_for_valid_pattern() {
6379 let prev = make_ohlcv_bar(dec!(110), dec!(115), dec!(95), dec!(100));
6381 let bar = make_ohlcv_bar(dec!(98), dec!(115), dec!(95), dec!(112));
6382 assert!(bar.is_bullish_engulfing(&prev));
6383 }
6384
6385 #[test]
6386 fn test_is_bullish_engulfing_false_when_bearish() {
6387 let prev = make_ohlcv_bar(dec!(110), dec!(115), dec!(95), dec!(100));
6388 let bar = make_ohlcv_bar(dec!(108), dec!(115), dec!(95), dec!(95));
6389 assert!(!bar.is_bullish_engulfing(&prev));
6390 }
6391
6392 #[test]
6395 fn test_close_gap_positive_for_gap_up() {
6396 let prev = make_ohlcv_bar(dec!(100), dec!(105), dec!(95), dec!(102));
6397 let bar = make_ohlcv_bar(dec!(106), dec!(110), dec!(103), dec!(108)); assert_eq!(bar.close_gap(&prev), dec!(4));
6399 }
6400
6401 #[test]
6402 fn test_close_gap_negative_for_gap_down() {
6403 let prev = make_ohlcv_bar(dec!(100), dec!(105), dec!(95), dec!(102));
6404 let bar = make_ohlcv_bar(dec!(98), dec!(100), dec!(95), dec!(97)); assert_eq!(bar.close_gap(&prev), dec!(-4));
6406 }
6407
6408 #[test]
6409 fn test_close_gap_zero_when_no_gap() {
6410 let prev = make_ohlcv_bar(dec!(100), dec!(105), dec!(95), dec!(102));
6411 let bar = make_ohlcv_bar(dec!(102), dec!(110), dec!(100), dec!(108));
6412 assert_eq!(bar.close_gap(&prev), dec!(0));
6413 }
6414
6415 #[test]
6418 fn test_close_above_midpoint_true_when_above_mid() {
6419 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
6421 assert!(bar.close_above_midpoint());
6422 }
6423
6424 #[test]
6425 fn test_close_above_midpoint_false_when_at_mid() {
6426 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100)); assert!(!bar.close_above_midpoint());
6428 }
6429
6430 #[test]
6433 fn test_close_momentum_positive_when_rising() {
6434 let prev = make_ohlcv_bar(dec!(100), dec!(105), dec!(95), dec!(100));
6435 let bar = make_ohlcv_bar(dec!(100), dec!(115), dec!(98), dec!(110));
6436 assert_eq!(bar.close_momentum(&prev), dec!(10));
6437 }
6438
6439 #[test]
6440 fn test_close_momentum_zero_when_unchanged() {
6441 let prev = make_ohlcv_bar(dec!(100), dec!(105), dec!(95), dec!(100));
6442 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(95), dec!(100));
6443 assert_eq!(bar.close_momentum(&prev), dec!(0));
6444 }
6445
6446 #[test]
6449 fn test_bar_range_correct() {
6450 let bar = make_ohlcv_bar(dec!(100), dec!(120), dec!(90), dec!(110));
6451 assert_eq!(bar.bar_range(), dec!(30));
6452 }
6453
6454 #[test]
6457 fn test_linear_regression_slope_none_for_single_bar() {
6458 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
6459 assert!(OhlcvBar::linear_regression_slope(&[bar]).is_none());
6460 }
6461
6462 #[test]
6463 fn test_linear_regression_slope_positive_for_rising_closes() {
6464 let bars = vec![
6465 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100)),
6466 make_ohlcv_bar(dec!(100), dec!(115), dec!(95), dec!(110)),
6467 make_ohlcv_bar(dec!(110), dec!(120), dec!(105), dec!(120)),
6468 ];
6469 let slope = OhlcvBar::linear_regression_slope(&bars).unwrap();
6470 assert!(slope > 0.0, "slope should be positive for rising closes");
6471 }
6472
6473 #[test]
6474 fn test_linear_regression_slope_negative_for_falling_closes() {
6475 let bars = vec![
6476 make_ohlcv_bar(dec!(120), dec!(125), dec!(115), dec!(120)),
6477 make_ohlcv_bar(dec!(120), dec!(115), dec!(105), dec!(110)),
6478 make_ohlcv_bar(dec!(110), dec!(108), dec!(95), dec!(100)),
6479 ];
6480 let slope = OhlcvBar::linear_regression_slope(&bars).unwrap();
6481 assert!(slope < 0.0, "slope should be negative for falling closes");
6482 }
6483
6484 #[test]
6485 fn test_linear_regression_slope_near_zero_for_flat_closes() {
6486 let bars = vec![
6487 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100)),
6488 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100)),
6489 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100)),
6490 ];
6491 let slope = OhlcvBar::linear_regression_slope(&bars).unwrap();
6492 assert!(slope.abs() < 1e-10, "slope should be ~0 for identical closes");
6493 }
6494
6495 #[test]
6498 fn test_volume_slope_none_for_single_bar() {
6499 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
6500 assert!(OhlcvBar::volume_slope(&[bar]).is_none());
6501 }
6502
6503 #[test]
6504 fn test_volume_slope_positive_for_rising_volume() {
6505 let make_bar_with_vol = |v: u64| {
6506 let mut b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
6507 b.volume = Decimal::from(v);
6508 b
6509 };
6510 let bars = vec![make_bar_with_vol(100), make_bar_with_vol(200), make_bar_with_vol(300)];
6511 assert!(OhlcvBar::volume_slope(&bars).unwrap() > 0.0);
6512 }
6513
6514 #[test]
6517 fn test_highest_close_none_for_empty_slice() {
6518 assert!(OhlcvBar::highest_close(&[]).is_none());
6519 }
6520
6521 #[test]
6522 fn test_highest_close_returns_max_close() {
6523 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
6524 let b2 = make_ohlcv_bar(dec!(100), dec!(120), dec!(95), dec!(115));
6525 let b3 = make_ohlcv_bar(dec!(100), dec!(108), dec!(90), dec!(102));
6526 assert_eq!(OhlcvBar::highest_close(&[b1, b2, b3]), Some(dec!(115)));
6527 }
6528
6529 #[test]
6530 fn test_lowest_close_none_for_empty_slice() {
6531 assert!(OhlcvBar::lowest_close(&[]).is_none());
6532 }
6533
6534 #[test]
6535 fn test_lowest_close_returns_min_close() {
6536 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
6537 let b2 = make_ohlcv_bar(dec!(100), dec!(120), dec!(95), dec!(115));
6538 let b3 = make_ohlcv_bar(dec!(100), dec!(108), dec!(90), dec!(102));
6539 assert_eq!(OhlcvBar::lowest_close(&[b1, b2, b3]), Some(dec!(102)));
6540 }
6541
6542 #[test]
6545 fn test_close_range_none_for_empty_slice() {
6546 assert!(OhlcvBar::close_range(&[]).is_none());
6547 }
6548
6549 #[test]
6550 fn test_close_range_correct() {
6551 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(102));
6552 let b2 = make_ohlcv_bar(dec!(100), dec!(120), dec!(95), dec!(115));
6553 assert_eq!(OhlcvBar::close_range(&[b1, b2]), Some(dec!(13)));
6555 }
6556
6557 #[test]
6558 fn test_momentum_none_for_insufficient_bars() {
6559 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
6560 assert!(OhlcvBar::momentum(&[bar], 1).is_none());
6561 }
6562
6563 #[test]
6564 fn test_momentum_positive_for_rising_close() {
6565 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
6566 let b2 = make_ohlcv_bar(dec!(100), dec!(115), dec!(95), dec!(110));
6567 let mom = OhlcvBar::momentum(&[b1, b2], 1).unwrap();
6569 assert!((mom - 0.1).abs() < 1e-10);
6570 }
6571
6572 #[test]
6573 fn test_momentum_negative_for_falling_close() {
6574 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(110));
6575 let b2 = make_ohlcv_bar(dec!(100), dec!(108), dec!(88), dec!(99));
6576 let mom = OhlcvBar::momentum(&[b1, b2], 1).unwrap();
6578 assert!(mom < 0.0);
6579 }
6580
6581 #[test]
6584 fn test_mean_close_none_for_empty_slice() {
6585 assert!(OhlcvBar::mean_close(&[]).is_none());
6586 }
6587
6588 #[test]
6589 fn test_mean_close_single_bar() {
6590 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
6591 assert_eq!(OhlcvBar::mean_close(&[bar]), Some(dec!(105)));
6592 }
6593
6594 #[test]
6595 fn test_mean_close_multiple_bars() {
6596 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
6597 let b2 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(110));
6598 let b3 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(120));
6599 assert_eq!(OhlcvBar::mean_close(&[b1, b2, b3]), Some(dec!(110)));
6601 }
6602
6603 #[test]
6606 fn test_close_std_dev_none_for_single_bar() {
6607 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
6608 assert!(OhlcvBar::close_std_dev(&[bar]).is_none());
6609 }
6610
6611 #[test]
6612 fn test_close_std_dev_zero_for_identical_closes() {
6613 let bars = vec![
6614 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100)),
6615 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100)),
6616 ];
6617 let sd = OhlcvBar::close_std_dev(&bars).unwrap();
6618 assert!(sd.abs() < 1e-10, "std_dev should be ~0 for identical closes");
6619 }
6620
6621 #[test]
6622 fn test_close_std_dev_positive_for_varied_closes() {
6623 let bars = vec![
6624 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(90)),
6625 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(110)),
6626 ];
6627 assert!(OhlcvBar::close_std_dev(&bars).unwrap() > 0.0);
6628 }
6629
6630 #[test]
6633 fn test_price_efficiency_ratio_none_for_single_bar() {
6634 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
6635 assert!(OhlcvBar::price_efficiency_ratio(&[bar]).is_none());
6636 }
6637
6638 #[test]
6639 fn test_price_efficiency_ratio_one_for_trending_price() {
6640 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
6642 let b2 = make_ohlcv_bar(dec!(110), dec!(120), dec!(100), dec!(110));
6643 let b3 = make_ohlcv_bar(dec!(120), dec!(130), dec!(110), dec!(120));
6644 let ratio = OhlcvBar::price_efficiency_ratio(&[b1, b2, b3]).unwrap();
6646 assert!(ratio > 0.0 && ratio <= 1.0);
6647 }
6648
6649 #[test]
6650 fn test_price_efficiency_ratio_none_for_zero_total_range() {
6651 let bars = vec![
6653 make_ohlcv_bar(dec!(100), dec!(100), dec!(100), dec!(100)),
6654 make_ohlcv_bar(dec!(100), dec!(100), dec!(100), dec!(100)),
6655 ];
6656 assert!(OhlcvBar::price_efficiency_ratio(&bars).is_none());
6657 }
6658
6659 #[test]
6662 fn test_clv_plus_one_when_close_at_high() {
6663 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(110));
6665 let clv = bar.close_location_value().unwrap();
6666 assert!((clv - 1.0).abs() < 1e-10, "CLV should be 1.0 when close == high, got {clv}");
6667 }
6668
6669 #[test]
6670 fn test_clv_minus_one_when_close_at_low() {
6671 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(90));
6672 let clv = bar.close_location_value().unwrap();
6673 assert!((clv + 1.0).abs() < 1e-10, "CLV should be -1.0 when close == low, got {clv}");
6674 }
6675
6676 #[test]
6677 fn test_clv_zero_when_close_at_midpoint() {
6678 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
6679 let clv = bar.close_location_value().unwrap();
6680 assert!(clv.abs() < 1e-10, "CLV should be 0 at midpoint, got {clv}");
6681 }
6682
6683 #[test]
6684 fn test_clv_none_for_zero_range_bar() {
6685 let bar = make_ohlcv_bar(dec!(100), dec!(100), dec!(100), dec!(100));
6686 assert!(bar.close_location_value().is_none());
6687 }
6688
6689 #[test]
6690 fn test_mean_clv_none_for_empty_slice() {
6691 assert!(OhlcvBar::mean_clv(&[]).is_none());
6692 }
6693
6694 #[test]
6695 fn test_mean_clv_positive_for_bullish_closes() {
6696 let bars = vec![
6697 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(108)), make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(106)), ];
6700 let clv = OhlcvBar::mean_clv(&bars).unwrap();
6701 assert!(clv > 0.0, "mean CLV should be positive when closes are near highs");
6702 }
6703
6704 #[test]
6705 fn test_mean_range_none_for_empty_slice() {
6706 assert!(OhlcvBar::mean_range(&[]).is_none());
6707 }
6708
6709 #[test]
6710 fn test_mean_range_single_bar() {
6711 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
6712 assert_eq!(OhlcvBar::mean_range(&[bar]), Some(dec!(20)));
6713 }
6714
6715 #[test]
6716 fn test_mean_range_multiple_bars() {
6717 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)));
6720 }
6721
6722 #[test]
6723 fn test_close_z_score_none_for_empty_slice() {
6724 assert!(OhlcvBar::close_z_score(&[], dec!(100)).is_none());
6725 }
6726
6727 #[test]
6728 fn test_close_z_score_of_mean_is_zero() {
6729 let bars = vec![
6730 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100)),
6731 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100)),
6732 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(110)),
6733 ];
6734 let mean = (dec!(100) + dec!(100) + dec!(110)) / dec!(3);
6736 let z = OhlcvBar::close_z_score(&bars, mean).unwrap();
6737 assert!(z.abs() < 1e-6);
6738 }
6739
6740 #[test]
6741 fn test_close_z_score_positive_above_mean() {
6742 let bars = vec![
6743 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100)),
6744 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100)),
6745 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(110)),
6746 ];
6747 let z = OhlcvBar::close_z_score(&bars, dec!(120)).unwrap();
6748 assert!(z > 0.0);
6749 }
6750
6751 #[test]
6752 fn test_bollinger_band_width_none_for_empty_slice() {
6753 assert!(OhlcvBar::bollinger_band_width(&[]).is_none());
6754 }
6755
6756 #[test]
6757 fn test_bollinger_band_width_zero_for_identical_closes() {
6758 let bars = vec![
6759 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100)),
6760 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100)),
6761 ];
6762 assert_eq!(OhlcvBar::bollinger_band_width(&bars), Some(0.0));
6763 }
6764
6765 #[test]
6766 fn test_bollinger_band_width_positive_for_varying_closes() {
6767 let bars = vec![
6768 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(90)),
6769 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100)),
6770 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(110)),
6771 ];
6772 let bw = OhlcvBar::bollinger_band_width(&bars).unwrap();
6773 assert!(bw > 0.0);
6774 }
6775
6776 #[test]
6777 fn test_up_down_ratio_none_for_no_bearish_bars() {
6778 let bars = vec![
6779 make_ohlcv_bar(dec!(90), dec!(110), dec!(85), dec!(105)), ];
6781 assert!(OhlcvBar::up_down_ratio(&bars).is_none());
6782 }
6783
6784 #[test]
6785 fn test_up_down_ratio_two_to_one() {
6786 let bull = make_ohlcv_bar(dec!(90), dec!(110), dec!(85), dec!(105));
6787 let bear = make_ohlcv_bar(dec!(110), dec!(115), dec!(85), dec!(95));
6788 let bars = vec![bull.clone(), bull, bear];
6789 let ratio = OhlcvBar::up_down_ratio(&bars).unwrap();
6790 assert!((ratio - 2.0).abs() < 1e-9);
6791 }
6792
6793 #[test]
6796 fn test_volume_weighted_close_none_for_empty_slice() {
6797 assert!(OhlcvBar::volume_weighted_close(&[]).is_none());
6798 }
6799
6800 #[test]
6801 fn test_volume_weighted_close_single_bar() {
6802 let mut bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
6803 bar.volume = dec!(10);
6804 assert_eq!(OhlcvBar::volume_weighted_close(&[bar]), Some(dec!(105)));
6805 }
6806
6807 #[test]
6808 fn test_volume_weighted_close_weights_by_volume() {
6809 let mut b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
6810 b1.volume = dec!(1);
6811 let mut b2 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(200));
6812 b2.volume = dec!(3);
6813 assert_eq!(OhlcvBar::volume_weighted_close(&[b1, b2]), Some(dec!(175)));
6815 }
6816
6817 #[test]
6820 fn test_rolling_return_none_for_empty_slice() {
6821 assert!(OhlcvBar::rolling_return(&[]).is_none());
6822 }
6823
6824 #[test]
6825 fn test_rolling_return_none_for_single_bar() {
6826 assert!(OhlcvBar::rolling_return(&[make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105))]).is_none());
6827 }
6828
6829 #[test]
6830 fn test_rolling_return_positive_when_close_rises() {
6831 let b1 = make_ohlcv_bar(dec!(100), dec!(105), dec!(95), dec!(100));
6832 let b2 = make_ohlcv_bar(dec!(100), dec!(115), dec!(95), dec!(110));
6833 let ret = OhlcvBar::rolling_return(&[b1, b2]).unwrap();
6834 assert!((ret - 0.1).abs() < 1e-9);
6835 }
6836
6837 #[test]
6840 fn test_average_high_none_for_empty_slice() {
6841 assert!(OhlcvBar::average_high(&[]).is_none());
6842 }
6843
6844 #[test]
6845 fn test_average_high_single_bar() {
6846 let bar = make_ohlcv_bar(dec!(100), dec!(120), dec!(90), dec!(105));
6847 assert_eq!(OhlcvBar::average_high(&[bar]), Some(dec!(120)));
6848 }
6849
6850 #[test]
6851 fn test_average_high_multiple_bars() {
6852 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
6853 let b2 = make_ohlcv_bar(dec!(100), dec!(130), dec!(90), dec!(105));
6854 assert_eq!(OhlcvBar::average_high(&[b1, b2]), Some(dec!(120)));
6855 }
6856
6857 #[test]
6858 fn test_average_low_none_for_empty_slice() {
6859 assert!(OhlcvBar::average_low(&[]).is_none());
6860 }
6861
6862 #[test]
6863 fn test_average_low_single_bar() {
6864 let bar = make_ohlcv_bar(dec!(100), dec!(120), dec!(80), dec!(105));
6865 assert_eq!(OhlcvBar::average_low(&[bar]), Some(dec!(80)));
6866 }
6867
6868 #[test]
6869 fn test_average_low_multiple_bars() {
6870 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(80), dec!(105));
6871 let b2 = make_ohlcv_bar(dec!(100), dec!(130), dec!(60), dec!(105));
6872 assert_eq!(OhlcvBar::average_low(&[b1, b2]), Some(dec!(70)));
6873 }
6874
6875 #[test]
6878 fn test_min_body_none_for_empty_slice() {
6879 assert!(OhlcvBar::min_body(&[]).is_none());
6880 }
6881
6882 #[test]
6883 fn test_min_body_returns_smallest_body() {
6884 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)));
6887 }
6888
6889 #[test]
6890 fn test_max_body_none_for_empty_slice() {
6891 assert!(OhlcvBar::max_body(&[]).is_none());
6892 }
6893
6894 #[test]
6895 fn test_max_body_returns_largest_body() {
6896 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)));
6899 }
6900
6901 #[test]
6904 fn test_atr_pct_none_for_single_bar() {
6905 assert!(OhlcvBar::atr_pct(&[make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105))]).is_none());
6906 }
6907
6908 #[test]
6909 fn test_atr_pct_positive_for_normal_bars() {
6910 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
6911 let b2 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
6912 let pct = OhlcvBar::atr_pct(&[b1, b2]).unwrap();
6913 assert!(pct > 0.0);
6914 }
6915
6916 #[test]
6919 fn test_breakout_count_zero_for_empty_slice() {
6920 assert_eq!(OhlcvBar::breakout_count(&[]), 0);
6921 }
6922
6923 #[test]
6924 fn test_breakout_count_zero_for_single_bar() {
6925 assert_eq!(OhlcvBar::breakout_count(&[make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105))]), 0);
6926 }
6927
6928 #[test]
6929 fn test_breakout_count_detects_close_above_prev_high() {
6930 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(108));
6932 let b2 = make_ohlcv_bar(dec!(108), dec!(120), dec!(105), dec!(115));
6933 assert_eq!(OhlcvBar::breakout_count(&[b1, b2]), 1);
6934 }
6935
6936 #[test]
6937 fn test_breakout_count_zero_when_close_at_prev_high() {
6938 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(108));
6940 let b2 = make_ohlcv_bar(dec!(108), dec!(115), dec!(105), dec!(110));
6941 assert_eq!(OhlcvBar::breakout_count(&[b1, b2]), 0);
6942 }
6943
6944 #[test]
6947 fn test_doji_count_zero_for_empty_slice() {
6948 assert_eq!(OhlcvBar::doji_count(&[], dec!(0.001)), 0);
6949 }
6950
6951 #[test]
6952 fn test_doji_count_detects_doji_bars() {
6953 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);
6956 }
6957
6958 #[test]
6961 fn test_channel_width_none_for_empty_slice() {
6962 assert!(OhlcvBar::channel_width(&[]).is_none());
6963 }
6964
6965 #[test]
6966 fn test_channel_width_correct() {
6967 let b1 = make_ohlcv_bar(dec!(100), dec!(120), dec!(90), dec!(105));
6968 let b2 = make_ohlcv_bar(dec!(100), dec!(115), dec!(80), dec!(100));
6969 assert_eq!(OhlcvBar::channel_width(&[b1, b2]), Some(dec!(40)));
6971 }
6972
6973 #[test]
6976 fn test_sma_none_for_zero_period() {
6977 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
6978 assert!(OhlcvBar::sma(&[bar], 0).is_none());
6979 }
6980
6981 #[test]
6982 fn test_sma_none_when_fewer_bars_than_period() {
6983 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
6984 assert!(OhlcvBar::sma(&[bar], 3).is_none());
6985 }
6986
6987 #[test]
6988 fn test_sma_correct_for_last_n_bars() {
6989 let bars = vec![
6990 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100)),
6991 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(110)),
6992 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(120)),
6993 ];
6994 assert_eq!(OhlcvBar::sma(&bars, 3), Some(dec!(110)));
6996 }
6997
6998 #[test]
7001 fn test_mean_wick_ratio_none_for_empty_slice() {
7002 assert!(OhlcvBar::mean_wick_ratio(&[]).is_none());
7003 }
7004
7005 #[test]
7006 fn test_mean_wick_ratio_in_range_zero_to_one() {
7007 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
7008 let b2 = make_ohlcv_bar(dec!(100), dec!(115), dec!(85), dec!(100));
7009 let ratio = OhlcvBar::mean_wick_ratio(&[b1, b2]).unwrap();
7010 assert!(ratio >= 0.0 && ratio <= 1.0);
7011 }
7012
7013 #[test]
7016 fn test_bullish_volume_zero_for_empty_slice() {
7017 assert_eq!(OhlcvBar::bullish_volume(&[]), dec!(0));
7018 }
7019
7020 #[test]
7021 fn test_bullish_volume_sums_bullish_bars() {
7022 let mut bull = make_ohlcv_bar(dec!(90), dec!(110), dec!(85), dec!(105));
7023 bull.volume = dec!(100);
7024 let mut bear = make_ohlcv_bar(dec!(110), dec!(115), dec!(85), dec!(95));
7025 bear.volume = dec!(50);
7026 assert_eq!(OhlcvBar::bullish_volume(&[bull, bear]), dec!(100));
7027 }
7028
7029 #[test]
7030 fn test_bearish_volume_zero_for_empty_slice() {
7031 assert_eq!(OhlcvBar::bearish_volume(&[]), dec!(0));
7032 }
7033
7034 #[test]
7035 fn test_bearish_volume_sums_bearish_bars() {
7036 let mut bull = make_ohlcv_bar(dec!(90), dec!(110), dec!(85), dec!(105));
7037 bull.volume = dec!(100);
7038 let mut bear = make_ohlcv_bar(dec!(110), dec!(115), dec!(85), dec!(95));
7039 bear.volume = dec!(50);
7040 assert_eq!(OhlcvBar::bearish_volume(&[bull, bear]), dec!(50));
7041 }
7042
7043 #[test]
7046 fn test_close_above_mid_count_zero_for_empty_slice() {
7047 assert_eq!(OhlcvBar::close_above_mid_count(&[]), 0);
7048 }
7049
7050 #[test]
7051 fn test_close_above_mid_count_correct() {
7052 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);
7056 }
7057
7058 #[test]
7061 fn test_ema_none_for_empty_slice() {
7062 assert!(OhlcvBar::ema(&[], 0.5).is_none());
7063 }
7064
7065 #[test]
7066 fn test_ema_single_bar_equals_close() {
7067 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
7068 let e = OhlcvBar::ema(&[bar], 0.5).unwrap();
7069 assert!((e - 105.0).abs() < 1e-9);
7070 }
7071
7072 #[test]
7073 fn test_ema_alpha_one_equals_last_close() {
7074 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
7075 let b2 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(200));
7076 let e = OhlcvBar::ema(&[b1, b2], 1.0).unwrap();
7077 assert!((e - 200.0).abs() < 1e-9);
7078 }
7079
7080 #[test]
7083 fn test_highest_open_none_for_empty_slice() {
7084 assert!(OhlcvBar::highest_open(&[]).is_none());
7085 }
7086
7087 #[test]
7088 fn test_highest_open_returns_max() {
7089 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
7090 let b2 = make_ohlcv_bar(dec!(130), dec!(140), dec!(120), dec!(135));
7091 assert_eq!(OhlcvBar::highest_open(&[b1, b2]), Some(dec!(130)));
7092 }
7093
7094 #[test]
7095 fn test_lowest_open_none_for_empty_slice() {
7096 assert!(OhlcvBar::lowest_open(&[]).is_none());
7097 }
7098
7099 #[test]
7100 fn test_lowest_open_returns_min() {
7101 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
7102 let b2 = make_ohlcv_bar(dec!(130), dec!(140), dec!(120), dec!(135));
7103 assert_eq!(OhlcvBar::lowest_open(&[b1, b2]), Some(dec!(100)));
7104 }
7105
7106 #[test]
7109 fn test_rising_close_count_zero_for_empty_slice() {
7110 assert_eq!(OhlcvBar::rising_close_count(&[]), 0);
7111 }
7112
7113 #[test]
7114 fn test_rising_close_count_zero_for_single_bar() {
7115 assert_eq!(OhlcvBar::rising_close_count(&[make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105))]), 0);
7116 }
7117
7118 #[test]
7119 fn test_rising_close_count_correct() {
7120 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
7121 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);
7125 }
7126
7127 #[test]
7130 fn test_mean_body_ratio_none_for_empty_slice() {
7131 assert!(OhlcvBar::mean_body_ratio(&[]).is_none());
7132 }
7133
7134 #[test]
7135 fn test_mean_body_ratio_in_range_zero_to_one() {
7136 let b1 = make_ohlcv_bar(dec!(100), dec!(120), dec!(80), dec!(110));
7137 let b2 = make_ohlcv_bar(dec!(100), dec!(120), dec!(80), dec!(100));
7138 let ratio = OhlcvBar::mean_body_ratio(&[b1, b2]).unwrap();
7139 assert!(ratio >= 0.0 && ratio <= 1.0);
7140 }
7141
7142 #[test]
7145 fn test_volume_std_dev_none_for_single_bar() {
7146 let mut b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
7147 b.volume = dec!(100);
7148 assert!(OhlcvBar::volume_std_dev(&[b]).is_none());
7149 }
7150
7151 #[test]
7152 fn test_volume_std_dev_zero_for_identical_volumes() {
7153 let mut b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)); b1.volume = dec!(50);
7154 let mut b2 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)); b2.volume = dec!(50);
7155 assert_eq!(OhlcvBar::volume_std_dev(&[b1, b2]), Some(0.0));
7156 }
7157
7158 #[test]
7159 fn test_volume_std_dev_positive_for_varied_volumes() {
7160 let mut b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)); b1.volume = dec!(10);
7161 let mut b2 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)); b2.volume = dec!(100);
7162 let std = OhlcvBar::volume_std_dev(&[b1, b2]).unwrap();
7163 assert!(std > 0.0);
7164 }
7165
7166 #[test]
7169 fn test_max_volume_bar_none_for_empty_slice() {
7170 assert!(OhlcvBar::max_volume_bar(&[]).is_none());
7171 }
7172
7173 #[test]
7174 fn test_max_volume_bar_returns_highest_volume() {
7175 let mut b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)); b1.volume = dec!(10);
7176 let mut b2 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)); b2.volume = dec!(100);
7177 let bars = [b1, b2];
7178 let bar = OhlcvBar::max_volume_bar(&bars).unwrap();
7179 assert_eq!(bar.volume, dec!(100));
7180 }
7181
7182 #[test]
7183 fn test_min_volume_bar_returns_lowest_volume() {
7184 let mut b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)); b1.volume = dec!(10);
7185 let mut b2 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)); b2.volume = dec!(100);
7186 let bars = [b1, b2];
7187 let bar = OhlcvBar::min_volume_bar(&bars).unwrap();
7188 assert_eq!(bar.volume, dec!(10));
7189 }
7190
7191 #[test]
7194 fn test_gap_sum_zero_for_empty_slice() {
7195 assert_eq!(OhlcvBar::gap_sum(&[]), dec!(0));
7196 }
7197
7198 #[test]
7199 fn test_gap_sum_zero_for_single_bar() {
7200 assert_eq!(OhlcvBar::gap_sum(&[make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105))]), dec!(0));
7201 }
7202
7203 #[test]
7204 fn test_gap_sum_positive_for_gap_up_sequence() {
7205 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
7207 let b2 = make_ohlcv_bar(dec!(110), dec!(120), dec!(105), dec!(115));
7208 assert_eq!(OhlcvBar::gap_sum(&[b1, b2]), dec!(10));
7209 }
7210
7211 #[test]
7212 fn test_gap_sum_negative_for_gap_down_sequence() {
7213 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
7215 let b2 = make_ohlcv_bar(dec!(90), dec!(95), dec!(80), dec!(85));
7216 assert_eq!(OhlcvBar::gap_sum(&[b1, b2]), dec!(-10));
7217 }
7218
7219 #[test]
7222 fn test_three_white_soldiers_false_for_fewer_than_3_bars() {
7223 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
7224 assert!(!OhlcvBar::three_white_soldiers(&[b]));
7225 }
7226
7227 #[test]
7228 fn test_three_white_soldiers_true_for_classic_pattern() {
7229 let b1 = make_ohlcv_bar(dec!(100), dec!(115), dec!(98), dec!(112));
7230 let b2 = make_ohlcv_bar(dec!(112), dec!(128), dec!(110), dec!(125));
7231 let b3 = make_ohlcv_bar(dec!(125), dec!(142), dec!(123), dec!(140));
7232 assert!(OhlcvBar::three_white_soldiers(&[b1, b2, b3]));
7233 }
7234
7235 #[test]
7236 fn test_three_white_soldiers_false_for_bearish_bar_in_sequence() {
7237 let b1 = make_ohlcv_bar(dec!(100), dec!(115), dec!(98), dec!(112));
7239 let b2 = make_ohlcv_bar(dec!(115), dec!(120), dec!(105), dec!(108));
7240 let b3 = make_ohlcv_bar(dec!(108), dec!(130), dec!(106), dec!(128));
7241 assert!(!OhlcvBar::three_white_soldiers(&[b1, b2, b3]));
7242 }
7243
7244 #[test]
7247 fn test_three_black_crows_false_for_fewer_than_3_bars() {
7248 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(95));
7249 assert!(!OhlcvBar::three_black_crows(&[b]));
7250 }
7251
7252 #[test]
7253 fn test_three_black_crows_true_for_classic_pattern() {
7254 let b1 = make_ohlcv_bar(dec!(140), dec!(142), dec!(110), dec!(112));
7255 let b2 = make_ohlcv_bar(dec!(112), dec!(114), dec!(95), dec!(97));
7256 let b3 = make_ohlcv_bar(dec!(97), dec!(99), dec!(80), dec!(82));
7257 assert!(OhlcvBar::three_black_crows(&[b1, b2, b3]));
7258 }
7259
7260 #[test]
7261 fn test_three_black_crows_false_for_bullish_bar_in_sequence() {
7262 let b1 = make_ohlcv_bar(dec!(140), dec!(142), dec!(110), dec!(112));
7264 let b2 = make_ohlcv_bar(dec!(108), dec!(120), dec!(106), dec!(118));
7265 let b3 = make_ohlcv_bar(dec!(115), dec!(116), dec!(90), dec!(92));
7266 assert!(!OhlcvBar::three_black_crows(&[b1, b2, b3]));
7267 }
7268
7269 #[test]
7272 fn test_is_gap_bar_true_when_open_differs_from_prev_close() {
7273 let bar = make_ohlcv_bar(dec!(105), dec!(115), dec!(100), dec!(110));
7274 assert!(OhlcvBar::is_gap_bar(&bar, dec!(100)));
7275 }
7276
7277 #[test]
7278 fn test_is_gap_bar_false_when_open_equals_prev_close() {
7279 let bar = make_ohlcv_bar(dec!(100), dec!(115), dec!(98), dec!(110));
7280 assert!(!OhlcvBar::is_gap_bar(&bar, dec!(100)));
7281 }
7282
7283 #[test]
7286 fn test_gap_bars_count_zero_for_empty_slice() {
7287 assert_eq!(OhlcvBar::gap_bars_count(&[]), 0);
7288 }
7289
7290 #[test]
7291 fn test_gap_bars_count_zero_when_no_gaps() {
7292 let b1 = make_ohlcv_bar(dec!(90), dec!(105), dec!(88), dec!(100));
7294 let b2 = make_ohlcv_bar(dec!(100), dec!(115), dec!(98), dec!(110));
7295 assert_eq!(OhlcvBar::gap_bars_count(&[b1, b2]), 0);
7296 }
7297
7298 #[test]
7299 fn test_gap_bars_count_counts_all_gaps() {
7300 let b1 = make_ohlcv_bar(dec!(90), dec!(105), dec!(88), dec!(100));
7301 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);
7305 }
7306
7307 #[test]
7310 fn test_inside_bar_true_when_range_inside_prior_v2() {
7311 let prior = make_ohlcv_bar(dec!(100), dec!(120), dec!(80), dec!(110));
7312 let bar = make_ohlcv_bar(dec!(105), dec!(115), dec!(90), dec!(108));
7313 assert!(bar.inside_bar(&prior));
7314 }
7315
7316 #[test]
7317 fn test_inside_bar_false_when_high_exceeds_prior_v2() {
7318 let prior = make_ohlcv_bar(dec!(100), dec!(120), dec!(80), dec!(110));
7319 let bar = make_ohlcv_bar(dec!(105), dec!(125), dec!(90), dec!(118));
7320 assert!(!bar.inside_bar(&prior));
7321 }
7322
7323 #[test]
7324 fn test_outside_bar_true_when_range_engulfs_prior_v2() {
7325 let prior = make_ohlcv_bar(dec!(100), dec!(115), dec!(90), dec!(108));
7326 let bar = make_ohlcv_bar(dec!(95), dec!(120), dec!(85), dec!(112));
7327 assert!(bar.outside_bar(&prior));
7328 }
7329
7330 #[test]
7331 fn test_outside_bar_false_when_range_is_inside_v2() {
7332 let prior = make_ohlcv_bar(dec!(100), dec!(120), dec!(80), dec!(110));
7333 let bar = make_ohlcv_bar(dec!(105), dec!(115), dec!(90), dec!(108));
7334 assert!(!bar.outside_bar(&prior));
7335 }
7336
7337 #[test]
7340 fn test_bar_efficiency_none_for_zero_range_bar() {
7341 let bar = make_ohlcv_bar(dec!(100), dec!(100), dec!(100), dec!(100));
7342 assert!(OhlcvBar::bar_efficiency(&bar).is_none());
7343 }
7344
7345 #[test]
7346 fn test_bar_efficiency_one_for_full_trend_bar() {
7347 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(100), dec!(110));
7349 let eff = OhlcvBar::bar_efficiency(&bar).unwrap();
7350 assert!((eff - 1.0).abs() < 1e-9);
7351 }
7352
7353 #[test]
7354 fn test_bar_efficiency_between_zero_and_one() {
7355 let bar = make_ohlcv_bar(dec!(100), dec!(115), dec!(95), dec!(108));
7356 let eff = OhlcvBar::bar_efficiency(&bar).unwrap();
7357 assert!(eff >= 0.0 && eff <= 1.0);
7358 }
7359
7360 #[test]
7363 fn test_wicks_sum_zero_for_empty_slice() {
7364 assert_eq!(OhlcvBar::wicks_sum(&[]), dec!(0));
7365 }
7366
7367 #[test]
7368 fn test_wicks_sum_correct_for_doji_like_bar() {
7369 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
7371 assert_eq!(OhlcvBar::wicks_sum(&[bar]), dec!(20));
7372 }
7373
7374 #[test]
7377 fn test_avg_close_to_high_none_for_empty_slice() {
7378 assert!(OhlcvBar::avg_close_to_high(&[]).is_none());
7379 }
7380
7381 #[test]
7382 fn test_avg_close_to_high_correct_for_two_bars() {
7383 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(95), dec!(105));
7385 let b2 = make_ohlcv_bar(dec!(110), dec!(120), dec!(108), dec!(115));
7386 let avg = OhlcvBar::avg_close_to_high(&[b1, b2]).unwrap();
7387 assert!((avg - 5.0).abs() < 1e-9);
7388 }
7389
7390 #[test]
7393 fn test_avg_range_r65_none_for_empty() {
7394 assert!(OhlcvBar::avg_range(&[]).is_none());
7395 }
7396
7397 #[test]
7398 fn test_avg_range_r65_correct() {
7399 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
7400 let b2 = make_ohlcv_bar(dec!(105), dec!(120), dec!(100), dec!(115));
7401 let avg = OhlcvBar::avg_range(&[b1, b2]).unwrap();
7402 assert!((avg - 20.0).abs() < 1e-9);
7403 }
7404
7405 #[test]
7408 fn test_max_close_r65_none_empty() {
7409 assert!(OhlcvBar::max_close(&[]).is_none());
7410 }
7411
7412 #[test]
7413 fn test_max_close_r65_highest() {
7414 let b1 = make_ohlcv_bar(dec!(100), dec!(115), dec!(95), dec!(110));
7415 let b2 = make_ohlcv_bar(dec!(110), dec!(130), dec!(108), dec!(125));
7416 let b3 = make_ohlcv_bar(dec!(115), dec!(120), dec!(112), dec!(118));
7417 assert_eq!(OhlcvBar::max_close(&[b1, b2, b3]), Some(dec!(125)));
7418 }
7419
7420 #[test]
7421 fn test_min_close_r65_lowest() {
7422 let b1 = make_ohlcv_bar(dec!(100), dec!(115), dec!(95), dec!(110));
7423 let b2 = make_ohlcv_bar(dec!(110), dec!(130), dec!(108), dec!(125));
7424 let b3 = make_ohlcv_bar(dec!(90), dec!(105), dec!(88), dec!(95));
7425 assert_eq!(OhlcvBar::min_close(&[b1, b2, b3]), Some(dec!(95)));
7426 }
7427
7428 #[test]
7431 fn test_trend_strength_r65_none_single() {
7432 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
7433 assert!(OhlcvBar::trend_strength(&[b]).is_none());
7434 }
7435
7436 #[test]
7437 fn test_trend_strength_r65_one_bullish() {
7438 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(95), dec!(105));
7439 let b2 = make_ohlcv_bar(dec!(105), dec!(120), dec!(103), dec!(115));
7440 let b3 = make_ohlcv_bar(dec!(115), dec!(130), dec!(113), dec!(128));
7441 let s = OhlcvBar::trend_strength(&[b1, b2, b3]).unwrap();
7442 assert!((s - 1.0).abs() < 1e-9);
7443 }
7444
7445 #[test]
7446 fn test_trend_strength_r65_zero_bearish() {
7447 let b1 = make_ohlcv_bar(dec!(128), dec!(130), dec!(113), dec!(128));
7448 let b2 = make_ohlcv_bar(dec!(115), dec!(120), dec!(103), dec!(110));
7449 let b3 = make_ohlcv_bar(dec!(105), dec!(110), dec!(95), dec!(100));
7450 let s = OhlcvBar::trend_strength(&[b1, b2, b3]).unwrap();
7451 assert!((s - 0.0).abs() < 1e-9);
7452 }
7453
7454 #[test]
7457 fn test_net_change_none_for_empty() {
7458 assert!(OhlcvBar::net_change(&[]).is_none());
7459 }
7460
7461 #[test]
7462 fn test_net_change_positive_for_bullish_bar() {
7463 let b = make_ohlcv_bar(dec!(100), dec!(115), dec!(98), dec!(112));
7464 assert_eq!(OhlcvBar::net_change(&[b]), Some(dec!(12)));
7465 }
7466
7467 #[test]
7468 fn test_net_change_negative_for_bearish_bar() {
7469 let b = make_ohlcv_bar(dec!(110), dec!(112), dec!(95), dec!(100));
7470 assert_eq!(OhlcvBar::net_change(&[b]), Some(dec!(-10)));
7471 }
7472
7473 #[test]
7476 fn test_open_to_close_pct_none_for_empty() {
7477 assert!(OhlcvBar::open_to_close_pct(&[]).is_none());
7478 }
7479
7480 #[test]
7481 fn test_open_to_close_pct_correct() {
7482 let b = make_ohlcv_bar(dec!(100), dec!(115), dec!(98), dec!(110));
7484 let pct = OhlcvBar::open_to_close_pct(&[b]).unwrap();
7485 assert!((pct - 10.0).abs() < 1e-9);
7486 }
7487
7488 #[test]
7491 fn test_high_to_low_pct_none_for_empty() {
7492 assert!(OhlcvBar::high_to_low_pct(&[]).is_none());
7493 }
7494
7495 #[test]
7496 fn test_high_to_low_pct_correct() {
7497 let b = make_ohlcv_bar(dec!(150), dec!(200), dec!(100), dec!(160));
7499 let pct = OhlcvBar::high_to_low_pct(&[b]).unwrap();
7500 assert!((pct - 50.0).abs() < 1e-9);
7501 }
7502
7503 #[test]
7506 fn test_consecutive_highs_zero_for_single_bar() {
7507 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
7508 assert_eq!(OhlcvBar::consecutive_highs(&[b]), 0);
7509 }
7510
7511 #[test]
7512 fn test_consecutive_highs_counts_trailing_highs() {
7513 let b1 = make_ohlcv_bar(dec!(95), dec!(110), dec!(90), dec!(105));
7515 let b2 = make_ohlcv_bar(dec!(105), dec!(120), dec!(103), dec!(115));
7516 let b3 = make_ohlcv_bar(dec!(115), dec!(130), dec!(113), dec!(125));
7517 assert_eq!(OhlcvBar::consecutive_highs(&[b1, b2, b3]), 2);
7518 }
7519
7520 #[test]
7521 fn test_consecutive_lows_counts_trailing_lows() {
7522 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
7524 let b2 = make_ohlcv_bar(dec!(95), dec!(108), dec!(80), dec!(100));
7525 let b3 = make_ohlcv_bar(dec!(90), dec!(102), dec!(70), dec!(95));
7526 assert_eq!(OhlcvBar::consecutive_lows(&[b1, b2, b3]), 2);
7527 }
7528
7529 #[test]
7532 fn test_volume_change_pct_none_for_single_bar() {
7533 let mut b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
7534 b.volume = dec!(100);
7535 assert!(OhlcvBar::volume_change_pct(&[b]).is_none());
7536 }
7537
7538 #[test]
7539 fn test_volume_change_pct_correct() {
7540 let mut b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)); b1.volume = dec!(100);
7542 let mut b2 = make_ohlcv_bar(dec!(105), dec!(115), dec!(103), dec!(110)); b2.volume = dec!(150);
7543 let pct = OhlcvBar::volume_change_pct(&[b1, b2]).unwrap();
7544 assert!((pct - 50.0).abs() < 1e-9);
7545 }
7546
7547 #[test]
7550 fn test_clv_r67_plus_one_at_high() {
7551 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(110));
7553 let clv = b.close_location_value().unwrap();
7554 assert!((clv - 1.0).abs() < 1e-9);
7555 }
7556
7557 #[test]
7558 fn test_clv_r67_minus_one_at_low() {
7559 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(90));
7561 let clv = b.close_location_value().unwrap();
7562 assert!((clv - (-1.0)).abs() < 1e-9);
7563 }
7564
7565 #[test]
7566 fn test_clv_r67_none_for_zero_range() {
7567 let b = make_ohlcv_bar(dec!(100), dec!(100), dec!(100), dec!(100));
7568 assert!(b.close_location_value().is_none());
7569 }
7570
7571 #[test]
7574 fn test_body_pct_r67_none_for_zero_range() {
7575 let b = make_ohlcv_bar(dec!(100), dec!(100), dec!(100), dec!(100));
7576 assert!(b.body_pct().is_none());
7577 }
7578
7579 #[test]
7580 fn test_body_pct_r67_100_for_full_body() {
7581 let b = make_ohlcv_bar(dec!(90), dec!(110), dec!(90), dec!(110));
7583 assert_eq!(b.body_pct(), Some(dec!(100)));
7584 }
7585
7586 #[test]
7589 fn test_bullish_count_r67_correct() {
7590 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);
7594 }
7595
7596 #[test]
7597 fn test_bearish_count_r67_correct() {
7598 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);
7601 }
7602
7603 #[test]
7606 fn test_open_gap_pct_none_for_single_bar() {
7607 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
7608 assert!(OhlcvBar::open_gap_pct(&[b]).is_none());
7609 }
7610
7611 #[test]
7612 fn test_open_gap_pct_positive_for_gap_up() {
7613 let b1 = make_ohlcv_bar(dec!(90), dec!(105), dec!(88), dec!(100));
7615 let b2 = make_ohlcv_bar(dec!(105), dec!(115), dec!(103), dec!(110));
7616 let pct = OhlcvBar::open_gap_pct(&[b1, b2]).unwrap();
7617 assert!((pct - 5.0).abs() < 1e-9);
7618 }
7619
7620 #[test]
7623 fn test_volume_cumulative_zero_for_empty() {
7624 assert_eq!(OhlcvBar::volume_cumulative(&[]), dec!(0));
7625 }
7626
7627 #[test]
7628 fn test_volume_cumulative_sums_all_volumes() {
7629 let mut b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)); b1.volume = dec!(100);
7630 let mut b2 = make_ohlcv_bar(dec!(105), dec!(115), dec!(103), dec!(110)); b2.volume = dec!(200);
7631 assert_eq!(OhlcvBar::volume_cumulative(&[b1, b2]), dec!(300));
7632 }
7633
7634 #[test]
7637 fn test_price_position_none_for_empty() {
7638 assert!(OhlcvBar::price_position(&[]).is_none());
7639 }
7640
7641 #[test]
7642 fn test_price_position_one_when_close_at_highest() {
7643 let b1 = make_ohlcv_bar(dec!(85), dec!(100), dec!(80), dec!(95));
7645 let b2 = make_ohlcv_bar(dec!(110), dec!(120), dec!(100), dec!(120));
7646 let pos = OhlcvBar::price_position(&[b1, b2]).unwrap();
7647 assert!((pos - 1.0).abs() < 1e-9);
7648 }
7649
7650 #[test]
7653 fn test_close_above_open_count_zero_for_empty() {
7654 assert_eq!(OhlcvBar::close_above_open_count(&[]), 0);
7655 }
7656
7657 #[test]
7658 fn test_close_above_open_count_correct() {
7659 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(95), dec!(108));
7661 let b2 = make_ohlcv_bar(dec!(110), dec!(115), dec!(100), dec!(102));
7662 assert_eq!(OhlcvBar::close_above_open_count(&[b1, b2]), 1);
7663 }
7664
7665 #[test]
7668 fn test_volume_price_correlation_none_for_single_bar() {
7669 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
7670 assert!(OhlcvBar::volume_price_correlation(&[b]).is_none());
7671 }
7672
7673 #[test]
7674 fn test_volume_price_correlation_positive_for_comoving() {
7675 let mut b1 = make_ohlcv_bar(dec!(100), dec!(105), dec!(98), dec!(102)); b1.volume = dec!(100);
7677 let mut b2 = make_ohlcv_bar(dec!(102), dec!(110), dec!(100), dec!(108)); b2.volume = dec!(200);
7678 let corr = OhlcvBar::volume_price_correlation(&[b1, b2]).unwrap();
7679 assert!(corr > 0.0, "expected positive correlation, got {}", corr);
7680 }
7681
7682 #[test]
7685 fn test_body_consistency_none_for_empty() {
7686 assert!(OhlcvBar::body_consistency(&[]).is_none());
7687 }
7688
7689 #[test]
7690 fn test_body_consistency_one_for_all_big_bodies() {
7691 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(100), dec!(108));
7693 let b2 = make_ohlcv_bar(dec!(110), dec!(120), dec!(110), dec!(118));
7694 let r = OhlcvBar::body_consistency(&[b1, b2]).unwrap();
7695 assert!((r - 1.0).abs() < 1e-9, "expected 1.0, got {}", r);
7696 }
7697
7698 #[test]
7701 fn test_close_volatility_ratio_none_for_single_bar() {
7702 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
7703 assert!(OhlcvBar::close_volatility_ratio(&[b]).is_none());
7704 }
7705
7706 #[test]
7707 fn test_close_volatility_ratio_positive_for_varied_closes() {
7708 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
7709 let b2 = make_ohlcv_bar(dec!(110), dec!(120), dec!(105), dec!(115));
7710 let r = OhlcvBar::close_volatility_ratio(&[b1, b2]).unwrap();
7711 assert!(r > 0.0, "expected positive ratio, got {}", r);
7712 }
7713
7714 #[test]
7715 fn test_close_volatility_ratio_zero_for_identical_closes() {
7716 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
7717 let b2 = make_ohlcv_bar(dec!(100), dec!(112), dec!(88), dec!(105));
7718 let r = OhlcvBar::close_volatility_ratio(&[b1, b2]).unwrap();
7719 assert!((r - 0.0).abs() < 1e-9, "expected 0.0 for identical closes, got {}", r);
7720 }
7721
7722 #[test]
7725 fn test_is_trending_up_false_for_empty() {
7726 assert!(!OhlcvBar::is_trending_up(&[], 3));
7727 }
7728
7729 #[test]
7730 fn test_is_trending_up_false_for_n_less_than_2() {
7731 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
7732 assert!(!OhlcvBar::is_trending_up(&[b], 1));
7733 }
7734
7735 #[test]
7736 fn test_is_trending_up_true_for_rising_closes() {
7737 let b1 = make_ohlcv_bar(dec!(100), dec!(105), dec!(98), dec!(102));
7738 let b2 = make_ohlcv_bar(dec!(102), dec!(110), dec!(100), dec!(107));
7739 let b3 = make_ohlcv_bar(dec!(107), dec!(115), dec!(105), dec!(112));
7740 assert!(OhlcvBar::is_trending_up(&[b1, b2, b3], 3));
7741 }
7742
7743 #[test]
7744 fn test_is_trending_down_true_for_falling_closes() {
7745 let b1 = make_ohlcv_bar(dec!(112), dec!(115), dec!(105), dec!(110));
7746 let b2 = make_ohlcv_bar(dec!(110), dec!(112), dec!(100), dec!(105));
7747 let b3 = make_ohlcv_bar(dec!(105), dec!(108), dec!(95), dec!(98));
7748 assert!(OhlcvBar::is_trending_down(&[b1, b2, b3], 3));
7749 }
7750
7751 #[test]
7754 fn test_volume_acceleration_none_for_single_bar() {
7755 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
7756 assert!(OhlcvBar::volume_acceleration(&[b]).is_none());
7757 }
7758
7759 #[test]
7760 fn test_volume_acceleration_positive_when_volume_rises() {
7761 let mut b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)); b1.volume = dec!(100);
7762 let mut b2 = make_ohlcv_bar(dec!(105), dec!(115), dec!(103), dec!(110)); b2.volume = dec!(150);
7763 let acc = OhlcvBar::volume_acceleration(&[b1, b2]).unwrap();
7764 assert!(acc > 0.0, "volume rose so acceleration should be positive, got {}", acc);
7765 }
7766
7767 #[test]
7770 fn test_wick_body_ratio_none_for_empty() {
7771 assert!(OhlcvBar::wick_body_ratio(&[]).is_none());
7772 }
7773
7774 #[test]
7775 fn test_wick_body_ratio_none_for_doji_bar() {
7776 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
7778 assert!(OhlcvBar::wick_body_ratio(&[b]).is_none());
7779 }
7780
7781 #[test]
7782 fn test_wick_body_ratio_positive_for_wicked_bar() {
7783 let b = make_ohlcv_bar(dec!(100), dec!(115), dec!(95), dec!(105));
7785 let r = OhlcvBar::wick_body_ratio(&[b]).unwrap();
7786 assert!(r > 0.0, "expected positive wick/body ratio, got {}", r);
7787 }
7788
7789 #[test]
7792 fn test_close_momentum_score_none_for_empty() {
7793 assert!(OhlcvBar::close_momentum_score(&[]).is_none());
7794 }
7795
7796 #[test]
7797 fn test_close_momentum_score_half_for_symmetric() {
7798 let b1 = make_ohlcv_bar(dec!(88), dec!(95), dec!(85), dec!(90));
7800 let b2 = make_ohlcv_bar(dec!(108), dec!(115), dec!(105), dec!(110));
7801 let score = OhlcvBar::close_momentum_score(&[b1, b2]).unwrap();
7802 assert!((score - 0.5).abs() < 1e-9, "expected 0.5, got {}", score);
7803 }
7804
7805 #[test]
7808 fn test_range_expansion_count_zero_for_single_bar() {
7809 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
7810 assert_eq!(OhlcvBar::range_expansion_count(&[b]), 0);
7811 }
7812
7813 #[test]
7814 fn test_range_expansion_count_correct() {
7815 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
7817 let b2 = make_ohlcv_bar(dec!(105), dec!(120), dec!(90), dec!(110));
7818 assert_eq!(OhlcvBar::range_expansion_count(&[b1, b2]), 1);
7819 }
7820
7821 #[test]
7824 fn test_gap_count_zero_for_single_bar() {
7825 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
7826 assert_eq!(OhlcvBar::gap_count(&[b]), 0);
7827 }
7828
7829 #[test]
7830 fn test_gap_count_detects_gap() {
7831 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
7832 let b2 = make_ohlcv_bar(dec!(108), dec!(115), dec!(106), dec!(112));
7834 assert_eq!(OhlcvBar::gap_count(&[b1, b2]), 1);
7835 }
7836
7837 #[test]
7838 fn test_gap_count_zero_when_open_equals_close() {
7839 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
7840 let b2 = make_ohlcv_bar(dec!(105), dec!(115), dec!(103), dec!(112));
7842 assert_eq!(OhlcvBar::gap_count(&[b1, b2]), 0);
7843 }
7844
7845 #[test]
7848 fn test_avg_wick_size_none_for_empty() {
7849 assert!(OhlcvBar::avg_wick_size(&[]).is_none());
7850 }
7851
7852 #[test]
7853 fn test_avg_wick_size_correct() {
7854 let b = make_ohlcv_bar(dec!(100), dec!(115), dec!(95), dec!(105));
7857 let ws = OhlcvBar::avg_wick_size(&[b]).unwrap();
7858 assert!((ws - 15.0).abs() < 1e-6, "expected 15.0, got {}", ws);
7859 }
7860
7861 #[test]
7864 fn test_mean_volume_ratio_empty_for_empty_slice() {
7865 assert!(OhlcvBar::mean_volume_ratio(&[]).is_empty());
7866 }
7867
7868 #[test]
7869 fn test_mean_volume_ratio_sums_to_n_times_mean() {
7870 let mut b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)); b1.volume = dec!(100);
7871 let mut b2 = make_ohlcv_bar(dec!(105), dec!(115), dec!(103), dec!(110)); b2.volume = dec!(300);
7872 let ratios = OhlcvBar::mean_volume_ratio(&[b1, b2]);
7874 assert_eq!(ratios.len(), 2);
7875 let r0 = ratios[0].unwrap();
7876 let r1 = ratios[1].unwrap();
7877 assert!((r0 - 0.5).abs() < 1e-6, "expected 0.5, got {}", r0);
7878 assert!((r1 - 1.5).abs() < 1e-6, "expected 1.5, got {}", r1);
7879 }
7880
7881 #[test]
7884 fn test_price_compression_ratio_none_for_zero_range() {
7885 let b = make_ohlcv_bar(dec!(100), dec!(100), dec!(100), dec!(100));
7887 assert!(OhlcvBar::price_compression_ratio(&[b]).is_none());
7888 }
7889
7890 #[test]
7891 fn test_price_compression_ratio_in_range() {
7892 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(108));
7893 let r = OhlcvBar::price_compression_ratio(&[b]).unwrap();
7894 assert!(r >= 0.0 && r <= 1.0, "expected value in [0,1], got {}", r);
7895 }
7896
7897 #[test]
7900 fn test_open_close_spread_none_for_empty() {
7901 assert!(OhlcvBar::open_close_spread(&[]).is_none());
7902 }
7903
7904 #[test]
7905 fn test_open_close_spread_zero_for_doji() {
7906 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
7907 let s = OhlcvBar::open_close_spread(&[b]).unwrap();
7908 assert!((s - 0.0).abs() < 1e-9, "doji should have spread=0, got {}", s);
7909 }
7910
7911 #[test]
7912 fn test_open_close_spread_positive_for_directional_bar() {
7913 let b = make_ohlcv_bar(dec!(100), dec!(115), dec!(98), dec!(110));
7914 let s = OhlcvBar::open_close_spread(&[b]).unwrap();
7915 assert!(s > 0.0, "directional bar should have positive spread, got {}", s);
7916 }
7917
7918 #[test]
7921 fn test_close_above_high_ma_zero_for_too_few_bars() {
7922 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
7923 assert_eq!(OhlcvBar::close_above_high_ma(&[b], 2), 0);
7924 }
7925
7926 #[test]
7927 fn test_close_above_high_ma_detects_breakout() {
7928 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
7930 let b2 = make_ohlcv_bar(dec!(110), dec!(120), dec!(108), dec!(118));
7931 assert_eq!(OhlcvBar::close_above_high_ma(&[b1, b2], 2), 1);
7932 }
7933
7934 #[test]
7937 fn test_max_consecutive_gains_zero_for_single_bar() {
7938 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
7939 assert_eq!(OhlcvBar::max_consecutive_gains(&[b]), 0);
7940 }
7941
7942 #[test]
7943 fn test_max_consecutive_gains_correct() {
7944 let b1 = make_ohlcv_bar(dec!(98), dec!(102), dec!(96), dec!(100));
7946 let b2 = make_ohlcv_bar(dec!(100), dec!(108), dec!(99), dec!(105));
7947 let b3 = make_ohlcv_bar(dec!(105), dec!(112), dec!(104), dec!(110));
7948 let b4 = make_ohlcv_bar(dec!(110), dec!(111), dec!(105), dec!(108));
7949 let b5 = make_ohlcv_bar(dec!(108), dec!(116), dec!(107), dec!(115));
7950 assert_eq!(OhlcvBar::max_consecutive_gains(&[b1, b2, b3, b4, b5]), 2);
7951 }
7952
7953 #[test]
7954 fn test_max_consecutive_losses_correct() {
7955 let b1 = make_ohlcv_bar(dec!(112), dec!(115), dec!(108), dec!(110));
7957 let b2 = make_ohlcv_bar(dec!(110), dec!(112), dec!(103), dec!(105));
7958 let b3 = make_ohlcv_bar(dec!(105), dec!(108), dec!(98), dec!(100));
7959 let b4 = make_ohlcv_bar(dec!(100), dec!(112), dec!(98), dec!(108));
7960 assert_eq!(OhlcvBar::max_consecutive_losses(&[b1, b2, b3, b4]), 2);
7961 }
7962
7963 #[test]
7966 fn test_price_path_length_none_for_single_bar() {
7967 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
7968 assert!(OhlcvBar::price_path_length(&[b]).is_none());
7969 }
7970
7971 #[test]
7972 fn test_price_path_length_correct() {
7973 let b1 = make_ohlcv_bar(dec!(98), dec!(102), dec!(96), dec!(100));
7975 let b2 = make_ohlcv_bar(dec!(100), dec!(112), dec!(99), dec!(110));
7976 let b3 = make_ohlcv_bar(dec!(110), dec!(112), dec!(103), dec!(105));
7977 let len = OhlcvBar::price_path_length(&[b1, b2, b3]).unwrap();
7978 assert!((len - 15.0).abs() < 1e-6, "expected 15.0, got {}", len);
7979 }
7980
7981 #[test]
7984 fn test_close_reversion_count_zero_for_single_bar() {
7985 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
7986 assert_eq!(OhlcvBar::close_reversion_count(&[b]), 0);
7987 }
7988
7989 #[test]
7990 fn test_close_reversion_count_returns_usize() {
7991 let b1 = make_ohlcv_bar(dec!(98), dec!(102), dec!(96), dec!(100));
7992 let b2 = make_ohlcv_bar(dec!(100), dec!(112), dec!(99), dec!(110));
7993 let b3 = make_ohlcv_bar(dec!(110), dec!(112), dec!(103), dec!(105));
7994 let _ = OhlcvBar::close_reversion_count(&[b1, b2, b3]);
7996 }
7997
7998 #[test]
8001 fn test_atr_ratio_none_for_single_bar() {
8002 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8003 assert!(OhlcvBar::atr_ratio(&[b]).is_none());
8004 }
8005
8006 #[test]
8007 fn test_atr_ratio_positive_for_valid_bars() {
8008 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8009 let b2 = make_ohlcv_bar(dec!(105), dec!(115), dec!(103), dec!(110));
8010 let r = OhlcvBar::atr_ratio(&[b1, b2]).unwrap();
8011 assert!(r > 0.0, "expected positive ATR ratio, got {}", r);
8012 }
8013
8014 #[test]
8017 fn test_volume_trend_strength_none_for_single_bar() {
8018 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8019 assert!(OhlcvBar::volume_trend_strength(&[b]).is_none());
8020 }
8021
8022 #[test]
8023 fn test_volume_trend_strength_positive_for_rising_volume() {
8024 let mut b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)); b1.volume = dec!(100);
8025 let mut b2 = make_ohlcv_bar(dec!(105), dec!(115), dec!(103), dec!(110)); b2.volume = dec!(200);
8026 let mut b3 = make_ohlcv_bar(dec!(110), dec!(120), dec!(108), dec!(115)); b3.volume = dec!(300);
8027 let s = OhlcvBar::volume_trend_strength(&[b1, b2, b3]).unwrap();
8028 assert!(s > 0.0, "rising volume should give positive strength, got {}", s);
8029 }
8030
8031 #[test]
8034 fn test_high_close_spread_none_for_empty() {
8035 assert!(OhlcvBar::high_close_spread(&[]).is_none());
8036 }
8037
8038 #[test]
8039 fn test_high_close_spread_zero_when_close_equals_high() {
8040 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(110));
8042 let s = OhlcvBar::high_close_spread(&[b]).unwrap();
8043 assert!((s - 0.0).abs() < 1e-9, "expected 0.0, got {}", s);
8044 }
8045
8046 #[test]
8047 fn test_high_close_spread_positive_for_wicked_bar() {
8048 let b = make_ohlcv_bar(dec!(100), dec!(115), dec!(95), dec!(105));
8049 let s = OhlcvBar::high_close_spread(&[b]).unwrap();
8050 assert!(s > 0.0, "expected positive spread, got {}", s);
8051 }
8052
8053 #[test]
8056 fn test_open_range_none_for_empty() {
8057 assert!(OhlcvBar::open_range(&[]).is_none());
8058 }
8059
8060 #[test]
8061 fn test_open_range_zero_for_doji() {
8062 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
8063 let r = OhlcvBar::open_range(&[b]).unwrap();
8064 assert!((r - 0.0).abs() < 1e-9, "doji should have open_range=0, got {}", r);
8065 }
8066
8067 #[test]
8068 fn test_open_range_positive_for_directional() {
8069 let b = make_ohlcv_bar(dec!(100), dec!(115), dec!(98), dec!(110));
8070 let r = OhlcvBar::open_range(&[b]).unwrap();
8071 assert!(r > 0.0, "directional bar should have positive open_range, got {}", r);
8072 }
8073
8074 #[test]
8077 fn test_normalized_close_none_for_single_bar() {
8078 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8079 assert!(OhlcvBar::normalized_close(&[b]).is_none());
8080 }
8081
8082 #[test]
8083 fn test_normalized_close_one_when_last_close_is_max() {
8084 let b1 = make_ohlcv_bar(dec!(98), dec!(105), dec!(96), dec!(100));
8085 let b2 = make_ohlcv_bar(dec!(100), dec!(115), dec!(99), dec!(110));
8086 let nc = OhlcvBar::normalized_close(&[b1, b2]).unwrap();
8087 assert!((nc - 1.0).abs() < 1e-9, "last close = max should give 1.0, got {}", nc);
8088 }
8089
8090 #[test]
8091 fn test_normalized_close_zero_when_last_close_is_min() {
8092 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(90));
8093 let b2 = make_ohlcv_bar(dec!(90), dec!(105), dec!(88), dec!(100));
8094 let nc = OhlcvBar::normalized_close(&[b1, b2]).unwrap();
8097 assert!(nc >= 0.0 && nc <= 1.0, "normalized close should be in [0,1], got {}", nc);
8098 }
8099
8100 #[test]
8103 fn test_candle_score_none_for_empty() {
8104 assert!(OhlcvBar::candle_score(&[]).is_none());
8105 }
8106
8107 #[test]
8108 fn test_candle_score_one_for_strong_bull_bar() {
8109 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(99), dec!(108));
8111 let s = OhlcvBar::candle_score(&[b]).unwrap();
8112 assert_eq!(s, 1.0, "strong bullish bar should score 1.0, got {}", s);
8113 }
8114
8115 #[test]
8116 fn test_candle_score_zero_for_bear_bar() {
8117 let b = make_ohlcv_bar(dec!(108), dec!(110), dec!(99), dec!(100));
8119 let s = OhlcvBar::candle_score(&[b]).unwrap();
8120 assert_eq!(s, 0.0, "bearish bar should score 0.0, got {}", s);
8121 }
8122
8123 #[test]
8126 fn test_bar_speed_none_for_empty() {
8127 assert!(OhlcvBar::bar_speed(&[]).is_none());
8128 }
8129
8130 #[test]
8133 fn test_higher_highs_count_zero_for_single_bar() {
8134 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8135 assert_eq!(OhlcvBar::higher_highs_count(&[b]), 0);
8136 }
8137
8138 #[test]
8139 fn test_higher_highs_count_correct() {
8140 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8141 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);
8144 }
8145
8146 #[test]
8147 fn test_lower_lows_count_correct() {
8148 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8149 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);
8152 }
8153
8154 #[test]
8157 fn test_close_minus_open_pct_none_for_empty() {
8158 assert!(OhlcvBar::close_minus_open_pct(&[]).is_none());
8159 }
8160
8161 #[test]
8162 fn test_close_minus_open_pct_positive_for_bull_bar() {
8163 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(98), dec!(110));
8164 let p = OhlcvBar::close_minus_open_pct(&[b]).unwrap();
8165 assert!(p > 0.0, "bullish bar should give positive pct, got {}", p);
8166 }
8167
8168 #[test]
8171 fn test_volume_per_range_none_for_empty() {
8172 assert!(OhlcvBar::volume_per_range(&[]).is_none());
8173 }
8174
8175 #[test]
8176 fn test_volume_per_range_positive_for_valid_bar() {
8177 let mut b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)); b.volume = dec!(100);
8178 let r = OhlcvBar::volume_per_range(&[b]).unwrap();
8179 assert!(r > 0.0, "expected positive volume/range, got {}", r);
8180 }
8181
8182 #[test]
8183 fn test_up_volume_fraction_none_for_empty() {
8184 assert!(OhlcvBar::up_volume_fraction(&[]).is_none());
8185 }
8186
8187 #[test]
8188 fn test_up_volume_fraction_all_up() {
8189 let mut b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(95), dec!(108)); b1.volume = dec!(50);
8191 let mut b2 = make_ohlcv_bar(dec!(105), dec!(115), dec!(100), dec!(112)); b2.volume = dec!(50);
8192 let f = OhlcvBar::up_volume_fraction(&[b1, b2]).unwrap();
8193 assert!((f - 1.0).abs() < 1e-9, "all up bars → fraction=1.0, got {}", f);
8194 }
8195
8196 #[test]
8197 fn test_tail_upper_fraction_none_for_empty() {
8198 assert!(OhlcvBar::tail_upper_fraction(&[]).is_none());
8199 }
8200
8201 #[test]
8202 fn test_tail_upper_fraction_correct() {
8203 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8205 let f = OhlcvBar::tail_upper_fraction(&[b]).unwrap();
8206 assert!((f - 0.25).abs() < 1e-9, "expected 0.25, got {}", f);
8207 }
8208
8209 #[test]
8210 fn test_tail_lower_fraction_none_for_empty() {
8211 assert!(OhlcvBar::tail_lower_fraction(&[]).is_none());
8212 }
8213
8214 #[test]
8215 fn test_tail_lower_fraction_correct() {
8216 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8218 let f = OhlcvBar::tail_lower_fraction(&[b]).unwrap();
8219 assert!((f - 0.5).abs() < 1e-9, "expected 0.5, got {}", f);
8220 }
8221
8222 #[test]
8223 fn test_range_std_dev_none_for_single_bar() {
8224 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8225 assert!(OhlcvBar::range_std_dev(&[b]).is_none());
8226 }
8227
8228 #[test]
8229 fn test_range_std_dev_zero_for_equal_ranges() {
8230 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8231 let b2 = make_ohlcv_bar(dec!(200), dec!(210), dec!(190), dec!(205));
8232 let sd = OhlcvBar::range_std_dev(&[b1, b2]).unwrap();
8233 assert!(sd.abs() < 1e-9, "equal ranges → std_dev=0, got {}", sd);
8234 }
8235
8236 #[test]
8237 fn test_body_fraction_none_for_empty() {
8238 assert!(OhlcvBar::body_fraction(&[]).is_none());
8239 }
8240
8241 #[test]
8242 fn test_body_fraction_doji_is_zero() {
8243 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
8245 let f = OhlcvBar::body_fraction(&[b]).unwrap();
8246 assert!(f.abs() < 1e-9, "doji → body_fraction=0, got {}", f);
8247 }
8248
8249 #[test]
8250 fn test_bullish_ratio_none_for_empty() {
8251 assert!(OhlcvBar::bullish_ratio(&[]).is_none());
8252 }
8253
8254 #[test]
8255 fn test_bullish_ratio_all_bullish() {
8256 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(95), dec!(108));
8257 let b2 = make_ohlcv_bar(dec!(105), dec!(115), dec!(100), dec!(112));
8258 let r = OhlcvBar::bullish_ratio(&[b1, b2]).unwrap();
8259 assert!((r - 1.0).abs() < 1e-9, "all bullish → ratio=1.0, got {}", r);
8260 }
8261
8262 #[test]
8263 fn test_peak_trough_close_none_for_empty() {
8264 assert!(OhlcvBar::peak_close(&[]).is_none());
8265 assert!(OhlcvBar::trough_close(&[]).is_none());
8266 }
8267
8268 #[test]
8269 fn test_peak_trough_close_correct() {
8270 let bars = vec![
8271 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)),
8272 make_ohlcv_bar(dec!(105), dec!(120), dec!(100), dec!(115)),
8273 make_ohlcv_bar(dec!(110), dec!(115), dec!(95), dec!(98)),
8274 ];
8275 assert_eq!(OhlcvBar::peak_close(&bars).unwrap(), dec!(115));
8276 assert_eq!(OhlcvBar::trough_close(&bars).unwrap(), dec!(98));
8277 }
8278
8279 #[test]
8284 fn test_close_to_range_position_none_for_empty() {
8285 assert!(OhlcvBar::close_to_range_position(&[]).is_none());
8286 }
8287
8288 #[test]
8289 fn test_close_to_range_position_one_when_close_at_high() {
8290 let bar = make_ohlcv_bar(dec!(100), dec!(120), dec!(80), dec!(120));
8291 let r = OhlcvBar::close_to_range_position(&[bar]).unwrap();
8292 assert!((r - 1.0).abs() < 1e-9, "close at high → position=1, got {}", r);
8293 }
8294
8295 #[test]
8296 fn test_close_to_range_position_zero_when_close_at_low() {
8297 let bar = make_ohlcv_bar(dec!(100), dec!(120), dec!(80), dec!(80));
8298 let r = OhlcvBar::close_to_range_position(&[bar]).unwrap();
8299 assert!(r.abs() < 1e-9, "close at low → position=0, got {}", r);
8300 }
8301
8302 #[test]
8305 fn test_volume_oscillator_none_for_insufficient_bars() {
8306 let bars = vec![make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105))];
8307 assert!(OhlcvBar::volume_oscillator(&bars, 1, 3).is_none());
8308 }
8309
8310 #[test]
8311 fn test_volume_oscillator_none_when_short_ge_long() {
8312 let bars: Vec<_> = (0..5)
8313 .map(|_| make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)))
8314 .collect();
8315 assert!(OhlcvBar::volume_oscillator(&bars, 3, 2).is_none());
8316 }
8317
8318 #[test]
8319 fn test_volume_oscillator_zero_for_constant_volume() {
8320 let bars: Vec<_> = (0..5)
8321 .map(|_| make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)))
8322 .collect();
8323 let v = OhlcvBar::volume_oscillator(&bars, 2, 4).unwrap();
8324 assert!(v.abs() < 1e-9, "constant volume → oscillator=0, got {}", v);
8325 }
8326
8327 #[test]
8330 fn test_direction_reversal_count_zero_for_single_bar() {
8331 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8332 assert_eq!(OhlcvBar::direction_reversal_count(&[bar]), 0);
8333 }
8334
8335 #[test]
8336 fn test_direction_reversal_count_zero_for_all_bullish() {
8337 let bars = vec![
8338 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)),
8339 make_ohlcv_bar(dec!(105), dec!(115), dec!(95), dec!(112)),
8340 ];
8341 assert_eq!(OhlcvBar::direction_reversal_count(&bars), 0);
8342 }
8343
8344 #[test]
8345 fn test_direction_reversal_count_two_for_alternating() {
8346 let bull = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(108));
8347 let bear = make_ohlcv_bar(dec!(108), dec!(112), dec!(95), dec!(102));
8348 let bull2 = make_ohlcv_bar(dec!(102), dec!(115), dec!(98), dec!(110));
8349 let bear2 = make_ohlcv_bar(dec!(110), dec!(115), dec!(100), dec!(104));
8350 assert_eq!(OhlcvBar::direction_reversal_count(&[bull, bear, bull2, bear2]), 3);
8351 }
8352
8353 #[test]
8356 fn test_upper_wick_dominance_fraction_none_for_empty() {
8357 assert!(OhlcvBar::upper_wick_dominance_fraction(&[]).is_none());
8358 }
8359
8360 #[test]
8361 fn test_upper_wick_dominance_fraction_one_when_all_upper() {
8362 let bar = make_ohlcv_bar(dec!(100), dec!(130), dec!(99), dec!(101));
8364 let r = OhlcvBar::upper_wick_dominance_fraction(&[bar]).unwrap();
8365 assert!((r - 1.0).abs() < 1e-9, "all upper dominant → 1.0, got {}", r);
8366 }
8367
8368 #[test]
8371 fn test_avg_open_to_high_ratio_none_for_empty() {
8372 assert!(OhlcvBar::avg_open_to_high_ratio(&[]).is_none());
8373 }
8374
8375 #[test]
8376 fn test_avg_open_to_high_ratio_one_when_open_at_low() {
8377 let bar = make_ohlcv_bar(dec!(80), dec!(120), dec!(80), dec!(100));
8379 let r = OhlcvBar::avg_open_to_high_ratio(&[bar]).unwrap();
8380 assert!((r - 1.0).abs() < 1e-9, "open at low → ratio=1, got {}", r);
8381 }
8382
8383 #[test]
8386 fn test_volume_weighted_range_none_for_empty() {
8387 assert!(OhlcvBar::volume_weighted_range(&[]).is_none());
8388 }
8389
8390 #[test]
8391 fn test_volume_weighted_range_positive() {
8392 let b1 = make_ohlcv_bar(dec!(100), dec!(120), dec!(80), dec!(110));
8393 let b2 = make_ohlcv_bar(dec!(110), dec!(130), dec!(100), dec!(120));
8394 let r = OhlcvBar::volume_weighted_range(&[b1, b2]).unwrap();
8395 assert!(r > 0.0, "should be positive, got {}", r);
8396 }
8397
8398 #[test]
8401 fn test_bar_strength_index_none_for_empty() {
8402 assert!(OhlcvBar::bar_strength_index(&[]).is_none());
8403 }
8404
8405 #[test]
8406 fn test_bar_strength_index_positive_when_closes_near_high() {
8407 let bar = make_ohlcv_bar(dec!(100), dec!(120), dec!(80), dec!(120));
8408 let s = OhlcvBar::bar_strength_index(&[bar]).unwrap();
8409 assert!(s > 0.0, "close at high → positive strength, got {}", s);
8410 }
8411
8412 #[test]
8415 fn test_shadow_to_body_ratio_none_for_empty() {
8416 assert!(OhlcvBar::shadow_to_body_ratio(&[]).is_none());
8417 }
8418
8419 #[test]
8420 fn test_shadow_to_body_ratio_zero_for_marubozu() {
8421 let bar = make_ohlcv_bar(dec!(80), dec!(120), dec!(80), dec!(120));
8423 let r = OhlcvBar::shadow_to_body_ratio(&[bar]).unwrap();
8424 assert!(r.abs() < 1e-9, "marubozu → ratio=0, got {}", r);
8425 }
8426
8427 #[test]
8430 fn test_first_last_close_pct_none_for_empty() {
8431 assert!(OhlcvBar::first_last_close_pct(&[]).is_none());
8432 }
8433
8434 #[test]
8435 fn test_first_last_close_pct_zero_for_same_close() {
8436 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
8437 let r = OhlcvBar::first_last_close_pct(&[bar]).unwrap();
8438 assert!(r.abs() < 1e-9, "same open/close → pct=0, got {}", r);
8439 }
8440
8441 #[test]
8442 fn test_first_last_close_pct_positive_for_rise() {
8443 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
8444 let b2 = make_ohlcv_bar(dec!(100), dec!(120), dec!(95), dec!(110));
8445 let r = OhlcvBar::first_last_close_pct(&[b1, b2]).unwrap();
8446 assert!(r > 0.0, "price rose → positive pct, got {}", r);
8447 }
8448
8449 #[test]
8452 fn test_open_to_close_volatility_none_for_single_bar() {
8453 let bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8454 assert!(OhlcvBar::open_to_close_volatility(&[bar]).is_none());
8455 }
8456
8457 #[test]
8458 fn test_open_to_close_volatility_zero_for_identical_bars() {
8459 let bars = vec![
8460 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)),
8461 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)),
8462 make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)),
8463 ];
8464 let v = OhlcvBar::open_to_close_volatility(&bars).unwrap();
8465 assert!(v.abs() < 1e-9, "identical bars → volatility=0, got {}", v);
8466 }
8467
8468 #[test]
8471 fn test_close_recovery_ratio_none_for_empty() {
8472 assert!(OhlcvBar::close_recovery_ratio(&[]).is_none());
8473 }
8474
8475 #[test]
8476 fn test_close_recovery_ratio_one_for_close_at_high() {
8477 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(110));
8479 let r = OhlcvBar::close_recovery_ratio(&[b]).unwrap();
8480 assert!((r - 1.0).abs() < 1e-9, "close at high → ratio=1, got {}", r);
8481 }
8482
8483 #[test]
8484 fn test_median_range_none_for_empty() {
8485 assert!(OhlcvBar::median_range(&[]).is_none());
8486 }
8487
8488 #[test]
8489 fn test_median_range_correct_odd() {
8490 let bars = vec![
8491 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)), ];
8495 assert_eq!(OhlcvBar::median_range(&bars).unwrap(), dec!(25));
8496 }
8497
8498 #[test]
8499 fn test_mean_typical_price_none_for_empty() {
8500 assert!(OhlcvBar::mean_typical_price(&[]).is_none());
8501 }
8502
8503 #[test]
8504 fn test_mean_typical_price_correct() {
8505 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8507 let expected = b.typical_price();
8508 let b2 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8509 let tp = OhlcvBar::mean_typical_price(&[b2]).unwrap();
8510 assert_eq!(tp, expected);
8511 }
8512
8513 #[test]
8514 fn test_directional_volume_ratio_none_for_empty() {
8515 assert!(OhlcvBar::directional_volume_ratio(&[]).is_none());
8516 }
8517
8518 #[test]
8519 fn test_directional_volume_ratio_one_for_all_bullish() {
8520 let mut b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(108)); b1.volume = dec!(50);
8521 let mut b2 = make_ohlcv_bar(dec!(105), dec!(115), dec!(100), dec!(112)); b2.volume = dec!(50);
8522 let r = OhlcvBar::directional_volume_ratio(&[b1, b2]).unwrap();
8523 assert!((r - 1.0).abs() < 1e-9, "all bullish → ratio=1, got {}", r);
8524 }
8525
8526 #[test]
8527 fn test_inside_bar_fraction_none_for_single_bar() {
8528 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8529 assert!(OhlcvBar::inside_bar_fraction(&[b]).is_none());
8530 }
8531
8532 #[test]
8533 fn test_body_momentum_empty_is_zero() {
8534 assert_eq!(OhlcvBar::body_momentum(&[]), Decimal::ZERO);
8535 }
8536
8537 #[test]
8538 fn test_body_momentum_bullish_positive() {
8539 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(108));
8540 let m = OhlcvBar::body_momentum(&[b]);
8541 assert!(m > Decimal::ZERO, "bullish bar → positive body momentum");
8542 }
8543
8544 #[test]
8545 fn test_avg_trade_count_none_for_empty() {
8546 assert!(OhlcvBar::avg_trade_count(&[]).is_none());
8547 }
8548
8549 #[test]
8550 fn test_max_trade_count_none_for_empty() {
8551 assert!(OhlcvBar::max_trade_count(&[]).is_none());
8552 }
8553
8554 #[test]
8555 fn test_max_trade_count_returns_max() {
8556 let mut b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)); b1.trade_count = 5;
8557 let mut b2 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)); b2.trade_count = 10;
8558 assert_eq!(OhlcvBar::max_trade_count(&[b1, b2]).unwrap(), 10);
8559 }
8560
8561 #[test]
8564 fn test_close_to_high_std_none_for_single_bar() {
8565 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8566 assert!(OhlcvBar::close_to_high_std(&[b]).is_none());
8567 }
8568
8569 #[test]
8570 fn test_close_to_high_std_zero_for_identical_bars() {
8571 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8572 let b2 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8573 let sd = OhlcvBar::close_to_high_std(&[b1, b2]).unwrap();
8574 assert!(sd.abs() < 1e-9, "identical bars → std=0, got {}", sd);
8575 }
8576
8577 #[test]
8578 fn test_avg_open_volume_ratio_none_for_empty() {
8579 assert!(OhlcvBar::avg_open_volume_ratio(&[]).is_none());
8580 }
8581
8582 #[test]
8583 fn test_typical_price_std_none_for_single_bar() {
8584 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8585 assert!(OhlcvBar::typical_price_std(&[b]).is_none());
8586 }
8587
8588 #[test]
8589 fn test_typical_price_std_zero_for_identical_bars() {
8590 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8591 let b2 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8592 let sd = OhlcvBar::typical_price_std(&[b1, b2]).unwrap();
8593 assert!(sd.abs() < 1e-9, "identical bars → std=0, got {}", sd);
8594 }
8595
8596 #[test]
8597 fn test_vwap_deviation_avg_none_for_empty() {
8598 assert!(OhlcvBar::vwap_deviation_avg(&[]).is_none());
8599 }
8600
8601 #[test]
8602 fn test_avg_high_low_ratio_none_for_empty() {
8603 assert!(OhlcvBar::avg_high_low_ratio(&[]).is_none());
8604 }
8605
8606 #[test]
8607 fn test_avg_high_low_ratio_one_for_doji() {
8608 let b = make_ohlcv_bar(dec!(100), dec!(100), dec!(100), dec!(100));
8610 let r = OhlcvBar::avg_high_low_ratio(&[b]).unwrap();
8611 assert!((r - 1.0).abs() < 1e-9, "high==low → ratio=1, got {}", r);
8612 }
8613
8614 #[test]
8615 fn test_gap_fill_fraction_none_for_empty() {
8616 assert!(OhlcvBar::gap_fill_fraction(&[]).is_none());
8617 }
8618
8619 #[test]
8620 fn test_gap_fill_fraction_zero_for_no_gaps() {
8621 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8622 let f = OhlcvBar::gap_fill_fraction(&[b]).unwrap();
8623 assert!(f.abs() < 1e-9, "no gap fills → fraction=0, got {}", f);
8624 }
8625
8626 #[test]
8627 fn test_complete_bar_count_zero_for_incomplete() {
8628 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8629 assert_eq!(OhlcvBar::complete_bar_count(&[b]), 0);
8630 }
8631
8632 #[test]
8633 fn test_min_trade_count_none_for_empty() {
8634 assert!(OhlcvBar::min_trade_count(&[]).is_none());
8635 }
8636
8637 #[test]
8638 fn test_min_trade_count_returns_min() {
8639 let mut b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)); b1.trade_count = 5;
8640 let mut b2 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)); b2.trade_count = 2;
8641 assert_eq!(OhlcvBar::min_trade_count(&[b1, b2]).unwrap(), 2);
8642 }
8643
8644 #[test]
8647 fn test_avg_bar_range_none_for_empty() {
8648 assert!(OhlcvBar::avg_bar_range(&[]).is_none());
8649 }
8650
8651 #[test]
8652 fn test_avg_bar_range_correct_value() {
8653 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();
8656 assert_eq!(r, dec!(20));
8657 }
8658
8659 #[test]
8660 fn test_max_up_move_none_for_empty() {
8661 assert!(OhlcvBar::max_up_move(&[]).is_none());
8662 }
8663
8664 #[test]
8665 fn test_max_up_move_largest_bullish_body() {
8666 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));
8669 }
8670
8671 #[test]
8672 fn test_max_down_move_none_for_empty() {
8673 assert!(OhlcvBar::max_down_move(&[]).is_none());
8674 }
8675
8676 #[test]
8677 fn test_max_down_move_largest_bearish_body() {
8678 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));
8681 }
8682
8683 #[test]
8684 fn test_avg_close_position_none_for_doji_only() {
8685 let b = make_ohlcv_bar(dec!(100), dec!(100), dec!(100), dec!(100)); assert!(OhlcvBar::avg_close_position(&[b]).is_none());
8687 }
8688
8689 #[test]
8690 fn test_avg_close_position_one_for_close_at_high() {
8691 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(100), dec!(110));
8692 let pos = OhlcvBar::avg_close_position(&[b]).unwrap();
8693 assert!((pos - 1.0).abs() < 1e-9, "close at high → position=1, got {}", pos);
8694 }
8695
8696 #[test]
8697 fn test_volume_std_none_for_single_bar() {
8698 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8699 assert!(OhlcvBar::volume_std(&[b]).is_none());
8700 }
8701
8702 #[test]
8703 fn test_volume_std_zero_for_equal_volumes() {
8704 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8705 let b2 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8706 let s = OhlcvBar::volume_std(&[b1, b2]).unwrap();
8707 assert!(s.abs() < 1e-9, "equal volumes → std=0, got {}", s);
8708 }
8709
8710 #[test]
8711 fn test_avg_wick_ratio_none_for_doji_only() {
8712 let b = make_ohlcv_bar(dec!(100), dec!(100), dec!(100), dec!(100));
8713 assert!(OhlcvBar::avg_wick_ratio(&[b]).is_none());
8714 }
8715
8716 #[test]
8717 fn test_avg_wick_ratio_in_range() {
8718 let b = make_ohlcv_bar(dec!(100), dec!(115), dec!(85), dec!(105));
8719 let r = OhlcvBar::avg_wick_ratio(&[b]).unwrap();
8720 assert!(r >= 0.0 && r <= 1.0, "wick ratio should be in [0,1], got {}", r);
8721 }
8722
8723 #[test]
8724 fn test_open_gap_mean_none_for_single_bar() {
8725 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8726 assert!(OhlcvBar::open_gap_mean(&[b]).is_none());
8727 }
8728
8729 #[test]
8730 fn test_open_gap_mean_zero_for_no_gap() {
8731 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8732 let mut b2 = make_ohlcv_bar(dec!(105), dec!(115), dec!(95), dec!(110));
8733 b2.open = dec!(105); let g = OhlcvBar::open_gap_mean(&[b1, b2]).unwrap();
8735 assert!(g.abs() < 1e-9, "no gap → mean=0, got {}", g);
8736 }
8737
8738 #[test]
8739 fn test_net_directional_move_none_for_empty() {
8740 assert!(OhlcvBar::net_directional_move(&[]).is_none());
8741 }
8742
8743 #[test]
8744 fn test_net_directional_move_positive_for_rising_close() {
8745 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8746 let b2 = make_ohlcv_bar(dec!(105), dec!(120), dec!(100), dec!(115));
8747 let m = OhlcvBar::net_directional_move(&[b1, b2]).unwrap();
8748 assert!(m > 0.0, "rising bar sequence → positive move, got {}", m);
8749 }
8750
8751 #[test]
8754 fn test_close_above_median_fraction_none_for_empty() {
8755 assert!(OhlcvBar::close_above_median_fraction(&[]).is_none());
8756 }
8757
8758 #[test]
8759 fn test_close_above_median_fraction_half_for_symmetric() {
8760 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(95));
8761 let b2 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8762 let b3 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(95));
8763 let b4 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(108));
8764 let f = OhlcvBar::close_above_median_fraction(&[b1, b2, b3, b4]).unwrap();
8765 assert!(f >= 0.0 && f <= 1.0, "fraction in [0,1], got {}", f);
8766 }
8767
8768 #[test]
8769 fn test_avg_range_to_open_none_for_empty() {
8770 assert!(OhlcvBar::avg_range_to_open(&[]).is_none());
8771 }
8772
8773 #[test]
8774 fn test_close_sum_zero_for_empty() {
8775 assert_eq!(OhlcvBar::close_sum(&[]), dec!(0));
8776 }
8777
8778 #[test]
8779 fn test_close_sum_sums_all_closes() {
8780 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8781 let b2 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(107));
8782 assert_eq!(OhlcvBar::close_sum(&[b1, b2]), dec!(212));
8783 }
8784
8785 #[test]
8786 fn test_above_avg_volume_count_zero_for_empty() {
8787 assert_eq!(OhlcvBar::above_avg_volume_count(&[]), 0);
8788 }
8789
8790 #[test]
8791 fn test_median_close_none_for_empty() {
8792 assert!(OhlcvBar::median_close(&[]).is_none());
8793 }
8794
8795 #[test]
8796 fn test_median_close_correct_for_sorted() {
8797 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
8798 let b2 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8799 let b3 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(110));
8800 let m = OhlcvBar::median_close(&[b1, b2, b3]).unwrap();
8801 assert_eq!(m, dec!(105));
8802 }
8803
8804 #[test]
8805 fn test_flat_bar_fraction_none_for_empty() {
8806 assert!(OhlcvBar::flat_bar_fraction(&[]).is_none());
8807 }
8808
8809 #[test]
8810 fn test_avg_body_to_range_none_for_empty() {
8811 assert!(OhlcvBar::avg_body_to_range(&[]).is_none());
8812 }
8813
8814 #[test]
8815 fn test_avg_body_to_range_in_range() {
8816 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8817 let r = OhlcvBar::avg_body_to_range(&[b]).unwrap();
8818 assert!(r >= 0.0 && r <= 1.0, "body-to-range in [0,1], got {}", r);
8819 }
8820
8821 #[test]
8822 fn test_max_open_gap_none_for_single_bar() {
8823 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8824 assert!(OhlcvBar::max_open_gap(&[b]).is_none());
8825 }
8826
8827 #[test]
8828 fn test_volume_trend_slope_none_for_single_bar() {
8829 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8830 assert!(OhlcvBar::volume_trend_slope(&[b]).is_none());
8831 }
8832
8833 #[test]
8834 fn test_up_close_fraction_none_for_empty() {
8835 assert!(OhlcvBar::up_close_fraction(&[]).is_none());
8836 }
8837
8838 #[test]
8839 fn test_avg_upper_shadow_ratio_none_for_doji_only() {
8840 let b = make_ohlcv_bar(dec!(100), dec!(100), dec!(100), dec!(100));
8841 assert!(OhlcvBar::avg_upper_shadow_ratio(&[b]).is_none());
8842 }
8843
8844 #[test]
8847 fn test_avg_lower_shadow_ratio_none_for_empty() {
8848 assert!(OhlcvBar::avg_lower_shadow_ratio(&[]).is_none());
8849 }
8850
8851 #[test]
8852 fn test_avg_lower_shadow_ratio_in_range() {
8853 let b = make_ohlcv_bar(dec!(100), dec!(115), dec!(85), dec!(105));
8854 let r = OhlcvBar::avg_lower_shadow_ratio(&[b]).unwrap();
8855 assert!(r >= 0.0 && r <= 1.0, "lower shadow ratio in [0,1], got {}", r);
8856 }
8857
8858 #[test]
8859 fn test_close_to_open_range_ratio_none_for_doji_only() {
8860 let b = make_ohlcv_bar(dec!(100), dec!(100), dec!(100), dec!(100));
8861 assert!(OhlcvBar::close_to_open_range_ratio(&[b]).is_none());
8862 }
8863
8864 #[test]
8865 fn test_max_high_none_for_empty() {
8866 assert!(OhlcvBar::max_high(&[]).is_none());
8867 }
8868
8869 #[test]
8870 fn test_max_high_returns_maximum() {
8871 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8872 let b2 = make_ohlcv_bar(dec!(100), dec!(120), dec!(90), dec!(110));
8873 assert_eq!(OhlcvBar::max_high(&[b1, b2]).unwrap(), dec!(120));
8874 }
8875
8876 #[test]
8877 fn test_min_low_none_for_empty() {
8878 assert!(OhlcvBar::min_low(&[]).is_none());
8879 }
8880
8881 #[test]
8882 fn test_min_low_returns_minimum() {
8883 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(85), dec!(105));
8884 let b2 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8885 assert_eq!(OhlcvBar::min_low(&[b1, b2]).unwrap(), dec!(85));
8886 }
8887
8888 #[test]
8889 fn test_avg_bar_efficiency_none_for_doji_only() {
8890 let b = make_ohlcv_bar(dec!(100), dec!(100), dec!(100), dec!(100));
8891 assert!(OhlcvBar::avg_bar_efficiency(&[b]).is_none());
8892 }
8893
8894 #[test]
8895 fn test_avg_bar_efficiency_one_for_full_body_bar() {
8896 let b = make_ohlcv_bar(dec!(90), dec!(110), dec!(90), dec!(110));
8897 let e = OhlcvBar::avg_bar_efficiency(&[b]).unwrap();
8898 assert!((e - 1.0).abs() < 1e-9, "full body → efficiency=1, got {}", e);
8899 }
8900
8901 #[test]
8902 fn test_open_range_fraction_none_for_empty() {
8903 assert!(OhlcvBar::open_range_fraction(&[]).is_none());
8904 }
8905
8906 #[test]
8907 fn test_open_range_fraction_in_range() {
8908 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8909 let f = OhlcvBar::open_range_fraction(&[b]).unwrap();
8910 assert!(f >= 0.0 && f <= 1.0, "fraction in [0,1], got {}", f);
8911 }
8912
8913 #[test]
8914 fn test_close_skewness_none_for_two_bars() {
8915 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8916 let b2 = make_ohlcv_bar(dec!(105), dec!(115), dec!(95), dec!(110));
8917 assert!(OhlcvBar::close_skewness(&[b1, b2]).is_none());
8918 }
8919
8920 #[test]
8921 fn test_close_skewness_returns_value_for_three_bars() {
8922 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
8923 let b2 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8924 let b3 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(200));
8925 let s = OhlcvBar::close_skewness(&[b1, b2, b3]);
8926 assert!(s.is_some(), "skewness should be computed for 3 bars");
8927 }
8928
8929 #[test]
8930 fn test_volume_above_median_fraction_none_for_empty() {
8931 assert!(OhlcvBar::volume_above_median_fraction(&[]).is_none());
8932 }
8933
8934 #[test]
8935 fn test_volume_above_median_fraction_in_range() {
8936 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8937 let b2 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
8938 let f = OhlcvBar::volume_above_median_fraction(&[b1, b2]).unwrap();
8939 assert!(f >= 0.0 && f <= 1.0, "fraction in [0,1], got {}", f);
8940 }
8941
8942 #[test]
8943 fn test_typical_price_sum_zero_for_empty() {
8944 assert_eq!(OhlcvBar::typical_price_sum(&[]), dec!(0));
8945 }
8946
8947 #[test]
8948 fn test_typical_price_sum_correct_value() {
8949 let b = make_ohlcv_bar(dec!(100), dec!(120), dec!(80), dec!(100));
8950 assert_eq!(OhlcvBar::typical_price_sum(&[b]), dec!(100));
8952 }
8953
8954 #[test]
8955 fn test_max_body_size_none_for_empty() {
8956 assert!(OhlcvBar::max_body_size(&[]).is_none());
8957 }
8958
8959 #[test]
8960 fn test_max_body_size_correct_value() {
8961 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(108));
8962 let b2 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(103));
8963 assert_eq!(OhlcvBar::max_body_size(&[b1, b2]).unwrap(), dec!(8));
8964 }
8965
8966 #[test]
8967 fn test_min_body_size_none_for_empty() {
8968 assert!(OhlcvBar::min_body_size(&[]).is_none());
8969 }
8970
8971 #[test]
8972 fn test_avg_lower_wick_to_range_none_for_empty() {
8973 assert!(OhlcvBar::avg_lower_wick_to_range(&[]).is_none());
8974 }
8975
8976 #[test]
8977 fn test_avg_lower_wick_to_range_zero_for_open_at_low() {
8978 let b = make_ohlcv_bar(dec!(90), dec!(110), dec!(90), dec!(105));
8980 let r = OhlcvBar::avg_lower_wick_to_range(&[b]).unwrap();
8981 assert!(r.abs() < 1e-9, "open=low → lower wick=0, got {}", r);
8982 }
8983
8984 #[test]
8987 fn test_total_range_zero_for_empty() {
8988 assert_eq!(OhlcvBar::total_range(&[]), dec!(0));
8989 }
8990
8991 #[test]
8992 fn test_total_range_sum_of_ranges() {
8993 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));
8996 }
8997
8998 #[test]
8999 fn test_close_at_high_fraction_none_for_empty() {
9000 assert!(OhlcvBar::close_at_high_fraction(&[]).is_none());
9001 }
9002
9003 #[test]
9004 fn test_close_at_high_fraction_one_when_all_close_at_high() {
9005 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(110));
9006 let f = OhlcvBar::close_at_high_fraction(&[b]).unwrap();
9007 assert!((f - 1.0).abs() < 1e-9, "close=high → fraction=1, got {}", f);
9008 }
9009
9010 #[test]
9011 fn test_close_at_low_fraction_none_for_empty() {
9012 assert!(OhlcvBar::close_at_low_fraction(&[]).is_none());
9013 }
9014
9015 #[test]
9016 fn test_close_at_low_fraction_one_when_all_close_at_low() {
9017 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(90));
9018 let f = OhlcvBar::close_at_low_fraction(&[b]).unwrap();
9019 assert!((f - 1.0).abs() < 1e-9, "close=low → fraction=1, got {}", f);
9020 }
9021
9022 #[test]
9023 fn test_avg_high_above_open_ratio_none_for_empty() {
9024 assert!(OhlcvBar::avg_high_above_open_ratio(&[]).is_none());
9025 }
9026
9027 #[test]
9028 fn test_avg_high_above_open_ratio_in_range() {
9029 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
9030 let r = OhlcvBar::avg_high_above_open_ratio(&[b]).unwrap();
9031 assert!(r >= 0.0 && r <= 1.0, "ratio in [0,1], got {}", r);
9032 }
9033
9034 #[test]
9035 fn test_continuation_bar_count_zero_for_single() {
9036 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
9037 assert_eq!(OhlcvBar::continuation_bar_count(&[b]), 0);
9038 }
9039
9040 #[test]
9041 fn test_down_close_volume_zero_for_all_up_close() {
9042 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)); assert_eq!(OhlcvBar::down_close_volume(&[b]), dec!(0));
9044 }
9045
9046 #[test]
9047 fn test_up_close_volume_zero_for_all_down_close() {
9048 let b = make_ohlcv_bar(dec!(105), dec!(110), dec!(90), dec!(100)); assert_eq!(OhlcvBar::up_close_volume(&[b]), dec!(0));
9050 }
9051
9052 #[test]
9055 fn test_mean_open_none_for_empty() {
9056 assert!(OhlcvBar::mean_open(&[]).is_none());
9057 }
9058
9059 #[test]
9060 fn test_mean_open_correct() {
9061 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
9062 let b2 = make_ohlcv_bar(dec!(200), dec!(210), dec!(190), dec!(205));
9063 assert_eq!(OhlcvBar::mean_open(&[b1, b2]).unwrap(), dec!(150));
9064 }
9065
9066 #[test]
9067 fn test_new_high_count_zero_for_single() {
9068 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
9069 assert_eq!(OhlcvBar::new_high_count(&[b]), 0);
9070 }
9071
9072 #[test]
9073 fn test_new_high_count_correct() {
9074 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
9075 let b2 = make_ohlcv_bar(dec!(100), dec!(120), dec!(90), dec!(115));
9076 let b3 = make_ohlcv_bar(dec!(100), dec!(115), dec!(90), dec!(110));
9077 assert_eq!(OhlcvBar::new_high_count(&[b1, b2, b3]), 1);
9078 }
9079
9080 #[test]
9081 fn test_new_low_count_zero_for_single() {
9082 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
9083 assert_eq!(OhlcvBar::new_low_count(&[b]), 0);
9084 }
9085
9086 #[test]
9087 fn test_close_std_none_for_single() {
9088 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
9089 assert!(OhlcvBar::close_std(&[b]).is_none());
9090 }
9091
9092 #[test]
9093 fn test_close_std_zero_for_constant_close() {
9094 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
9095 let b2 = make_ohlcv_bar(dec!(101), dec!(111), dec!(91), dec!(105));
9096 let s = OhlcvBar::close_std(&[b1, b2]).unwrap();
9097 assert!(s.abs() < 1e-9, "constant close → std=0, got {}", s);
9098 }
9099
9100 #[test]
9101 fn test_zero_volume_fraction_none_for_empty() {
9102 assert!(OhlcvBar::zero_volume_fraction(&[]).is_none());
9103 }
9104
9105 #[test]
9106 fn test_zero_volume_fraction_zero_when_no_zero_volume() {
9107 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
9108 let f = OhlcvBar::zero_volume_fraction(&[b]).unwrap();
9109 assert!(f.abs() < 1e-9, "bar has volume → zero_vol_fraction=0, got {}", f);
9110 }
9111
9112 #[test]
9115 fn test_avg_open_to_close_none_for_empty() {
9116 assert!(OhlcvBar::avg_open_to_close(&[]).is_none());
9117 }
9118
9119 #[test]
9120 fn test_avg_open_to_close_positive_when_all_bullish() {
9121 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105)); let r = OhlcvBar::avg_open_to_close(&[b]).unwrap();
9123 assert!(r > dec!(0), "bullish bar → avg_open_to_close > 0, got {}", r);
9124 }
9125
9126 #[test]
9127 fn test_avg_open_to_close_zero_for_doji() {
9128 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100)); let r = OhlcvBar::avg_open_to_close(&[b]).unwrap();
9130 assert_eq!(r, dec!(0));
9131 }
9132
9133 #[test]
9134 fn test_max_bar_volume_none_for_empty() {
9135 assert!(OhlcvBar::max_bar_volume(&[]).is_none());
9136 }
9137
9138 #[test]
9139 fn test_max_bar_volume_selects_largest() {
9140 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
9141 let b2 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
9142 let vol = OhlcvBar::max_bar_volume(&[b1, b2]).unwrap();
9145 assert!(vol > dec!(0));
9146 }
9147
9148 #[test]
9149 fn test_min_bar_volume_none_for_empty() {
9150 assert!(OhlcvBar::min_bar_volume(&[]).is_none());
9151 }
9152
9153 #[test]
9154 fn test_body_to_range_std_none_for_single() {
9155 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
9156 assert!(OhlcvBar::body_to_range_std(&[b]).is_none());
9157 }
9158
9159 #[test]
9160 fn test_body_to_range_std_nonneg_for_varied_bars() {
9161 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
9162 let b2 = make_ohlcv_bar(dec!(100), dec!(120), dec!(80), dec!(100));
9163 let s = OhlcvBar::body_to_range_std(&[b1, b2]).unwrap();
9164 assert!(s >= 0.0, "std dev should be non-negative, got {}", s);
9165 }
9166
9167 #[test]
9168 fn test_avg_wick_symmetry_none_for_empty() {
9169 assert!(OhlcvBar::avg_wick_symmetry(&[]).is_none());
9170 }
9171
9172 #[test]
9173 fn test_avg_wick_symmetry_in_range() {
9174 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
9175 let s = OhlcvBar::avg_wick_symmetry(&[b]).unwrap();
9177 assert!(s >= 0.0 && s <= 1.0, "symmetry in [0,1], got {}", s);
9178 }
9179
9180 #[test]
9183 fn test_avg_range_pct_of_open_none_for_empty() {
9184 assert!(OhlcvBar::avg_range_pct_of_open(&[]).is_none());
9185 }
9186
9187 #[test]
9188 fn test_avg_range_pct_of_open_correct() {
9189 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
9191 let r = OhlcvBar::avg_range_pct_of_open(&[b]).unwrap();
9192 assert!((r - 0.2).abs() < 1e-9, "range/open = 0.2, got {}", r);
9193 }
9194
9195 #[test]
9196 fn test_high_volume_fraction_none_for_empty() {
9197 assert!(OhlcvBar::high_volume_fraction(&[]).is_none());
9198 }
9199
9200 #[test]
9201 fn test_close_cluster_count_zero_for_single_bar() {
9202 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
9203 assert_eq!(OhlcvBar::close_cluster_count(&[b]), 0);
9204 }
9205
9206 #[test]
9207 fn test_mean_vwap_none_for_bars_without_vwap() {
9208 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
9209 assert!(OhlcvBar::mean_vwap(&[b]).is_none());
9210 }
9211
9212 #[test]
9213 fn test_complete_fraction_none_for_empty() {
9214 assert!(OhlcvBar::complete_fraction(&[]).is_none());
9215 }
9216
9217 #[test]
9218 fn test_complete_fraction_zero_when_none_complete() {
9219 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
9220 let f = OhlcvBar::complete_fraction(&[b]).unwrap();
9222 assert!(f.abs() < 1e-9, "no complete bars → fraction=0, got {}", f);
9223 }
9224
9225 #[test]
9226 fn test_total_body_movement_zero_for_empty() {
9227 assert_eq!(OhlcvBar::total_body_movement(&[]), rust_decimal::Decimal::ZERO);
9228 }
9229
9230 #[test]
9231 fn test_total_body_movement_correct() {
9232 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));
9235 }
9236
9237 #[test]
9238 fn test_open_std_none_for_single() {
9239 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
9240 assert!(OhlcvBar::open_std(&[b]).is_none());
9241 }
9242
9243 #[test]
9244 fn test_open_std_zero_for_constant_open() {
9245 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
9246 let b2 = make_ohlcv_bar(dec!(100), dec!(120), dec!(80), dec!(110));
9247 let s = OhlcvBar::open_std(&[b1, b2]).unwrap();
9248 assert!(s.abs() < 1e-9, "constant open → std=0, got {}", s);
9249 }
9250
9251 #[test]
9252 fn test_mean_high_low_ratio_none_for_empty() {
9253 assert!(OhlcvBar::mean_high_low_ratio(&[]).is_none());
9254 }
9255
9256 #[test]
9257 fn test_mean_high_low_ratio_above_one() {
9258 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
9259 let r = OhlcvBar::mean_high_low_ratio(&[b]).unwrap();
9260 assert!(r > 1.0, "high > low → ratio > 1, got {}", r);
9261 }
9262
9263 #[test]
9266 fn test_max_consecutive_up_bars_zero_for_empty() {
9267 assert_eq!(OhlcvBar::max_consecutive_up_bars(&[]), 0);
9268 }
9269
9270 #[test]
9271 fn test_max_consecutive_up_bars_zero_for_all_down() {
9272 let b = make_ohlcv_bar(dec!(105), dec!(110), dec!(90), dec!(100)); assert_eq!(OhlcvBar::max_consecutive_up_bars(&[b]), 0);
9274 }
9275
9276 #[test]
9277 fn test_max_consecutive_up_bars_correct_run() {
9278 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);
9283 }
9284
9285 #[test]
9286 fn test_avg_upper_shadow_fraction_none_for_empty() {
9287 assert!(OhlcvBar::avg_upper_shadow_fraction(&[]).is_none());
9288 }
9289
9290 #[test]
9291 fn test_avg_upper_shadow_fraction_in_range() {
9292 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100)); let f = OhlcvBar::avg_upper_shadow_fraction(&[b]).unwrap();
9294 assert!(f >= 0.0 && f <= 1.0, "fraction in [0,1], got {}", f);
9295 }
9296
9297 #[test]
9298 fn test_up_down_bar_ratio_none_for_no_down_bars() {
9299 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
9301 assert!(OhlcvBar::up_down_bar_ratio(&[b]).is_none());
9302 }
9303
9304 #[test]
9305 fn test_up_down_bar_ratio_one_for_balanced() {
9306 let up_bar = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
9307 let dn_bar = make_ohlcv_bar(dec!(105), dec!(110), dec!(90), dec!(100));
9308 let r = OhlcvBar::up_down_bar_ratio(&[up_bar, dn_bar]).unwrap();
9309 assert!((r - 1.0).abs() < 1e-9, "1 up / 1 down → 1.0, got {}", r);
9310 }
9311
9312 #[test]
9315 fn test_close_range_fraction_none_for_empty() {
9316 assert!(OhlcvBar::close_range_fraction(&[]).is_none());
9317 }
9318
9319 #[test]
9320 fn test_close_range_fraction_one_for_close_at_high() {
9321 let b = make_ohlcv_bar(dec!(90), dec!(110), dec!(90), dec!(110));
9323 let f = OhlcvBar::close_range_fraction(&[b]).unwrap();
9324 assert!((f - 1.0).abs() < 1e-9, "close=high → 1.0, got {}", f);
9325 }
9326
9327 #[test]
9328 fn test_close_range_fraction_zero_for_close_at_low() {
9329 let b = make_ohlcv_bar(dec!(110), dec!(120), dec!(90), dec!(90));
9331 let f = OhlcvBar::close_range_fraction(&[b]).unwrap();
9332 assert!((f - 0.0).abs() < 1e-9, "close=low → 0.0, got {}", f);
9333 }
9334
9335 #[test]
9336 fn test_tail_symmetry_none_for_empty() {
9337 assert!(OhlcvBar::tail_symmetry(&[]).is_none());
9338 }
9339
9340 #[test]
9341 fn test_tail_symmetry_one_for_symmetric_bar() {
9342 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100)); let s = OhlcvBar::tail_symmetry(&[b]).unwrap();
9345 assert!((s - 1.0).abs() < 1e-9, "symmetric bar → 1.0, got {}", s);
9346 }
9347
9348 #[test]
9349 fn test_bar_trend_strength_none_for_single_bar() {
9350 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
9351 assert!(OhlcvBar::bar_trend_strength(&[b]).is_none());
9352 }
9353
9354 #[test]
9355 fn test_bar_trend_strength_one_for_monotone_up() {
9356 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(100));
9357 let b2 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
9358 let b3 = make_ohlcv_bar(dec!(105), dec!(115), dec!(95), dec!(110));
9359 let s = OhlcvBar::bar_trend_strength(&[b1, b2, b3]).unwrap();
9361 assert!((s - 1.0).abs() < 1e-9, "monotone up → 1.0, got {}", s);
9362 }
9363
9364 #[test]
9367 fn test_gap_up_count_zero_for_single_bar() {
9368 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
9369 assert_eq!(OhlcvBar::gap_up_count(&[b]), 0);
9370 }
9371
9372 #[test]
9373 fn test_gap_up_count_one_for_gap() {
9374 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
9376 let b2 = make_ohlcv_bar(dec!(110), dec!(120), dec!(105), dec!(115));
9377 assert_eq!(OhlcvBar::gap_up_count(&[b1, b2]), 1);
9378 }
9379
9380 #[test]
9381 fn test_gap_down_count_zero_for_single_bar() {
9382 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
9383 assert_eq!(OhlcvBar::gap_down_count(&[b]), 0);
9384 }
9385
9386 #[test]
9387 fn test_gap_down_count_one_for_gap() {
9388 let b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
9390 let b2 = make_ohlcv_bar(dec!(95), dec!(100), dec!(85), dec!(90));
9391 assert_eq!(OhlcvBar::gap_down_count(&[b1, b2]), 1);
9392 }
9393
9394 #[test]
9395 fn test_mean_bar_efficiency_none_for_empty() {
9396 assert!(OhlcvBar::mean_bar_efficiency(&[]).is_none());
9397 }
9398
9399 #[test]
9400 fn test_mean_bar_efficiency_one_for_full_body() {
9401 let b = make_ohlcv_bar(dec!(90), dec!(110), dec!(90), dec!(110));
9403 let e = OhlcvBar::mean_bar_efficiency(&[b]).unwrap();
9404 assert!((e - 1.0).abs() < 1e-9, "full body → 1.0, got {}", e);
9405 }
9406
9407 #[test]
9408 fn test_volume_trend_slope_none_for_single_bar() {
9409 let b = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
9410 assert!(OhlcvBar::volume_trend_slope(&[b]).is_none());
9411 }
9412
9413 #[test]
9414 fn test_volume_trend_slope_positive_for_rising_volume() {
9415 let mut b1 = make_ohlcv_bar(dec!(100), dec!(110), dec!(90), dec!(105));
9417 b1.volume = dec!(100);
9418 let mut b2 = make_ohlcv_bar(dec!(105), dec!(115), dec!(95), dec!(110));
9419 b2.volume = dec!(200);
9420 let s = OhlcvBar::volume_trend_slope(&[b1, b2]).unwrap();
9421 assert!(s > 0.0, "rising volume → positive slope, got {}", s);
9422 }
9423}