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