use crate::ohlcv::Candle;
pub trait Indicator {
type Input;
type Output;
fn update(&mut self, input: Self::Input) -> Option<Self::Output>;
fn reset(&mut self);
fn warmup_period(&self) -> usize;
fn is_ready(&self) -> bool;
fn name(&self) -> &'static str;
fn batch_nan_into(&mut self, inputs: &[Self::Input], out: &mut [f64])
where
Self::Input: Copy,
Self::Output: Into<f64>,
{
assert_eq!(
inputs.len(),
out.len(),
"batch output length must equal input length"
);
for (slot, &x) in out.iter_mut().zip(inputs) {
*slot = self.update(x).map_or(f64::NAN, Into::into);
}
}
fn batch_fast_into(&mut self, inputs: &[Self::Input], out: &mut [f64])
where
Self::Input: Copy,
Self::Output: Into<f64>,
{
self.batch_nan_into(inputs, out);
}
}
pub trait BatchExt: Indicator {
fn batch(&mut self, inputs: &[Self::Input]) -> Vec<Option<Self::Output>>
where
Self::Input: Clone,
{
let mut out = Vec::with_capacity(inputs.len());
for x in inputs {
out.push(self.update(x.clone()));
}
out
}
#[cfg(feature = "parallel")]
fn batch_parallel<F>(
inputs_per_asset: &[Vec<Self::Input>],
make: F,
) -> Vec<Vec<Option<Self::Output>>>
where
Self: Sized + Send,
Self::Input: Sync + Clone,
Self::Output: Send,
F: Fn() -> Self + Sync + Send,
{
use rayon::prelude::*;
inputs_per_asset
.par_iter()
.map(|series| {
let mut ind = make();
ind.batch(series)
})
.collect()
}
}
impl<T: Indicator> BatchExt for T {}
pub trait BatchNanExt: Indicator<Input = f64, Output = f64> {
fn batch_nan(&mut self, inputs: &[f64]) -> Vec<f64> {
let mut out = vec![0.0; inputs.len()];
self.batch_nan_into(inputs, &mut out);
out
}
fn batch_fast(&mut self, inputs: &[f64]) -> Vec<f64> {
let mut out = vec![0.0; inputs.len()];
self.batch_fast_into(inputs, &mut out);
out
}
}
impl<T: Indicator<Input = f64, Output = f64>> BatchNanExt for T {}
pub trait BarBuilder {
type Bar;
fn update(&mut self, candle: Candle) -> Vec<Self::Bar>;
fn reset(&mut self);
fn name(&self) -> &'static str;
fn batch(&mut self, candles: &[Candle]) -> Vec<Self::Bar> {
let mut out = Vec::new();
for candle in candles {
out.extend(self.update(*candle));
}
out
}
}
#[derive(Debug, Clone)]
pub struct Chain<A, B>
where
A: Indicator<Input = f64, Output = f64>,
B: Indicator<Input = f64>,
{
first: A,
second: B,
}
impl<A, B> Chain<A, B>
where
A: Indicator<Input = f64, Output = f64>,
B: Indicator<Input = f64>,
{
pub const fn new(first: A, second: B) -> Self {
Self { first, second }
}
pub fn then<C>(self, third: C) -> Chain<Self, C>
where
C: Indicator<Input = f64>,
Self: Indicator<Input = f64, Output = f64>,
{
Chain::new(self, third)
}
pub const fn first(&self) -> &A {
&self.first
}
pub const fn second(&self) -> &B {
&self.second
}
}
impl<A, B> Indicator for Chain<A, B>
where
A: Indicator<Input = f64, Output = f64>,
B: Indicator<Input = f64>,
{
type Input = f64;
type Output = B::Output;
fn update(&mut self, input: f64) -> Option<Self::Output> {
self.first.update(input).and_then(|v| self.second.update(v))
}
fn reset(&mut self) {
self.first.reset();
self.second.reset();
}
fn warmup_period(&self) -> usize {
self.first.warmup_period().max(1) + self.second.warmup_period().max(1) - 1
}
fn is_ready(&self) -> bool {
self.first.is_ready() && self.second.is_ready()
}
fn name(&self) -> &'static str {
"Chain"
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug, Default)]
struct Identity {
seen: bool,
}
impl Indicator for Identity {
type Input = f64;
type Output = f64;
fn update(&mut self, input: f64) -> Option<f64> {
self.seen = true;
Some(input)
}
fn reset(&mut self) {
self.seen = false;
}
fn warmup_period(&self) -> usize {
0
}
fn is_ready(&self) -> bool {
self.seen
}
fn name(&self) -> &'static str {
"Identity"
}
}
#[derive(Debug, Default)]
struct Doubler {
seen: bool,
}
impl Indicator for Doubler {
type Input = f64;
type Output = f64;
fn update(&mut self, input: f64) -> Option<f64> {
self.seen = true;
Some(input * 2.0)
}
fn reset(&mut self) {
self.seen = false;
}
fn warmup_period(&self) -> usize {
0
}
fn is_ready(&self) -> bool {
self.seen
}
fn name(&self) -> &'static str {
"Doubler"
}
}
#[test]
fn batch_replays_update() {
let mut id = Identity::default();
let out = id.batch(&[1.0, 2.0, 3.0]);
assert_eq!(out, vec![Some(1.0), Some(2.0), Some(3.0)]);
}
#[test]
fn batch_nan_default_maps_none_to_nan() {
let mut id = Identity::default();
let out = id.batch_nan(&[1.0, 2.0, 3.0]);
assert_eq!(out, vec![1.0, 2.0, 3.0]);
}
#[test]
fn batch_nan_into_default_fills_caller_buffer() {
let mut id = Identity::default();
let mut out = [f64::INFINITY; 3];
id.batch_nan_into(&[4.0, 5.0, 6.0], &mut out);
assert_eq!(out, [4.0, 5.0, 6.0]);
}
#[test]
#[should_panic(expected = "batch output length must equal input length")]
fn batch_nan_into_rejects_mismatched_lengths() {
let mut id = Identity::default();
let mut out = [0.0; 2];
id.batch_nan_into(&[1.0, 2.0, 3.0], &mut out);
}
#[test]
fn batch_fast_default_is_the_exact_batch() {
let exact = Doubler::default().batch_nan(&[1.5, -2.0, 3.25]);
let fast = Doubler::default().batch_fast(&[1.5, -2.0, 3.25]);
assert_eq!(exact, fast);
let mut out = [0.0; 3];
Doubler::default().batch_fast_into(&[1.5, -2.0, 3.25], &mut out);
assert_eq!(out.to_vec(), exact);
}
#[test]
fn batch_traits_are_dyn_compatible() {
let mut boxed: Box<dyn BatchNanExt<Input = f64, Output = f64>> =
Box::new(Doubler::default());
assert_eq!(boxed.batch_nan(&[1.0, 2.0]), vec![2.0, 4.0]);
assert_eq!(boxed.batch_fast(&[3.0]), vec![6.0]);
let series: Vec<f64> = (0..64).map(|i| f64::from(i % 7) * 1.25 + 2.0).collect();
let mut plain: Box<dyn Indicator<Input = f64, Output = f64>> =
Box::new(crate::Sma::new(5).unwrap());
let mut exact = vec![0.0; series.len()];
plain.batch_nan_into(&series, &mut exact);
let bits = |v: &[f64]| v.iter().map(|x| x.to_bits()).collect::<Vec<_>>();
let want = crate::Sma::new(5).unwrap().batch_nan(&series);
assert_eq!(bits(&exact), bits(&want));
plain.reset();
let mut fast = vec![0.0; series.len()];
plain.batch_fast_into(&series, &mut fast);
let want = crate::Sma::new(5).unwrap().batch_fast(&series);
assert_eq!(bits(&fast), bits(&want));
}
#[test]
fn chain_pipes_first_into_second() {
let mut c = Chain::new(Doubler::default(), Doubler::default());
assert_eq!(c.update(5.0), Some(20.0));
}
#[test]
fn chain_is_ready_only_after_both_stages_emit() {
let mut c = Chain::new(Doubler::default(), Doubler::default());
assert!(!c.is_ready());
c.update(1.0);
assert!(c.is_ready());
}
#[test]
fn chain_reset_propagates() {
let mut c = Chain::new(Doubler::default(), Doubler::default());
c.update(1.0);
assert!(c.is_ready());
c.reset();
assert!(!c.is_ready());
}
#[test]
fn chain_three_levels_via_then() {
let c = Chain::new(Doubler::default(), Doubler::default()).then(Doubler::default());
let mut c = c;
assert_eq!(c.update(1.0), Some(8.0));
}
#[test]
fn chain_accessors_and_metadata() {
let chain = Chain::new(Doubler::default(), Doubler::default());
assert_eq!(chain.first().name(), "Doubler");
assert_eq!(chain.second().name(), "Doubler");
assert_eq!(chain.first().warmup_period(), 0);
assert_eq!(chain.second().warmup_period(), 0);
assert_eq!(chain.warmup_period(), 1);
assert_eq!(chain.name(), "Chain");
}
#[test]
fn identity_helper_full_indicator_surface() {
let mut id = Identity::default();
assert_eq!(id.warmup_period(), 0);
assert_eq!(id.name(), "Identity");
assert!(!id.is_ready());
let out = id.update(42.0);
assert_eq!(out, Some(42.0));
assert!(id.is_ready());
id.reset();
assert!(!id.is_ready());
}
#[cfg(feature = "parallel")]
#[test]
fn batch_parallel_runs_independent_instances() {
let series: Vec<Vec<f64>> = vec![vec![1.0, 2.0, 3.0], vec![4.0, 5.0, 6.0]];
let out = Doubler::batch_parallel(&series, Doubler::default);
assert_eq!(out.len(), 2);
assert_eq!(out[0], vec![Some(2.0), Some(4.0), Some(6.0)]);
assert_eq!(out[1], vec![Some(8.0), Some(10.0), Some(12.0)]);
}
}