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.hpp
Go to the documentation of this file.
1#pragma once
2
3/// @file include/srfm/geodesic_path.hpp
4/// @brief Geodesic Portfolio Path — Round 5 public API.
5///
6/// # Module: Geodesic Portfolio Path
7///
8/// ## Concept
9/// In financial spacetime, the geodesic between two portfolio states is the
10/// path of minimum action. The Lagrangian is:
11///
12/// L = (1/2) ||dw/dt||^2 - V(w)
13///
14/// where V(w) = lambda * sum(w_i^2) is a concentration penalty.
15///
16/// The Euler-Lagrange equations yield:
17///
18/// d^2w_i/dt^2 = -dV/dw_i = -2 * lambda * w_i
19///
20/// This is simple harmonic oscillator motion with omega = sqrt(2 * lambda).
21///
22/// ## Analytical Solution
23///
24/// w_i(t) = A_i * cos(omega * t) + B_i * sin(omega * t)
25///
26/// Boundary conditions w_i(0) = start.weights[i] and
27/// w_i(T) = end.weights[i] determine A_i and B_i.
28///
29/// ## Classes
30///
31/// - `PortfolioState` : weights vector + timestamp_ms
32/// - `Geodesic` : discretised path (vector of PortfolioState)
33/// - `GeodesicSolver` : solves for the geodesic path between two states
34/// - `GeodesicLength` : computes the integrated arc length of a geodesic
35///
36/// ## Guarantees
37/// - All methods noexcept where possible
38/// - No dynamic allocation beyond the returned Geodesic
39/// - Thread-safe (pure functions; no shared mutable state)
40
41#include <cmath>
42#include <cstdint>
43#include <optional>
44#include <stdexcept>
45#include <vector>
46
47namespace srfm::portfolio {
48
49// ─── PortfolioState ───────────────────────────────────────────────────────────
50
51/// A point in portfolio space + time.
53 std::vector<double> weights; ///< Portfolio weights (any length >= 1)
54 int64_t timestamp_ms; ///< Wall-clock time in milliseconds
55
56 /// Dimension of the portfolio (number of assets).
57 int dim() const noexcept { return static_cast<int>(weights.size()); }
58
59 /// Returns true if weights is non-empty and all elements are finite.
60 bool is_valid() const noexcept;
61
62 /// Sum of all weights.
63 double sum_weights() const noexcept;
64};
65
66// ─── Geodesic ─────────────────────────────────────────────────────────────────
67
68/// Discretised path through portfolio space, representing the geodesic
69/// between two PortfolioState endpoints.
70struct Geodesic {
71 std::vector<PortfolioState> states; ///< Ordered from start to end
72
73 /// Number of waypoints (including endpoints).
74 int size() const noexcept { return static_cast<int>(states.size()); }
75
76 /// True if the path contains at least two states.
77 bool is_valid() const noexcept { return states.size() >= 2; }
78
79 /// Returns the start state (first waypoint).
80 const PortfolioState& start() const { return states.front(); }
81
82 /// Returns the end state (last waypoint).
83 const PortfolioState& end() const { return states.back(); }
84};
85
86// ─── GeodesicSolver ───────────────────────────────────────────────────────────
87
88/// Computes the geodesic between two portfolio states under a concentration
89/// penalty Lagrangian.
90///
91/// The analytical solution:
92/// omega = sqrt(2 * lambda)
93/// A_i = start.weights[i]
94/// B_i = (end.weights[i] - A_i * cos(omega)) / sin(omega) if sin(omega) != 0
95/// (linear interpolation fallback when omega is near zero)
96/// w_i(t_norm) = A_i * cos(omega * t_norm) + B_i * sin(omega * t_norm)
97///
98/// where t_norm in [0, 1] is the normalised time along the geodesic.
100public:
101 /// Solve for the geodesic between start and end.
102 ///
103 /// @param start Initial portfolio state.
104 /// @param end Target portfolio state. Must have the same dimension as start.
105 /// @param n_steps Number of interior discretisation steps (>= 1).
106 /// Total waypoints = n_steps + 1 (endpoints included).
107 /// @param lambda Concentration penalty coefficient (>= 0).
108 /// lambda = 0 → straight-line (flat spacetime) geodesic.
109 ///
110 /// @returns Geodesic with (n_steps + 1) waypoints.
111 ///
112 /// @throws std::invalid_argument if dimensions mismatch or n_steps < 1.
113 static Geodesic solve(
114 const PortfolioState& start,
115 const PortfolioState& end,
116 int n_steps,
117 double lambda
118 );
119
120private:
121 /// Compute the normalised-time trajectory for a single weight dimension.
122 /// t_norm in [0, 1].
123 static double trajectory(
124 double w_start,
125 double w_end,
126 double t_norm,
127 double omega
128 ) noexcept;
129};
130
131// ─── GeodesicLength ───────────────────────────────────────────────────────────
132
133/// Computes the integrated arc length of a Geodesic.
134///
135/// Arc length = integral of ||dw/dt|| dt
136///
137/// Approximated numerically as sum of Euclidean distances between consecutive
138/// waypoints.
140public:
141 /// Compute the arc length of a discretised geodesic.
142 ///
143 /// @param geodesic The path to measure.
144 /// @param dt Time step between waypoints in the same units as
145 /// timestamp_ms differences. If zero, defaults to 1.0.
146 ///
147 /// @returns Arc length (>= 0). Returns 0 for a path with < 2 states.
148 static double compute(const Geodesic& geodesic, double dt = 1.0) noexcept;
149
150 /// Compute the Euclidean distance between two PortfolioState weight vectors.
151 static double distance(
152 const PortfolioState& a,
153 const PortfolioState& b
154 ) noexcept;
155};
156
157} // namespace srfm::portfolio
std::vector< PortfolioState > states
Ordered from start to end.
int size() const noexcept
Number of waypoints (including endpoints).
const PortfolioState & end() const
Returns the end state (last waypoint).
const PortfolioState & start() const
Returns the start state (first waypoint).
bool is_valid() const noexcept
True if the path contains at least two states.
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.