use crate::error::FinError;
use crate::signals::{BarInput, Signal, SignalValue};
pub trait WarmupContract {
fn warmup_period(&self) -> usize;
fn is_ready(&self) -> bool;
fn bars_remaining(&self) -> usize;
}
impl<S: Signal> WarmupContract for S {
fn warmup_period(&self) -> usize {
<S as Signal>::period(self)
}
fn is_ready(&self) -> bool {
<S as Signal>::is_ready(self)
}
fn bars_remaining(&self) -> usize {
if <S as Signal>::is_ready(self) {
0
} else {
<S as Signal>::period(self)
}
}
}
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
#[error("Signal '{name}' not ready: {bars_remaining} bars still needed (period = {warmup_period})")]
pub struct NotReady {
pub name: String,
pub warmup_period: usize,
pub bars_remaining: usize,
}
pub struct WarmupGuard<S: Signal> {
inner: S,
bars_seen: usize,
}
impl<S: Signal> WarmupGuard<S> {
pub fn new(signal: S) -> Self {
Self { inner: signal, bars_seen: 0 }
}
pub fn update_checked(&mut self, bar: &BarInput) -> Result<SignalValue, WarmupError> {
self.bars_seen += 1;
let value = self.inner.update(bar).map_err(WarmupError::Signal)?;
match &value {
SignalValue::Unavailable => {
let period = self.inner.period();
let remaining = period.saturating_sub(self.bars_seen);
Err(WarmupError::NotReady(NotReady {
name: self.inner.name().to_owned(),
warmup_period: period,
bars_remaining: remaining,
}))
}
SignalValue::Scalar(_) => Ok(value),
}
}
pub fn is_ready(&self) -> bool {
self.inner.is_ready()
}
pub fn bars_remaining(&self) -> usize {
self.inner.period().saturating_sub(self.bars_seen)
}
pub fn warmup_period(&self) -> usize {
self.inner.period()
}
pub fn bars_seen(&self) -> usize {
self.bars_seen
}
pub fn reset(&mut self) {
self.inner.reset();
self.bars_seen = 0;
}
pub fn inner(&self) -> &S {
&self.inner
}
pub fn into_inner(self) -> S {
self.inner
}
}
#[derive(Debug, thiserror::Error)]
pub enum WarmupError {
#[error("{0}")]
NotReady(NotReady),
#[error("signal error: {0}")]
Signal(FinError),
}
#[derive(Debug, Clone, PartialEq)]
pub struct SignalWarmupStatus {
pub name: String,
pub warmup_period: usize,
pub is_ready: bool,
pub bars_remaining: usize,
}
impl SignalWarmupStatus {
pub fn summary(&self) -> String {
if self.is_ready {
format!("[READY] {} (period={})", self.name, self.warmup_period)
} else {
format!(
"[WARMING] {} (period={}, remaining={})",
self.name, self.warmup_period, self.bars_remaining
)
}
}
}
#[derive(Debug, Clone)]
pub struct WarmupReport {
pub statuses: Vec<SignalWarmupStatus>,
pub bars_consumed: usize,
}
impl WarmupReport {
pub fn all_ready(&self) -> bool {
self.statuses.iter().all(|s| s.is_ready)
}
pub fn any_warming(&self) -> bool {
self.statuses.iter().any(|s| !s.is_ready)
}
pub fn ready_count(&self) -> usize {
self.statuses.iter().filter(|s| s.is_ready).count()
}
pub fn warming_count(&self) -> usize {
self.statuses.iter().filter(|s| !s.is_ready).count()
}
pub fn pipeline_bars_remaining(&self) -> usize {
self.statuses.iter().map(|s| s.bars_remaining).max().unwrap_or(0)
}
pub fn ready_signals(&self) -> impl Iterator<Item = &SignalWarmupStatus> {
self.statuses.iter().filter(|s| s.is_ready)
}
pub fn warming_signals(&self) -> impl Iterator<Item = &SignalWarmupStatus> {
self.statuses.iter().filter(|s| !s.is_ready)
}
pub fn display(&self) -> String {
let mut lines = vec![format!(
"WarmupReport [bars_consumed={}, ready={}/{}, pipeline_remaining={}]",
self.bars_consumed,
self.ready_count(),
self.statuses.len(),
self.pipeline_bars_remaining(),
)];
for status in &self.statuses {
lines.push(format!(" {}", status.summary()));
}
lines.join("\n")
}
}
pub struct WarmupReporter {
names: Vec<String>,
periods: Vec<usize>,
bars_consumed: usize,
}
impl WarmupReporter {
pub fn new(periods: Vec<usize>, names: Vec<String>) -> Self {
debug_assert_eq!(periods.len(), names.len(), "periods and names must have equal length");
Self { names, periods, bars_consumed: 0 }
}
pub fn tick(&mut self) {
self.bars_consumed += 1;
}
pub fn tick_n(&mut self, n: usize) {
self.bars_consumed += n;
}
pub fn bars_consumed(&self) -> usize {
self.bars_consumed
}
pub fn reset(&mut self) {
self.bars_consumed = 0;
}
pub fn report(&self, bars_consumed: usize) -> WarmupReport {
let statuses = self
.names
.iter()
.zip(self.periods.iter())
.map(|(name, &period)| {
let is_ready = bars_consumed >= period;
let bars_remaining = period.saturating_sub(bars_consumed);
SignalWarmupStatus {
name: name.clone(),
warmup_period: period,
is_ready,
bars_remaining,
}
})
.collect();
WarmupReport { statuses, bars_consumed }
}
pub fn signal_count(&self) -> usize {
self.names.len()
}
pub fn max_warmup_period(&self) -> usize {
self.periods.iter().copied().max().unwrap_or(0)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::signals::indicators::{Ema, Rsi, Sma};
use rust_decimal_macros::dec;
fn bar(close: &str) -> BarInput {
BarInput::from_close(close.parse().unwrap())
}
#[test]
fn test_warmup_contract_sma_period() {
let sma = Sma::new("sma10", 10).unwrap();
assert_eq!(sma.warmup_period(), 10);
}
#[test]
fn test_warmup_contract_rsi_period() {
let rsi = Rsi::new("rsi14", 14).unwrap();
assert_eq!(rsi.warmup_period(), 14);
}
#[test]
fn test_warmup_contract_ema_not_ready_initially() {
let ema = Ema::new("ema5", 5).unwrap();
assert!(!WarmupContract::is_ready(&ema));
}
#[test]
fn test_warmup_contract_sma_ready_after_period() {
let mut sma = Sma::new("sma3", 3).unwrap();
sma.update(&bar("10")).unwrap();
sma.update(&bar("20")).unwrap();
assert!(!WarmupContract::is_ready(&sma));
sma.update(&bar("30")).unwrap();
assert!(WarmupContract::is_ready(&sma));
assert_eq!(sma.bars_remaining(), 0);
}
#[test]
fn test_warmup_guard_returns_err_before_ready() {
let sma = Sma::new("sma5", 5).unwrap();
let mut guard = WarmupGuard::new(sma);
for _ in 0..4 {
let result = guard.update_checked(&bar("100"));
assert!(
matches!(result, Err(WarmupError::NotReady(_))),
"expected NotReady error"
);
}
}
#[test]
fn test_warmup_guard_returns_ok_after_warmup() {
let sma = Sma::new("sma3", 3).unwrap();
let mut guard = WarmupGuard::new(sma);
guard.update_checked(&bar("10")).ok();
guard.update_checked(&bar("20")).ok();
let result = guard.update_checked(&bar("30"));
assert!(result.is_ok(), "expected Ok after warmup");
assert!(matches!(result.unwrap(), SignalValue::Scalar(_)));
}
#[test]
fn test_warmup_guard_is_ready_tracks_correctly() {
let sma = Sma::new("sma2", 2).unwrap();
let mut guard = WarmupGuard::new(sma);
assert!(!guard.is_ready());
guard.update_checked(&bar("10")).ok();
assert!(!guard.is_ready());
guard.update_checked(&bar("20")).ok();
assert!(guard.is_ready());
}
#[test]
fn test_warmup_guard_bars_remaining_decrements() {
let sma = Sma::new("sma4", 4).unwrap();
let mut guard = WarmupGuard::new(sma);
assert_eq!(guard.bars_remaining(), 4);
guard.update_checked(&bar("1")).ok();
assert_eq!(guard.bars_remaining(), 3);
guard.update_checked(&bar("1")).ok();
assert_eq!(guard.bars_remaining(), 2);
}
#[test]
fn test_warmup_guard_bars_remaining_zero_when_ready() {
let sma = Sma::new("sma2", 2).unwrap();
let mut guard = WarmupGuard::new(sma);
guard.update_checked(&bar("10")).ok();
guard.update_checked(&bar("20")).ok();
assert_eq!(guard.bars_remaining(), 0);
}
#[test]
fn test_warmup_guard_reset_restarts_warmup() {
let sma = Sma::new("sma2", 2).unwrap();
let mut guard = WarmupGuard::new(sma);
guard.update_checked(&bar("10")).ok();
guard.update_checked(&bar("20")).ok();
assert!(guard.is_ready());
guard.reset();
assert!(!guard.is_ready());
assert_eq!(guard.bars_seen(), 0);
let result = guard.update_checked(&bar("10"));
assert!(matches!(result, Err(WarmupError::NotReady(_))));
}
#[test]
fn test_warmup_guard_not_ready_error_has_correct_name() {
let sma = Sma::new("my_sma", 5).unwrap();
let mut guard = WarmupGuard::new(sma);
match guard.update_checked(&bar("100")) {
Err(WarmupError::NotReady(e)) => {
assert_eq!(e.name, "my_sma");
assert_eq!(e.warmup_period, 5);
}
_ => panic!("expected NotReady"),
}
}
#[test]
fn test_warmup_guard_rsi_warmup_period() {
let rsi = Rsi::new("rsi14", 14).unwrap();
let guard = WarmupGuard::new(rsi);
assert_eq!(guard.warmup_period(), 14);
}
#[test]
fn test_warmup_guard_into_inner() {
let sma = Sma::new("sma3", 3).unwrap();
let guard = WarmupGuard::new(sma);
let inner = guard.into_inner();
assert_eq!(inner.name(), "sma3");
}
#[test]
fn test_warmup_reporter_all_warming_at_zero_bars() {
let reporter = WarmupReporter::new(vec![5, 14], vec!["sma5".into(), "rsi14".into()]);
let report = reporter.report(0);
assert!(!report.all_ready());
assert_eq!(report.warming_count(), 2);
assert_eq!(report.ready_count(), 0);
}
#[test]
fn test_warmup_reporter_partial_ready() {
let reporter = WarmupReporter::new(vec![5, 14], vec!["sma5".into(), "rsi14".into()]);
let report = reporter.report(5);
assert!(!report.all_ready());
assert_eq!(report.ready_count(), 1);
assert_eq!(report.warming_count(), 1);
assert!(report.statuses[0].is_ready);
assert!(!report.statuses[1].is_ready);
}
#[test]
fn test_warmup_reporter_all_ready() {
let reporter = WarmupReporter::new(vec![5, 14], vec!["sma5".into(), "rsi14".into()]);
let report = reporter.report(14);
assert!(report.all_ready());
assert_eq!(report.pipeline_bars_remaining(), 0);
}
#[test]
fn test_warmup_reporter_tick_advances_count() {
let mut reporter = WarmupReporter::new(vec![3], vec!["sma3".into()]);
reporter.tick();
reporter.tick();
reporter.tick();
assert_eq!(reporter.bars_consumed(), 3);
let report = reporter.report(reporter.bars_consumed());
assert!(report.all_ready());
}
#[test]
fn test_warmup_reporter_tick_n() {
let mut reporter = WarmupReporter::new(vec![10], vec!["sma10".into()]);
reporter.tick_n(10);
let report = reporter.report(reporter.bars_consumed());
assert!(report.statuses[0].is_ready);
}
#[test]
fn test_warmup_reporter_pipeline_bars_remaining() {
let reporter = WarmupReporter::new(vec![5, 14, 20], vec!["a".into(), "b".into(), "c".into()]);
let report = reporter.report(10);
assert_eq!(report.pipeline_bars_remaining(), 10);
}
#[test]
fn test_warmup_reporter_max_warmup_period() {
let reporter = WarmupReporter::new(vec![5, 14, 200], vec!["a".into(), "b".into(), "c".into()]);
assert_eq!(reporter.max_warmup_period(), 200);
}
#[test]
fn test_warmup_report_display_contains_signal_names() {
let reporter = WarmupReporter::new(vec![5], vec!["mysig".into()]);
let report = reporter.report(0);
let display = report.display();
assert!(display.contains("mysig"));
assert!(display.contains("WARMING"));
}
#[test]
fn test_warmup_report_display_shows_ready() {
let reporter = WarmupReporter::new(vec![5], vec!["mysig".into()]);
let report = reporter.report(5);
let display = report.display();
assert!(display.contains("READY"));
}
#[test]
fn test_signal_warmup_status_summary_ready() {
let status = SignalWarmupStatus {
name: "sma5".into(),
warmup_period: 5,
is_ready: true,
bars_remaining: 0,
};
assert!(status.summary().contains("READY"));
assert!(status.summary().contains("sma5"));
}
#[test]
fn test_signal_warmup_status_summary_warming() {
let status = SignalWarmupStatus {
name: "rsi14".into(),
warmup_period: 14,
is_ready: false,
bars_remaining: 7,
};
let s = status.summary();
assert!(s.contains("WARMING"));
assert!(s.contains("remaining=7"));
}
#[test]
fn test_warmup_reporter_reset() {
let mut reporter = WarmupReporter::new(vec![3], vec!["sma3".into()]);
reporter.tick_n(10);
reporter.reset();
assert_eq!(reporter.bars_consumed(), 0);
let report = reporter.report(reporter.bars_consumed());
assert!(!report.all_ready());
}
#[test]
fn test_warmup_reporter_empty_reports_all_ready() {
let reporter = WarmupReporter::new(vec![], vec![]);
let report = reporter.report(0);
assert!(report.all_ready());
assert_eq!(report.pipeline_bars_remaining(), 0);
}
#[test]
fn test_warmup_guard_period_1_immediate() {
let sma = Sma::new("sma1", 1).unwrap();
let mut guard = WarmupGuard::new(sma);
let result = guard.update_checked(&bar("42"));
assert!(result.is_ok());
}
#[test]
fn test_not_ready_error_display() {
let err = NotReady {
name: "sma10".into(),
warmup_period: 10,
bars_remaining: 5,
};
let msg = err.to_string();
assert!(msg.contains("sma10"));
assert!(msg.contains("5 bars"));
}
#[test]
fn test_warmup_guard_bars_seen_tracks_all_updates() {
let sma = Sma::new("sma20", 20).unwrap();
let mut guard = WarmupGuard::new(sma);
for i in 0..7 {
guard.update_checked(&bar("10")).ok();
assert_eq!(guard.bars_seen(), i + 1);
}
}
#[test]
fn test_warmup_contract_ema_warmup_period_matches_period() {
let ema = Ema::new("ema20", 20).unwrap();
assert_eq!(ema.warmup_period(), ema.period());
}
}