synthetic_backtesting
Concept Overview
Section titled “Concept Overview”AFML Chapter 13’s answer to profit-taking and stop-loss overfitting. Rather than searching the PT/SL mesh on the single historical path you have — where the winning cell is mostly luck — it calibrates an Ornstein-Uhlenbeck process to that path, generates thousands of synthetic paths from the fitted parameters, and evaluates the whole mesh across all of them. detect_no_stable_optimum then asks whether the resulting Sharpe surface has a peak worth trusting at all.
When to Use
Section titled “When to Use”Use it before committing to any exit rule. Its most valuable output is often the negative one: when the fitted persistence is close to 1 the price is near a random walk, the Sharpe surface is flat, and no_stable_optimum says so — meaning no PT/SL pair is defensible and the honest move is to skip the optimisation rather than take the argmax of noise. It complements backtesting_engine rather than replacing it, since that validates on the real path.
Mathematical Foundations
Section titled “Mathematical Foundations”Discrete O-U (AR(1))
Section titled “Discrete O-U (AR(1))”
Equilibrium Level
Section titled “Equilibrium Level”
OTR Objective over Rule Mesh
Section titled “OTR Objective over Rule Mesh”
Usage Examples
Section titled “Usage Examples”End-to-end synthetic OTR workflow
Section titled “End-to-end synthetic OTR workflow”use openquant::synthetic_backtesting::{ run_synthetic_otr_workflow, StabilityCriteria, SyntheticBacktestConfig,};
// A realised price history is fitted to obtain the O-U parameters the synthetic// paths are drawn from.let historical_prices: Vec<f64> = (0..500).map(|i| 100.0 + (i as f64 * 0.05).sin() * 3.0).collect();
let cfg = SyntheticBacktestConfig { initial_price: historical_prices[historical_prices.len() - 1], n_paths: 10_000, horizon: 128, seed: 42, profit_taking_grid: vec![0.5, 1.0, 1.5, 2.0, 3.0], stop_loss_grid: vec![0.5, 1.0, 1.5, 2.0, 3.0], max_holding_steps: 64, annualization_factor: 1.0, stability_criteria: StabilityCriteria::default(),};
let out = run_synthetic_otr_workflow(&historical_prices, &cfg)?;if out.diagnostics.no_stable_optimum { println!("Skip OTR optimization: {}", out.diagnostics.reason);} else { println!("Best PT/SL: {:?}", out.best_rule);}API Reference
Section titled “API Reference”Python API
Section titled “Python API”synthetic_bt.calibrate_ou_paramssynthetic_bt.generate_ou_pathssynthetic_bt.evaluate_rule_on_pathssynthetic_bt.detect_no_stable_optimumsynthetic_bt.run_synthetic_otr_workflowsynthetic_bt.search_optimal_trading_rule
Rust API
Section titled “Rust API”calibrate_ou_paramsgenerate_ou_pathsevaluate_rule_on_pathssearch_optimal_trading_ruledetect_no_stable_optimumrun_synthetic_otr_workflow
Risk Notes and Caveats
Section titled “Risk Notes and Caveats”- Near-random-walk estimates (|phi| close to 1) often produce flat Sharpe heatmaps where any selected rule is unstable out-of-sample.
- Calibrating to process parameters and evaluating many synthetic paths reduces single-path lucky-fit risk compared to brute-force historical optimization.