Skip to main content

wickra_backtest_core/
lib.rs

1//! # wickra-backtest-core
2//!
3//! Streaming-native, event-driven backtest engine built on the
4//! [`wickra-core`](https://crates.io/crates/wickra-core) indicator kernels.
5//!
6//! The engine is **feed-agnostic**: it consumes a stream of bars and a
7//! data-driven [`StrategySpec`], evaluates entry/exit rules over the exact same
8//! O(1) indicator updates that power live Wickra, and produces a
9//! [`BacktestReport`]. Because the indicator math is identical to live, and the
10//! strategy is data (JSON) rather than code, a backtest and a live run over the
11//! same spec produce identical signals — across all Wickra language bindings.
12//!
13//! This crate is the shared engine core, and `wickra-backtest` is the facade over
14//! it. Live execution is not a second crate: it is the same engine driven one bar
15//! at a time through [`StreamingBacktest`] instead of over a stored series, so
16//! "backtest == live" holds because there is one implementation, not because two
17//! of them agree.
18
19// docs.rs builds on nightly with --cfg docsrs, which makes rustdoc annotate
20// feature-gated items with the feature that provides them. No job in this
21// repository runs nightly, so this line is the one thing here CI cannot check
22// -- the sibling repository lost a release to exactly that blind spot.
23#![cfg_attr(docsrs, feature(doc_cfg))]
24#![forbid(unsafe_code)]
25
26pub mod data;
27pub mod engine;
28pub mod error;
29pub mod metrics;
30pub mod portfolio;
31pub mod registry;
32pub mod report;
33pub mod request;
34pub mod rules;
35pub mod spec;
36
37pub use data::{
38    Candle, CrossSection, CrossSectionMember, DerivativesTick, Level, OrderBook, TradePrint,
39    TradeSide,
40};
41pub use engine::{
42    run, run_stream, run_with_capital, run_with_cross_section, run_with_deriv, run_with_orderbook,
43    run_with_ref, run_with_trades, Feeds, StreamingBacktest, DEFAULT_CAPITAL,
44};
45pub use error::{BacktestError, Result};
46pub use metrics::Metrics;
47pub use portfolio::Trade;
48pub use registry::EvalIndicator;
49pub use report::{BacktestReport, EquityPoint, REPORT_SCHEMA_VERSION};
50pub use request::{run_json, RunRequest, StepFeeds, StepRequest};
51pub use spec::{
52    Condition, Costs, Execution, Feed, FillTiming, IndicatorSpec, IntPredicate, Operand,
53    OperandExpr, OrderType, PriceField, Risk, Sizing, Slippage, StrategySpec, SPEC_VERSION,
54};
55
56/// The crate version, surfaced for diagnostics and binding parity checks.
57#[must_use]
58pub fn version() -> &'static str {
59    env!("CARGO_PKG_VERSION")
60}
61
62/// The JSON Schema for [`StrategySpec`], pretty-printed. Editors and tooling can
63/// validate strategy specs against it; the committed
64/// `schema/strategy_spec.schema.json` is generated from this.
65#[must_use]
66pub fn strategy_spec_schema() -> String {
67    let schema = schemars::schema_for!(StrategySpec);
68    serde_json::to_string_pretty(&schema).unwrap_or_default()
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74
75    #[test]
76    fn version_is_reported() {
77        assert!(!version().is_empty());
78    }
79
80    #[test]
81    fn strategy_spec_schema_is_committed() {
82        // The committed schema must match what schemars generates. Regenerate with
83        //   WICKRA_BLESS=1 cargo test -p wickra-backtest-core strategy_spec_schema
84        let schema = strategy_spec_schema();
85        assert!(schema.contains("StrategySpec"));
86        let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
87            .join("../../schema/strategy_spec.schema.json");
88        if std::env::var("WICKRA_BLESS").is_ok() {
89            std::fs::create_dir_all(path.parent().unwrap()).unwrap();
90            std::fs::write(&path, format!("{schema}\n")).unwrap();
91            return;
92        }
93        let committed =
94            std::fs::read_to_string(&path).expect("schema file missing (run WICKRA_BLESS=1)");
95        assert_eq!(schema, committed.trim_end(), "schema drift");
96    }
97
98    #[test]
99    fn errors_render() {
100        let e = BacktestError::UnknownIndicator("Foo".into());
101        assert!(e.to_string().contains("Foo"));
102    }
103}