1use crate::error::FinError;
45use crate::signals::{BarInput, Signal, SignalValue};
46
47pub trait WarmupContract {
55 fn warmup_period(&self) -> usize;
60
61 fn is_ready(&self) -> bool;
66
67 fn bars_remaining(&self) -> usize;
71}
72
73impl<S: Signal> WarmupContract for S {
81 fn warmup_period(&self) -> usize {
82 <S as Signal>::period(self)
84 }
85
86 fn is_ready(&self) -> bool {
87 <S as Signal>::is_ready(self)
89 }
90
91 fn bars_remaining(&self) -> usize {
92 if <S as Signal>::is_ready(self) {
93 0
94 } else {
95 <S as Signal>::period(self)
99 }
100 }
101}
102
103#[derive(Debug, Clone, PartialEq, thiserror::Error)]
107#[error("Signal '{name}' not ready: {bars_remaining} bars still needed (period = {warmup_period})")]
108pub struct NotReady {
109 pub name: String,
111 pub warmup_period: usize,
113 pub bars_remaining: usize,
115}
116
117pub struct WarmupGuard<S: Signal> {
144 inner: S,
145 bars_seen: usize,
147}
148
149impl<S: Signal> WarmupGuard<S> {
150 pub fn new(signal: S) -> Self {
152 Self { inner: signal, bars_seen: 0 }
153 }
154
155 pub fn update_checked(&mut self, bar: &BarInput) -> Result<SignalValue, WarmupError> {
165 self.bars_seen += 1;
166 let value = self.inner.update(bar).map_err(WarmupError::Signal)?;
167 match &value {
168 SignalValue::Unavailable => {
169 let period = self.inner.period();
170 let remaining = period.saturating_sub(self.bars_seen);
171 Err(WarmupError::NotReady(NotReady {
172 name: self.inner.name().to_owned(),
173 warmup_period: period,
174 bars_remaining: remaining,
175 }))
176 }
177 SignalValue::Scalar(_) => Ok(value),
178 }
179 }
180
181 pub fn is_ready(&self) -> bool {
183 self.inner.is_ready()
184 }
185
186 pub fn bars_remaining(&self) -> usize {
190 self.inner.period().saturating_sub(self.bars_seen)
191 }
192
193 pub fn warmup_period(&self) -> usize {
195 self.inner.period()
196 }
197
198 pub fn bars_seen(&self) -> usize {
200 self.bars_seen
201 }
202
203 pub fn reset(&mut self) {
205 self.inner.reset();
206 self.bars_seen = 0;
207 }
208
209 pub fn inner(&self) -> &S {
211 &self.inner
212 }
213
214 pub fn into_inner(self) -> S {
216 self.inner
217 }
218}
219
220#[derive(Debug, thiserror::Error)]
224pub enum WarmupError {
225 #[error("{0}")]
227 NotReady(NotReady),
228
229 #[error("signal error: {0}")]
231 Signal(FinError),
232}
233
234#[derive(Debug, Clone, PartialEq)]
238pub struct SignalWarmupStatus {
239 pub name: String,
241 pub warmup_period: usize,
243 pub is_ready: bool,
245 pub bars_remaining: usize,
247}
248
249impl SignalWarmupStatus {
250 pub fn summary(&self) -> String {
252 if self.is_ready {
253 format!("[READY] {} (period={})", self.name, self.warmup_period)
254 } else {
255 format!(
256 "[WARMING] {} (period={}, remaining={})",
257 self.name, self.warmup_period, self.bars_remaining
258 )
259 }
260 }
261}
262
263#[derive(Debug, Clone)]
285pub struct WarmupReport {
286 pub statuses: Vec<SignalWarmupStatus>,
288 pub bars_consumed: usize,
290}
291
292impl WarmupReport {
293 pub fn all_ready(&self) -> bool {
295 self.statuses.iter().all(|s| s.is_ready)
296 }
297
298 pub fn any_warming(&self) -> bool {
300 self.statuses.iter().any(|s| !s.is_ready)
301 }
302
303 pub fn ready_count(&self) -> usize {
305 self.statuses.iter().filter(|s| s.is_ready).count()
306 }
307
308 pub fn warming_count(&self) -> usize {
310 self.statuses.iter().filter(|s| !s.is_ready).count()
311 }
312
313 pub fn pipeline_bars_remaining(&self) -> usize {
318 self.statuses.iter().map(|s| s.bars_remaining).max().unwrap_or(0)
319 }
320
321 pub fn ready_signals(&self) -> impl Iterator<Item = &SignalWarmupStatus> {
323 self.statuses.iter().filter(|s| s.is_ready)
324 }
325
326 pub fn warming_signals(&self) -> impl Iterator<Item = &SignalWarmupStatus> {
328 self.statuses.iter().filter(|s| !s.is_ready)
329 }
330
331 pub fn display(&self) -> String {
333 let mut lines = vec![format!(
334 "WarmupReport [bars_consumed={}, ready={}/{}, pipeline_remaining={}]",
335 self.bars_consumed,
336 self.ready_count(),
337 self.statuses.len(),
338 self.pipeline_bars_remaining(),
339 )];
340 for status in &self.statuses {
341 lines.push(format!(" {}", status.summary()));
342 }
343 lines.join("\n")
344 }
345}
346
347pub struct WarmupReporter {
377 names: Vec<String>,
379 periods: Vec<usize>,
381 bars_consumed: usize,
383}
384
385impl WarmupReporter {
386 pub fn new(periods: Vec<usize>, names: Vec<String>) -> Self {
394 debug_assert_eq!(periods.len(), names.len(), "periods and names must have equal length");
395 Self { names, periods, bars_consumed: 0 }
396 }
397
398 pub fn tick(&mut self) {
403 self.bars_consumed += 1;
404 }
405
406 pub fn tick_n(&mut self, n: usize) {
408 self.bars_consumed += n;
409 }
410
411 pub fn bars_consumed(&self) -> usize {
413 self.bars_consumed
414 }
415
416 pub fn reset(&mut self) {
418 self.bars_consumed = 0;
419 }
420
421 pub fn report(&self, bars_consumed: usize) -> WarmupReport {
426 let statuses = self
427 .names
428 .iter()
429 .zip(self.periods.iter())
430 .map(|(name, &period)| {
431 let is_ready = bars_consumed >= period;
432 let bars_remaining = period.saturating_sub(bars_consumed);
433 SignalWarmupStatus {
434 name: name.clone(),
435 warmup_period: period,
436 is_ready,
437 bars_remaining,
438 }
439 })
440 .collect();
441 WarmupReport { statuses, bars_consumed }
442 }
443
444 pub fn signal_count(&self) -> usize {
446 self.names.len()
447 }
448
449 pub fn max_warmup_period(&self) -> usize {
454 self.periods.iter().copied().max().unwrap_or(0)
455 }
456}
457
458#[cfg(test)]
461mod tests {
462 use super::*;
463 use crate::signals::indicators::{Ema, Rsi, Sma};
464 use rust_decimal_macros::dec;
465
466 fn bar(close: &str) -> BarInput {
467 BarInput::from_close(close.parse().unwrap())
468 }
469
470 #[test]
473 fn test_warmup_contract_sma_period() {
474 let sma = Sma::new("sma10", 10).unwrap();
475 assert_eq!(sma.warmup_period(), 10);
476 }
477
478 #[test]
479 fn test_warmup_contract_rsi_period() {
480 let rsi = Rsi::new("rsi14", 14).unwrap();
481 assert_eq!(rsi.warmup_period(), 14);
482 }
483
484 #[test]
485 fn test_warmup_contract_ema_not_ready_initially() {
486 let ema = Ema::new("ema5", 5).unwrap();
487 assert!(!WarmupContract::is_ready(&ema));
488 }
489
490 #[test]
491 fn test_warmup_contract_sma_ready_after_period() {
492 let mut sma = Sma::new("sma3", 3).unwrap();
493 sma.update(&bar("10")).unwrap();
494 sma.update(&bar("20")).unwrap();
495 assert!(!WarmupContract::is_ready(&sma));
496 sma.update(&bar("30")).unwrap();
497 assert!(WarmupContract::is_ready(&sma));
498 assert_eq!(sma.bars_remaining(), 0);
499 }
500
501 #[test]
504 fn test_warmup_guard_returns_err_before_ready() {
505 let sma = Sma::new("sma5", 5).unwrap();
506 let mut guard = WarmupGuard::new(sma);
507 for _ in 0..4 {
508 let result = guard.update_checked(&bar("100"));
509 assert!(
510 matches!(result, Err(WarmupError::NotReady(_))),
511 "expected NotReady error"
512 );
513 }
514 }
515
516 #[test]
517 fn test_warmup_guard_returns_ok_after_warmup() {
518 let sma = Sma::new("sma3", 3).unwrap();
519 let mut guard = WarmupGuard::new(sma);
520 guard.update_checked(&bar("10")).ok();
521 guard.update_checked(&bar("20")).ok();
522 let result = guard.update_checked(&bar("30"));
523 assert!(result.is_ok(), "expected Ok after warmup");
524 assert!(matches!(result.unwrap(), SignalValue::Scalar(_)));
525 }
526
527 #[test]
528 fn test_warmup_guard_is_ready_tracks_correctly() {
529 let sma = Sma::new("sma2", 2).unwrap();
530 let mut guard = WarmupGuard::new(sma);
531 assert!(!guard.is_ready());
532 guard.update_checked(&bar("10")).ok();
533 assert!(!guard.is_ready());
534 guard.update_checked(&bar("20")).ok();
535 assert!(guard.is_ready());
536 }
537
538 #[test]
539 fn test_warmup_guard_bars_remaining_decrements() {
540 let sma = Sma::new("sma4", 4).unwrap();
541 let mut guard = WarmupGuard::new(sma);
542 assert_eq!(guard.bars_remaining(), 4);
543 guard.update_checked(&bar("1")).ok();
544 assert_eq!(guard.bars_remaining(), 3);
545 guard.update_checked(&bar("1")).ok();
546 assert_eq!(guard.bars_remaining(), 2);
547 }
548
549 #[test]
550 fn test_warmup_guard_bars_remaining_zero_when_ready() {
551 let sma = Sma::new("sma2", 2).unwrap();
552 let mut guard = WarmupGuard::new(sma);
553 guard.update_checked(&bar("10")).ok();
554 guard.update_checked(&bar("20")).ok();
555 assert_eq!(guard.bars_remaining(), 0);
556 }
557
558 #[test]
559 fn test_warmup_guard_reset_restarts_warmup() {
560 let sma = Sma::new("sma2", 2).unwrap();
561 let mut guard = WarmupGuard::new(sma);
562 guard.update_checked(&bar("10")).ok();
563 guard.update_checked(&bar("20")).ok();
564 assert!(guard.is_ready());
565 guard.reset();
566 assert!(!guard.is_ready());
567 assert_eq!(guard.bars_seen(), 0);
568 let result = guard.update_checked(&bar("10"));
569 assert!(matches!(result, Err(WarmupError::NotReady(_))));
570 }
571
572 #[test]
573 fn test_warmup_guard_not_ready_error_has_correct_name() {
574 let sma = Sma::new("my_sma", 5).unwrap();
575 let mut guard = WarmupGuard::new(sma);
576 match guard.update_checked(&bar("100")) {
577 Err(WarmupError::NotReady(e)) => {
578 assert_eq!(e.name, "my_sma");
579 assert_eq!(e.warmup_period, 5);
580 }
581 _ => panic!("expected NotReady"),
582 }
583 }
584
585 #[test]
586 fn test_warmup_guard_rsi_warmup_period() {
587 let rsi = Rsi::new("rsi14", 14).unwrap();
588 let guard = WarmupGuard::new(rsi);
589 assert_eq!(guard.warmup_period(), 14);
590 }
591
592 #[test]
593 fn test_warmup_guard_into_inner() {
594 let sma = Sma::new("sma3", 3).unwrap();
595 let guard = WarmupGuard::new(sma);
596 let inner = guard.into_inner();
597 assert_eq!(inner.name(), "sma3");
598 }
599
600 #[test]
603 fn test_warmup_reporter_all_warming_at_zero_bars() {
604 let reporter = WarmupReporter::new(vec![5, 14], vec!["sma5".into(), "rsi14".into()]);
605 let report = reporter.report(0);
606 assert!(!report.all_ready());
607 assert_eq!(report.warming_count(), 2);
608 assert_eq!(report.ready_count(), 0);
609 }
610
611 #[test]
612 fn test_warmup_reporter_partial_ready() {
613 let reporter = WarmupReporter::new(vec![5, 14], vec!["sma5".into(), "rsi14".into()]);
614 let report = reporter.report(5);
615 assert!(!report.all_ready());
616 assert_eq!(report.ready_count(), 1);
617 assert_eq!(report.warming_count(), 1);
618 assert!(report.statuses[0].is_ready);
619 assert!(!report.statuses[1].is_ready);
620 }
621
622 #[test]
623 fn test_warmup_reporter_all_ready() {
624 let reporter = WarmupReporter::new(vec![5, 14], vec!["sma5".into(), "rsi14".into()]);
625 let report = reporter.report(14);
626 assert!(report.all_ready());
627 assert_eq!(report.pipeline_bars_remaining(), 0);
628 }
629
630 #[test]
631 fn test_warmup_reporter_tick_advances_count() {
632 let mut reporter = WarmupReporter::new(vec![3], vec!["sma3".into()]);
633 reporter.tick();
634 reporter.tick();
635 reporter.tick();
636 assert_eq!(reporter.bars_consumed(), 3);
637 let report = reporter.report(reporter.bars_consumed());
638 assert!(report.all_ready());
639 }
640
641 #[test]
642 fn test_warmup_reporter_tick_n() {
643 let mut reporter = WarmupReporter::new(vec![10], vec!["sma10".into()]);
644 reporter.tick_n(10);
645 let report = reporter.report(reporter.bars_consumed());
646 assert!(report.statuses[0].is_ready);
647 }
648
649 #[test]
650 fn test_warmup_reporter_pipeline_bars_remaining() {
651 let reporter = WarmupReporter::new(vec![5, 14, 20], vec!["a".into(), "b".into(), "c".into()]);
652 let report = reporter.report(10);
653 assert_eq!(report.pipeline_bars_remaining(), 10);
655 }
656
657 #[test]
658 fn test_warmup_reporter_max_warmup_period() {
659 let reporter = WarmupReporter::new(vec![5, 14, 200], vec!["a".into(), "b".into(), "c".into()]);
660 assert_eq!(reporter.max_warmup_period(), 200);
661 }
662
663 #[test]
664 fn test_warmup_report_display_contains_signal_names() {
665 let reporter = WarmupReporter::new(vec![5], vec!["mysig".into()]);
666 let report = reporter.report(0);
667 let display = report.display();
668 assert!(display.contains("mysig"));
669 assert!(display.contains("WARMING"));
670 }
671
672 #[test]
673 fn test_warmup_report_display_shows_ready() {
674 let reporter = WarmupReporter::new(vec![5], vec!["mysig".into()]);
675 let report = reporter.report(5);
676 let display = report.display();
677 assert!(display.contains("READY"));
678 }
679
680 #[test]
681 fn test_signal_warmup_status_summary_ready() {
682 let status = SignalWarmupStatus {
683 name: "sma5".into(),
684 warmup_period: 5,
685 is_ready: true,
686 bars_remaining: 0,
687 };
688 assert!(status.summary().contains("READY"));
689 assert!(status.summary().contains("sma5"));
690 }
691
692 #[test]
693 fn test_signal_warmup_status_summary_warming() {
694 let status = SignalWarmupStatus {
695 name: "rsi14".into(),
696 warmup_period: 14,
697 is_ready: false,
698 bars_remaining: 7,
699 };
700 let s = status.summary();
701 assert!(s.contains("WARMING"));
702 assert!(s.contains("remaining=7"));
703 }
704
705 #[test]
706 fn test_warmup_reporter_reset() {
707 let mut reporter = WarmupReporter::new(vec![3], vec!["sma3".into()]);
708 reporter.tick_n(10);
709 reporter.reset();
710 assert_eq!(reporter.bars_consumed(), 0);
711 let report = reporter.report(reporter.bars_consumed());
712 assert!(!report.all_ready());
713 }
714
715 #[test]
716 fn test_warmup_reporter_empty_reports_all_ready() {
717 let reporter = WarmupReporter::new(vec![], vec![]);
718 let report = reporter.report(0);
719 assert!(report.all_ready());
721 assert_eq!(report.pipeline_bars_remaining(), 0);
722 }
723
724 #[test]
725 fn test_warmup_guard_period_1_immediate() {
726 let sma = Sma::new("sma1", 1).unwrap();
727 let mut guard = WarmupGuard::new(sma);
728 let result = guard.update_checked(&bar("42"));
729 assert!(result.is_ok());
730 }
731
732 #[test]
733 fn test_not_ready_error_display() {
734 let err = NotReady {
735 name: "sma10".into(),
736 warmup_period: 10,
737 bars_remaining: 5,
738 };
739 let msg = err.to_string();
740 assert!(msg.contains("sma10"));
741 assert!(msg.contains("5 bars"));
742 }
743
744 #[test]
745 fn test_warmup_guard_bars_seen_tracks_all_updates() {
746 let sma = Sma::new("sma20", 20).unwrap();
747 let mut guard = WarmupGuard::new(sma);
748 for i in 0..7 {
749 guard.update_checked(&bar("10")).ok();
750 assert_eq!(guard.bars_seen(), i + 1);
751 }
752 }
753
754 #[test]
755 fn test_warmup_contract_ema_warmup_period_matches_period() {
756 let ema = Ema::new("ema20", 20).unwrap();
757 assert_eq!(ema.warmup_period(), ema.period());
758 }
759}