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