1use crate::stocks::ta::bollinger::{BollingerParams, ValidatedBollinger};
112use crate::stocks::ta::common::{require_hlc, validate_positive_volume};
113use crate::stocks::ta::keltner::{KeltnerParams, ValidatedKeltner};
114use crate::stocks::ta::macd::{MacdParams, ValidatedMacd};
115use crate::stocks::ta::ring::{RingF64, RingPv, SlidingMax, SlidingMin};
116use crate::stocks::ta::rvol::{RvolParams, ValidatedRvol};
117use crate::stocks::ta::stochastic::{StochasticParams, ValidatedStochastic};
118use crate::stocks::ta::vwap::{ValidatedVwap, VwapMode, VwapParams, VwapPriceSource};
119use crate::util::error::{require_finite, FinanceError, FinanceResult};
120
121pub use crate::stocks::ta::moving_average::{EmaState, SmaState};
124
125#[derive(Clone, Debug)]
152pub struct StochState {
153 params: StochasticParams,
154 high_max: SlidingMax,
155 low_min: SlidingMin,
156 raw_k: RingF64,
157 smooth_k: RingF64,
158 last_k: Option<f64>,
159 last_d: Option<f64>,
160 prev_raw_k: Option<f64>,
162}
163
164impl StochState {
165 pub fn new(params: StochasticParams) -> FinanceResult<Self> {
166 let _ = ValidatedStochastic::new(params)?;
167 Ok(Self {
168 params,
169 high_max: SlidingMax::with_window(params.k_period),
170 low_min: SlidingMin::with_window(params.k_period),
171 raw_k: RingF64::with_capacity(params.k_smooth),
172 smooth_k: RingF64::with_capacity(params.d_period),
173 last_k: None,
174 last_d: None,
175 prev_raw_k: None,
176 })
177 }
178
179 pub fn from_history(
180 params: StochasticParams,
181 high: &[f64],
182 low: &[f64],
183 close: &[f64],
184 ) -> FinanceResult<Self> {
185 let mut s = Self::new(params)?;
186 require_hlc(high, low, close)?;
187 for i in 0..close.len() {
188 s.push(high[i], low[i], close[i])?;
189 }
190 Ok(s)
191 }
192
193 pub fn params(&self) -> StochasticParams {
194 self.params
195 }
196
197 pub fn reset(&mut self) {
198 self.high_max.clear();
199 self.low_min.clear();
200 self.raw_k.clear();
201 self.smooth_k.clear();
202 self.last_k = None;
203 self.last_d = None;
204 self.prev_raw_k = None;
205 }
206
207 pub fn push(&mut self, high: f64, low: f64, close: f64) -> FinanceResult<Option<(f64, f64)>> {
212 let d = self.push_detail(high, low, close)?;
213 match (d.k, d.d) {
214 (Some(k), Some(dd)) => Ok(Some((k, dd))),
215 _ => Ok(None),
216 }
217 }
218
219 pub fn push_detail(
221 &mut self,
222 high: f64,
223 low: f64,
224 close: f64,
225 ) -> FinanceResult<StochBarOutput> {
226 require_finite("high", high)?;
227 require_finite("low", low)?;
228 require_finite("close", close)?;
229 if high < low {
230 return Err(FinanceError::InvalidCashflow {
231 message: "high must be >= low for each bar",
232 });
233 }
234 let hh = self.high_max.push(high).unwrap();
235 let ll = self.low_min.push(low).unwrap();
236
237 let mut k_out = None;
238 let mut d_out = None;
239
240 if self.high_max.is_full() {
241 let range = hh - ll;
242 let raw = if range == 0.0 {
244 self.prev_raw_k.unwrap_or(50.0)
245 } else {
246 100.0 * (close - ll) / range
247 };
248 self.prev_raw_k = Some(raw);
249 let _ = self.raw_k.push(raw);
250 if self.raw_k.is_full() {
251 let sk = self.raw_k.sum() / self.params.k_smooth as f64;
252 self.last_k = Some(sk);
253 k_out = Some(sk);
254 let _ = self.smooth_k.push(sk);
255 if self.smooth_k.is_full() {
256 let d = self.smooth_k.sum() / self.params.d_period as f64;
257 self.last_d = Some(d);
258 d_out = Some(d);
259 }
260 }
261 }
262 Ok(StochBarOutput { k: k_out, d: d_out })
263 }
264
265 pub fn last_kd(&self) -> Option<(f64, f64)> {
266 Some((self.last_k?, self.last_d?))
267 }
268
269 pub fn push_bars(
271 &mut self,
272 high: &[f64],
273 low: &[f64],
274 close: &[f64],
275 ) -> FinanceResult<Vec<StochBarOutput>> {
276 require_hlc(high, low, close)?;
277 let mut out = Vec::with_capacity(close.len());
278 for i in 0..close.len() {
279 out.push(self.push_detail(high[i], low[i], close[i])?);
280 }
281 Ok(out)
282 }
283}
284
285#[derive(Clone, Copy, Debug, PartialEq)]
287pub struct StochBarOutput {
288 pub k: Option<f64>,
289 pub d: Option<f64>,
290}
291
292#[derive(Clone, Debug)]
298pub struct MacdState {
299 params: MacdParams,
300 fast: EmaState,
301 slow: EmaState,
302 signal: EmaState,
303 last: Option<(f64, f64, f64)>,
304}
305
306impl MacdState {
307 pub fn new(params: MacdParams) -> FinanceResult<Self> {
308 let _ = ValidatedMacd::new(params)?;
309 Ok(Self {
310 params,
311 fast: EmaState::new(params.fast)?,
312 slow: EmaState::new(params.slow)?,
313 signal: EmaState::new(params.signal)?,
314 last: None,
315 })
316 }
317
318 pub fn from_history(params: MacdParams, closes: &[f64]) -> FinanceResult<Self> {
319 let mut s = Self::new(params)?;
320 for &c in closes {
321 s.push(c)?;
322 }
323 Ok(s)
324 }
325
326 pub fn params(&self) -> MacdParams {
327 self.params
328 }
329
330 pub fn reset(&mut self) {
331 self.fast.reset();
332 self.slow.reset();
333 self.signal.reset();
334 self.last = None;
335 }
336
337 pub fn push(&mut self, close: f64) -> FinanceResult<Option<(f64, f64, f64)>> {
339 let d = self.push_detail(close)?;
340 match (d.macd, d.signal, d.histogram) {
341 (Some(m), Some(s), Some(h)) => Ok(Some((m, s, h))),
342 _ => Ok(None),
343 }
344 }
345
346 pub fn push_detail(&mut self, close: f64) -> FinanceResult<MacdBarOutput> {
348 let f = self.fast.push(close)?;
349 let s = self.slow.push(close)?;
350 let macd_line = match (f, s) {
351 (Some(a), Some(b)) => a - b,
352 _ => {
353 self.last = None;
354 return Ok(MacdBarOutput {
355 macd: None,
356 signal: None,
357 histogram: None,
358 });
359 }
360 };
361 let sig = self.signal.push(macd_line)?;
362 match sig {
363 Some(signal) => {
364 let hist = macd_line - signal;
365 self.last = Some((macd_line, signal, hist));
366 Ok(MacdBarOutput {
367 macd: Some(macd_line),
368 signal: Some(signal),
369 histogram: Some(hist),
370 })
371 }
372 None => {
373 self.last = None;
374 Ok(MacdBarOutput {
375 macd: Some(macd_line),
376 signal: None,
377 histogram: None,
378 })
379 }
380 }
381 }
382
383 pub fn last(&self) -> Option<(f64, f64, f64)> {
384 self.last
385 }
386
387 pub fn push_bars(&mut self, closes: &[f64]) -> FinanceResult<Vec<Option<(f64, f64, f64)>>> {
389 let mut out = Vec::with_capacity(closes.len());
390 for &c in closes {
391 out.push(self.push(c)?);
392 }
393 Ok(out)
394 }
395
396 pub fn push_bars_detail(&mut self, closes: &[f64]) -> FinanceResult<Vec<MacdBarOutput>> {
398 let mut out = Vec::with_capacity(closes.len());
399 for &c in closes {
400 out.push(self.push_detail(c)?);
401 }
402 Ok(out)
403 }
404}
405
406#[derive(Clone, Copy, Debug, PartialEq)]
408pub struct MacdBarOutput {
409 pub macd: Option<f64>,
410 pub signal: Option<f64>,
411 pub histogram: Option<f64>,
412}
413
414#[derive(Clone, Debug)]
423pub struct BollingerState {
424 params: BollingerParams,
425 ring: RingF64,
426 sum_sq: f64,
427 last: Option<BollingerBarOutput>,
428}
429
430#[derive(Clone, Copy, Debug, PartialEq)]
432pub struct BollingerBarOutput {
433 pub middle: f64,
434 pub upper: f64,
435 pub lower: f64,
436 pub pct_b: Option<f64>,
437}
438
439impl BollingerState {
440 pub fn new(params: BollingerParams) -> FinanceResult<Self> {
441 let _ = ValidatedBollinger::new(params)?;
442 Ok(Self {
443 params,
444 ring: RingF64::with_capacity(params.period),
445 sum_sq: 0.0,
446 last: None,
447 })
448 }
449
450 pub fn from_history(params: BollingerParams, closes: &[f64]) -> FinanceResult<Self> {
451 let mut s = Self::new(params)?;
452 for &c in closes {
453 s.push(c)?;
454 }
455 Ok(s)
456 }
457
458 pub fn params(&self) -> BollingerParams {
459 self.params
460 }
461
462 pub fn reset(&mut self) {
463 self.ring.clear();
464 self.sum_sq = 0.0;
465 self.last = None;
466 }
467
468 pub fn push(&mut self, close: f64) -> FinanceResult<Option<BollingerBarOutput>> {
469 require_finite("close", close)?;
470 let n = self.params.period as f64;
471 if let Some(old) = self.ring.push(close) {
472 self.sum_sq += close * close - old * old;
473 } else {
474 self.sum_sq += close * close;
475 }
476 if !self.ring.is_full() {
477 self.last = None;
478 return Ok(None);
479 }
480 let sum = self.ring.sum();
481 let mid = sum / n;
482 let ss = (self.sum_sq - sum * sum / n).max(0.0);
484 let sd = match self.params.stdev {
485 crate::stocks::ta::common::StdevKind::Population => (ss / n).sqrt(),
486 crate::stocks::ta::common::StdevKind::Sample => {
487 if n > 1.0 {
488 (ss / (n - 1.0)).sqrt()
489 } else {
490 0.0
491 }
492 }
493 };
494 let band = self.params.num_std * sd;
495 let upper = mid + band;
496 let lower = mid - band;
497 let width = upper - lower;
498 let pct_b = if width > 0.0 {
499 Some((close - lower) / width)
500 } else {
501 None
502 };
503 let out = BollingerBarOutput {
504 middle: mid,
505 upper,
506 lower,
507 pct_b,
508 };
509 self.last = Some(out);
510 Ok(Some(out))
511 }
512
513 pub fn last(&self) -> Option<BollingerBarOutput> {
514 self.last
515 }
516
517 pub fn push_bars(&mut self, closes: &[f64]) -> FinanceResult<Vec<Option<BollingerBarOutput>>> {
519 let mut out = Vec::with_capacity(closes.len());
520 for &c in closes {
521 out.push(self.push(c)?);
522 }
523 Ok(out)
524 }
525}
526
527#[derive(Clone, Debug)]
533pub struct KeltnerState {
534 params: KeltnerParams,
535 mid: EmaState,
536 atr: crate::stocks::ta::atr::AtrState,
537 last: Option<KeltnerBarOutput>,
538}
539
540#[derive(Clone, Copy, Debug, PartialEq)]
541pub struct KeltnerBarOutput {
542 pub middle: f64,
543 pub upper: f64,
544 pub lower: f64,
545 pub atr: f64,
546}
547
548impl KeltnerState {
549 pub fn new(params: KeltnerParams) -> FinanceResult<Self> {
550 let _ = ValidatedKeltner::new(params)?;
551 Ok(Self {
552 params,
553 mid: EmaState::new(params.ema_period)?,
554 atr: crate::stocks::ta::atr::AtrState::new(crate::stocks::ta::atr::AtrParams::new(
555 params.atr_period,
556 ))?,
557 last: None,
558 })
559 }
560
561 pub fn from_history(
562 params: KeltnerParams,
563 high: &[f64],
564 low: &[f64],
565 close: &[f64],
566 ) -> FinanceResult<Self> {
567 let mut s = Self::new(params)?;
568 require_hlc(high, low, close)?;
569 for i in 0..close.len() {
570 s.push(high[i], low[i], close[i])?;
571 }
572 Ok(s)
573 }
574
575 pub fn params(&self) -> KeltnerParams {
576 self.params
577 }
578
579 pub fn reset(&mut self) {
580 self.mid.reset();
581 self.atr.reset();
582 self.last = None;
583 }
584
585 pub fn push(
586 &mut self,
587 high: f64,
588 low: f64,
589 close: f64,
590 ) -> FinanceResult<Option<KeltnerBarOutput>> {
591 let atr_val = self.atr.push(high, low, close)?;
592 let mid = self.mid.push(close)?;
593 match (mid, atr_val) {
594 (Some(m), Some(a)) => {
595 let out = KeltnerBarOutput {
596 middle: m,
597 upper: m + self.params.atr_mult * a,
598 lower: m - self.params.atr_mult * a,
599 atr: a,
600 };
601 self.last = Some(out);
602 Ok(Some(out))
603 }
604 _ => {
605 self.last = None;
606 Ok(None)
607 }
608 }
609 }
610
611 pub fn last(&self) -> Option<KeltnerBarOutput> {
612 self.last
613 }
614
615 pub fn push_bars(
617 &mut self,
618 high: &[f64],
619 low: &[f64],
620 close: &[f64],
621 ) -> FinanceResult<Vec<Option<KeltnerBarOutput>>> {
622 require_hlc(high, low, close)?;
623 let mut out = Vec::with_capacity(close.len());
624 for i in 0..close.len() {
625 out.push(self.push(high[i], low[i], close[i])?);
626 }
627 Ok(out)
628 }
629}
630
631#[derive(Clone, Debug)]
648pub struct VwapState {
649 params: VwapParams,
650 cum_pv: f64,
652 cum_v: f64,
653 rolling: Option<RingPv>,
655 last: Option<f64>,
656}
657
658impl VwapState {
659 pub fn new(params: VwapParams) -> FinanceResult<Self> {
660 let _ = ValidatedVwap::new(params)?;
661 let rolling = match params.mode {
662 VwapMode::Cumulative => None,
663 VwapMode::Rolling { period } => Some(RingPv::with_capacity(period)),
664 };
665 Ok(Self {
666 params,
667 cum_pv: 0.0,
668 cum_v: 0.0,
669 rolling,
670 last: None,
671 })
672 }
673
674 pub fn from_history(
675 params: VwapParams,
676 high: &[f64],
677 low: &[f64],
678 close: &[f64],
679 volume: &[f64],
680 ) -> FinanceResult<Self> {
681 let mut s = Self::new(params)?;
682 require_hlc(high, low, close)?;
683 validate_positive_volume(volume)?;
684 if close.len() != volume.len() {
685 return Err(FinanceError::LengthMismatch {
686 left: close.len(),
687 right: volume.len(),
688 context: "close/volume",
689 });
690 }
691 for i in 0..close.len() {
692 s.push(high[i], low[i], close[i], volume[i])?;
693 }
694 Ok(s)
695 }
696
697 pub fn params(&self) -> VwapParams {
698 self.params
699 }
700
701 pub fn reset(&mut self) {
703 self.cum_pv = 0.0;
704 self.cum_v = 0.0;
705 if let Some(r) = self.rolling.as_mut() {
706 r.clear();
707 }
708 self.last = None;
709 }
710
711 pub fn push(
712 &mut self,
713 high: f64,
714 low: f64,
715 close: f64,
716 volume: f64,
717 ) -> FinanceResult<Option<f64>> {
718 require_finite("high", high)?;
719 require_finite("low", low)?;
720 require_finite("close", close)?;
721 require_finite("volume", volume)?;
722 if high < low {
723 return Err(FinanceError::InvalidCashflow {
724 message: "high must be >= low for each bar",
725 });
726 }
727 if volume < 0.0 {
728 return Err(FinanceError::InvalidCashflow {
729 message: "volume must be non-negative",
730 });
731 }
732 let price = match self.params.price_source {
733 VwapPriceSource::Typical => (high + low + close) / 3.0,
734 VwapPriceSource::Close => close,
735 };
736 let out = match self.params.mode {
737 VwapMode::Cumulative => {
738 self.cum_pv += price * volume;
739 self.cum_v += volume;
740 if self.cum_v > 0.0 {
741 Some(self.cum_pv / self.cum_v)
742 } else {
743 None
744 }
745 }
746 VwapMode::Rolling { period } => {
747 let ring = self.rolling.as_mut().unwrap();
748 ring.push(price, volume);
749 if ring.len() >= period {
750 ring.vwap()
751 } else {
752 None
753 }
754 }
755 };
756 self.last = out;
757 Ok(out)
758 }
759
760 pub fn last(&self) -> Option<f64> {
761 self.last
762 }
763
764 pub fn push_bars(
766 &mut self,
767 high: &[f64],
768 low: &[f64],
769 close: &[f64],
770 volume: &[f64],
771 ) -> FinanceResult<Vec<Option<f64>>> {
772 require_hlc(high, low, close)?;
773 validate_positive_volume(volume)?;
774 if close.len() != volume.len() {
775 return Err(FinanceError::LengthMismatch {
776 left: close.len(),
777 right: volume.len(),
778 context: "close/volume",
779 });
780 }
781 let mut out = Vec::with_capacity(close.len());
782 for i in 0..close.len() {
783 out.push(self.push(high[i], low[i], close[i], volume[i])?);
784 }
785 Ok(out)
786 }
787}
788
789#[derive(Clone, Debug)]
795pub struct RvolState {
796 params: RvolParams,
797 ring: RingF64,
798 last: Option<f64>,
799}
800
801impl RvolState {
802 pub fn new(params: RvolParams) -> FinanceResult<Self> {
803 let _ = ValidatedRvol::new(params)?;
804 Ok(Self {
805 params,
806 ring: RingF64::with_capacity(params.lookback),
807 last: None,
808 })
809 }
810
811 pub fn from_history(params: RvolParams, volume: &[f64]) -> FinanceResult<Self> {
812 let mut s = Self::new(params)?;
813 for &v in volume {
814 s.push(v)?;
815 }
816 Ok(s)
817 }
818
819 pub fn params(&self) -> RvolParams {
820 self.params
821 }
822
823 pub fn reset(&mut self) {
824 self.ring.clear();
825 self.last = None;
826 }
827
828 pub fn push(&mut self, volume: f64) -> FinanceResult<Option<f64>> {
829 require_finite("volume", volume)?;
830 if volume < 0.0 {
831 return Err(FinanceError::InvalidCashflow {
832 message: "volume must be non-negative",
833 });
834 }
835 let _ = self.ring.push(volume);
836 if !self.ring.is_full() {
837 self.last = None;
838 return Ok(None);
839 }
840 let mean = self.ring.sum() / self.params.lookback as f64;
841 let out = if mean > 0.0 {
842 Some(volume / mean)
843 } else {
844 None
845 };
846 self.last = out;
847 Ok(out)
848 }
849
850 pub fn last(&self) -> Option<f64> {
851 self.last
852 }
853
854 pub fn push_bars(&mut self, volume: &[f64]) -> FinanceResult<Vec<Option<f64>>> {
856 let mut out = Vec::with_capacity(volume.len());
857 for &v in volume {
858 out.push(self.push(v)?);
859 }
860 Ok(out)
861 }
862}
863
864#[cfg(test)]
869mod tests {
870 use super::*;
871 use crate::stocks::ta::bollinger::bollinger;
872 use crate::stocks::ta::keltner::keltner;
873 use crate::stocks::ta::macd::macd;
874 use crate::stocks::ta::moving_average::{ema, sma};
875 use crate::stocks::ta::rvol::rvol;
876 use crate::stocks::ta::stochastic::stochastics;
877 use crate::stocks::ta::vwap::vwap;
878
879 fn path(n: usize) -> (Vec<f64>, Vec<f64>, Vec<f64>, Vec<f64>) {
880 let close: Vec<_> = (0..n)
881 .map(|i| 100.0 + i as f64 * 0.13 + ((i % 7) as f64) * 0.04)
882 .collect();
883 let high: Vec<_> = close.iter().map(|c| c + 0.35).collect();
884 let low: Vec<_> = close.iter().map(|c| c - 0.35).collect();
885 let vol: Vec<_> = (0..n).map(|i| 800.0 + i as f64 * 3.0).collect();
886 (high, low, close, vol)
887 }
888
889 fn approx_opt(a: Option<f64>, b: Option<f64>) {
890 match (a, b) {
891 (None, None) => {}
892 (Some(x), Some(y)) => assert!((x - y).abs() < 1e-9, "{x} vs {y}"),
893 _ => panic!("Option mismatch {a:?} vs {b:?}"),
894 }
895 }
896
897 #[test]
898 fn sma_parity() {
899 let (_, _, c, _) = path(40);
900 let batch = sma(&c, 10).unwrap();
901 let mut st = SmaState::new(10).unwrap();
902 for i in 0..c.len() {
903 approx_opt(st.push(c[i]).unwrap(), batch[i]);
904 }
905 }
906
907 #[test]
908 fn ema_parity() {
909 let (_, _, c, _) = path(40);
910 let batch = ema(&c, 10).unwrap();
911 let mut st = EmaState::new(10).unwrap();
912 for i in 0..c.len() {
913 approx_opt(st.push(c[i]).unwrap(), batch[i]);
914 }
915 }
916
917 #[test]
918 fn stoch_parity() {
919 let (h, l, c, _) = path(50);
920 let p = StochasticParams::full(14, 3, 3);
921 let batch = stochastics(&h, &l, &c, p).unwrap();
922 let mut st = StochState::new(p).unwrap();
923 for i in 0..c.len() {
924 let d = st.push_detail(h[i], l[i], c[i]).unwrap();
925 approx_opt(d.k, batch.k[i]);
926 approx_opt(d.d, batch.d[i]);
927 }
928 }
929
930 #[test]
931 fn macd_parity() {
932 let (_, _, c, _) = path(60);
933 let p = MacdParams::standard();
934 let batch = macd(&c, p).unwrap();
935 let mut st = MacdState::new(p).unwrap();
936 for i in 0..c.len() {
937 let o = st.push(c[i]).unwrap();
938 match (o, batch.signal[i], batch.histogram[i], batch.macd[i]) {
939 (Some((m, s, h)), Some(bs), Some(bh), Some(bm)) => {
940 assert!((m - bm).abs() < 1e-8, "macd {i}");
941 assert!((s - bs).abs() < 1e-8, "signal {i}");
942 assert!((h - bh).abs() < 1e-8, "hist {i}");
943 }
944 (None, None, None, _) => {} other => panic!("macd parity at {i}: {other:?}"),
946 }
947 }
948 let bl = batch.last().unwrap();
949 let sl = st.last().unwrap();
950 assert!((bl.0 - sl.0).abs() < 1e-8);
951 assert!((bl.1 - sl.1).abs() < 1e-8);
952 assert!((bl.2 - sl.2).abs() < 1e-8);
953 }
954
955 #[test]
956 fn bollinger_parity() {
957 let (_, _, c, _) = path(40);
958 let p = BollingerParams::standard();
959 let batch = bollinger(&c, p).unwrap();
960 let mut st = BollingerState::new(p).unwrap();
961 for i in 0..c.len() {
962 let o = st.push(c[i]).unwrap();
963 match (o, batch.middle[i]) {
964 (None, None) => {}
965 (Some(bo), Some(m)) => {
966 assert!((bo.middle - m).abs() < 1e-9);
967 assert!((bo.upper - batch.upper[i].unwrap()).abs() < 1e-9);
968 assert!((bo.lower - batch.lower[i].unwrap()).abs() < 1e-9);
969 }
970 other => panic!("{other:?}"),
971 }
972 }
973 }
974
975 #[test]
976 fn keltner_parity() {
977 let (h, l, c, _) = path(45);
978 let p = KeltnerParams::standard();
979 let batch = keltner(&h, &l, &c, p).unwrap();
980 let mut st = KeltnerState::new(p).unwrap();
981 for i in 0..c.len() {
982 let o = st.push(h[i], l[i], c[i]).unwrap();
983 match (o, batch.middle[i], batch.upper[i], batch.atr[i]) {
984 (Some(ko), Some(m), Some(u), Some(a)) => {
985 assert!((ko.middle - m).abs() < 1e-8, "mid {i}");
986 assert!((ko.upper - u).abs() < 1e-8, "upper {i}");
987 assert!((ko.atr - a).abs() < 1e-8, "atr {i}");
988 }
989 (None, _, None, _) | (None, None, _, _) => {} other => panic!("keltner parity {i}: {other:?}"),
991 }
992 }
993 let sl = st.last().unwrap();
994 let bl_m = batch.middle.iter().rev().find_map(|x| *x).unwrap();
995 let bl_a = batch.atr.iter().rev().find_map(|x| *x).unwrap();
996 assert!((bl_m - sl.middle).abs() < 1e-8);
997 assert!((bl_a - sl.atr).abs() < 1e-8);
998 }
999
1000 #[test]
1001 fn vwap_cum_parity() {
1002 let (h, l, c, v) = path(30);
1003 let p = VwapParams::cumulative_typical();
1004 let batch = vwap(&h, &l, &c, &v, p).unwrap();
1005 let mut st = VwapState::new(p).unwrap();
1006 for i in 0..c.len() {
1007 approx_opt(st.push(h[i], l[i], c[i], v[i]).unwrap(), batch.vwap[i]);
1008 }
1009 }
1010
1011 #[test]
1012 fn vwap_reset() {
1013 let mut st = VwapState::new(VwapParams::cumulative_typical()).unwrap();
1014 st.push(10.0, 10.0, 10.0, 100.0).unwrap();
1015 st.reset();
1016 let x = st.push(20.0, 20.0, 20.0, 50.0).unwrap().unwrap();
1017 assert!((x - 20.0).abs() < 1e-12);
1018 }
1019
1020 #[test]
1021 fn rvol_parity() {
1022 let (_, _, _, v) = path(40);
1023 let p = RvolParams::days_20();
1024 let batch = rvol(&v, p).unwrap();
1025 let mut st = RvolState::new(p).unwrap();
1026 for i in 0..v.len() {
1027 approx_opt(st.push(v[i]).unwrap(), batch.rvol[i]);
1028 }
1029 }
1030
1031 #[test]
1032 fn rsi_atr_parity_via_from_history() {
1033 let (h, l, c, _) = path(60);
1034 let rsi_b = crate::stocks::ta::rsi::rsi(&c, crate::stocks::ta::rsi::RsiParams::period_14())
1035 .unwrap();
1036 let rsi_s = crate::stocks::ta::rsi::RsiState::from_history(
1037 crate::stocks::ta::rsi::RsiParams::period_14(),
1038 &c,
1039 )
1040 .unwrap();
1041 approx_opt(rsi_b.last(), rsi_s.last());
1042
1043 let atr_b =
1044 crate::stocks::ta::atr::atr(&h, &l, &c, crate::stocks::ta::atr::AtrParams::period_14())
1045 .unwrap();
1046 let atr_s = crate::stocks::ta::atr::AtrState::from_history(
1047 crate::stocks::ta::atr::AtrParams::period_14(),
1048 &h,
1049 &l,
1050 &c,
1051 )
1052 .unwrap();
1053 approx_opt(atr_b.last(), atr_s.last());
1054 }
1055
1056 #[test]
1057 fn from_history_matches_push() {
1058 let (h, l, c, _) = path(25);
1059 let p = StochasticParams::fast(9, 3);
1060 let a = StochState::from_history(p, &h, &l, &c).unwrap();
1061 let mut b = StochState::new(p).unwrap();
1062 for i in 0..c.len() {
1063 b.push(h[i], l[i], c[i]).unwrap();
1064 }
1065 assert_eq!(a.last_kd(), b.last_kd());
1066 }
1067}