Skip to main content

qs_backtest/strategy/
replay.rs

1//! Historical strategy integration with FutureQuote replay.
2
3use chrono::NaiveDateTime;
4
5use super::{
6    AnalysisError, BarSeriesSpec, ConfiguredEntryProfileError,
7    ConfiguredStrategyAdapterPreflightError, SeriesError, SeriesViewError, StrategyRequirements,
8    StrategyRuntimeError,
9};
10
11/// Errors returned while running a historical strategy through FutureQuote.
12#[derive(Debug, thiserror::Error)]
13pub enum StrategyReplayError<FeedError, StrategyError> {
14    #[error("market-data stream failed: {0}")]
15    Feed(FeedError),
16    #[error("strategy replay was cancelled")]
17    Cancelled,
18    #[error("strategy replay input is invalid: {0}")]
19    Input(#[from] StrategyReplayInputError),
20    #[error("historical series failed: {0}")]
21    Series(#[from] SeriesError),
22    #[error("historical series view failed: {0}")]
23    SeriesView(#[from] SeriesViewError),
24    #[error("historical analysis failed: {0}")]
25    Analysis(#[from] AnalysisError),
26    #[error("strategy callback failed: {0}")]
27    Strategy(StrategyError),
28    #[error("strategy output failed validation: {0}")]
29    Runtime(#[from] StrategyRuntimeError),
30    #[error("strategy emitted economic signals during warmup at {timestamp}")]
31    WarmupSignals { timestamp: NaiveDateTime },
32    #[error("generated signal {signal_index} is invalid: {reason}")]
33    InvalidGeneratedSignal { signal_index: usize, reason: String },
34    #[error(
35        "strategy requires tick execution but received a primary bar for {symbol} at {timestamp}"
36    )]
37    TickExecutionRequired {
38        symbol: String,
39        timestamp: NaiveDateTime,
40    },
41}
42
43/// Preflight failures for strategy series and replay configuration.
44#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
45pub enum StrategyReplayInputError {
46    #[error("FutureQuote configuration is invalid: {0}")]
47    FutureQuote(String),
48    #[error("management profile is invalid: {0}")]
49    ManagementProfile(String),
50    #[error("configured strategy entry cannot be routed to a management profile: {0}")]
51    ConfiguredEntryProfile(#[from] ConfiguredEntryProfileError),
52    #[error("configured historical adapter is incompatible with replay limits: {0}")]
53    ConfiguredAdapter(#[from] ConfiguredStrategyAdapterPreflightError),
54    #[error("series '{series_id}' is required but was not supplied")]
55    MissingSeries { series_id: String },
56    #[error("series '{series_id}' does not match the strategy requirement")]
57    ConflictingSeries { series_id: String },
58    #[error("series '{series_id}' was supplied but is not declared by the strategy")]
59    UndeclaredSeries { series_id: String },
60}
61
62pub(crate) fn validate_series_specs(
63    requirements: &StrategyRequirements,
64    specs: &[BarSeriesSpec],
65) -> Result<(), StrategyReplayInputError> {
66    for requirement in requirements.series() {
67        let Some(spec) = specs
68            .iter()
69            .find(|spec| spec.requirement().id() == requirement.id())
70        else {
71            return Err(StrategyReplayInputError::MissingSeries {
72                series_id: requirement.id().to_string(),
73            });
74        };
75        if spec.requirement() != requirement {
76            return Err(StrategyReplayInputError::ConflictingSeries {
77                series_id: requirement.id().to_string(),
78            });
79        }
80    }
81    for spec in specs {
82        if !requirements
83            .series()
84            .iter()
85            .any(|requirement| requirement.id() == spec.requirement().id())
86        {
87            return Err(StrategyReplayInputError::UndeclaredSeries {
88                series_id: spec.requirement().id().to_string(),
89            });
90        }
91    }
92    Ok(())
93}