qs_backtest/strategy/
replay.rs1use chrono::NaiveDateTime;
4
5use super::{
6 AnalysisError, BarSeriesSpec, ConfiguredStrategyAdapterPreflightError, SeriesError,
7 SeriesViewError, StrategyRequirements, StrategyRuntimeError,
8};
9
10#[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#[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}