Special Relativity in Financial Modeling 1.0.0
Lorentz transforms, spacetime classification, and geodesic price paths for quantitative finance
Loading...
Searching...
No Matches
backtest_runner.cpp
Go to the documentation of this file.
1/// @file src/validation/backtest_runner.cpp
2/// @brief Empirical Validation Binary — Three-Strategy Backtest Runner
3///
4/// # Binary: backtest_runner
5///
6/// ## Purpose
7/// Reads a regime CSV (output from regime_validator) and runs all three
8/// SRFM strategies (RAW, RELATIVISTIC, GEODESIC_DEVIATION) via
9/// ExtendedBacktester. Writes one output row per strategy per ticker:
10///
11/// ticker, strategy, sharpe, sortino, max_drawdown
12///
13/// Also prints a formatted comparison table to stdout.
14///
15/// ## Usage
16/// backtest_runner --input <regime_csv> --output-dir <dir> [--ticker <name>]
17///
18/// ## Input Format (regime CSV)
19/// ticker, bar_index, interval_type, next_bar_abs_return, beta, geodesic_deviation
20///
21/// ## Exit Codes
22/// 0 — success
23/// 1 — fatal error
24
26#include "srfm/backtest.hpp"
27#include "srfm/constants.hpp"
28
29#include <algorithm>
30#include <cmath>
31#include <cstdlib>
32#include <filesystem>
33#include <fstream>
34#include <iostream>
35#include <optional>
36#include <sstream>
37#include <string>
38#include <vector>
39
40namespace fs = std::filesystem;
41
42// ─── CSV Parsing ─────────────────────────────────────────────────────────────
43
44static std::vector<std::string> split_csv(const std::string& line) {
45 std::vector<std::string> fields;
46 std::string field;
47 std::istringstream ss(line);
48 while (std::getline(ss, field, ',')) {
49 auto start = field.find_first_not_of(" \t\r\n");
50 auto end = field.find_last_not_of(" \t\r\n");
51 if (start != std::string::npos) {
52 fields.push_back(field.substr(start, end - start + 1));
53 } else {
54 fields.push_back("");
55 }
56 }
57 return fields;
58}
59
60static std::optional<double> safe_double(const std::string& s) {
61 if (s.empty()) return std::nullopt;
62 try {
63 std::size_t pos = 0;
64 double v = std::stod(s, &pos);
65 if (pos != s.size()) return std::nullopt;
66 if (!std::isfinite(v)) return std::nullopt;
67 return v;
68 } catch (...) {
69 return std::nullopt;
70 }
71}
72
73static int find_col(const std::vector<std::string>& hdr, const std::string& name) {
74 for (int i = 0; i < static_cast<int>(hdr.size()); ++i) {
75 std::string h = hdr[static_cast<std::size_t>(i)];
76 std::transform(h.begin(), h.end(), h.begin(), ::tolower);
77 if (h == name) return i;
78 }
79 return -1;
80}
81
82// ─── Regime Row ──────────────────────────────────────────────────────────────
83
84struct RegimeRow {
85 std::size_t bar_index = 0;
86 std::string interval_type;
87 double next_bar_abs_return = 0.0;
88 double next_bar_return = 0.0; ///< Signed return (may be 0 if column absent)
89 double beta = 0.0;
90 double geodesic_deviation = 0.0;
91};
92
93static std::optional<std::vector<RegimeRow>> load_regime_csv(const std::string& path,
94 std::string& err) {
95 std::ifstream file(path);
96 if (!file.is_open()) {
97 err = "Cannot open: " + path;
98 return std::nullopt;
99 }
100
101 std::string line;
102 if (!std::getline(file, line)) {
103 err = "Empty file: " + path;
104 return std::nullopt;
105 }
106 auto hdr = split_csv(line);
107
108 int col_idx = find_col(hdr, "bar_index");
109 int col_type = find_col(hdr, "interval_type");
110 int col_ret = find_col(hdr, "next_bar_abs_return");
111 int col_sret = find_col(hdr, "next_bar_return");
112 int col_beta = find_col(hdr, "beta");
113 int col_geo = find_col(hdr, "geodesic_deviation");
114
115 if (col_type < 0 || col_ret < 0) {
116 err = "CSV missing required columns interval_type / next_bar_abs_return";
117 return std::nullopt;
118 }
119
120 std::vector<RegimeRow> rows;
121 std::size_t auto_idx = 0;
122 while (std::getline(file, line)) {
123 if (line.empty()) continue;
124 auto fields = split_csv(line);
125 if (static_cast<int>(fields.size()) <= col_type) continue;
126
127 auto maybe_ret = safe_double(fields[static_cast<std::size_t>(col_ret)]);
128 if (!maybe_ret) continue;
129
130 RegimeRow row;
131 row.bar_index = (col_idx >= 0)
132 ? static_cast<std::size_t>(
133 safe_double(fields[static_cast<std::size_t>(col_idx)]).value_or(
134 static_cast<double>(auto_idx)))
135 : auto_idx;
136 row.interval_type = fields[static_cast<std::size_t>(col_type)];
137 row.next_bar_abs_return = *maybe_ret;
138 row.next_bar_return = (col_sret >= 0)
139 ? safe_double(fields[static_cast<std::size_t>(col_sret)]).value_or(0.0)
140 : 0.0;
141 row.beta = (col_beta >= 0)
142 ? safe_double(fields[static_cast<std::size_t>(col_beta)]).value_or(0.0)
143 : 0.0;
144 row.geodesic_deviation = (col_geo >= 0)
145 ? safe_double(fields[static_cast<std::size_t>(col_geo)]).value_or(0.0)
146 : 0.0;
147
148 rows.push_back(row);
149 ++auto_idx;
150 }
151 return rows;
152}
153
154// ─── Build GeodesicBarData ────────────────────────────────────────────────────
155
156/// Convert regime rows to GeodesicBarData.
157///
158/// Signal convention: +1 for TIMELIKE (trust causal momentum), −1 for SPACELIKE.
159/// Asset return = next_bar_abs_return with sign derived from interval type.
160static std::pair<std::vector<srfm::backtest::GeodesicBarData>, std::vector<double>>
161build_bars(const std::vector<RegimeRow>& rows) {
162 std::vector<srfm::backtest::GeodesicBarData> bars;
163 std::vector<double> asset_returns;
164 bars.reserve(rows.size());
165 asset_returns.reserve(rows.size());
166
167 for (const auto& row : rows) {
168 // Raw signal: causal bars get +1, stochastic get -1
169 double raw_signal = (row.interval_type == "Timelike") ? 1.0 : -1.0;
170 double beta_val = std::clamp(row.beta, 0.0, srfm::constants::BETA_MAX_SAFE - 0.001);
171
173 .raw_signal = raw_signal,
174 .beta = srfm::BetaVelocity{beta_val},
175 .benchmark = 0.0, // no benchmark return available
176 };
177
178 bars.push_back(srfm::backtest::GeodesicBarData{
179 .base = base,
180 .geodesic_deviation = row.geodesic_deviation,
181 });
182
183 // Asset return = signed market return.
184 // Backtester applies position sign via raw_signal: return = sign(signal) * market_return.
185 // Geodesic strategy uses 0/1 positions applied to this same signed market return,
186 // ensuring the return series contains both positive and negative values (Sortino computable).
187 // Fall back to abs return if signed column is absent (legacy CSVs without next_bar_return).
188 double asset_ret = (row.next_bar_return != 0.0 || row.next_bar_abs_return == 0.0)
189 ? row.next_bar_return
190 : row.next_bar_abs_return;
191 asset_returns.push_back(asset_ret);
192 }
193
194 return {bars, asset_returns};
195}
196
197// ─── Output ───────────────────────────────────────────────────────────────────
198
200 const std::string& ticker,
202 const std::string& output_dir,
203 std::string& err)
204{
205 fs::create_directories(output_dir);
206 std::string safe_ticker = ticker;
207 std::replace(safe_ticker.begin(), safe_ticker.end(), '-', '_');
208
209 std::string out_path = output_dir + "/" + safe_ticker + "_backtest.csv";
210 std::ofstream out(out_path);
211 if (!out.is_open()) {
212 err = "Cannot open output: " + out_path;
213 return false;
214 }
215
216 out << "ticker,strategy,sharpe,sortino,max_drawdown\n";
217 auto write_row = [&](const std::string& strategy,
219 out << ticker << "," << strategy << ",";
220 out << std::fixed;
221 out.precision(6);
222 out << m.sharpe_ratio << ","
223 << m.sortino_ratio << ","
224 << m.max_drawdown << "\n";
225 };
226
227 write_row("RAW", result.raw);
228 write_row("RELATIVISTIC", result.relativistic);
229 write_row("GEODESIC_DEVIATION", result.geodesic);
230
231 std::cout << "[" << ticker << "] Backtest CSV written to " << out_path << "\n";
232 return true;
233}
234
235// ─── CLI Args ─────────────────────────────────────────────────────────────────
236
237struct Args {
238 std::string input_path;
239 std::string output_dir;
240 std::string ticker;
241};
242
243static std::optional<Args> parse_args(int argc, char* argv[]) {
244 Args args;
245 for (int i = 1; i < argc - 1; ++i) {
246 std::string key(argv[i]);
247 std::string val(argv[i + 1]);
248 if (key == "--input") {
249 args.input_path = val; ++i;
250 } else if (key == "--output-dir") {
251 args.output_dir = val; ++i;
252 } else if (key == "--ticker") {
253 args.ticker = val; ++i;
254 }
255 }
256 if (args.input_path.empty() || args.output_dir.empty()) return std::nullopt;
257 if (args.ticker.empty()) args.ticker = "UNKNOWN";
258 return args;
259}
260
261// ─── main ─────────────────────────────────────────────────────────────────────
262
263int main(int argc, char* argv[]) {
264 auto maybe_args = parse_args(argc, argv);
265 if (!maybe_args.has_value()) {
266 std::cerr << "Usage: backtest_runner --input <regime_csv> "
267 "--output-dir <dir> [--ticker <name>]\n";
268 return 1;
269 }
270 const Args& args = *maybe_args;
271
272 // ── Load regime data ───────────────────────────────────────────────────────
273 std::string load_err;
274 auto maybe_rows = load_regime_csv(args.input_path, load_err);
275 if (!maybe_rows.has_value()) {
276 std::cerr << "[FATAL] " << load_err << "\n";
277 return 1;
278 }
279 std::vector<RegimeRow> rows = std::move(*maybe_rows);
280
281 std::cout << "[" << args.ticker << "] Loaded " << rows.size() << " regime rows\n";
282
283 if (rows.size() < static_cast<std::size_t>(srfm::constants::MIN_RETURN_SERIES_LENGTH)) {
284 std::cerr << "[FATAL] Too few rows (" << rows.size()
285 << ") — need at least " << srfm::constants::MIN_RETURN_SERIES_LENGTH << "\n";
286 return 1;
287 }
288
289 // ── Build bars ─────────────────────────────────────────────────────────────
290 auto [bars, asset_returns] = build_bars(rows);
291
292 // ── Run backtest ───────────────────────────────────────────────────────────
295 100 // rolling window for p75
296 );
297
298 auto maybe_result = backtester.run_triple(bars, asset_returns, args.ticker);
299 if (!maybe_result.has_value()) {
300 std::cerr << "[FATAL] Backtest failed for " << args.ticker
301 << " — check data quality and minimum series length\n";
302 return 1;
303 }
304
305 // ── Print summary ──────────────────────────────────────────────────────────
306 std::cout << maybe_result->to_string();
307
308 // ── Write CSV ──────────────────────────────────────────────────────────────
309 std::string write_err;
310 if (!write_backtest_csv(args.ticker, *maybe_result, args.output_dir, write_err)) {
311 std::cerr << "[FATAL] " << write_err << "\n";
312 return 1;
313 }
314
315 return 0;
316}
Relativistic Backtester — AGT-05 public API.
int main(int argc, char *argv[])
static std::optional< std::vector< RegimeRow > > load_regime_csv(const std::string &path, std::string &err)
static std::pair< std::vector< srfm::backtest::GeodesicBarData >, std::vector< double > > build_bars(const std::vector< RegimeRow > &rows)
static int find_col(const std::vector< std::string > &hdr, const std::string &name)
static std::vector< std::string > split_csv(const std::string &line)
static bool write_backtest_csv(const std::string &ticker, const srfm::backtest::ExtendedBacktester::TripleComparison &result, const std::string &output_dir, std::string &err)
static std::optional< double > safe_double(const std::string &s)
static std::optional< Args > parse_args(int argc, char *argv[])
std::optional< TripleComparison > run_triple(const std::vector< GeodesicBarData > &bars, const std::vector< double > &asset_returns, const std::string &ticker="") const noexcept
Physical and financial constants for the SRFM system.
Extended Backtester with Geodesic Deviation Strategy — AGT-07.
static constexpr std::size_t MIN_RETURN_SERIES_LENGTH
Definition constants.hpp:49
static constexpr double BETA_MAX_SAFE
Definition constants.hpp:17
std::string input_path
std::string output_dir
std::string ticker
double geodesic_deviation
std::string interval_type
double next_bar_return
Signed return (may be 0 if column absent)
double next_bar_abs_return
std::size_t bar_index
Configuration for a backtest run.
Definition backtest.hpp:113
A single time-bar of backtester input.
Definition backtest.hpp:57
double raw_signal
Strategy signal before relativistic correction.
Definition backtest.hpp:58
Three-way performance comparison across all strategy modes.
PerformanceMetrics geodesic
GEODESIC_DEVIATION strategy metrics.
PerformanceMetrics raw
RAW strategy metrics.
PerformanceMetrics relativistic
RELATIVISTIC strategy metrics.
BarData base
Standard bar: raw_signal, beta, benchmark.
Performance metrics for a single strategy evaluation.
Definition backtest.hpp:76