8#include <Eigen/Cholesky>
24constexpr double clamp_d(
double v,
double lo,
double hi)
noexcept {
25 return v < lo ? lo : (v > hi ? hi : v);
29Eigen::VectorXd log_returns(
const Eigen::VectorXd& prev,
30 const Eigen::VectorXd& curr)
noexcept {
31 assert(prev.size() == curr.size());
32 Eigen::VectorXd ret(prev.size());
33 for (Eigen::Index i = 0; i < prev.size(); ++i) {
34 if (prev[i] > 0.0 && curr[i] > 0.0) {
35 ret[i] = std::log(curr[i] / prev[i]);
44double lorentz_gamma(
double beta)
noexcept {
45 const double b2 = beta * beta;
48 return 1.0 / std::sqrt(1.0 - b2);
60 const Eigen::MatrixXd& metric)
noexcept
62 if (!a.is_valid() || !b.is_valid())
return std::nullopt;
63 if (a.n_assets() != b.n_assets())
return std::nullopt;
65 const std::size_t N = a.n_assets();
66 const Eigen::Index dim =
static_cast<Eigen::Index
>(N + 1);
67 if (metric.rows() != dim || metric.cols() != dim)
return std::nullopt;
70 Eigen::VectorXd dx(dim);
71 const double dt =
static_cast<double>(b.timestamp - a.timestamp) / 1000.0;
73 for (std::size_t i = 0; i < N; ++i) {
74 dx[
static_cast<Eigen::Index
>(i + 1)] = b.prices[i] - a.prices[i];
78 const double ds2 = (dx.transpose() * metric * dx).value();
84 if (std::abs(ds2) <= kLightlikeEps)
return IntervalType::Lightlike;
85 if (ds2 < 0.0)
return IntervalType::Timelike;
86 return IntervalType::Spacelike;
89std::optional<std::pair<double, MultiAssetInterval::IntervalType>>
92 const Eigen::MatrixXd& metric)
noexcept
94 auto ds2 = compute(a, b, metric);
95 if (!ds2)
return std::nullopt;
96 return std::make_pair(*ds2, classify(*ds2));
104 : n_assets_(n_assets)
107 assert(n_assets_ > 0);
109 for (
auto& v : log_returns_window_) {
110 v.setZero(
static_cast<Eigen::Index
>(n_assets_));
112 prev_prices_.setZero(
static_cast<Eigen::Index
>(n_assets_));
115std::optional<Eigen::MatrixXd>
117 if (prices.size() != n_assets_)
return std::nullopt;
120 Eigen::VectorXd p(
static_cast<Eigen::Index
>(n_assets_));
121 for (std::size_t i = 0; i < n_assets_; ++i)
122 p[
static_cast<Eigen::Index
>(i)] = prices[i];
132 Eigen::VectorXd ret = log_returns(prev_prices_, p);
133 log_returns_window_[window_head_] = ret;
134 window_head_ = (window_head_ + 1) % cfg_.
window_size;
138 if (obs_count_ < 2)
return std::nullopt;
140 metric_dirty_ =
true;
142 return current_metric_;
145std::optional<Eigen::MatrixXd>
147 return current_metric_;
150std::optional<Eigen::MatrixXd>
152 if (!current_metric_)
return std::nullopt;
153 const Eigen::Index N =
static_cast<Eigen::Index
>(n_assets_);
155 Eigen::MatrixXd spatial = current_metric_->block(1, 1, N, N);
156 Eigen::VectorXd variances = spatial.diagonal();
157 Eigen::MatrixXd corr(N, N);
158 for (Eigen::Index i = 0; i < N; ++i) {
159 for (Eigen::Index j = 0; j < N; ++j) {
160 const double denom = std::sqrt(variances[i] * variances[j]);
161 corr(i, j) = (denom > 1e-12) ? spatial(i, j) / denom : (i == j ? 1.0 : 0.0);
171 metric_dirty_ =
true;
172 current_metric_ = std::nullopt;
173 for (
auto& v : log_returns_window_)
174 v.setZero(
static_cast<Eigen::Index
>(n_assets_));
177Eigen::MatrixXd CorrelationMetric::compute_covariance()
const {
178 const Eigen::Index N =
static_cast<Eigen::Index
>(n_assets_);
180 const std::size_t n_obs = std::min(obs_count_ - 1, cfg_.
window_size);
181 if (n_obs < 2)
return Eigen::MatrixXd::Identity(N, N) * 1e-4;
184 Eigen::VectorXd mean = Eigen::VectorXd::Zero(N);
185 for (std::size_t k = 0; k < n_obs; ++k) {
188 mean += log_returns_window_[idx];
190 mean /=
static_cast<double>(n_obs);
193 Eigen::MatrixXd cov = Eigen::MatrixXd::Zero(N, N);
194 for (std::size_t k = 0; k < n_obs; ++k) {
196 const Eigen::VectorXd z = log_returns_window_[idx] - mean;
197 cov += z * z.transpose();
199 cov /=
static_cast<double>(n_obs - 1);
204void CorrelationMetric::recompute_metric() {
205 const Eigen::Index N =
static_cast<Eigen::Index
>(n_assets_);
206 const Eigen::Index dim = N + 1;
208 Eigen::MatrixXd cov = compute_covariance();
211 Eigen::LLT<Eigen::MatrixXd> llt(cov);
212 if (llt.info() != Eigen::Success) {
217 Eigen::MatrixXd g = Eigen::MatrixXd::Zero(dim, dim);
219 g.block(1, 1, N, N) = cov;
222 metric_dirty_ =
false;
229std::optional<MultiAssetLorentz::TransformResult>
232 const Eigen::MatrixXd& metric)
noexcept
234 if (!a.is_valid() || !b.is_valid())
return std::nullopt;
235 if (a.n_assets() != b.n_assets())
return std::nullopt;
237 const std::size_t N = a.n_assets();
238 const Eigen::Index dim =
static_cast<Eigen::Index
>(N + 1);
239 if (metric.rows() != dim || metric.cols() != dim)
return std::nullopt;
241 const double dt =
static_cast<double>(b.timestamp - a.timestamp) / 1000.0;
242 if (std::abs(dt) < 1e-12)
return std::nullopt;
245 const double c = std::sqrt(std::abs(metric(0, 0)));
246 std::vector<double> betas(N);
247 std::vector<double> gammas(N);
248 for (std::size_t i = 0; i < N; ++i) {
249 const double dp = b.prices[i] - a.prices[i];
250 const double beta = clamp_d(dp / (c * dt),
254 gammas[i] = lorentz_gamma(beta);
258 const double beta_p = portfolio_beta(betas, metric);
259 const double gamma_p = lorentz_gamma(beta_p);
262 std::vector<double> adjusted(N);
263 for (std::size_t i = 0; i < N; ++i) {
264 adjusted[i] = gammas[i] * (b.prices[i] - a.prices[i]);
274 const Eigen::MatrixXd& metric)
noexcept
276 const std::size_t N = betas.size();
277 if (N == 0)
return 0.0;
278 const Eigen::Index dim = metric.rows();
279 if (dim <
static_cast<Eigen::Index
>(N + 1))
return 0.0;
282 Eigen::VectorXd b(
static_cast<Eigen::Index
>(N));
283 for (std::size_t i = 0; i < N; ++i)
284 b[
static_cast<Eigen::Index
>(i)] = betas[i];
287 const Eigen::MatrixXd g_spatial = metric.block(1, 1,
288 static_cast<Eigen::Index
>(N),
289 static_cast<Eigen::Index
>(N));
292 const double b2 = (b.transpose() * g_spatial * b).value();
293 if (b2 <= 0.0)
return 0.0;
302std::vector<PortfolioGeodesic::GeodesicStep>
304 const Eigen::VectorXd& four_velocity,
305 const Eigen::MatrixXd& metric,
309 if (!initial.is_valid() || n_steps == 0 || std::abs(dt) < 1e-15)
312 const std::size_t N = initial.n_assets();
313 const Eigen::Index dim =
static_cast<Eigen::Index
>(N + 1);
314 if (four_velocity.size() != dim)
return {};
315 if (metric.rows() != dim || metric.cols() != dim)
return {};
317 std::vector<GeodesicStep> path;
318 path.reserve(n_steps);
321 Eigen::VectorXd x(dim);
322 x[0] =
static_cast<double>(initial.timestamp) / 1000.0;
323 for (std::size_t i = 0; i < N; ++i)
324 x[
static_cast<Eigen::Index
>(i + 1)] = initial.prices[i];
330 double arc_length = 0.0;
332 for (std::size_t step = 0; step < n_steps; ++step) {
334 x += four_velocity * dt;
338 const double ds2_dot = (four_velocity.transpose() * metric * four_velocity).value();
339 const double ds = std::sqrt(std::abs(ds2_dot)) * std::abs(dt);
344 ev.
timestamp =
static_cast<int64_t
>(x[0] * 1000.0);
346 for (std::size_t i = 0; i < N; ++i)
347 ev.
prices[i] = x[
static_cast<Eigen::Index
>(i + 1)];
348 if (!initial.symbols.empty()) ev.
symbols = initial.symbols;
349 if (!initial.volumes.empty()) ev.
volumes = initial.volumes;
351 path.push_back({ev, tau, arc_length, 0.0 });
358 const std::vector<GeodesicStep>& predicted,
359 const std::vector<MultiAssetEvent>& actual)
noexcept
361 if (predicted.size() != actual.size())
return {};
362 std::vector<double> devs;
363 devs.reserve(predicted.size());
365 for (std::size_t i = 0; i < predicted.size(); ++i) {
366 const auto& pred_prices = predicted[i].event.prices;
367 const auto& actual_prices = actual[i].prices;
368 if (pred_prices.size() != actual_prices.size()) {
373 for (std::size_t j = 0; j < pred_prices.size(); ++j) {
374 const double d = pred_prices[j] - actual_prices[j];
377 devs.push_back(std::sqrt(sq_sum));
382std::vector<Eigen::VectorXd>
384 const Eigen::VectorXd& four_velocity,
385 double gross_exposure)
noexcept
387 if (steps.empty() || four_velocity.size() < 2)
return {};
388 const Eigen::Index N = four_velocity.size() - 1;
391 const Eigen::VectorXd u_spatial = four_velocity.tail(N);
392 const double norm = u_spatial.lpNorm<1>();
394 std::vector<Eigen::VectorXd> weights;
395 weights.reserve(steps.size());
397 for (std::size_t i = 0; i < steps.size(); ++i) {
398 Eigen::VectorXd w(N);
402 w = u_spatial * (gross_exposure / norm);
404 weights.push_back(w);
std::optional< Eigen::MatrixXd > update(const std::vector< double > &prices)
Add a new bar observation and update the metric.
std::optional< Eigen::MatrixXd > current_metric() const noexcept
Return the current metric tensor (last computed).
CorrelationMetric(std::size_t n_assets, Config cfg={})
Construct with given configuration.
std::optional< Eigen::MatrixXd > correlation_matrix() const noexcept
Return the rolling correlation matrix (spatial block only).
void reset() noexcept
Reset the window (clears all observations).
static std::optional< double > compute(const MultiAssetEvent &a, const MultiAssetEvent &b, const Eigen::MatrixXd &metric) noexcept
Compute ds² between two N-asset spacetime events.
static IntervalType classify(double ds2) noexcept
Classify a pre-computed ds² value.
static std::optional< std::pair< double, IntervalType > > compute_and_classify(const MultiAssetEvent &a, const MultiAssetEvent &b, const Eigen::MatrixXd &metric) noexcept
Compute and classify in one call.
static double portfolio_beta(const std::vector< double > &betas, const Eigen::MatrixXd &metric) noexcept
Compute portfolio β from individual asset velocities and the metric.
static std::optional< TransformResult > transform(const MultiAssetEvent &a, const MultiAssetEvent &b, const Eigen::MatrixXd &metric) noexcept
Apply the multi-asset Lorentz transform.
static std::vector< double > deviation_series(const std::vector< GeodesicStep > &predicted, const std::vector< MultiAssetEvent > &actual) noexcept
Compute the geodesic deviation between the predicted and actual path.
static std::vector< Eigen::VectorXd > portfolio_weights(const std::vector< GeodesicStep > &steps, const Eigen::VectorXd &four_velocity, double gross_exposure=1.0) noexcept
Compute portfolio weights along the geodesic path.
static std::vector< GeodesicStep > integrate(const MultiAssetEvent &initial, const Eigen::VectorXd &four_velocity, const Eigen::MatrixXd &metric, std::size_t n_steps, double dt) noexcept
Compute the geodesic from an initial event with given four-velocity.
Physical and financial constants for the SRFM system.
Multi-asset spacetime extension for the SRFM library.
static constexpr double BETA_MAX_SAFE
IntervalType
Causal character of a spacetime interval.
Lorentz factor γ = 1/√(1−β²). Always ≥ 1.0 for valid beta.
double c_market
Market speed-of-light.
std::size_t window_size
Rolling window (number of bars).
double regularisation_eps
Ridge regularisation for ill-conditioned Σ.
A snapshot of N correlated financial assets at a point in time.
int64_t timestamp
Unix epoch milliseconds.
std::vector< std::string > symbols
Asset ticker symbols (for labelling).
std::vector< double > prices
Mid-prices for each asset.
std::vector< double > volumes
Traded volumes for each asset.