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