Special Relativity in Financial Modeling 1.0.0
Lorentz transforms, spacetime classification, and geodesic price paths for quantitative finance
Loading...
Searching...
No Matches
geodesic_path.cpp
Go to the documentation of this file.
1/**
2 * @file src/geodesic_path.cpp
3 * @brief Geodesic Portfolio Path implementation — Round 5.
4 *
5 * See include/srfm/geodesic_path.hpp for the full module contract.
6 *
7 * The geodesic equation d^2w_i/dt^2 = -2*lambda*w_i is a simple harmonic
8 * oscillator with omega = sqrt(2*lambda). The analytical solution is:
9 *
10 * w_i(t) = A_i * cos(omega * t) + B_i * sin(omega * t)
11 *
12 * Boundary conditions:
13 * w_i(0) = start.weights[i] → A_i = start.weights[i]
14 * w_i(1) = end.weights[i] → B_i = (end.weights[i] - A_i*cos(omega))
15 * / sin(omega)
16 *
17 * Degenerate cases:
18 * - lambda == 0 (omega == 0): straight-line interpolation.
19 * - sin(omega) ≈ 0 (omega near pi, 2pi, ...): linear interpolation fallback.
20 */
21
23
24#include <algorithm>
25#include <cmath>
26#include <stdexcept>
27#include <string>
28
29namespace srfm::portfolio {
30
31// ─── PortfolioState ───────────────────────────────────────────────────────────
32
33bool PortfolioState::is_valid() const noexcept {
34 if (weights.empty()) return false;
35 for (double w : weights) {
36 if (!std::isfinite(w)) return false;
37 }
38 return true;
39}
40
41double PortfolioState::sum_weights() const noexcept {
42 double s = 0.0;
43 for (double w : weights) s += w;
44 return s;
45}
46
47// ─── GeodesicSolver ───────────────────────────────────────────────────────────
48
49// static
50double GeodesicSolver::trajectory(
51 double w_start,
52 double w_end,
53 double t_norm,
54 double omega
55) noexcept {
56 // t_norm ∈ [0, 1]
57 if (omega < 1e-12) {
58 // Flat spacetime (lambda ≈ 0): straight-line interpolation
59 return w_start + (w_end - w_start) * t_norm;
60 }
61
62 const double sin_omega = std::sin(omega);
63 if (std::fabs(sin_omega) < 1e-12) {
64 // sin(omega) ≈ 0: oscillator period divides the interval exactly.
65 // Fall back to linear interpolation to avoid division by zero.
66 return w_start + (w_end - w_start) * t_norm;
67 }
68
69 // Harmonic oscillator coefficients from boundary conditions
70 const double A = w_start;
71 const double B = (w_end - A * std::cos(omega)) / sin_omega;
72
73 return A * std::cos(omega * t_norm) + B * std::sin(omega * t_norm);
74}
75
76// static
78 const PortfolioState& start,
79 const PortfolioState& end,
80 int n_steps,
81 double lambda
82) {
83 if (n_steps < 1) {
84 throw std::invalid_argument(
85 "GeodesicSolver::solve — n_steps must be >= 1, got " +
86 std::to_string(n_steps)
87 );
88 }
89 if (start.dim() != end.dim()) {
90 throw std::invalid_argument(
91 "GeodesicSolver::solve — start and end must have the same dimension ("
92 + std::to_string(start.dim()) + " vs " + std::to_string(end.dim()) + ")"
93 );
94 }
95 if (start.weights.empty()) {
96 throw std::invalid_argument(
97 "GeodesicSolver::solve — portfolio dimension must be >= 1"
98 );
99 }
100 if (lambda < 0.0) {
101 throw std::invalid_argument(
102 "GeodesicSolver::solve — lambda must be >= 0, got " +
103 std::to_string(lambda)
104 );
105 }
106
107 const int dim = start.dim();
108 const double omega = std::sqrt(2.0 * lambda);
109
110 // Total number of waypoints = n_steps + 1 (inclusive of both endpoints)
111 const int n_points = n_steps + 1;
112
113 // Linearly interpolate timestamps
114 const int64_t t0 = start.timestamp_ms;
115 const int64_t t1 = end.timestamp_ms;
116
117 Geodesic result;
118 result.states.reserve(static_cast<std::size_t>(n_points));
119
120 for (int step = 0; step < n_points; ++step) {
121 const double t_norm = (n_steps == 0)
122 ? 0.0
123 : static_cast<double>(step) / static_cast<double>(n_steps);
124
125 PortfolioState state;
126 state.weights.resize(static_cast<std::size_t>(dim));
127 state.timestamp_ms = t0 + static_cast<int64_t>(
128 std::round(static_cast<double>(t1 - t0) * t_norm)
129 );
130
131 for (int i = 0; i < dim; ++i) {
132 state.weights[static_cast<std::size_t>(i)] = trajectory(
133 start.weights[static_cast<std::size_t>(i)],
134 end.weights[static_cast<std::size_t>(i)],
135 t_norm,
136 omega
137 );
138 }
139
140 result.states.push_back(std::move(state));
141 }
142
143 return result;
144}
145
146// ─── GeodesicLength ───────────────────────────────────────────────────────────
147
148// static
150 const PortfolioState& a,
151 const PortfolioState& b
152) noexcept {
153 if (a.weights.size() != b.weights.size()) return 0.0;
154 double sum_sq = 0.0;
155 for (std::size_t i = 0; i < a.weights.size(); ++i) {
156 const double diff = a.weights[i] - b.weights[i];
157 sum_sq += diff * diff;
158 }
159 return std::sqrt(sum_sq);
160}
161
162// static
163double GeodesicLength::compute(const Geodesic& geodesic, double dt) noexcept {
164 if (geodesic.states.size() < 2) return 0.0;
165 const double effective_dt = (dt <= 0.0) ? 1.0 : dt;
166
167 double length = 0.0;
168 for (std::size_t i = 1; i < geodesic.states.size(); ++i) {
169 // Arc-length contribution: ||dw/dt|| * dt
170 // Approximate ||dw|| as Euclidean distance between consecutive states,
171 // then divide by effective_dt to get velocity magnitude, then multiply.
172 // Net effect: sum of Euclidean distances (independent of dt scaling).
173 const double seg = distance(geodesic.states[i - 1], geodesic.states[i]);
174 length += seg;
175 }
176 // Scale by effective_dt to convert from displacement to arc length
177 // (velocity = displacement / dt → arc length = velocity * dt = displacement)
178 // So arc length equals the sum of displacements directly.
179 (void)effective_dt; // dt cancels; length = sum of segment distances
180 return length;
181}
182
183} // namespace srfm::portfolio
static double compute(const Geodesic &geodesic, double dt=1.0) noexcept
static double distance(const PortfolioState &a, const PortfolioState &b) noexcept
Compute the Euclidean distance between two PortfolioState weight vectors.
static Geodesic solve(const PortfolioState &start, const PortfolioState &end, int n_steps, double lambda)
Geodesic Portfolio Path — Round 5 public API.
std::vector< PortfolioState > states
Ordered from start to end.
A point in portfolio space + time.
int64_t timestamp_ms
Wall-clock time in milliseconds.
std::vector< double > weights
Portfolio weights (any length >= 1)
bool is_valid() const noexcept
Returns true if weights is non-empty and all elements are finite.
int dim() const noexcept
Dimension of the portfolio (number of assets).
double sum_weights() const noexcept
Sum of all weights.