use crate::indicator::{Indicator, IndicatorOutput};
use crate::model::{Bar, BarValidationError, SeriesCapabilities};
#[derive(Debug, Clone, PartialEq)]
pub struct TimestampedOutput {
pub timestamp: i64,
pub output: Option<IndicatorOutput>,
}
pub fn run_batch<I: Indicator + ?Sized>(indicator: &mut I, bars: &[Bar]) -> Vec<TimestampedOutput> {
indicator.reset();
bars.iter()
.map(|bar| TimestampedOutput {
timestamp: bar.timestamp,
output: indicator.on_bar(bar),
})
.collect()
}
pub fn run_batch_checked<I: Indicator + ?Sized>(
indicator: &mut I,
bars: &[Bar],
) -> Result<Vec<TimestampedOutput>, (Vec<TimestampedOutput>, BarValidationError)> {
indicator.reset();
let mut series = Vec::with_capacity(bars.len());
for bar in bars {
match indicator.on_checked_bar(bar) {
Ok(output) => series.push(TimestampedOutput {
timestamp: bar.timestamp,
output,
}),
Err(err) => return Err((series, err)),
}
}
Ok(series)
}
#[derive(Debug, Clone, PartialEq)]
pub struct BatchResult {
pub series: Vec<TimestampedOutput>,
pub applicability: crate::applicability::Applicability,
}
pub fn run_batch_with_applicability<I: Indicator + ?Sized>(
name: &str,
indicator: &mut I,
bars: &[Bar],
capabilities: &SeriesCapabilities,
) -> BatchResult {
let requirements = crate::applicability::data_requirements(name);
let applicability = crate::applicability::check_applicability(&requirements, capabilities);
let series = run_batch(indicator, bars)
.into_iter()
.map(|entry| TimestampedOutput {
timestamp: entry.timestamp,
output: entry
.output
.map(|output| output.with_capabilities(*capabilities)),
})
.collect();
BatchResult {
series,
applicability,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::indicator::moving_averages::SmaEngine;
fn sample_bars(closes: &[f64]) -> Vec<Bar> {
closes
.iter()
.enumerate()
.map(|(i, &c)| {
let c = c + 100.0;
Bar::new((i as i64) * 60, c, c + 1.0, c - 1.0, c, 100.0)
})
.collect()
}
#[test]
fn test_run_batch_is_timestamp_aligned_and_warmup_aware() {
let bars = sample_bars(&[1.0, 2.0, 3.0, 4.0, 5.0]);
let mut sma = SmaEngine::new(3);
let series = run_batch(&mut sma, &bars);
assert_eq!(series.len(), bars.len());
for (entry, bar) in series.iter().zip(&bars) {
assert_eq!(entry.timestamp, bar.timestamp);
}
assert!(series[0].output.is_none());
assert!(series[1].output.is_none());
assert!(series[2].output.is_some());
assert_eq!(series[2].output.as_ref().unwrap().value, 102.0);
assert_eq!(series[4].output.as_ref().unwrap().value, 104.0);
}
#[test]
fn test_run_batch_is_deterministic_and_resets_prior_state() {
let bars = sample_bars(&[1.0, 2.0, 3.0, 4.0, 5.0]);
let mut sma = SmaEngine::new(3);
let first = run_batch(&mut sma, &bars);
let second = run_batch(&mut sma, &bars);
assert_eq!(first, second);
}
#[test]
fn test_run_batch_checked_stops_at_invalid_bar() {
let mut bars = sample_bars(&[10.0, 20.0]);
bars.push(Bar::new(120, f64::NAN, 1.0, -1.0, 1.0, 100.0));
bars.push(Bar::new(180, 3.0, 4.0, 2.0, 3.0, 100.0));
let mut sma = SmaEngine::new(2);
let result = run_batch_checked(&mut sma, &bars);
let (partial, err) = result.unwrap_err();
assert_eq!(partial.len(), 2);
assert_eq!(err, BarValidationError::NonFiniteValue);
}
fn real_volume_capabilities(volume: crate::model::VolumeKind) -> SeriesCapabilities {
use crate::model::{
ContinuityKind, LiquidityTier, PriceAdjustment, Provenance, SessionKind,
};
SeriesCapabilities {
volume,
trade_direction: false,
session: SessionKind::Regular,
continuity: ContinuityKind::SingleContract,
price_adjustment: PriceAdjustment::Raw,
provenance: Provenance::Exchange,
liquidity_tier: LiquidityTier::Deep,
}
}
#[test]
fn test_run_batch_with_applicability_is_applicable_with_real_volume() {
let bars = sample_bars(&[1.0, 2.0, 3.0, 4.0, 5.0]);
let mut sma = SmaEngine::new(3);
let capabilities = real_volume_capabilities(crate::model::VolumeKind::RealTurnover);
let result = run_batch_with_applicability("vwap", &mut sma, &bars, &capabilities);
assert_eq!(result.series.len(), bars.len());
assert_eq!(
result.applicability,
crate::applicability::Applicability::Applicable
);
}
#[test]
fn test_run_batch_with_applicability_flags_unsuitable_but_still_computes() {
let bars = sample_bars(&[1.0, 2.0, 3.0, 4.0, 5.0]);
let mut sma = SmaEngine::new(3);
let capabilities = real_volume_capabilities(crate::model::VolumeKind::Tick);
let result = run_batch_with_applicability("vwap", &mut sma, &bars, &capabilities);
assert_eq!(result.series.len(), bars.len());
assert!(matches!(
result.applicability,
crate::applicability::Applicability::Unsuitable { .. }
));
}
#[test]
fn test_run_batch_with_applicability_tags_every_output_with_capabilities() {
let bars = sample_bars(&[1.0, 2.0, 3.0, 4.0, 5.0]);
let mut sma = SmaEngine::new(3);
let capabilities = real_volume_capabilities(crate::model::VolumeKind::RealTurnover);
let result = run_batch_with_applicability("vwap", &mut sma, &bars, &capabilities);
for entry in &result.series {
if let Some(output) = &entry.output {
assert_eq!(output.series_capabilities, Some(capabilities));
}
}
assert!(result.series.iter().any(|e| e.output.is_some()));
}
#[test]
fn test_run_batch_leaves_capabilities_unset() {
let bars = sample_bars(&[1.0, 2.0, 3.0, 4.0, 5.0]);
let mut sma = SmaEngine::new(3);
let series = run_batch(&mut sma, &bars);
for entry in &series {
if let Some(output) = &entry.output {
assert_eq!(output.series_capabilities, None);
}
}
}
}