Special Relativity in Financial Modeling 1.0.0
Lorentz transforms, spacetime classification, and geodesic price paths for quantitative finance
Loading...
Searching...
No Matches
regime_validator.cpp
Go to the documentation of this file.
1/// @file src/validation/regime_validator.cpp
2/// @brief Empirical Validation Binary — Regime Classification + Return Recording
3///
4/// # Binary: regime_validator
5///
6/// ## Purpose
7/// Reads a 1-minute OHLCV CSV file (produced by validation/fetch_data.py),
8/// runs the full SRFM pipeline on each bar, and writes one output row per bar:
9///
10/// ticker, bar_index, interval_type, next_bar_abs_return, beta, geodesic_deviation
11///
12/// This output feeds the Q1 and Q2 Python analysis scripts.
13///
14/// ## Pipeline
15/// 1. Parse OHLCV CSV (no external library — std::stringstream)
16/// 2. Compute SpacetimeEvent for each bar (time, close, volume, momentum)
17/// 3. Normalize events via CoordinateNormalizer (rolling z-score, window=20)
18/// 4. Classify each bar via MarketManifold::process (TIMELIKE/SPACELIKE/LIGHTLIKE)
19/// 5. Compute β for each bar via MarketManifold::beta
20/// 6. Compute geodesic deviation via GeodesicDeviationCalculator::compute
21/// 7. Write output CSV
22///
23/// ## Usage
24/// regime_validator --input <csv> --output <csv> --ticker <name>
25///
26/// ## Exit Codes
27/// 0 — success
28/// 1 — fatal error (missing file, bad args, etc.)
29
30#include "srfm/manifold.hpp"
31#include "srfm/normalizer.hpp"
32#include "srfm/tensor.hpp"
34#include "srfm/constants.hpp"
35
36#include <algorithm>
37#include <cmath>
38#include <cstdlib>
39#include <fstream>
40#include <iostream>
41#include <optional>
42#include <sstream>
43#include <string>
44#include <vector>
45
46// ─── Raw OHLCV Bar ────────────────────────────────────────────────────────────
47
48struct OhlcvBar {
49 std::string timestamp;
50 double open = 0.0;
51 double high = 0.0;
52 double low = 0.0;
53 double close = 0.0;
54 double volume = 0.0;
55};
56
57// ─── CSV Helpers ──────────────────────────────────────────────────────────────
58
59/// Split a CSV line on commas (no quote handling needed for numeric data).
60static std::vector<std::string> split_csv(const std::string& line) {
61 std::vector<std::string> fields;
62 std::string field;
63 std::istringstream ss(line);
64 while (std::getline(ss, field, ',')) {
65 // Trim whitespace
66 auto start = field.find_first_not_of(" \t\r\n");
67 auto end = field.find_last_not_of(" \t\r\n");
68 if (start != std::string::npos) {
69 fields.push_back(field.substr(start, end - start + 1));
70 } else {
71 fields.push_back("");
72 }
73 }
74 return fields;
75}
76
77/// Safely parse a double from a string. Returns nullopt on failure.
78static std::optional<double> safe_parse_double(const std::string& s) {
79 if (s.empty()) return std::nullopt;
80 try {
81 std::size_t pos = 0;
82 double val = std::stod(s, &pos);
83 if (pos != s.size()) return std::nullopt;
84 if (!std::isfinite(val)) return std::nullopt;
85 return val;
86 } catch (...) {
87 return std::nullopt;
88 }
89}
90
91/// Find the column index of a header name (case-insensitive).
92static int find_col(const std::vector<std::string>& header,
93 const std::string& name) {
94 for (int i = 0; i < static_cast<int>(header.size()); ++i) {
95 std::string h = header[static_cast<std::size_t>(i)];
96 std::transform(h.begin(), h.end(), h.begin(), ::tolower);
97 if (h == name) return i;
98 }
99 return -1;
100}
101
102// ─── Data Loading ─────────────────────────────────────────────────────────────
103
104static std::optional<std::vector<OhlcvBar>> load_csv(const std::string& path,
105 std::string& err) {
106 std::ifstream file(path);
107 if (!file.is_open()) {
108 err = "Cannot open input file: " + path;
109 return std::nullopt;
110 }
111
112 std::vector<OhlcvBar> bars;
113 std::string line;
114
115 // Read header
116 if (!std::getline(file, line)) {
117 err = "Empty CSV file: " + path;
118 return std::nullopt;
119 }
120 auto header = split_csv(line);
121
122 int col_ts = find_col(header, "timestamp");
123 int col_open = find_col(header, "open");
124 int col_high = find_col(header, "high");
125 int col_low = find_col(header, "low");
126 int col_close = find_col(header, "close");
127 int col_volume = find_col(header, "volume");
128
129 if (col_close < 0 || col_volume < 0) {
130 err = "CSV missing required columns 'close' or 'volume'";
131 return std::nullopt;
132 }
133
134 std::size_t row_idx = 0;
135 std::size_t skipped = 0;
136 while (std::getline(file, line)) {
137 ++row_idx;
138 if (line.empty()) continue;
139
140 auto fields = split_csv(line);
141 if (static_cast<int>(fields.size()) <= std::max(col_close, col_volume)) {
142 ++skipped;
143 continue;
144 }
145
146 auto maybe_close = safe_parse_double(fields[static_cast<std::size_t>(col_close)]);
147 auto maybe_volume = safe_parse_double(fields[static_cast<std::size_t>(col_volume)]);
148
149 if (!maybe_close || !maybe_volume) {
150 ++skipped;
151 continue;
152 }
153 if (*maybe_close <= 0.0 || *maybe_volume < 0.0) {
154 ++skipped;
155 continue;
156 }
157
158 OhlcvBar bar;
159 bar.timestamp = (col_ts >= 0) ? fields[static_cast<std::size_t>(col_ts)] : std::to_string(row_idx);
160 bar.close = *maybe_close;
161 bar.volume = *maybe_volume;
162
163 if (col_open >= 0) {
164 auto v = safe_parse_double(fields[static_cast<std::size_t>(col_open)]);
165 bar.open = v.value_or(bar.close);
166 }
167 if (col_high >= 0) {
168 auto v = safe_parse_double(fields[static_cast<std::size_t>(col_high)]);
169 bar.high = v.value_or(bar.close);
170 }
171 if (col_low >= 0) {
172 auto v = safe_parse_double(fields[static_cast<std::size_t>(col_low)]);
173 bar.low = v.value_or(bar.close);
174 }
175
176 bars.push_back(bar);
177 }
178
179 if (skipped > 0) {
180 std::cerr << "[regime_validator] Skipped " << skipped << " non-finite rows\n";
181 }
182 return bars;
183}
184
185// ─── SRFM Pipeline ────────────────────────────────────────────────────────────
186
188 std::size_t bar_index = 0;
189 std::string interval_type; // "Timelike", "Spacelike", "Lightlike"
191 double next_bar_return = 0.0; ///< Signed next-bar return (for backtesting)
192 double beta = 0.0;
193 double geodesic_deviation = 0.0;
194};
195
196static std::vector<ClassifiedBar> classify_bars(
197 const std::vector<OhlcvBar>& bars,
198 const std::string& ticker)
199{
200 const std::size_t n = bars.size();
201 if (n < 3) {
202 std::cerr << "[" << ticker << "] Too few bars (" << n << ") — need at least 3\n";
203 return {};
204 }
205
206 // ── Build SpacetimeEvents (raw) ────────────────────────────────────────────
207 // momentum = (close - prev_close) / prev_close (simple return as momentum proxy)
208 std::vector<srfm::manifold::SpacetimeEvent> raw_events;
209 raw_events.reserve(n);
210 for (std::size_t i = 0; i < n; ++i) {
211 double momentum = 0.0;
212 if (i > 0 && bars[i - 1].close > 0.0) {
213 momentum = (bars[i].close - bars[i - 1].close) / bars[i - 1].close;
214 }
215 raw_events.push_back({
216 .time = static_cast<double>(i),
217 .price = bars[i].close,
218 .volume = bars[i].volume,
219 .momentum = momentum,
220 });
221 }
222
223 // ── Normalizer ────────────────────────────────────────────────────────────
224 srfm::CoordinateNormalizer normalizer(20);
225
226 // Warm up normalizer on first 2 bars (these become the "prev" for pipeline)
227 auto maybe_prev0 = normalizer.normalize(raw_events[0]);
228 srfm::manifold::SpacetimeEvent prev_normalized =
229 maybe_prev0.value_or(srfm::manifold::SpacetimeEvent{});
230 (void)normalizer.normalize(raw_events[1]); // warm-up second bar
231
232 // Pre-normalize all events for geodesic computation
234 std::vector<srfm::manifold::SpacetimeEvent> all_normalized;
235 all_normalized.reserve(n);
236 for (const auto& ev : raw_events) {
237 auto maybe_norm = norm2.normalize(ev);
238 if (maybe_norm.has_value()) {
239 all_normalized.push_back(*maybe_norm);
240 } else {
241 all_normalized.push_back(srfm::manifold::SpacetimeEvent{});
242 }
243 }
244
245 // ── Geodesic deviation ────────────────────────────────────────────────────
246 auto metric = srfm::tensor::MetricTensor::make_minkowski(1.0, 1.0);
248 auto geo_signals = geo_calc.compute(all_normalized);
249
250 // ── Main classification loop ───────────────────────────────────────────────
251 // Start from bar 2 (warmup bars 0,1 are skipped)
252 std::vector<ClassifiedBar> results;
253 results.reserve(n - 2);
254
255 // Reset normalizer — re-normalize in order for classify loop
257 {
258 auto maybe_p = norm3.normalize(raw_events[0]);
259 prev_normalized = maybe_p.value_or(srfm::manifold::SpacetimeEvent{});
260 }
261
262 for (std::size_t i = 1; i + 1 < n; ++i) {
263 // Classify interval i-1 → i
265 norm3, prev_normalized, raw_events[i]);
266 {
267 auto maybe_p = norm3.normalize(raw_events[i]); // update for next step
268 prev_normalized = maybe_p.value_or(srfm::manifold::SpacetimeEvent{});
269 }
270
271 if (!maybe_type.has_value()) {
272 continue;
273 }
274
275 // β = |Δspatial| / (c · |Δt|)
276 auto maybe_beta = srfm::manifold::MarketManifold::beta(
277 all_normalized[i - 1], all_normalized[i]);
278 double beta = maybe_beta.value_or(0.0);
279
280 // next-bar absolute return = |close[i+1] / close[i] - 1|
281 double next_abs_ret = 0.0;
282 double next_ret = 0.0;
283 if (bars[i].close > 0.0) {
284 next_ret = bars[i + 1].close / bars[i].close - 1.0;
285 next_abs_ret = std::abs(next_ret);
286 }
287
288 // Geodesic deviation at bar i
289 double geo_dev = 0.0;
290 if (i < geo_signals.size() && geo_signals[i].is_valid) {
291 geo_dev = geo_signals[i].geodesic_deviation;
292 }
293
294 const char* type_str = srfm::manifold::to_string(*maybe_type);
295
296 results.push_back(ClassifiedBar{
297 .bar_index = i,
298 .interval_type = std::string(type_str),
299 .next_bar_abs_return = next_abs_ret,
300 .next_bar_return = next_ret,
301 .beta = beta,
302 .geodesic_deviation = geo_dev,
303 });
304 }
305
306 return results;
307}
308
309// ─── Output ───────────────────────────────────────────────────────────────────
310
311static bool write_output(
312 const std::string& ticker,
313 const std::vector<ClassifiedBar>& bars,
314 const std::string& output_path,
315 std::string& err)
316{
317 std::ofstream out(output_path);
318 if (!out.is_open()) {
319 err = "Cannot open output file: " + output_path;
320 return false;
321 }
322
323 out << "ticker,bar_index,interval_type,next_bar_abs_return,next_bar_return,beta,geodesic_deviation\n";
324
325 for (const auto& b : bars) {
326 out << ticker << ","
327 << b.bar_index << ","
328 << b.interval_type << ","
329 << std::fixed;
330 out.precision(10);
331 out << b.next_bar_abs_return << ","
332 << b.next_bar_return << ","
333 << b.beta << ","
334 << b.geodesic_deviation << "\n";
335 }
336 return true;
337}
338
339// ─── CLI Argument Parsing ─────────────────────────────────────────────────────
340
341struct Args {
342 std::string input_path;
343 std::string output_path;
344 std::string ticker;
345};
346
347static std::optional<Args> parse_args(int argc, char* argv[]) {
348 Args args;
349 for (int i = 1; i < argc - 1; ++i) {
350 std::string key(argv[i]);
351 std::string val(argv[i + 1]);
352 if (key == "--input") {
353 args.input_path = val;
354 ++i;
355 } else if (key == "--output") {
356 args.output_path = val;
357 ++i;
358 } else if (key == "--ticker") {
359 args.ticker = val;
360 ++i;
361 }
362 }
363 if (args.input_path.empty() || args.output_path.empty()) {
364 return std::nullopt;
365 }
366 if (args.ticker.empty()) {
367 args.ticker = "UNKNOWN";
368 }
369 return args;
370}
371
372// ─── main ─────────────────────────────────────────────────────────────────────
373
374int main(int argc, char* argv[]) {
375 auto maybe_args = parse_args(argc, argv);
376 if (!maybe_args.has_value()) {
377 std::cerr << "Usage: regime_validator --input <csv> --output <csv> "
378 "[--ticker <name>]\n";
379 return 1;
380 }
381
382 const Args& args = *maybe_args;
383
384 // ── Load data ─────────────────────────────────────────────────────────────
385 std::string load_err;
386 auto maybe_bars = load_csv(args.input_path, load_err);
387 if (!maybe_bars.has_value()) {
388 std::cerr << "[FATAL] " << load_err << "\n";
389 return 1;
390 }
391 std::vector<OhlcvBar> bars = std::move(*maybe_bars);
392
393 if (bars.empty()) {
394 std::cerr << "[FATAL] No valid bars loaded from " << args.input_path << "\n";
395 return 1;
396 }
397
398 std::cout << "[" << args.ticker << "] Loaded " << bars.size() << " bars\n";
399
400 // ── Classify ──────────────────────────────────────────────────────────────
401 auto classified = classify_bars(bars, args.ticker);
402
403 if (classified.empty()) {
404 std::cerr << "[FATAL] No bars classified — check data quality\n";
405 return 1;
406 }
407
408 // ── Summary ───────────────────────────────────────────────────────────────
409 std::size_t n_tl = 0, n_sl = 0, n_ll = 0;
410 for (const auto& b : classified) {
411 if (b.interval_type == "Timelike") ++n_tl;
412 else if (b.interval_type == "Spacelike") ++n_sl;
413 else ++n_ll;
414 }
415
416 std::cout << "[" << args.ticker << "] Classified " << classified.size() << " bars\n";
417 std::cout << " TIMELIKE: " << n_tl << " (" << (100.0 * n_tl / classified.size()) << "%)\n";
418 std::cout << " SPACELIKE: " << n_sl << " (" << (100.0 * n_sl / classified.size()) << "%)\n";
419 std::cout << " LIGHTLIKE: " << n_ll << " (" << (100.0 * n_ll / classified.size()) << "%)\n";
420
421 // ── Write output ──────────────────────────────────────────────────────────
422 std::string write_err;
423 if (!write_output(args.ticker, classified, args.output_path, write_err)) {
424 std::cerr << "[FATAL] " << write_err << "\n";
425 return 1;
426 }
427
428 std::cout << "[" << args.ticker << "] Output written to " << args.output_path << "\n";
429 return 0;
430}
std::optional< manifold::SpacetimeEvent > normalize(const manifold::SpacetimeEvent &raw) noexcept
static std::optional< double > beta(const SpacetimeEvent &a, const SpacetimeEvent &b, double c_market=constants::SPEED_OF_INFORMATION) noexcept
static std::optional< IntervalType > process(srfm::CoordinateNormalizer &normalizer, const SpacetimeEvent &prev_normalized, const SpacetimeEvent &curr_raw) noexcept
std::vector< GeodesicSignal > compute(const std::vector< manifold::SpacetimeEvent > &events) const noexcept
static MetricTensor make_minkowski(double time_scale=1.0, double spatial_scale=1.0)
Physical and financial constants for the SRFM system.
Geodesic Deviation Signal — AGT-07 public API.
Spacetime Market Manifold — AGT-02 public API (implemented by AGT-06).
const char * to_string(IntervalType t) noexcept
Convert IntervalType to a human-readable string.
CoordinateNormalizer — rolling z-score normalizer for SpacetimeEvent.
int main(int argc, char *argv[])
static std::optional< double > safe_parse_double(const std::string &s)
Safely parse a double from a string. Returns nullopt on failure.
static std::optional< std::vector< OhlcvBar > > load_csv(const std::string &path, std::string &err)
static std::vector< ClassifiedBar > classify_bars(const std::vector< OhlcvBar > &bars, const std::string &ticker)
static std::vector< std::string > split_csv(const std::string &line)
Split a CSV line on commas (no quote handling needed for numeric data).
static bool write_output(const std::string &ticker, const std::vector< ClassifiedBar > &bars, const std::string &output_path, std::string &err)
static std::optional< Args > parse_args(int argc, char *argv[])
static int find_col(const std::vector< std::string > &header, const std::string &name)
Find the column index of a header name (case-insensitive).
std::string input_path
std::string ticker
std::string output_path
double next_bar_return
Signed next-bar return (for backtesting)
std::size_t bar_index
std::string interval_type
std::string timestamp
A point in 4D spacetime (t, x, y, z).
Tensor Calculus & Covariance Engine — AGT-04 public API.