1use super::types::TrendDirection;
14use std::collections::VecDeque;
15
16#[derive(Debug, Clone)]
25pub struct EMA {
26 period: usize,
27 multiplier: f64,
28 current_value: Option<f64>,
29 initialized: bool,
30 warmup_count: usize,
31}
32
33impl EMA {
34 pub fn new(period: usize) -> Self {
36 let multiplier = 2.0 / (period as f64 + 1.0);
37 Self {
38 period,
39 multiplier,
40 current_value: None,
41 initialized: false,
42 warmup_count: 0,
43 }
44 }
45
46 pub fn update(&mut self, price: f64) -> Option<f64> {
48 self.warmup_count += 1;
49
50 match self.current_value {
51 Some(prev_ema) => {
52 let new_ema = (price - prev_ema) * self.multiplier + prev_ema;
53 self.current_value = Some(new_ema);
54
55 if self.warmup_count >= self.period {
56 self.initialized = true;
57 }
58 }
59 None => {
60 self.current_value = Some(price);
61 }
62 }
63
64 if self.initialized {
65 self.current_value
66 } else {
67 None
68 }
69 }
70
71 pub fn value(&self) -> Option<f64> {
73 if self.initialized {
74 self.current_value
75 } else {
76 None
77 }
78 }
79
80 pub fn is_ready(&self) -> bool {
82 self.initialized
83 }
84
85 pub fn period(&self) -> usize {
87 self.period
88 }
89
90 pub fn reset(&mut self) {
92 self.current_value = None;
93 self.initialized = false;
94 self.warmup_count = 0;
95 }
96}
97
98#[derive(Debug, Clone)]
107pub struct ATR {
108 period: usize,
109 values: VecDeque<f64>,
110 prev_close: Option<f64>,
111 current_atr: Option<f64>,
112}
113
114impl ATR {
115 pub fn new(period: usize) -> Self {
117 Self {
118 period,
119 values: VecDeque::with_capacity(period),
120 prev_close: None,
121 current_atr: None,
122 }
123 }
124
125 pub fn update(&mut self, high: f64, low: f64, close: f64) -> Option<f64> {
127 let true_range = match self.prev_close {
128 Some(prev_c) => {
129 let hl = high - low;
130 let hc = (high - prev_c).abs();
131 let lc = (low - prev_c).abs();
132 hl.max(hc).max(lc)
133 }
134 None => high - low,
135 };
136
137 self.prev_close = Some(close);
138 self.values.push_back(true_range);
139
140 if self.values.len() > self.period {
141 self.values.pop_front();
142 }
143
144 if self.values.len() >= self.period {
145 if let Some(prev_atr) = self.current_atr {
147 let new_atr =
148 (prev_atr * (self.period - 1) as f64 + true_range) / self.period as f64;
149 self.current_atr = Some(new_atr);
150 } else {
151 let sum: f64 = self.values.iter().sum();
152 self.current_atr = Some(sum / self.period as f64);
153 }
154 }
155
156 self.current_atr
157 }
158
159 pub fn value(&self) -> Option<f64> {
161 self.current_atr
162 }
163
164 pub fn is_ready(&self) -> bool {
166 self.current_atr.is_some()
167 }
168
169 pub fn period(&self) -> usize {
171 self.period
172 }
173
174 pub fn reset(&mut self) {
176 self.values.clear();
177 self.prev_close = None;
178 self.current_atr = None;
179 }
180}
181
182#[derive(Debug, Clone)]
193pub struct ADX {
194 period: usize,
195 atr: ATR,
196 plus_dm_ema: EMA,
197 minus_dm_ema: EMA,
198 dx_values: VecDeque<f64>,
199 prev_high: Option<f64>,
200 prev_low: Option<f64>,
201 current_adx: Option<f64>,
202 plus_dir_index: Option<f64>,
203 minus_dir_index: Option<f64>,
204}
205
206impl ADX {
207 pub fn new(period: usize) -> Self {
209 Self {
210 period,
211 atr: ATR::new(period),
212 plus_dm_ema: EMA::new(period),
213 minus_dm_ema: EMA::new(period),
214 dx_values: VecDeque::with_capacity(period),
215 prev_high: None,
216 prev_low: None,
217 current_adx: None,
218 plus_dir_index: None,
219 minus_dir_index: None,
220 }
221 }
222
223 pub fn update(&mut self, high: f64, low: f64, close: f64) -> Option<f64> {
225 let (plus_dm, minus_dm) = match (self.prev_high, self.prev_low) {
227 (Some(prev_h), Some(prev_l)) => {
228 let up_move = high - prev_h;
229 let down_move = prev_l - low;
230
231 let plus = if up_move > down_move && up_move > 0.0 {
232 up_move
233 } else {
234 0.0
235 };
236
237 let minus = if down_move > up_move && down_move > 0.0 {
238 down_move
239 } else {
240 0.0
241 };
242
243 (plus, minus)
244 }
245 _ => (0.0, 0.0),
246 };
247
248 self.prev_high = Some(high);
249 self.prev_low = Some(low);
250
251 let atr = self.atr.update(high, low, close);
253
254 let smoothed_plus_dm = self.plus_dm_ema.update(plus_dm);
256 let smoothed_minus_dm = self.minus_dm_ema.update(minus_dm);
257
258 if let (Some(atr_val), Some(plus_dm_smooth), Some(minus_dm_smooth)) =
260 (atr, smoothed_plus_dm, smoothed_minus_dm)
261 && atr_val > 0.0
262 {
263 let plus_dir_index = (plus_dm_smooth / atr_val) * 100.0;
264 let minus_dir_index = (minus_dm_smooth / atr_val) * 100.0;
265 self.plus_dir_index = Some(plus_dir_index);
266 self.minus_dir_index = Some(minus_dir_index);
267
268 let di_sum = plus_dir_index + minus_dir_index;
270 if di_sum > 0.0 {
271 let di_diff = (plus_dir_index - minus_dir_index).abs();
272 let dx = (di_diff / di_sum) * 100.0;
273
274 self.dx_values.push_back(dx);
275 if self.dx_values.len() > self.period {
276 self.dx_values.pop_front();
277 }
278
279 if self.dx_values.len() >= self.period {
281 if let Some(prev_adx) = self.current_adx {
282 let new_adx =
283 (prev_adx * (self.period - 1) as f64 + dx) / self.period as f64;
284 self.current_adx = Some(new_adx);
285 } else {
286 let sum: f64 = self.dx_values.iter().sum();
287 self.current_adx = Some(sum / self.period as f64);
288 }
289 }
290 }
291 }
292
293 self.current_adx
294 }
295
296 pub fn value(&self) -> Option<f64> {
298 self.current_adx
299 }
300
301 pub fn plus_dir_index(&self) -> Option<f64> {
303 self.plus_dir_index
304 }
305
306 pub fn minus_dir_index(&self) -> Option<f64> {
308 self.minus_dir_index
309 }
310
311 pub fn trend_direction(&self) -> Option<TrendDirection> {
316 match (self.plus_dir_index, self.minus_dir_index) {
317 (Some(plus), Some(minus)) => {
318 if plus > minus {
319 Some(TrendDirection::Bullish)
320 } else {
321 Some(TrendDirection::Bearish)
322 }
323 }
324 _ => None,
325 }
326 }
327
328 pub fn is_ready(&self) -> bool {
330 self.current_adx.is_some()
331 }
332
333 pub fn period(&self) -> usize {
335 self.period
336 }
337
338 pub fn reset(&mut self) {
340 self.atr.reset();
341 self.plus_dm_ema.reset();
342 self.minus_dm_ema.reset();
343 self.dx_values.clear();
344 self.prev_high = None;
345 self.prev_low = None;
346 self.current_adx = None;
347 self.plus_dir_index = None;
348 self.minus_dir_index = None;
349 }
350}
351
352#[derive(Debug, Clone, Copy)]
358pub struct BollingerBandsValues {
359 pub upper: f64,
361 pub middle: f64,
363 pub lower: f64,
365 pub width: f64,
367 pub width_percentile: f64,
369 pub percent_b: f64,
371 pub std_dev: f64,
373}
374
375impl BollingerBandsValues {
376 pub fn is_overbought(&self) -> bool {
378 self.percent_b >= 0.95
379 }
380
381 pub fn is_oversold(&self) -> bool {
383 self.percent_b <= 0.05
384 }
385
386 pub fn is_high_volatility(&self, threshold_percentile: f64) -> bool {
388 self.width_percentile >= threshold_percentile
389 }
390
391 pub fn is_squeeze(&self, threshold_percentile: f64) -> bool {
393 self.width_percentile <= threshold_percentile
394 }
395}
396
397#[derive(Debug, Clone)]
402pub struct BollingerBands {
403 period: usize,
404 std_dev_multiplier: f64,
405 prices: VecDeque<f64>,
406 width_history: VecDeque<f64>,
407 width_history_size: usize,
408}
409
410impl BollingerBands {
411 pub fn new(period: usize, std_dev_multiplier: f64) -> Self {
417 Self {
418 period,
419 std_dev_multiplier,
420 prices: VecDeque::with_capacity(period),
421 width_history: VecDeque::with_capacity(100),
422 width_history_size: 100, }
424 }
425
426 pub fn update(&mut self, price: f64) -> Option<BollingerBandsValues> {
428 self.prices.push_back(price);
429 if self.prices.len() > self.period {
430 self.prices.pop_front();
431 }
432
433 if self.prices.len() < self.period {
434 return None;
435 }
436
437 let sum: f64 = self.prices.iter().sum();
439 let sma = sum / self.period as f64;
440
441 let variance: f64 =
443 self.prices.iter().map(|p| (p - sma).powi(2)).sum::<f64>() / self.period as f64;
444 let std_dev = variance.sqrt();
445
446 let upper = sma + (std_dev * self.std_dev_multiplier);
448 let lower = sma - (std_dev * self.std_dev_multiplier);
449 let width = if sma > 0.0 {
450 (upper - lower) / sma * 100.0 } else {
452 0.0
453 };
454
455 self.width_history.push_back(width);
457 if self.width_history.len() > self.width_history_size {
458 self.width_history.pop_front();
459 }
460
461 let width_percentile = self.calculate_width_percentile(width);
463
464 let percent_b = if upper - lower > 0.0 {
466 (price - lower) / (upper - lower)
467 } else {
468 0.5
469 };
470
471 Some(BollingerBandsValues {
472 upper,
473 middle: sma,
474 lower,
475 width,
476 width_percentile,
477 percent_b,
478 std_dev,
479 })
480 }
481
482 fn calculate_width_percentile(&self, current_width: f64) -> f64 {
484 if self.width_history.len() < 10 {
485 return 50.0; }
487
488 let count_below = self
489 .width_history
490 .iter()
491 .filter(|&&w| w < current_width)
492 .count();
493
494 (count_below as f64 / self.width_history.len() as f64) * 100.0
495 }
496
497 pub fn is_ready(&self) -> bool {
499 self.prices.len() >= self.period
500 }
501
502 pub fn period(&self) -> usize {
504 self.period
505 }
506
507 pub fn std_dev_multiplier(&self) -> f64 {
509 self.std_dev_multiplier
510 }
511
512 pub fn reset(&mut self) {
514 self.prices.clear();
515 self.width_history.clear();
516 }
517}
518
519#[derive(Debug, Clone)]
528pub struct RSI {
529 period: usize,
530 gains: EMA,
531 losses: EMA,
532 prev_close: Option<f64>,
533 last_rsi: Option<f64>,
534}
535
536impl RSI {
537 pub fn new(period: usize) -> Self {
539 Self {
540 period,
541 gains: EMA::new(period),
542 losses: EMA::new(period),
543 prev_close: None,
544 last_rsi: None,
545 }
546 }
547
548 pub fn update(&mut self, close: f64) -> Option<f64> {
550 if let Some(prev) = self.prev_close {
551 let change = close - prev;
552 let gain = if change > 0.0 { change } else { 0.0 };
553 let loss = if change < 0.0 { -change } else { 0.0 };
554
555 if let (Some(avg_gain), Some(avg_loss)) =
556 (self.gains.update(gain), self.losses.update(loss))
557 {
558 self.prev_close = Some(close);
559
560 let rsi = if avg_loss == 0.0 {
561 100.0
562 } else {
563 let rs = avg_gain / avg_loss;
564 100.0 - (100.0 / (1.0 + rs))
565 };
566 self.last_rsi = Some(rsi);
567 return self.last_rsi;
568 }
569 }
570
571 self.prev_close = Some(close);
572 None
573 }
574
575 pub fn value(&self) -> Option<f64> {
579 self.last_rsi
580 }
581
582 pub fn is_ready(&self) -> bool {
584 self.gains.is_ready() && self.losses.is_ready()
585 }
586
587 pub fn period(&self) -> usize {
589 self.period
590 }
591
592 pub fn reset(&mut self) {
594 self.gains.reset();
595 self.losses.reset();
596 self.prev_close = None;
597 self.last_rsi = None;
598 }
599}
600
601pub fn calculate_sma(prices: &[f64]) -> f64 {
607 if prices.is_empty() {
608 return 0.0;
609 }
610 prices.iter().sum::<f64>() / prices.len() as f64
611}
612
613#[cfg(test)]
618mod tests {
619 use super::*;
620
621 #[test]
624 fn test_ema_creation() {
625 let ema = EMA::new(10);
626 assert_eq!(ema.period(), 10);
627 assert!(!ema.is_ready());
628 assert!(ema.value().is_none());
629 }
630
631 #[test]
632 fn test_ema_warmup() {
633 let mut ema = EMA::new(10);
634
635 for i in 1..10 {
637 let result = ema.update(i as f64 * 10.0);
638 assert!(result.is_none(), "Should be None during warmup at step {i}");
639 }
640
641 let result = ema.update(100.0);
643 assert!(result.is_some(), "Should be ready after {0} updates", 10);
644 assert!(ema.is_ready());
645 }
646
647 #[test]
648 fn test_ema_calculation() {
649 let mut ema = EMA::new(10);
650
651 for i in 1..=10 {
653 ema.update(i as f64 * 10.0);
654 }
655
656 assert!(ema.is_ready());
657 let value = ema.value().unwrap();
658 assert!(value > 10.0 && value <= 100.0);
660 }
661
662 #[test]
663 fn test_ema_tracks_trend() {
664 let mut ema = EMA::new(5);
665
666 for _ in 0..5 {
668 ema.update(100.0);
669 }
670 let stable = ema.value().unwrap();
671
672 for _ in 0..10 {
674 ema.update(110.0);
675 }
676 let after_up = ema.value().unwrap();
677
678 assert!(after_up > stable, "EMA should increase with rising prices");
679 }
680
681 #[test]
682 fn test_ema_reset() {
683 let mut ema = EMA::new(5);
684 for _ in 0..10 {
685 ema.update(100.0);
686 }
687 assert!(ema.is_ready());
688
689 ema.reset();
690 assert!(!ema.is_ready());
691 assert!(ema.value().is_none());
692 }
693
694 #[test]
697 fn test_atr_creation() {
698 let atr = ATR::new(14);
699 assert_eq!(atr.period(), 14);
700 assert!(!atr.is_ready());
701 }
702
703 #[test]
704 fn test_atr_warmup() {
705 let mut atr = ATR::new(14);
706
707 for i in 1..=14 {
708 let base = 100.0 + i as f64;
709 let result = atr.update(base + 1.0, base - 1.0, base);
710 if i < 14 {
711 assert!(result.is_none());
712 }
713 }
714
715 assert!(atr.is_ready());
716 }
717
718 #[test]
719 fn test_atr_increases_with_volatility() {
720 let mut atr = ATR::new(14);
721
722 for i in 1..=14 {
724 let base = 100.0 + i as f64 * 0.1;
725 atr.update(base + 0.5, base - 0.5, base);
726 }
727 let low_vol_atr = atr.value().unwrap();
728
729 for i in 0..20 {
731 let base = 100.0 + if i % 2 == 0 { 5.0 } else { -5.0 };
732 atr.update(base + 3.0, base - 3.0, base);
733 }
734 let high_vol_atr = atr.value().unwrap();
735
736 assert!(
737 high_vol_atr > low_vol_atr,
738 "ATR should increase with volatility: {high_vol_atr} vs {low_vol_atr}"
739 );
740 }
741
742 #[test]
743 fn test_atr_reset() {
744 let mut atr = ATR::new(14);
745 for i in 0..20 {
746 let base = 100.0 + i as f64;
747 atr.update(base + 1.0, base - 1.0, base);
748 }
749 assert!(atr.is_ready());
750
751 atr.reset();
752 assert!(!atr.is_ready());
753 assert!(atr.value().is_none());
754 }
755
756 #[test]
759 fn test_adx_creation() {
760 let adx = ADX::new(14);
761 assert_eq!(adx.period(), 14);
762 assert!(!adx.is_ready());
763 }
764
765 #[test]
766 fn test_adx_trending_detection() {
767 let mut adx = ADX::new(14);
768
769 for i in 1..=50 {
771 let high = 100.0 + i as f64 * 2.0;
772 let low = 100.0 + i as f64 * 2.0 - 1.0;
773 let close = 100.0 + i as f64 * 2.0 - 0.5;
774 adx.update(high, low, close);
775 }
776
777 if let Some(adx_value) = adx.value() {
778 assert!(
779 adx_value > 20.0,
780 "ADX should indicate trend in strong uptrend: {adx_value}"
781 );
782 }
783 }
784
785 #[test]
786 fn test_adx_trend_direction() {
787 let mut adx = ADX::new(14);
788
789 for i in 1..=50 {
791 let high = 100.0 + i as f64 * 2.0;
792 let low = 100.0 + i as f64 * 2.0 - 1.0;
793 let close = 100.0 + i as f64 * 2.0 - 0.5;
794 adx.update(high, low, close);
795 }
796
797 if let Some(dir) = adx.trend_direction() {
798 assert_eq!(
799 dir,
800 TrendDirection::Bullish,
801 "Should detect bullish direction in uptrend"
802 );
803 }
804 }
805
806 #[test]
807 fn test_adx_di_values() {
808 let mut adx = ADX::new(14);
809
810 for i in 1..=50 {
811 let high = 100.0 + i as f64 * 2.0;
812 let low = 100.0 + i as f64 * 2.0 - 1.0;
813 let close = 100.0 + i as f64 * 2.0 - 0.5;
814 adx.update(high, low, close);
815 }
816
817 if let (Some(plus), Some(minus)) = (adx.plus_dir_index(), adx.minus_dir_index()) {
819 assert!(
820 plus > minus,
821 "+DI ({plus}) should be > -DI ({minus}) in uptrend"
822 );
823 }
824 }
825
826 #[test]
827 fn test_adx_reset() {
828 let mut adx = ADX::new(14);
829 for i in 1..=50 {
830 let base = 100.0 + i as f64;
831 adx.update(base + 1.0, base - 1.0, base);
832 }
833 assert!(adx.is_ready());
834
835 adx.reset();
836 assert!(!adx.is_ready());
837 assert!(adx.value().is_none());
838 assert!(adx.plus_dir_index().is_none());
839 assert!(adx.minus_dir_index().is_none());
840 }
841
842 #[test]
845 fn test_bb_creation() {
846 let bb = BollingerBands::new(20, 2.0);
847 assert_eq!(bb.period(), 20);
848 assert_eq!(bb.std_dev_multiplier(), 2.0);
849 assert!(!bb.is_ready());
850 }
851
852 #[test]
853 fn test_bb_warmup() {
854 let mut bb = BollingerBands::new(20, 2.0);
855
856 for i in 1..20 {
857 let result = bb.update(100.0 + i as f64 * 0.1);
858 assert!(result.is_none());
859 }
860
861 let result = bb.update(102.0);
862 assert!(result.is_some());
863 assert!(bb.is_ready());
864 }
865
866 #[test]
867 fn test_bb_band_ordering() {
868 let mut bb = BollingerBands::new(20, 2.0);
869
870 for i in 1..=25 {
871 let price = 100.0 + (i as f64 % 5.0);
872 bb.update(price);
873 }
874
875 let result = bb.update(102.0).unwrap();
876 assert!(
877 result.upper > result.middle,
878 "Upper band ({}) should be > middle ({})",
879 result.upper,
880 result.middle
881 );
882 assert!(
883 result.middle > result.lower,
884 "Middle ({}) should be > lower ({})",
885 result.middle,
886 result.lower
887 );
888 }
889
890 #[test]
891 fn test_bb_percent_b() {
892 let mut bb = BollingerBands::new(20, 2.0);
893
894 for i in 1..=20 {
896 bb.update(100.0 + (i as f64 % 3.0));
897 }
898
899 let values = bb.update(100.0 + 1.0);
901 if let Some(v) = values {
902 assert!(
904 v.percent_b >= 0.0 && v.percent_b <= 1.0,
905 "%B should be in [0,1]: {}",
906 v.percent_b
907 );
908 }
909 }
910
911 #[test]
912 fn test_bb_squeeze_detection() {
913 let mut bb = BollingerBands::new(20, 2.0);
914
915 for i in 0..50 {
917 let price = 100.0 + if i % 2 == 0 { 10.0 } else { -10.0 };
918 bb.update(price);
919 }
920
921 for _ in 0..50 {
923 bb.update(100.0);
924 }
925
926 let result = bb.update(100.0).unwrap();
927 assert!(
929 result.width_percentile < 50.0,
930 "Constant prices should produce low width percentile: {}",
931 result.width_percentile
932 );
933 }
934
935 #[test]
936 fn test_bb_overbought_oversold() {
937 let mut bb = BollingerBands::new(20, 2.0);
938
939 for _ in 0..20 {
941 bb.update(100.0);
942 }
943
944 let result = bb.update(110.0).unwrap();
946 assert!(
947 result.is_overbought(),
948 "Price far above bands should be overbought, %B = {}",
949 result.percent_b
950 );
951 }
952
953 #[test]
954 fn test_bb_reset() {
955 let mut bb = BollingerBands::new(20, 2.0);
956 for i in 0..25 {
957 bb.update(100.0 + i as f64);
958 }
959 assert!(bb.is_ready());
960
961 bb.reset();
962 assert!(!bb.is_ready());
963 }
964
965 #[test]
968 fn test_rsi_creation() {
969 let rsi = RSI::new(14);
970 assert_eq!(rsi.period(), 14);
971 assert!(!rsi.is_ready());
972 }
973
974 #[test]
975 fn test_rsi_bullish_market() {
976 let mut rsi = RSI::new(14);
977
978 let mut last_rsi = None;
980 for i in 0..30 {
981 let price = 100.0 + i as f64;
982 if let Some(val) = rsi.update(price) {
983 last_rsi = Some(val);
984 }
985 }
986
987 if let Some(val) = last_rsi {
988 assert!(
989 val > 50.0,
990 "RSI should be above 50 in bullish market: {val}"
991 );
992 }
993 }
994
995 #[test]
996 fn test_rsi_bearish_market() {
997 let mut rsi = RSI::new(14);
998
999 let mut last_rsi = None;
1001 for i in 0..30 {
1002 let price = 200.0 - i as f64;
1003 if let Some(val) = rsi.update(price) {
1004 last_rsi = Some(val);
1005 }
1006 }
1007
1008 if let Some(val) = last_rsi {
1009 assert!(
1010 val < 50.0,
1011 "RSI should be below 50 in bearish market: {val}"
1012 );
1013 }
1014 }
1015
1016 #[test]
1017 fn test_rsi_range() {
1018 let mut rsi = RSI::new(14);
1019
1020 for i in 0..50 {
1021 let price = 100.0 + (i as f64 * 0.7).sin() * 10.0;
1022 if let Some(val) = rsi.update(price) {
1023 assert!(
1024 (0.0..=100.0).contains(&val),
1025 "RSI should be in [0, 100]: {val}"
1026 );
1027 }
1028 }
1029 }
1030
1031 #[test]
1032 fn test_rsi_value_cached() {
1033 let mut rsi = RSI::new(14);
1034 assert!(
1035 rsi.value().is_none(),
1036 "value() should be None before warmup"
1037 );
1038
1039 let mut last_from_update = None;
1040 for i in 0..30 {
1041 let price = 100.0 + i as f64;
1042 if let Some(v) = rsi.update(price) {
1043 last_from_update = Some(v);
1044 }
1045 }
1046
1047 assert_eq!(
1049 rsi.value(),
1050 last_from_update,
1051 "value() must equal the last update() result"
1052 );
1053 }
1054
1055 #[test]
1056 fn test_rsi_reset_clears_value() {
1057 let mut rsi = RSI::new(14);
1058 for i in 0..30 {
1059 rsi.update(100.0 + i as f64);
1060 }
1061 assert!(rsi.value().is_some());
1062 rsi.reset();
1063 assert!(rsi.value().is_none(), "value() should be None after reset");
1064 }
1065
1066 #[test]
1069 fn test_calculate_sma() {
1070 assert_eq!(calculate_sma(&[1.0, 2.0, 3.0, 4.0, 5.0]), 3.0);
1071 assert_eq!(calculate_sma(&[100.0]), 100.0);
1072 assert_eq!(calculate_sma(&[]), 0.0);
1073 }
1074
1075 #[test]
1076 fn test_calculate_sma_precision() {
1077 let prices = vec![10.0, 20.0, 30.0];
1078 let sma = calculate_sma(&prices);
1079 assert!((sma - 20.0).abs() < f64::EPSILON);
1080 }
1081}