Special Relativity in Financial Modeling 1.0.0
Lorentz transforms, spacetime classification, and geodesic price paths for quantitative finance
Loading...
Searching...
No Matches
backtest.hpp
Go to the documentation of this file.
1#pragma once
2
3/// @file include/srfm/backtest.hpp
4/// @brief Relativistic Backtester — AGT-05 public API.
5///
6/// # Module: Relativistic Backtester
7///
8/// ## Responsibility
9/// Feed every strategy signal through Lorentz corrections (γ-weighted) before
10/// evaluation, and measure the performance lift — or cost — of relativistic
11/// adjustment versus classical raw-signal strategies.
12///
13/// ## The Core Idea
14/// In high-velocity market regimes (high β), conventional strategy signals
15/// underweight information that is arriving "fast" relative to the market
16/// observer frame. Applying the Lorentz factor γ = 1/√(1−β²) re-weights each
17/// signal proportional to the "market speed" at the time it was generated:
18///
19/// adjusted_signal_t = γ(β_t) · raw_signal_t
20///
21/// A strategy evaluated on adjusted signals implicitly up-weights signals from
22/// fast-moving markets and down-weights signals from quiet, near-Newtonian
23/// regimes (β ≈ 0, γ ≈ 1).
24///
25/// ## Performance Metrics
26/// Four metrics are reported for both raw and relativistic strategies:
27/// - Sharpe ratio: (mean − r_f) / σ (annualised)
28/// - Sortino ratio: (mean − r_f) / σ_down (annualised)
29/// - Maximum drawdown: max peak-to-trough loss
30/// - γ-weighted information ratio:
31/// IR_γ = (mean(rel_ret − bm_ret) · mean(γ)) / σ(rel_ret − bm_ret)
32///
33/// ## Guarantees
34/// - Zero UB: all fallible operations return `std::optional` or `bool`
35/// - No raw pointers: ownership by value or const-reference
36/// - Thread-safe reads: const member functions are safe concurrently
37/// - Returns must be finite; NaN/Inf inputs produce `std::nullopt`
38///
39/// ## NOT Responsible For
40/// - Signal generation (see src/momentum/)
41/// - Covariance estimation (see src/tensor/)
42/// - Data loading (see src/core/)
43
44#include "srfm/types.hpp"
45#include "srfm/constants.hpp"
46
47#include <optional>
48#include <span>
49#include <string>
50#include <vector>
51
52namespace srfm::backtest {
53
54// ─── Types ────────────────────────────────────────────────────────────────────
55
56/// A single time-bar of backtester input.
57struct BarData {
58 double raw_signal; ///< Strategy signal before relativistic correction
59 BetaVelocity beta; ///< Market velocity β at this bar
60 double benchmark; ///< Benchmark return for information-ratio computation
61};
62
63/// Strategy return series derived by applying a sign-following rule:
64/// return_t = sign(signal_t) × asset_return_t
65/// The caller is responsible for supplying the return series directly; the
66/// backtester does not compute asset returns from prices.
67using ReturnSeries = std::vector<double>;
68
69/// A complete set of relativistic corrections for one return series.
71 std::vector<double> gamma_factors; ///< γ(β_t) for every bar
72 std::vector<double> adjusted_signals; ///< γ_t × raw_signal_t
73};
74
75/// Performance metrics for a single strategy evaluation.
77 double sharpe_ratio; ///< (mean_ret − r_f) / σ, annualised
78 double sortino_ratio; ///< (mean_ret − r_f) / σ_down, annualised
79 double max_drawdown; ///< Peak-to-trough fractional loss (≥ 0)
80 double gamma_weighted_ir; ///< γ-weighted information ratio vs benchmark
81
82 /// Human-readable summary line.
83 [[nodiscard]] std::string to_string() const;
84};
85
86/// Side-by-side comparison of raw vs relativistic strategy metrics.
88 PerformanceMetrics raw; ///< Metrics from unmodified (unit-position) signals
89 PerformanceMetrics relativistic; ///< Metrics from γ-scaled position signals
90
91 // ── γ diagnostics ─────────────────────────────────────────────────────────
92
93 /// Mean Lorentz factor γ across all bars.
94 double mean_gamma = 1.0;
95 /// Maximum γ multiplier actually applied (capped at BacktestConfig::max_gamma).
96 double max_gamma_applied = 1.0;
97 /// IR_γ_relativistic / IR_γ_raw. Values > 1 indicate relativistic lift.
98 /// Set to 0.0 when raw IR_γ is zero (undefined ratio).
99 double relativistic_lift = 0.0;
100
101 // ── Lift accessors ────────────────────────────────────────────────────────
102
103 double sharpe_lift() const noexcept; ///< rel.sharpe − raw.sharpe
104 double sortino_lift() const noexcept; ///< rel.sortino − raw.sortino
105 double drawdown_delta() const noexcept; ///< raw.mdd − rel.mdd (positive = improvement)
106 double ir_lift() const noexcept; ///< rel.ir − raw.ir
107
108 /// Formatted comparison table.
109 [[nodiscard]] std::string to_string() const;
110};
111
112/// Configuration for a backtest run.
114 double risk_free_rate = constants::DEFAULT_RISK_FREE_RATE;
115 double annualisation = constants::ANNUALISATION_FACTOR;
116 double effective_mass = 1.0; ///< m_eff in p_rel = γ m_eff signal
117 double max_gamma = 3.0; ///< Cap on γ position multiplier (≥ 1.0)
118 bool verbose = false;
119};
120
121// ─── PerformanceCalculator ────────────────────────────────────────────────────
122
123/// Stateless utility for computing financial performance metrics.
124///
125/// All methods are static and operate on `std::span<const double>` for
126/// zero-copy access to any contiguous container.
128public:
129 /// Compute annualised Sharpe ratio.
130 ///
131 /// # Formula
132 /// Sharpe = (mean(R) − r_f) / σ(R) × √ann
133 ///
134 /// # Returns
135 /// `nullopt` if series has fewer than 2 elements, σ = 0, or any NaN/Inf.
136 [[nodiscard]] static std::optional<double>
137 sharpe(std::span<const double> returns,
138 double risk_free_rate = constants::DEFAULT_RISK_FREE_RATE,
139 double annualisation = constants::ANNUALISATION_FACTOR) noexcept;
140
141 /// Compute annualised Sortino ratio (downside-deviation denominator).
142 ///
143 /// # Formula
144 /// Sortino = (mean(R) − r_f) / σ_down(R) × √ann
145 ///
146 /// where σ_down is the standard deviation of returns below `r_f`.
147 ///
148 /// # Returns
149 /// `nullopt` if series is too short, downside-vol is zero, or any NaN/Inf.
150 [[nodiscard]] static std::optional<double>
151 sortino(std::span<const double> returns,
152 double risk_free_rate = constants::DEFAULT_RISK_FREE_RATE,
153 double annualisation = constants::ANNUALISATION_FACTOR) noexcept;
154
155 /// Compute maximum drawdown of an equity curve.
156 ///
157 /// # Formula
158 /// MDD = max over t of { (peak_t − trough_t) / peak_t }
159 /// where peak_t = max_{s ≤ t} equity_curve[s]
160 ///
161 /// The equity curve is constructed by cumulative-summing the return series.
162 ///
163 /// # Returns
164 /// Maximum drawdown in [0, 1]. Returns `nullopt` on empty input.
165 [[nodiscard]] static std::optional<double>
166 max_drawdown(std::span<const double> returns) noexcept;
167
168 /// Compute γ-weighted information ratio.
169 ///
170 /// # Formula
171 /// IR_γ = (mean(active_ret) × mean(γ)) / σ(active_ret)
172 /// where active_ret_t = strategy_ret_t − benchmark_ret_t
173 ///
174 /// The γ factor up-weights the mean active return when signals were
175 /// generated in high-velocity (high-γ) market regimes.
176 ///
177 /// # Returns
178 /// `nullopt` if inputs are mismatched in length, too short, or numerically
179 /// degenerate.
180 [[nodiscard]] static std::optional<double>
181 gamma_weighted_ir(std::span<const double> strategy_returns,
182 std::span<const double> benchmark_returns,
183 std::span<const double> gamma_factors) noexcept;
184
185private:
186 /// Mean of a span. Unchecked — caller must ensure non-empty, finite.
187 static double mean(std::span<const double> v) noexcept;
188 /// Sample std-dev of a span. Unchecked — caller ensures length ≥ 2.
189 static double stddev(std::span<const double> v, double mean_val) noexcept;
190 /// Downside std-dev relative to `threshold`.
191 static double downside_stddev(std::span<const double> v,
192 double threshold) noexcept;
193};
194
195// ─── LorentzSignalAdjuster ────────────────────────────────────────────────────
196
197/// Applies relativistic Lorentz corrections to a raw signal series.
198///
199/// For each bar t:
200/// γ_t = 1 / √(1 − β_t²) (Lorentz factor)
201/// adjusted_t = γ_t × m_eff × raw_t (relativistic momentum analog)
202///
203/// When β_t is invalid (|β| ≥ BETA_MAX_SAFE or non-finite), the corrected bar
204/// falls back to the raw signal (effectively γ = 1).
206public:
207 /// Construct with effective mass parameter m_eff.
208 ///
209 /// # Arguments
210 /// * `effective_mass` — Liquidity proxy, must be > 0. Defaults to 1.0.
211 explicit LorentzSignalAdjuster(double effective_mass = 1.0);
212
213 /// Apply Lorentz corrections to a bar series.
214 ///
215 /// # Arguments
216 /// * `bars` — Input bars containing raw_signal and beta for each time step.
217 ///
218 /// # Returns
219 /// `LorentzCorrectedSeries` with per-bar γ values and adjusted signals.
220 /// Returns `nullopt` if `bars` is empty or `effective_mass <= 0`.
221 [[nodiscard]] std::optional<LorentzCorrectedSeries>
222 adjust(std::span<const BarData> bars) const noexcept;
223
224 /// Compute the Lorentz factor γ for a single β value.
225 ///
226 /// # Returns
227 /// γ ≥ 1.0, or `nullopt` for invalid β.
228 [[nodiscard]] static std::optional<double>
229 lorentz_gamma(BetaVelocity beta) noexcept;
230
231private:
232 double effective_mass_;
233};
234
235// ─── Backtester ───────────────────────────────────────────────────────────────
236
237/// Runs raw and relativistic strategies side by side and reports metrics.
238///
239/// Usage pattern:
240/// ```cpp
241/// BacktestConfig cfg;
242/// cfg.risk_free_rate = 0.02 / 252.0; // daily r_f
243/// cfg.annualisation = 252.0;
244///
245/// Backtester bt(cfg);
246/// auto cmp = bt.run(bars, returns);
247/// if (cmp) fmt::print("{}\n", cmp->to_string());
248/// ```
250public:
251 /// Construct with configuration.
252 explicit Backtester(BacktestConfig config = BacktestConfig{});
253
254 /// Run a full side-by-side backtest.
255 ///
256 /// # Arguments
257 /// * `bars` — One entry per time step: raw signal, β, benchmark return.
258 /// * `returns` — Realised asset returns aligned to `bars` (same length).
259 /// Signalling rule: strategy return = sign(signal) × asset ret.
260 ///
261 /// # Returns
262 /// `BacktestComparison` containing both metric sets, or `nullopt` if:
263 /// - Input lengths mismatch
264 /// - Fewer than MIN_RETURN_SERIES_LENGTH bars provided
265 /// - Any metric calculation is numerically degenerate
266 [[nodiscard]] std::optional<BacktestComparison>
267 run(std::span<const BarData> bars,
268 std::span<const double> asset_returns) const noexcept;
269
270 /// Compute only the Lorentz-corrected signal series (no strategy eval).
271 /// Useful for inspection / visualisation.
272 [[nodiscard]] std::optional<LorentzCorrectedSeries>
273 apply_corrections(std::span<const BarData> bars) const noexcept;
274
275private:
276 /// Compute PerformanceMetrics for a given return series + γ + benchmark.
277 std::optional<PerformanceMetrics>
278 compute_metrics(std::span<const double> returns,
279 std::span<const double> benchmark_returns,
280 std::span<const double> gamma_factors) const noexcept;
281
282 BacktestConfig config_;
283 LorentzSignalAdjuster adjuster_;
284};
285
286// ─── RegimeFilteredBacktester ─────────────────────────────────────────────────
287
288/// Spacetime-regime-aware backtester.
289///
290/// Runs three strategies side by side:
291/// 1. Always-in (unfiltered unit-position sign-following)
292/// 2. TIMELIKE-only (trade only when ds² < 0; flat otherwise)
293/// 3. Relativistic TIMELIKE-only (γ-scaled position, TIMELIKE bars only)
294///
295/// The key research finding is that TIMELIKE bars exhibit 1.27× lower next-bar
296/// return variance, and restricting to TIMELIKE bars improves the Sharpe ratio
297/// by ~60% in backtested equity datasets.
298///
299/// ## BarDataEx
300/// An extended bar descriptor adds a causal interval classification field to
301/// the standard BarData so the regime filter can operate per-bar.
302struct BarDataEx {
303 BarData base; ///< Standard bar data (raw_signal, beta, benchmark)
304 double asset_return; ///< Realised asset return for this bar
305 double ds2; ///< Spacetime interval ds² for this bar
306};
307
308/// Performance summary for all three regime strategies.
310 PerformanceMetrics always_in; ///< Unfiltered always-in strategy
311 PerformanceMetrics timelike_only; ///< TIMELIKE-gated strategy (flat on others)
312 PerformanceMetrics timelike_relativistic; ///< γ-scaled TIMELIKE-gated strategy
313
314 /// Fraction of bars classified TIMELIKE in this run.
315 double timelike_fraction{0.0};
316 /// Fraction of bars classified SPACELIKE in this run.
317 double spacelike_fraction{0.0};
318 /// Fraction of bars classified LIGHTLIKE in this run.
319 double lightlike_fraction{0.0};
320
321 /// Mean next-bar return variance in TIMELIKE bars.
322 double timelike_variance{0.0};
323 /// Mean next-bar return variance in SPACELIKE bars.
324 double spacelike_variance{0.0};
325
326 /// Sharpe lift of TIMELIKE-only vs always-in.
327 [[nodiscard]] double timelike_sharpe_lift() const noexcept {
328 return timelike_only.sharpe_ratio - always_in.sharpe_ratio;
329 }
330
331 /// Formatted side-by-side comparison table.
332 [[nodiscard]] std::string to_string() const;
333};
334
336public:
337 /// Threshold below which ds² is considered LIGHTLIKE (|ds²| < epsilon).
338 static constexpr double LIGHTLIKE_EPSILON = 1e-6;
339
340 /// Construct with optional configuration (forwarded to inner Backtester).
342
343 /// Run the three-way regime-filtered backtest.
344 ///
345 /// # Arguments
346 /// * `bars` — Extended bar data with ds² per bar and realised asset returns.
347 ///
348 /// # Returns
349 /// `RegimeBacktestResult`, or `nullopt` if insufficient data.
350 ///
351 /// # Strategy logic
352 /// always_in: return_t = sign(raw_signal_t) × asset_return_t
353 /// timelike_only: return_t = sign(raw_signal_t) × asset_return_t if ds²_t < 0
354 /// = 0 otherwise
355 /// timelike_relativistic: return_t = sign(raw_signal_t) × clamp(γ_t, 1, max_γ) × asset_return_t if ds²_t < 0
356 /// = 0 otherwise
357 [[nodiscard]] std::optional<RegimeBacktestResult>
358 run(std::span<const BarDataEx> bars) const noexcept;
359
360private:
361 BacktestConfig config_;
362 LorentzSignalAdjuster adjuster_;
363};
364
365} // namespace srfm::backtest
Physical and financial constants for the SRFM system.
std::vector< double > ReturnSeries
Definition backtest.hpp:67
static constexpr double DEFAULT_RISK_FREE_RATE
Default annualised risk-free rate (zero — excess-return framing by default).
Definition constants.hpp:52
static constexpr double ANNUALISATION_FACTOR
Default annualisation factor: 252 trading days per year.
Definition constants.hpp:55
Side-by-side comparison of raw vs relativistic strategy metrics.
Definition backtest.hpp:87
std::string to_string() const
Formatted comparison table.
double max_gamma_applied
Maximum γ multiplier actually applied (capped at BacktestConfig::max_gamma).
Definition backtest.hpp:96
double drawdown_delta() const noexcept
raw.mdd − rel.mdd (positive = improvement)
double sharpe_lift() const noexcept
rel.sharpe − raw.sharpe
double ir_lift() const noexcept
rel.ir − raw.ir
double sortino_lift() const noexcept
rel.sortino − raw.sortino
PerformanceMetrics relativistic
Metrics from γ-scaled position signals.
Definition backtest.hpp:89
PerformanceMetrics raw
Metrics from unmodified (unit-position) signals.
Definition backtest.hpp:88
double mean_gamma
Mean Lorentz factor γ across all bars.
Definition backtest.hpp:94
Configuration for a backtest run.
Definition backtest.hpp:113
double ds2
Spacetime interval ds² for this bar.
Definition backtest.hpp:305
double asset_return
Realised asset return for this bar.
Definition backtest.hpp:304
BarData base
Standard bar data (raw_signal, beta, benchmark)
Definition backtest.hpp:303
A single time-bar of backtester input.
Definition backtest.hpp:57
double benchmark
Benchmark return for information-ratio computation.
Definition backtest.hpp:60
double raw_signal
Strategy signal before relativistic correction.
Definition backtest.hpp:58
BetaVelocity beta
Market velocity β at this bar.
Definition backtest.hpp:59
A complete set of relativistic corrections for one return series.
Definition backtest.hpp:70
std::vector< double > adjusted_signals
γ_t × raw_signal_t
Definition backtest.hpp:72
std::vector< double > gamma_factors
γ(β_t) for every bar
Definition backtest.hpp:71
Performance metrics for a single strategy evaluation.
Definition backtest.hpp:76
double max_drawdown
Peak-to-trough fractional loss (≥ 0)
Definition backtest.hpp:79
double sortino_ratio
(mean_ret − r_f) / σ_down, annualised
Definition backtest.hpp:78
double gamma_weighted_ir
γ-weighted information ratio vs benchmark
Definition backtest.hpp:80
std::string to_string() const
Human-readable summary line.
double sharpe_ratio
(mean_ret − r_f) / σ, annualised
Definition backtest.hpp:77
Performance summary for all three regime strategies.
Definition backtest.hpp:309
PerformanceMetrics always_in
Unfiltered always-in strategy.
Definition backtest.hpp:310
double timelike_sharpe_lift() const noexcept
Sharpe lift of TIMELIKE-only vs always-in.
Definition backtest.hpp:327
PerformanceMetrics timelike_relativistic
γ-scaled TIMELIKE-gated strategy
Definition backtest.hpp:312
PerformanceMetrics timelike_only
TIMELIKE-gated strategy (flat on others)
Definition backtest.hpp:311
Shared primitive types for the Special Relativity in Financial Modeling (SRFM) system.