Special Relativity in Financial Modeling 1.0.0
Lorentz transforms, spacetime classification, and geodesic price paths for quantitative finance
Loading...
Searching...
No Matches
performance_metrics.cpp
Go to the documentation of this file.
1/// @file src/backtest/performance_metrics.cpp
2/// @brief Implementation of PerformanceCalculator and LorentzSignalAdjuster.
3///
4/// All math is documented inline. Fallible paths return std::nullopt; no
5/// function ever calls abort(), assert(), or throws an exception.
6
7#include "srfm/backtest.hpp"
8#include "srfm/constants.hpp"
9
10#include <algorithm>
11#include <cmath>
12#include <fmt/format.h>
13#include <numeric>
14#include <span>
15
16namespace srfm::backtest {
17
18// ─── Internal helpers (file-local) ────────────────────────────────────────────
19
20namespace {
21
22/// Return false if any element of `v` is NaN or ±Inf.
23[[nodiscard]] bool all_finite(std::span<const double> v) noexcept {
24 for (double x : v) {
25 if (!std::isfinite(x)) return false;
26 }
27 return true;
28}
29
30/// Sample statistics need at least two observations (n - 1 denominator).
31constexpr std::size_t MIN_METRIC_SAMPLES = 2;
32
33/// True when a dispersion estimate is zero up to floating-point rounding of
34/// the mean. A constant series of 0.01 yields sigma of about 1e-18, not 0.
35[[nodiscard]] bool is_degenerate_dispersion(double sd, double mu) noexcept {
36 return sd <= 1e-12 * std::abs(mu);
37}
38
39} // namespace
40
41// ─── PerformanceCalculator — private statics ──────────────────────────────────
42
43double PerformanceCalculator::mean(std::span<const double> v) noexcept {
44 // Unchecked: caller guarantees non-empty, finite.
45 const double sum = std::accumulate(v.begin(), v.end(), 0.0);
46 return sum / static_cast<double>(v.size());
47}
48
49double PerformanceCalculator::stddev(std::span<const double> v,
50 double mean_val) noexcept {
51 // Sample std-dev (Bessel-corrected, n−1 denominator).
52 // Caller guarantees v.size() >= 2.
53 double sq_sum = 0.0;
54 for (double x : v) {
55 const double d = x - mean_val;
56 sq_sum += d * d;
57 }
58 return std::sqrt(sq_sum / static_cast<double>(v.size() - 1));
59}
60
61double PerformanceCalculator::downside_stddev(std::span<const double> v,
62 double threshold) noexcept {
63 // Target downside deviation (Sortino and Price, 1994):
64 // sigma_down = sqrt( sum_i min(0, x_i - T)^2 / n )
65 // The sum runs over all n observations, so bars above the target count
66 // as zero shortfall. Returns 0.0 when no observation is below `threshold`.
67 if (v.empty()) return 0.0;
68 double sq_sum = 0.0;
69 for (double x : v) {
70 if (x < threshold) {
71 const double d = x - threshold;
72 sq_sum += d * d;
73 }
74 }
75 return std::sqrt(sq_sum / static_cast<double>(v.size()));
76}
77
78// ─── PerformanceCalculator — Sharpe ──────────────────────────────────────────
79
80std::optional<double>
81PerformanceCalculator::sharpe(std::span<const double> returns,
82 double risk_free_rate,
83 double annualisation) noexcept {
84 if (returns.size() < MIN_METRIC_SAMPLES) return std::nullopt;
85 if (!all_finite(returns)) return std::nullopt;
86 if (!std::isfinite(risk_free_rate)) return std::nullopt;
87 if (annualisation <= 0.0) return std::nullopt;
88
89 const double mu = mean(returns);
90 const double sd = stddev(returns, mu);
91
92 // Zero variance: ratio undefined. Rounding leaves a constant series with a
93 // sigma of a few ulps of the mean, so compare against the mean's scale.
94 if (is_degenerate_dispersion(sd, mu)) return std::nullopt;
95
96 // Annualised Sharpe: (μ − r_f) / σ × √ann
97 return (mu - risk_free_rate) / sd * std::sqrt(annualisation);
98}
99
100// ─── PerformanceCalculator — Sortino ─────────────────────────────────────────
101
102std::optional<double>
103PerformanceCalculator::sortino(std::span<const double> returns,
104 double risk_free_rate,
105 double annualisation) noexcept {
106 if (returns.size() < MIN_METRIC_SAMPLES) return std::nullopt;
107 if (!all_finite(returns)) return std::nullopt;
108 if (!std::isfinite(risk_free_rate)) return std::nullopt;
109 if (annualisation <= 0.0) return std::nullopt;
110
111 const double mu = mean(returns);
112 const double sd_dn = downside_stddev(returns, risk_free_rate);
113
114 // Zero downside deviation → all returns above threshold → Sortino undefined
115 if (sd_dn <= 0.0) return std::nullopt;
116
117 // Annualised Sortino: (μ − r_f) / σ_down × √ann
118 return (mu - risk_free_rate) / sd_dn * std::sqrt(annualisation);
119}
120
121// ─── PerformanceCalculator — MaxDrawdown ──────────────────────────────────────
122
123std::optional<double>
124PerformanceCalculator::max_drawdown(std::span<const double> returns) noexcept {
125 if (returns.empty()) return std::nullopt;
126 if (!all_finite(returns)) return std::nullopt;
127
128 // Build equity curve from cumulative sum, starting at 1.0.
129 double equity = 1.0;
130 double peak = 1.0;
131 double max_dd = 0.0;
132
133 for (double r : returns) {
134 equity *= (1.0 + r);
135 if (equity > peak) {
136 peak = equity;
137 } else {
138 const double dd = (peak - equity) / peak;
139 if (dd > max_dd) max_dd = dd;
140 }
141 }
142 return max_dd;
143}
144
145// ─── PerformanceCalculator — γ-Weighted IR ───────────────────────────────────
146
147std::optional<double>
149 std::span<const double> strategy_returns,
150 std::span<const double> benchmark_returns,
151 std::span<const double> gamma_factors) noexcept {
152
153 const std::size_t n = strategy_returns.size();
154 if (n < MIN_METRIC_SAMPLES) return std::nullopt;
155 if (benchmark_returns.size() != n) return std::nullopt;
156 if (gamma_factors.size() != n) return std::nullopt;
157 if (!all_finite(strategy_returns)) return std::nullopt;
158 if (!all_finite(benchmark_returns)) return std::nullopt;
159 if (!all_finite(gamma_factors)) return std::nullopt;
160
161 // Active returns: strategy − benchmark
162 std::vector<double> active(n);
163 for (std::size_t i = 0; i < n; ++i) {
164 active[i] = strategy_returns[i] - benchmark_returns[i];
165 }
166
167 const double mu_active = mean(active);
168 const double sd_active = stddev(active, mu_active);
169 if (is_degenerate_dispersion(sd_active, mu_active)) return std::nullopt;
170
171 // Mean Lorentz factor over the window
172 const double mu_gamma = mean(gamma_factors);
173
174 // IR_γ = mean(active) × mean(γ) / σ(active)
175 return (mu_active * mu_gamma) / sd_active;
176}
177
178// ─── LorentzSignalAdjuster ────────────────────────────────────────────────────
179
181 : effective_mass_(effective_mass) {}
182
183std::optional<double>
185 // γ = 1 / √(1 − β²). Invalid for |β| ≥ BETA_MAX_SAFE or non-finite.
186 if (!std::isfinite(beta.value)) return std::nullopt;
187 if (std::abs(beta.value) >= constants::BETA_MAX_SAFE) return std::nullopt;
188
189 const double beta2 = beta.value * beta.value;
190 const double denom = std::sqrt(1.0 - beta2);
191 if (denom <= 0.0) return std::nullopt; // guard (BETA_MAX_SAFE ensures this)
192
193 return 1.0 / denom;
194}
195
196std::optional<LorentzCorrectedSeries>
197LorentzSignalAdjuster::adjust(std::span<const BarData> bars) const noexcept {
198 if (bars.empty()) return std::nullopt;
199 if (effective_mass_ <= 0.0) return std::nullopt;
200
201 const std::size_t n = bars.size();
203 out.gamma_factors.reserve(n);
204 out.adjusted_signals.reserve(n);
205
206 for (const auto& bar : bars) {
207 const auto g_opt = lorentz_gamma(bar.beta);
208 // If β is invalid, fall back to γ = 1 (Newtonian — no correction)
209 const double g = g_opt.value_or(1.0);
210 out.gamma_factors.push_back(g);
211 // Relativistic momentum analog: p = γ × m_eff × raw_signal
212 out.adjusted_signals.push_back(g * effective_mass_ * bar.raw_signal);
213 }
214
215 return out;
216}
217
218// ─── PerformanceMetrics ───────────────────────────────────────────────────────
219
220std::string PerformanceMetrics::to_string() const {
221 return fmt::format(
222 "Sharpe={:.4f} Sortino={:.4f} MaxDrawdown={:.4f} GammaIR={:.4f}",
224}
225
226// ─── BacktestComparison ───────────────────────────────────────────────────────
227
228double BacktestComparison::sharpe_lift() const noexcept {
230}
240
241std::string BacktestComparison::to_string() const {
242 return fmt::format(
243 "┌─────────────────────────────────────────────────────────┐\n"
244 "│ Relativistic Backtester — Side-by-Side │\n"
245 "├──────────────────┬──────────────┬──────────────┬────────┤\n"
246 "│ Metric │ Raw │ Relativistic │ Lift │\n"
247 "├──────────────────┼──────────────┼──────────────┼────────┤\n"
248 "│ Sharpe Ratio │ {:9.4f} │ {:9.4f} │ {:+.4f}│\n"
249 "│ Sortino Ratio │ {:9.4f} │ {:9.4f} │ {:+.4f}│\n"
250 "│ Max Drawdown │ {:9.4f} │ {:9.4f} │ {:+.4f}│\n"
251 "│ γ-Weighted IR │ {:9.4f} │ {:9.4f} │ {:+.4f}│\n"
252 "├──────────────────┴──────────────┴──────────────┴────────┤\n"
253 "│ Mean γ: {:.4f} Max γ applied: {:.4f} IR lift: {:.4f}x │\n"
254 "└──────────────────────────────────────────────────────────┘\n",
260}
261
262} // namespace srfm::backtest
Relativistic Backtester — AGT-05 public API.
static std::optional< double > lorentz_gamma(BetaVelocity beta) noexcept
LorentzSignalAdjuster(double effective_mass=1.0)
std::optional< LorentzCorrectedSeries > adjust(std::span< const BarData > bars) const noexcept
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
Physical and financial constants for the SRFM system.
static constexpr double BETA_MAX_SAFE
Definition constants.hpp:17
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
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
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