Special Relativity in Financial Modeling 1.0.0
Lorentz transforms, spacetime classification, and geodesic price paths for quantitative finance
Loading...
Searching...
No Matches
n_asset_engine.cpp
Go to the documentation of this file.
1/**
2 * @file n_asset_engine.cpp
3 * @brief Implementation of NAssetEngine.
4 *
5 * See include/srfm/engine/n_asset_engine.hpp for the public API contract.
6 *
7 * ## Pipeline summary
8 * 1. ingest(): push bars into deque ring buffer.
9 * 2. estimate_covariance(): compute N×N empirical covariance from log-returns.
10 * 3. process(): build manifold, per-asset momenta, portfolio interval.
11 */
12
13#include "../../include/srfm/engine/n_asset_engine.hpp"
14
15#include <algorithm>
16#include <cmath>
17#include <numeric>
18
19namespace srfm::engine {
20
21// ── Constructor ───────────────────────────────────────────────────────────────
22
24 EngineConfig cfg) noexcept
25 : universe_(std::move(universe))
26 , cfg_(cfg)
27{}
28
29// ── Ingestion ─────────────────────────────────────────────────────────────────
30
31std::optional<std::monostate>
32NAssetEngine::ingest(std::span<const OHLCVBar> bars) noexcept {
33 // Must provide exactly one bar per asset.
34 if (static_cast<int>(bars.size()) != universe_.n()) {
35 return std::nullopt;
36 }
37
38 // Append to ring buffer.
39 history_.push_back(std::vector<OHLCVBar>(bars.begin(), bars.end()));
40
41 // Trim ring buffer to avoid unbounded growth (keep 2× lookback).
42 const int max_history = cfg_.lookback_bars * 2;
43 while (static_cast<int>(history_.size()) > max_history) {
44 history_.pop_front();
45 }
46
47 return std::monostate{};
48}
49
50// ── Covariance estimation ─────────────────────────────────────────────────────
51
52std::optional<Eigen::MatrixXd>
53NAssetEngine::estimate_covariance() const noexcept {
54 const int N = universe_.n();
55 const int T = static_cast<int>(history_.size());
56
57 // Warm-up guard: prevents wild z-scores before sufficient data.
58 // Require at least lookback_bars bars so covariance is estimated from a
59 // statistically meaningful window, mirroring CoordinateNormalizer::warmed_up().
60 if (T < cfg_.lookback_bars) { return std::nullopt; }
61
62 // Need at least 2 bars to compute one log-return.
63 if (T < 2) { return std::nullopt; }
64
65 const int n_returns = T - 1;
66
67 // Compute log-returns matrix: rows = time, cols = asset.
68 Eigen::MatrixXd returns(n_returns, N);
69
70 for (int t = 0; t < n_returns; ++t) {
71 const auto& prev = history_[static_cast<std::size_t>(t)];
72 const auto& curr = history_[static_cast<std::size_t>(t + 1)];
73
74 for (int i = 0; i < N; ++i) {
75 double prev_close = prev[static_cast<std::size_t>(i)].close;
76 double curr_close = curr[static_cast<std::size_t>(i)].close;
77
78 // Guard against non-positive prices.
79 if (prev_close <= 0.0 || curr_close <= 0.0) {
80 return std::nullopt;
81 }
82
83 returns(t, i) = std::log(curr_close / prev_close);
84 }
85 }
86
87 // Demean columns.
88 Eigen::VectorXd mean = returns.colwise().mean();
89 Eigen::MatrixXd demeaned = returns.rowwise() - mean.transpose();
90
91 // Sample covariance: Cov = (1/(T-1)) * demeaned^T * demeaned.
92 if (n_returns < 2) {
93 // With only one return we cannot compute sample covariance.
94 // Use population covariance (divide by 1) — or return nullopt for safety.
95 // Return an identity-scaled matrix as a fallback.
96 Eigen::MatrixXd cov = demeaned.transpose() * demeaned;
97 // Add small regularisation to ensure positive definiteness.
98 cov += 1e-8 * Eigen::MatrixXd::Identity(N, N);
99 return cov;
100 }
101
102 Eigen::MatrixXd cov = (demeaned.transpose() * demeaned) /
103 static_cast<double>(n_returns - 1);
104
105 // Add small regularisation to ensure positive definiteness even when
106 // assets are nearly perfectly correlated.
107 cov += 1e-8 * Eigen::MatrixXd::Identity(N, N);
108
109 return cov;
110}
111
112// ── Beta computation ──────────────────────────────────────────────────────────
113
114std::optional<double>
115NAssetEngine::compute_beta(double prev_close, double curr_close) const noexcept {
116 if (prev_close <= 0.0) { return std::nullopt; }
117
118 double delta = curr_close - prev_close;
119 double raw_beta = std::abs(delta / (cfg_.c_market * prev_close));
120
121 // Clamp to [0, ENGINE_BETA_MAX_SAFE - 1e-9].
122 const double beta_max = ENGINE_BETA_MAX_SAFE - 1e-9;
123 if (raw_beta >= beta_max) {
124 raw_beta = beta_max;
125 }
126
127 return raw_beta;
128}
129
130// ── Portfolio interval ────────────────────────────────────────────────────────
131
132std::optional<std::pair<double, IntervalType>>
133NAssetEngine::compute_portfolio_interval(
134 const NAssetManifold& manifold) const noexcept {
135 const int T = static_cast<int>(history_.size());
136 if (T < 2) { return std::nullopt; }
137
138 const int N = universe_.n();
139
140 // Build events from the last two bar snapshots.
141 const auto& prev_bars = history_[static_cast<std::size_t>(T - 2)];
142 const auto& curr_bars = history_[static_cast<std::size_t>(T - 1)];
143
144 Eigen::VectorXd prev_prices(N), curr_prices(N);
145 for (int i = 0; i < N; ++i) {
146 prev_prices(i) = prev_bars[static_cast<std::size_t>(i)].close;
147 curr_prices(i) = curr_bars[static_cast<std::size_t>(i)].close;
148 }
149
150 double t_prev = prev_bars[0].timestamp;
151 double t_curr = curr_bars[0].timestamp;
152
153 auto ev_a = NAssetEvent::make(t_prev, prev_prices);
154 auto ev_b = NAssetEvent::make(t_curr, curr_prices);
155 if (!ev_a || !ev_b) { return std::nullopt; }
156
157 NAssetInterval interval_calc;
158 auto result = interval_calc.compute(*ev_a, *ev_b, manifold);
159 if (!result) { return std::nullopt; }
160
161 return std::make_pair(result->ds_sq, result->type);
162}
163
164// ── Processing ────────────────────────────────────────────────────────────────
165
166std::optional<EngineOutput> NAssetEngine::process() const noexcept {
167 if (!ready()) { return std::nullopt; }
168
169 const int N = universe_.n();
170
171 // 1. Estimate covariance.
172 auto cov_opt = estimate_covariance();
173 if (!cov_opt) { return std::nullopt; }
174
175 // 2. Build NAssetManifold.
176 auto manifold_opt = NAssetManifold::make(N, *cov_opt, cfg_.c_market);
177 if (!manifold_opt) { return std::nullopt; }
178 const NAssetManifold& manifold = *manifold_opt;
179
180 // 3. Retrieve latest two bar snapshots.
181 const int T = static_cast<int>(history_.size());
182 const auto& prev_bars = history_[static_cast<std::size_t>(T - 2)];
183 const auto& curr_bars = history_[static_cast<std::size_t>(T - 1)];
184
185 // 4. Compute per-asset results.
186 EngineOutput output;
187 output.timestamp = curr_bars[0].timestamp;
188 output.assets.reserve(static_cast<std::size_t>(N));
189
190 for (int i = 0; i < N; ++i) {
191 const OHLCVBar& prev = prev_bars[static_cast<std::size_t>(i)];
192 const OHLCVBar& curr = curr_bars[static_cast<std::size_t>(i)];
193
194 // Beta.
195 auto beta_opt = compute_beta(prev.close, curr.close);
196 if (!beta_opt) { return std::nullopt; }
197 double beta = *beta_opt;
198
199 // Gamma.
200 double gamma = 1.0 / std::sqrt(1.0 - beta * beta);
201
202 // Effective mass.
203 double m_eff = curr.volume / cfg_.adv_baseline;
204
205 // Relativistic momentum.
206 double p_rel = gamma * m_eff * curr.close;
207
208 // Per-asset regime: classify the price displacement as spacetime interval.
209 // Build single-asset events.
210 Eigen::VectorXd prev_p(1), curr_p(1);
211 prev_p(0) = prev.close;
212 curr_p(0) = curr.close;
213
214 // Use the full N-asset metric for interval classification.
215 // We project to the i-th spatial axis only.
216 // Simpler: classify via beta — β < 1 always, regime from interval.
217 // Use the single-asset sub-manifold for classification.
218 Eigen::MatrixXd cov_1x1(1, 1);
219 cov_1x1(0, 0) = (*cov_opt)(i, i);
220
221 IntervalType asset_regime = IntervalType::SPACELIKE;
222 auto sub_manifold_opt = NAssetManifold::make(1, cov_1x1, cfg_.c_market);
223 if (sub_manifold_opt) {
224 auto ea = NAssetEvent::make(prev.timestamp, prev_p);
225 auto eb = NAssetEvent::make(curr.timestamp, curr_p);
226 if (ea && eb) {
227 NAssetInterval calc;
228 auto ir = calc.compute(*ea, *eb, *sub_manifold_opt);
229 if (ir) {
230 asset_regime = ir->type;
231 }
232 }
233 }
234
236 res.asset_name = universe_.names[static_cast<std::size_t>(i)];
237 res.beta = beta;
238 res.gamma = gamma;
239 res.m_eff = m_eff;
240 res.relativistic_momentum = p_rel;
241 res.regime = asset_regime;
242
243 output.assets.push_back(std::move(res));
244 }
245
246 // 5. Portfolio interval.
247 auto portfolio_opt = compute_portfolio_interval(manifold);
248 if (!portfolio_opt) {
249 output.portfolio_interval_sq = 0.0;
250 output.portfolio_regime = IntervalType::LIGHTLIKE;
251 } else {
252 output.portfolio_interval_sq = portfolio_opt->first;
253 output.portfolio_regime = portfolio_opt->second;
254 }
255
256 return output;
257}
258
259std::optional<EngineOutput>
260NAssetEngine::ingest_and_process(std::span<const OHLCVBar> bars) noexcept {
261 auto r = ingest(bars);
262 if (!r) { return std::nullopt; }
263 return process();
264}
265
266} // namespace srfm::engine
std::optional< std::monostate > ingest(std::span< const OHLCVBar > bars) noexcept
Ingest one bar for every asset in the universe.
std::optional< EngineOutput > ingest_and_process(std::span< const OHLCVBar > bars) noexcept
Ingest bars then immediately process.
bool ready() const noexcept
Return true if enough bars have been accumulated.
std::optional< EngineOutput > process() const noexcept
Run the full pipeline on accumulated history.
NAssetEngine(AssetUniverse universe, EngineConfig cfg={}) noexcept
Construct an engine for the given asset universe.
Computes spacetime intervals between N-asset events.
std::optional< IntervalResult > compute(const NAssetEvent &a, const NAssetEvent &b, const NAssetManifold &manifold) const noexcept
Compute ds² = g_μν Δx^μ Δx^ν between two events.
(N+1)-dimensional Lorentzian manifold for N financial assets.
static std::optional< NAssetManifold > make(int n, Eigen::MatrixXd cov, double c_market=1.0) noexcept
Factory: validate inputs then construct.
constexpr double ENGINE_BETA_MAX_SAFE
Maximum safe beta value (mirrors BETA_MAX_SAFE from momentum.hpp).
Relativistic momentum result for a single asset.
double relativistic_momentum
p_rel = γ × m_eff × close.
double m_eff
Effective mass (volume / ADV_baseline).
std::string asset_name
Name of the asset.
double gamma
Lorentz gamma factor.
IntervalType regime
Interval type for this asset.
double beta
Lorentz beta (normalised velocity).
The universe of assets processed by the engine.
int n() const noexcept
Return the number of assets.
std::vector< std::string > names
Asset names in order.
Engine configuration parameters.
int lookback_bars
Bars required before covariance is valid.
double adv_baseline
ADV baseline for effective mass.
double c_market
Market speed of light.
Aggregated engine output for one process() call.
double timestamp
Timestamp of latest bar.
A single OHLCV bar for one asset.
double close
Closing price.
double timestamp
Bar timestamp (seconds since epoch or bar index).
static std::optional< NAssetEvent > make(double t, Eigen::VectorXd prices) noexcept
Factory: validate and construct an NAssetEvent.