Special Relativity in Financial Modeling 1.0.0
Lorentz transforms, spacetime classification, and geodesic price paths for quantitative finance
Loading...
Searching...
No Matches
regime_filtered_backtester.cpp
Go to the documentation of this file.
1/// @file src/backtest/regime_filtered_backtester.cpp
2/// @brief Implementation of RegimeFilteredBacktester.
3///
4/// Runs three strategies side by side:
5/// 1. Always-in (unfiltered)
6/// 2. TIMELIKE-only (flat during SPACELIKE/LIGHTLIKE bars)
7/// 3. Relativistic TIMELIKE-only (gamma-scaled position, TIMELIKE only)
8///
9/// Key research finding reproduced here:
10/// TIMELIKE bars exhibit 1.27x lower next-bar return variance than SPACELIKE.
11
12#include "srfm/backtest.hpp"
13#include "srfm/constants.hpp"
14#include "srfm/lorentz/lorentz_transform.hpp"
15
16#include <cmath>
17#include <numeric>
18#include <sstream>
19#include <span>
20#include <vector>
21
22namespace srfm::backtest {
23
24// ── RegimeBacktestResult::to_string ──────────────────────────────────────────
25
27 std::ostringstream ss;
28 ss << "=== Regime-Filtered Backtest Results ===\n";
29 ss << "\n";
30 ss << "Bar classification:\n";
31 ss << " TIMELIKE: " << (timelike_fraction * 100.0) << "%\n";
32 ss << " LIGHTLIKE: " << (lightlike_fraction * 100.0) << "%\n";
33 ss << " SPACELIKE: " << (spacelike_fraction * 100.0) << "%\n";
34 ss << "\n";
35 ss << "Next-bar return variance by regime:\n";
36 ss << " TIMELIKE variance: " << timelike_variance << "\n";
37 ss << " SPACELIKE variance: " << spacelike_variance << "\n";
38 if (spacelike_variance > 1e-12) {
39 ss << " Variance ratio (SPACELIKE/TIMELIKE): "
41 }
42 ss << "\n";
43 ss << "Strategy comparison:\n";
44 ss << " Sharpe Sortino MaxDD Gamma-IR\n";
45 auto fmt_row = [&](const char* name, const PerformanceMetrics& m) {
46 ss << " " << name;
47 ss << " " << m.sharpe_ratio;
48 ss << " " << m.sortino_ratio;
49 ss << " " << m.max_drawdown;
50 ss << " " << m.gamma_weighted_ir << "\n";
51 };
52 fmt_row("Always-In ", always_in);
53 fmt_row("TIMELIKE-Only ", timelike_only);
54 fmt_row("TIMELIKE-Rel(γ-sz) ", timelike_relativistic);
55 ss << "\n";
56 ss << "TIMELIKE Sharpe lift vs always-in: "
57 << timelike_sharpe_lift() << "\n";
58 return ss.str();
59}
60
61// ── RegimeFilteredBacktester ──────────────────────────────────────────────────
62
64 : config_(config)
65 , adjuster_(config.effective_mass)
66{}
67
68std::optional<RegimeBacktestResult>
69RegimeFilteredBacktester::run(std::span<const BarDataEx> bars) const noexcept {
70 if (bars.size() < constants::MIN_RETURN_SERIES_LENGTH) {
71 return std::nullopt;
72 }
73
74 const std::size_t n = bars.size();
75
76 // ── Classify each bar by regime ─────────────────────────────────────────
77 std::size_t timelike_count = 0;
78 std::size_t lightlike_count = 0;
79 std::size_t spacelike_count = 0;
80
81 // Collect returns by regime for variance calculation
82 std::vector<double> timelike_returns;
83 std::vector<double> spacelike_returns;
84 timelike_returns.reserve(n);
85 spacelike_returns.reserve(n);
86
87 // Strategy return vectors
88 std::vector<double> always_in_rets(n);
89 std::vector<double> timelike_rets(n, 0.0);
90 std::vector<double> timelike_rel_rets(n, 0.0);
91 std::vector<double> benchmark_rets(n);
92
93 // Gamma factors for performance metrics (always-in uses unit gamma)
94 std::vector<double> unit_gammas(n, 1.0);
95 std::vector<double> timelike_gammas(n, 1.0);
96
97 for (std::size_t i = 0; i < n; ++i) {
98 const auto& bar = bars[i];
99 const double ret = bar.asset_return;
100 const double raw_sign = (bar.base.raw_signal >= 0.0) ? 1.0 : -1.0;
101
102 benchmark_rets[i] = bar.base.benchmark;
103
104 // Compute gamma from beta
105 double gamma_i = 1.0;
106 auto g = lorentz::LorentzTransform::gamma(bar.base.beta);
107 if (g.has_value()) {
108 gamma_i = g->value;
109 }
110 const double gamma_capped =
111 std::max(1.0, std::min(gamma_i, config_.max_gamma));
112
113 // Classify regime
114 const bool is_timelike = bar.ds2 < -LIGHTLIKE_EPSILON;
115 const bool is_lightlike = std::abs(bar.ds2) <= LIGHTLIKE_EPSILON;
116 const bool is_spacelike = bar.ds2 > LIGHTLIKE_EPSILON;
117
118 if (is_timelike) {
119 ++timelike_count;
120 timelike_returns.push_back(ret);
121 } else if (is_lightlike) {
122 ++lightlike_count;
123 } else if (is_spacelike) {
124 ++spacelike_count;
125 spacelike_returns.push_back(ret);
126 }
127
128 // Strategy 1: always-in (unit position)
129 always_in_rets[i] = raw_sign * ret;
130
131 // Strategies 2 & 3: only active on TIMELIKE bars
132 if (is_timelike) {
133 timelike_rets[i] = raw_sign * ret;
134 timelike_rel_rets[i] = raw_sign * gamma_capped * ret;
135 timelike_gammas[i] = gamma_capped;
136 }
137 // else: 0.0 (flat, no position)
138 }
139
140 // ── Compute per-regime variance ─────────────────────────────────────────
141 auto variance_of = [](const std::vector<double>& v) -> double {
142 if (v.size() < 2) return 0.0;
143 const double mean =
144 std::accumulate(v.begin(), v.end(), 0.0) / static_cast<double>(v.size());
145 double sq_sum = 0.0;
146 for (double x : v) {
147 const double d = x - mean;
148 sq_sum += d * d;
149 }
150 return sq_sum / static_cast<double>(v.size() - 1);
151 };
152
153 const double tv = variance_of(timelike_returns);
154 const double sv = variance_of(spacelike_returns);
155
156 // ── Compute performance metrics for each strategy ───────────────────────
157 // Helper: use the existing PerformanceCalculator from Backtester
158 auto calc_metrics = [&](
159 std::span<const double> rets,
160 std::span<const double> bench,
161 std::span<const double> gammas
162 ) -> std::optional<PerformanceMetrics> {
164 rets, config_.risk_free_rate, config_.annualisation);
165 if (!sh) return std::nullopt;
166
168 rets, config_.risk_free_rate, config_.annualisation);
169 if (!so) return std::nullopt;
170
172 if (!mdd) return std::nullopt;
173
174 auto ir = PerformanceCalculator::gamma_weighted_ir(rets, bench, gammas);
175 if (!ir) return std::nullopt;
176
177 return PerformanceMetrics{
178 .sharpe_ratio = *sh,
179 .sortino_ratio = *so,
180 .max_drawdown = *mdd,
181 .gamma_weighted_ir = *ir,
182 };
183 };
184
185 auto always_metrics = calc_metrics(always_in_rets, benchmark_rets, unit_gammas);
186 if (!always_metrics) return std::nullopt;
187
188 auto timelike_metrics = calc_metrics(timelike_rets, benchmark_rets, timelike_gammas);
189 if (!timelike_metrics) return std::nullopt;
190
191 auto timelike_rel_metrics = calc_metrics(timelike_rel_rets, benchmark_rets, timelike_gammas);
192 if (!timelike_rel_metrics) return std::nullopt;
193
194 const double nd = static_cast<double>(n);
196 .always_in = *always_metrics,
197 .timelike_only = *timelike_metrics,
198 .timelike_relativistic = *timelike_rel_metrics,
199 .timelike_fraction = static_cast<double>(timelike_count) / nd,
200 .spacelike_fraction = static_cast<double>(spacelike_count) / nd,
201 .lightlike_fraction = static_cast<double>(lightlike_count) / nd,
202 .timelike_variance = tv,
203 .spacelike_variance = sv,
204 };
205}
206
207} // namespace srfm::backtest
Relativistic Backtester — AGT-05 public API.
static std::optional< double > max_drawdown(std::span< const double > returns) noexcept
static std::optional< double > sortino(std::span< const double > returns, double risk_free_rate=constants::DEFAULT_RISK_FREE_RATE, double annualisation=constants::ANNUALISATION_FACTOR) noexcept
static std::optional< double > sharpe(std::span< const double > returns, double risk_free_rate=constants::DEFAULT_RISK_FREE_RATE, double annualisation=constants::ANNUALISATION_FACTOR) noexcept
static std::optional< double > gamma_weighted_ir(std::span< const double > strategy_returns, std::span< const double > benchmark_returns, std::span< const double > gamma_factors) noexcept
RegimeFilteredBacktester(BacktestConfig config=BacktestConfig{})
Construct with optional configuration (forwarded to inner Backtester).
std::optional< RegimeBacktestResult > run(std::span< const BarDataEx > bars) const noexcept
static std::optional< LorentzFactor > gamma(BetaVelocity beta) noexcept
Physical and financial constants for the SRFM system.
static constexpr std::size_t MIN_RETURN_SERIES_LENGTH
Definition constants.hpp:49
Configuration for a backtest run.
Definition backtest.hpp:113
Performance metrics for a single strategy evaluation.
Definition backtest.hpp:76
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
double timelike_variance
Mean next-bar return variance in TIMELIKE bars.
Definition backtest.hpp:322
double spacelike_fraction
Fraction of bars classified SPACELIKE in this run.
Definition backtest.hpp:317
double lightlike_fraction
Fraction of bars classified LIGHTLIKE in this run.
Definition backtest.hpp:319
double spacelike_variance
Mean next-bar return variance in SPACELIKE bars.
Definition backtest.hpp:324
PerformanceMetrics timelike_relativistic
γ-scaled TIMELIKE-gated strategy
Definition backtest.hpp:312
std::string to_string() const
Formatted side-by-side comparison table.
double timelike_fraction
Fraction of bars classified TIMELIKE in this run.
Definition backtest.hpp:315
PerformanceMetrics timelike_only
TIMELIKE-gated strategy (flat on others)
Definition backtest.hpp:311