1#![deny(missing_docs)]
9#![deny(rustdoc::broken_intra_doc_links)]
10
11use serde::{Deserialize, Serialize};
12
13#[async_trait::async_trait]
15pub trait BacktestStore: Send + Sync {
16 async fn save_definition(
18 &self,
19 context: &af_context::RequestContext,
20 definition: BacktestDefinition,
21 ) -> Result<(), BacktestStoreError>;
22 async fn start_run(
24 &self,
25 context: &af_context::RequestContext,
26 definition_id: &af_context::BacktestDefinitionId,
27 input_snapshot: serde_json::Value,
28 ) -> Result<af_context::BacktestRunId, BacktestStoreError>;
29 async fn complete_run(
31 &self,
32 context: &af_context::RequestContext,
33 run_id: &af_context::BacktestRunId,
34 report: serde_json::Value,
35 max_bytes: usize,
36 ) -> Result<(), BacktestStoreError>;
37 async fn fail_run(
39 &self,
40 context: &af_context::RequestContext,
41 run_id: &af_context::BacktestRunId,
42 message: &str,
43 ) -> Result<(), BacktestStoreError>;
44}
45
46#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
48pub struct BacktestDefinition {
49 pub id: af_context::BacktestDefinitionId,
51 pub name: String,
53 pub model_version: String,
55 pub config: serde_json::Value,
57}
58
59#[derive(Debug, thiserror::Error, PartialEq, Eq)]
61pub enum BacktestStoreError {
62 #[error("backtest store unavailable: {0}")]
64 Unavailable(String),
65 #[error("backtest record not found")]
67 NotFound,
68 #[error("invalid backtest state: {0}")]
70 Invalid(String),
71 #[error("report size {actual} exceeds maximum {maximum}")]
73 ReportTooLarge {
74 actual: usize,
76 maximum: usize,
78 },
79}
80
81pub trait BacktestModel {
83 type Error: std::error::Error + Send + Sync + 'static;
85 type Event;
87 type State: Clone;
89 type Action: Clone;
91
92 fn initial_state(&self) -> Self::State;
94 fn initial_value(&self, state: &Self::State) -> f64;
96 fn timestamp(&self, event: &Self::Event) -> i64;
98
99 fn decide(
101 &mut self,
102 event: &Self::Event,
103 state: &Self::State,
104 ) -> Result<Vec<Self::Action>, Self::Error>;
105
106 fn apply(
108 &mut self,
109 event: &Self::Event,
110 action: &Self::Action,
111 state: &mut Self::State,
112 ) -> Result<(), Self::Error>;
113
114 fn value(&self, event: &Self::Event, state: &Self::State) -> f64;
116}
117
118#[derive(Debug, Clone, Copy, PartialEq, Eq)]
120pub struct BacktestConfig {
121 pub max_events: usize,
123 pub capture_every: usize,
125}
126
127impl Default for BacktestConfig {
128 fn default() -> Self {
129 Self {
130 max_events: 1_000_000,
131 capture_every: 1,
132 }
133 }
134}
135
136#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
138pub struct ValuePoint {
139 pub timestamp: i64,
141 pub value: f64,
143}
144
145#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
147pub struct ReplayStep<A> {
148 pub event_index: usize,
150 pub timestamp: i64,
152 pub actions: Vec<A>,
154 pub value: f64,
156}
157
158#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
160pub struct BacktestSummary {
161 pub events_processed: usize,
163 pub actions_applied: usize,
165 pub initial_value: f64,
167 pub final_value: f64,
169 pub total_return: f64,
171 pub max_drawdown: f64,
173}
174
175#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
177pub struct BacktestReport<S, A> {
178 pub summary: BacktestSummary,
180 pub final_state: S,
182 pub value_curve: Vec<ValuePoint>,
184 pub steps: Vec<ReplayStep<A>>,
186}
187
188#[derive(Debug, Clone, Copy, PartialEq, Eq)]
190pub struct ProjectionPolicy {
191 pub max_curve_points: usize,
193 pub max_steps: usize,
195}
196
197impl Default for ProjectionPolicy {
198 fn default() -> Self {
199 Self {
200 max_curve_points: 80,
201 max_steps: 24,
202 }
203 }
204}
205
206#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
208pub struct BacktestProjection<A> {
209 pub summary: BacktestSummary,
211 pub value_curve: Vec<ValuePoint>,
213 pub steps: Vec<ReplayStep<A>>,
215 pub total_curve_points: usize,
217 pub total_steps: usize,
219}
220
221impl<S, A: Clone> BacktestReport<S, A> {
222 pub fn project(
224 &self,
225 policy: ProjectionPolicy,
226 ) -> Result<BacktestProjection<A>, BacktestError> {
227 if policy.max_curve_points < 2 {
228 return Err(BacktestError::InvalidProjection(
229 "max_curve_points must be at least 2".into(),
230 ));
231 }
232 if policy.max_steps == 0 {
233 return Err(BacktestError::InvalidProjection(
234 "max_steps must be greater than 0".into(),
235 ));
236 }
237 Ok(BacktestProjection {
238 summary: self.summary.clone(),
239 value_curve: sample_evenly(&self.value_curve, policy.max_curve_points),
240 steps: sample_evenly(&self.steps, policy.max_steps),
241 total_curve_points: self.value_curve.len(),
242 total_steps: self.steps.len(),
243 })
244 }
245}
246
247#[derive(Debug, thiserror::Error, PartialEq)]
249pub enum BacktestError {
250 #[error("backtest requires at least one historical event")]
252 EmptyEvents,
253 #[error("max_events must be greater than 0")]
255 InvalidEventLimit,
256 #[error("capture_every must be greater than 0")]
258 InvalidCaptureRate,
259 #[error("event count {actual} exceeds configured maximum {maximum}")]
261 EventLimitExceeded {
262 actual: usize,
264 maximum: usize,
266 },
267 #[error("initial value must be finite and greater than zero, got {0}")]
269 InvalidInitialValue(f64),
270 #[error("event timestamps are out of order at index {index}: {previous} > {current}")]
272 NonMonotonicTimestamp {
273 index: usize,
275 previous: i64,
277 current: i64,
279 },
280 #[error("decision failed at event {index}: {message}")]
282 Decision {
283 index: usize,
285 message: String,
287 },
288 #[error("state transition failed at event {index}, action {action_index}: {message}")]
290 Transition {
291 index: usize,
293 action_index: usize,
295 message: String,
297 },
298 #[error("model returned a non-finite value at event {index}: {value}")]
300 NonFiniteValue {
301 index: usize,
303 value: f64,
305 },
306 #[error("invalid projection policy: {0}")]
308 InvalidProjection(String),
309 #[error("backtest cancelled")]
311 Cancelled,
312 #[error("backtest deadline exceeded")]
314 DeadlineExceeded,
315}
316
317pub fn run_backtest<M: BacktestModel>(
320 model: &mut M,
321 events: &[M::Event],
322 config: BacktestConfig,
323) -> Result<BacktestReport<M::State, M::Action>, BacktestError> {
324 if events.is_empty() {
325 return Err(BacktestError::EmptyEvents);
326 }
327 if config.max_events == 0 {
328 return Err(BacktestError::InvalidEventLimit);
329 }
330 if config.capture_every == 0 {
331 return Err(BacktestError::InvalidCaptureRate);
332 }
333 if events.len() > config.max_events {
334 return Err(BacktestError::EventLimitExceeded {
335 actual: events.len(),
336 maximum: config.max_events,
337 });
338 }
339
340 let timestamps = events
341 .iter()
342 .map(|event| model.timestamp(event))
343 .collect::<Vec<_>>();
344 for (index, pair) in timestamps.windows(2).enumerate() {
345 if pair[1] < pair[0] {
346 return Err(BacktestError::NonMonotonicTimestamp {
347 index: index + 1,
348 previous: pair[0],
349 current: pair[1],
350 });
351 }
352 }
353
354 let mut state = model.initial_state();
355 let initial_value = model.initial_value(&state);
356 if !initial_value.is_finite() || initial_value <= 0.0 {
357 return Err(BacktestError::InvalidInitialValue(initial_value));
358 }
359
360 let mut value_curve = Vec::with_capacity(events.len());
361 let mut steps = Vec::with_capacity(events.len().div_ceil(config.capture_every));
362 let mut actions_applied = 0;
363
364 for (index, event) in events.iter().enumerate() {
365 let timestamp = timestamps[index];
366
367 let actions = model
368 .decide(event, &state)
369 .map_err(|error| BacktestError::Decision {
370 index,
371 message: error.to_string(),
372 })?;
373 for (action_index, action) in actions.iter().enumerate() {
374 model
375 .apply(event, action, &mut state)
376 .map_err(|error| BacktestError::Transition {
377 index,
378 action_index,
379 message: error.to_string(),
380 })?;
381 actions_applied += 1;
382 }
383
384 let value = model.value(event, &state);
385 if !value.is_finite() {
386 return Err(BacktestError::NonFiniteValue { index, value });
387 }
388 value_curve.push(ValuePoint { timestamp, value });
389 if index % config.capture_every == 0 || index + 1 == events.len() {
390 steps.push(ReplayStep {
391 event_index: index,
392 timestamp,
393 actions,
394 value,
395 });
396 }
397 }
398
399 let final_value = value_curve
400 .last()
401 .map_or(initial_value, |point| point.value);
402 let summary = BacktestSummary {
403 events_processed: events.len(),
404 actions_applied,
405 initial_value,
406 final_value,
407 total_return: (final_value / initial_value) - 1.0,
408 max_drawdown: max_drawdown(initial_value, &value_curve),
409 };
410 Ok(BacktestReport {
411 summary,
412 final_state: state,
413 value_curve,
414 steps,
415 })
416}
417
418pub fn run_backtest_with_control<M: BacktestModel>(
421 model: &mut M,
422 events: &[M::Event],
423 config: BacktestConfig,
424 cancellation: &tokio_util::sync::CancellationToken,
425 deadline: std::time::Instant,
426) -> Result<BacktestReport<M::State, M::Action>, BacktestError> {
427 if cancellation.is_cancelled() {
428 return Err(BacktestError::Cancelled);
429 }
430 if std::time::Instant::now() >= deadline {
431 return Err(BacktestError::DeadlineExceeded);
432 }
433 let mut checked = CheckedModel {
434 inner: model,
435 cancellation,
436 deadline,
437 };
438 run_backtest(&mut checked, events, config).map_err(|error| match &error {
439 BacktestError::Decision { message, .. } | BacktestError::Transition { message, .. }
440 if message == "backtest cancelled" =>
441 {
442 BacktestError::Cancelled
443 }
444 BacktestError::Decision { message, .. } | BacktestError::Transition { message, .. }
445 if message == "backtest deadline exceeded" =>
446 {
447 BacktestError::DeadlineExceeded
448 }
449 _ => error,
450 })
451}
452
453struct CheckedModel<'a, M> {
454 inner: &'a mut M,
455 cancellation: &'a tokio_util::sync::CancellationToken,
456 deadline: std::time::Instant,
457}
458
459impl<M: BacktestModel> BacktestModel for CheckedModel<'_, M> {
460 type Error = ControlledModelError<M::Error>;
461 type Event = M::Event;
462 type State = M::State;
463 type Action = M::Action;
464
465 fn initial_state(&self) -> Self::State {
466 self.inner.initial_state()
467 }
468 fn initial_value(&self, state: &Self::State) -> f64 {
469 self.inner.initial_value(state)
470 }
471 fn timestamp(&self, event: &Self::Event) -> i64 {
472 self.inner.timestamp(event)
473 }
474 fn decide(
475 &mut self,
476 event: &Self::Event,
477 state: &Self::State,
478 ) -> Result<Vec<Self::Action>, Self::Error> {
479 if self.cancellation.is_cancelled() {
480 return Err(ControlledModelError::Cancelled);
481 }
482 if std::time::Instant::now() >= self.deadline {
483 return Err(ControlledModelError::DeadlineExceeded);
484 }
485 self.inner
486 .decide(event, state)
487 .map_err(ControlledModelError::Model)
488 }
489 fn apply(
490 &mut self,
491 event: &Self::Event,
492 action: &Self::Action,
493 state: &mut Self::State,
494 ) -> Result<(), Self::Error> {
495 self.inner
496 .apply(event, action, state)
497 .map_err(ControlledModelError::Model)
498 }
499 fn value(&self, event: &Self::Event, state: &Self::State) -> f64 {
500 self.inner.value(event, state)
501 }
502}
503
504#[derive(Debug, thiserror::Error)]
505enum ControlledModelError<E: std::error::Error> {
506 #[error("backtest cancelled")]
507 Cancelled,
508 #[error("backtest deadline exceeded")]
509 DeadlineExceeded,
510 #[error(transparent)]
511 Model(E),
512}
513
514fn max_drawdown(initial_value: f64, curve: &[ValuePoint]) -> f64 {
515 let mut peak = initial_value;
516 let mut maximum = 0.0_f64;
517 for point in curve {
518 peak = peak.max(point.value);
519 if peak > 0.0 {
520 maximum = maximum.max((peak - point.value) / peak);
521 }
522 }
523 maximum
524}
525
526fn sample_evenly<T: Clone>(items: &[T], maximum: usize) -> Vec<T> {
527 if items.len() <= maximum {
528 return items.to_vec();
529 }
530 if maximum == 1 {
531 return vec![items[items.len() - 1].clone()];
532 }
533 (0..maximum)
534 .map(|slot| {
535 let index = slot * (items.len() - 1) / (maximum - 1);
536 items[index].clone()
537 })
538 .collect()
539}