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
465 fn bar(close: &str) -> BarInput {
466 BarInput::from_close(close.parse().unwrap())
467 }
468
469 #[test]
472 fn test_warmup_contract_sma_period() {
473 let sma = Sma::new("sma10", 10).unwrap();
474 assert_eq!(sma.warmup_period(), 10);
475 }
476
477 #[test]
478 fn test_warmup_contract_rsi_period() {
479 let rsi = Rsi::new("rsi14", 14).unwrap();
480 assert_eq!(rsi.warmup_period(), 14);
481 }
482
483 #[test]
484 fn test_warmup_contract_ema_not_ready_initially() {
485 let ema = Ema::new("ema5", 5).unwrap();
486 assert!(!WarmupContract::is_ready(&ema));
487 }
488
489 #[test]
490 fn test_warmup_contract_sma_ready_after_period() {
491 let mut sma = Sma::new("sma3", 3).unwrap();
492 sma.update(&bar("10")).unwrap();
493 sma.update(&bar("20")).unwrap();
494 assert!(!WarmupContract::is_ready(&sma));
495 sma.update(&bar("30")).unwrap();
496 assert!(WarmupContract::is_ready(&sma));
497 assert_eq!(sma.bars_remaining(), 0);
498 }
499
500 #[test]
503 fn test_warmup_guard_returns_err_before_ready() {
504 let sma = Sma::new("sma5", 5).unwrap();
505 let mut guard = WarmupGuard::new(sma);
506 for _ in 0..4 {
507 let result = guard.update_checked(&bar("100"));
508 assert!(
509 matches!(result, Err(WarmupError::NotReady(_))),
510 "expected NotReady error"
511 );
512 }
513 }
514
515 #[test]
516 fn test_warmup_guard_returns_ok_after_warmup() {
517 let sma = Sma::new("sma3", 3).unwrap();
518 let mut guard = WarmupGuard::new(sma);
519 guard.update_checked(&bar("10")).ok();
520 guard.update_checked(&bar("20")).ok();
521 let result = guard.update_checked(&bar("30"));
522 assert!(result.is_ok(), "expected Ok after warmup");
523 assert!(matches!(result.unwrap(), SignalValue::Scalar(_)));
524 }
525
526 #[test]
527 fn test_warmup_guard_is_ready_tracks_correctly() {
528 let sma = Sma::new("sma2", 2).unwrap();
529 let mut guard = WarmupGuard::new(sma);
530 assert!(!guard.is_ready());
531 guard.update_checked(&bar("10")).ok();
532 assert!(!guard.is_ready());
533 guard.update_checked(&bar("20")).ok();
534 assert!(guard.is_ready());
535 }
536
537 #[test]
538 fn test_warmup_guard_bars_remaining_decrements() {
539 let sma = Sma::new("sma4", 4).unwrap();
540 let mut guard = WarmupGuard::new(sma);
541 assert_eq!(guard.bars_remaining(), 4);
542 guard.update_checked(&bar("1")).ok();
543 assert_eq!(guard.bars_remaining(), 3);
544 guard.update_checked(&bar("1")).ok();
545 assert_eq!(guard.bars_remaining(), 2);
546 }
547
548 #[test]
549 fn test_warmup_guard_bars_remaining_zero_when_ready() {
550 let sma = Sma::new("sma2", 2).unwrap();
551 let mut guard = WarmupGuard::new(sma);
552 guard.update_checked(&bar("10")).ok();
553 guard.update_checked(&bar("20")).ok();
554 assert_eq!(guard.bars_remaining(), 0);
555 }
556
557 #[test]
558 fn test_warmup_guard_reset_restarts_warmup() {
559 let sma = Sma::new("sma2", 2).unwrap();
560 let mut guard = WarmupGuard::new(sma);
561 guard.update_checked(&bar("10")).ok();
562 guard.update_checked(&bar("20")).ok();
563 assert!(guard.is_ready());
564 guard.reset();
565 assert!(!guard.is_ready());
566 assert_eq!(guard.bars_seen(), 0);
567 let result = guard.update_checked(&bar("10"));
568 assert!(matches!(result, Err(WarmupError::NotReady(_))));
569 }
570
571 #[test]
572 fn test_warmup_guard_not_ready_error_has_correct_name() {
573 let sma = Sma::new("my_sma", 5).unwrap();
574 let mut guard = WarmupGuard::new(sma);
575 match guard.update_checked(&bar("100")) {
576 Err(WarmupError::NotReady(e)) => {
577 assert_eq!(e.name, "my_sma");
578 assert_eq!(e.warmup_period, 5);
579 }
580 _ => panic!("expected NotReady"),
581 }
582 }
583
584 #[test]
585 fn test_warmup_guard_rsi_warmup_period() {
586 let rsi = Rsi::new("rsi14", 14).unwrap();
587 let guard = WarmupGuard::new(rsi);
588 assert_eq!(guard.warmup_period(), 14);
589 }
590
591 #[test]
592 fn test_warmup_guard_into_inner() {
593 let sma = Sma::new("sma3", 3).unwrap();
594 let guard = WarmupGuard::new(sma);
595 let inner = guard.into_inner();
596 assert_eq!(inner.name(), "sma3");
597 }
598
599 #[test]
602 fn test_warmup_reporter_all_warming_at_zero_bars() {
603 let reporter = WarmupReporter::new(vec![5, 14], vec!["sma5".into(), "rsi14".into()]);
604 let report = reporter.report(0);
605 assert!(!report.all_ready());
606 assert_eq!(report.warming_count(), 2);
607 assert_eq!(report.ready_count(), 0);
608 }
609
610 #[test]
611 fn test_warmup_reporter_partial_ready() {
612 let reporter = WarmupReporter::new(vec![5, 14], vec!["sma5".into(), "rsi14".into()]);
613 let report = reporter.report(5);
614 assert!(!report.all_ready());
615 assert_eq!(report.ready_count(), 1);
616 assert_eq!(report.warming_count(), 1);
617 assert!(report.statuses[0].is_ready);
618 assert!(!report.statuses[1].is_ready);
619 }
620
621 #[test]
622 fn test_warmup_reporter_all_ready() {
623 let reporter = WarmupReporter::new(vec![5, 14], vec!["sma5".into(), "rsi14".into()]);
624 let report = reporter.report(14);
625 assert!(report.all_ready());
626 assert_eq!(report.pipeline_bars_remaining(), 0);
627 }
628
629 #[test]
630 fn test_warmup_reporter_tick_advances_count() {
631 let mut reporter = WarmupReporter::new(vec![3], vec!["sma3".into()]);
632 reporter.tick();
633 reporter.tick();
634 reporter.tick();
635 assert_eq!(reporter.bars_consumed(), 3);
636 let report = reporter.report(reporter.bars_consumed());
637 assert!(report.all_ready());
638 }
639
640 #[test]
641 fn test_warmup_reporter_tick_n() {
642 let mut reporter = WarmupReporter::new(vec![10], vec!["sma10".into()]);
643 reporter.tick_n(10);
644 let report = reporter.report(reporter.bars_consumed());
645 assert!(report.statuses[0].is_ready);
646 }
647
648 #[test]
649 fn test_warmup_reporter_pipeline_bars_remaining() {
650 let reporter = WarmupReporter::new(vec![5, 14, 20], vec!["a".into(), "b".into(), "c".into()]);
651 let report = reporter.report(10);
652 assert_eq!(report.pipeline_bars_remaining(), 10);
654 }
655
656 #[test]
657 fn test_warmup_reporter_max_warmup_period() {
658 let reporter = WarmupReporter::new(vec![5, 14, 200], vec!["a".into(), "b".into(), "c".into()]);
659 assert_eq!(reporter.max_warmup_period(), 200);
660 }
661
662 #[test]
663 fn test_warmup_report_display_contains_signal_names() {
664 let reporter = WarmupReporter::new(vec![5], vec!["mysig".into()]);
665 let report = reporter.report(0);
666 let display = report.display();
667 assert!(display.contains("mysig"));
668 assert!(display.contains("WARMING"));
669 }
670
671 #[test]
672 fn test_warmup_report_display_shows_ready() {
673 let reporter = WarmupReporter::new(vec![5], vec!["mysig".into()]);
674 let report = reporter.report(5);
675 let display = report.display();
676 assert!(display.contains("READY"));
677 }
678
679 #[test]
680 fn test_signal_warmup_status_summary_ready() {
681 let status = SignalWarmupStatus {
682 name: "sma5".into(),
683 warmup_period: 5,
684 is_ready: true,
685 bars_remaining: 0,
686 };
687 assert!(status.summary().contains("READY"));
688 assert!(status.summary().contains("sma5"));
689 }
690
691 #[test]
692 fn test_signal_warmup_status_summary_warming() {
693 let status = SignalWarmupStatus {
694 name: "rsi14".into(),
695 warmup_period: 14,
696 is_ready: false,
697 bars_remaining: 7,
698 };
699 let s = status.summary();
700 assert!(s.contains("WARMING"));
701 assert!(s.contains("remaining=7"));
702 }
703
704 #[test]
705 fn test_warmup_reporter_reset() {
706 let mut reporter = WarmupReporter::new(vec![3], vec!["sma3".into()]);
707 reporter.tick_n(10);
708 reporter.reset();
709 assert_eq!(reporter.bars_consumed(), 0);
710 let report = reporter.report(reporter.bars_consumed());
711 assert!(!report.all_ready());
712 }
713
714 #[test]
715 fn test_warmup_reporter_empty_reports_all_ready() {
716 let reporter = WarmupReporter::new(vec![], vec![]);
717 let report = reporter.report(0);
718 assert!(report.all_ready());
720 assert_eq!(report.pipeline_bars_remaining(), 0);
721 }
722
723 #[test]
724 fn test_warmup_guard_period_1_immediate() {
725 let sma = Sma::new("sma1", 1).unwrap();
726 let mut guard = WarmupGuard::new(sma);
727 let result = guard.update_checked(&bar("42"));
728 assert!(result.is_ok());
729 }
730
731 #[test]
732 fn test_not_ready_error_display() {
733 let err = NotReady {
734 name: "sma10".into(),
735 warmup_period: 10,
736 bars_remaining: 5,
737 };
738 let msg = err.to_string();
739 assert!(msg.contains("sma10"));
740 assert!(msg.contains("5 bars"));
741 }
742
743 #[test]
744 fn test_warmup_guard_bars_seen_tracks_all_updates() {
745 let sma = Sma::new("sma20", 20).unwrap();
746 let mut guard = WarmupGuard::new(sma);
747 for i in 0..7 {
748 guard.update_checked(&bar("10")).ok();
749 assert_eq!(guard.bars_seen(), i + 1);
750 }
751 }
752
753 #[test]
754 fn test_warmup_contract_ema_warmup_period_matches_period() {
755 let ema = Ema::new("ema20", 20).unwrap();
756 assert_eq!(ema.warmup_period(), ema.period());
757 }
758}