Special Relativity in Financial Modeling 1.0.0
Lorentz transforms, spacetime classification, and geodesic price paths for quantitative finance
Loading...
Searching...
No Matches
beta_calculator.cpp
Go to the documentation of this file.
1/// @file src/lorentz/beta_calculator.cpp
2/// @brief BetaCalculator — financial-to-physics velocity mapping (AGT-01).
3
4#include "beta_calculator.hpp"
6
7#include <algorithm>
8#include <cmath>
9#include <numeric>
10
11namespace srfm::lorentz {
12
13// ─── Internal helpers ─────────────────────────────────────────────────────────
14
15namespace {
16
17/// Clamp a raw ratio to [0, BETA_MAX_SAFE).
18/// Negative raw_ratio is treated as positive (speed has no sign here).
19[[nodiscard]] BetaVelocity ratio_to_beta(double raw_ratio) noexcept {
20 // Take absolute value first: speed is magnitude-only.
21 const double abs_ratio = std::abs(raw_ratio);
22 // Clamp to just below BETA_MAX_SAFE so the result is always valid.
23 const double clamped = std::min(abs_ratio, constants::BETA_MAX_SAFE - 0.0);
24 return BetaVelocity{std::min(clamped, constants::BETA_MAX_SAFE - 1e-15)};
25}
26
27} // anonymous namespace
28
29// ─── fromPriceVelocity ────────────────────────────────────────────────────────
30
31std::optional<BetaVelocity>
33 double max_velocity) noexcept {
34 if (!std::isfinite(price_velocity)) {
35 return std::nullopt;
36 }
37 if (max_velocity <= 0.0) {
38 return std::nullopt;
39 }
40
41 // β = |price_velocity| / max_velocity, clamped to [0, BETA_MAX_SAFE)
42 const double raw_beta = std::abs(price_velocity) / max_velocity;
43 return ratio_to_beta(raw_beta);
44}
45
46// ─── fromReturn ───────────────────────────────────────────────────────────────
47
48std::optional<BetaVelocity>
49BetaCalculator::fromReturn(double period_return,
50 double max_return) noexcept {
51 if (!std::isfinite(period_return)) {
52 return std::nullopt;
53 }
54 if (max_return <= 0.0) {
55 return std::nullopt;
56 }
57
58 // β = |return| / max_return, clamped to safe range
59 const double raw_beta = std::abs(period_return) / max_return;
60 return ratio_to_beta(raw_beta);
61}
62
63// ─── meanAbsVelocity ─────────────────────────────────────────────────────────
64
65std::optional<double>
66BetaCalculator::meanAbsVelocity(std::span<const double> prices,
67 double time_delta) noexcept {
68 if (prices.size() < 2) {
69 return std::nullopt;
70 }
71 if (time_delta <= 0.0) {
72 return std::nullopt;
73 }
74
75 // Check all prices are finite before we start.
76 for (double p : prices) {
77 if (!std::isfinite(p)) {
78 return std::nullopt;
79 }
80 }
81
82 const std::size_t n = prices.size();
83 double total_abs_v = 0.0;
84 std::size_t count = 0;
85
86 for (std::size_t i = 0; i < n; ++i) {
87 double v{};
88 if (i == 0) {
89 // Forward difference at the left boundary
90 v = (prices[1] - prices[0]) / time_delta;
91 } else if (i == n - 1) {
92 // Backward difference at the right boundary
93 v = (prices[n - 1] - prices[n - 2]) / time_delta;
94 } else {
95 // Central difference at interior points (O(h²) accuracy)
96 v = (prices[i + 1] - prices[i - 1]) / (2.0 * time_delta);
97 }
98 total_abs_v += std::abs(v);
99 ++count;
100 }
101
102 return total_abs_v / static_cast<double>(count);
103}
104
105// ─── fromRollingWindow ────────────────────────────────────────────────────────
106
107std::optional<BetaVelocity>
108BetaCalculator::fromRollingWindow(std::span<const double> prices,
109 std::size_t window,
110 double max_velocity,
111 double time_delta) noexcept {
112 if (window < 2) {
113 return std::nullopt;
114 }
115 if (window > prices.size()) {
116 return std::nullopt;
117 }
118 if (max_velocity <= 0.0) {
119 return std::nullopt;
120 }
121
122 // Take the most-recent `window` prices.
123 const std::size_t offset = prices.size() - window;
124 auto recent = prices.subspan(offset, window);
125
126 auto vel = meanAbsVelocity(recent, time_delta);
127 if (!vel) {
128 return std::nullopt;
129 }
130
131 return fromPriceVelocity(*vel, max_velocity);
132}
133
134// ─── fromPriceVelocityOnline ──────────────────────────────────────────────────
135
136std::optional<std::vector<BetaVelocity>>
137BetaCalculator::fromPriceVelocityOnline(std::span<const double> prices,
138 double time_delta) noexcept {
139 if (prices.size() < 2) {
140 return std::nullopt;
141 }
142 if (time_delta <= 0.0) {
143 return std::nullopt;
144 }
145
146 // Guard: all prices must be finite before we begin.
147 for (double p : prices) {
148 if (!std::isfinite(p)) {
149 return std::nullopt;
150 }
151 }
152
153 const std::size_t n = prices.size();
154 std::vector<BetaVelocity> result;
155 result.reserve(n);
156
157 double running_max = 0.0; // running maximum of absolute instantaneous velocity
158
159 for (std::size_t i = 0; i < n; ++i) {
160 // Instantaneous velocity at bar i using one-sided or central differences.
161 // We use only prices[0..i] (i.e., no look-ahead).
162 double v{};
163 if (i == 0) {
164 // Forward difference (only one future point available — still causal
165 // because we treat this as the velocity between bar 0 and bar 1,
166 // which the strategy knows at bar 1). At bar 0 we can only estimate
167 // from a single lag: use backward difference from bar 1 retroactively.
168 // By convention, adopt the forward difference here since bar 1 is the
169 // next observation we receive:
170 v = std::abs((prices[1] - prices[0]) / time_delta);
171 } else if (i == 1) {
172 // With two points we can only use backward difference.
173 v = std::abs((prices[1] - prices[0]) / time_delta);
174 } else {
175 // Central difference at bar i using prices[i-1] and prices[i].
176 // (prices[i+1] would be look-ahead, so we use backward difference.)
177 v = std::abs((prices[i] - prices[i - 1]) / time_delta);
178 }
179
180 // Update running max — monotonically non-decreasing.
181 if (v > running_max) {
182 running_max = v;
183 }
184
185 // Compute β_i using only the running_max up to bar i.
186 BetaVelocity beta{};
187 if (running_max < 1e-15) {
188 // No velocity observed yet — Newtonian / stationary market.
189 beta = BetaVelocity{0.0};
190 } else {
191 const double raw_beta = v / running_max;
192 beta = ratio_to_beta(raw_beta);
193 }
194
195 result.push_back(beta);
196 }
197
198 return result;
199}
200
201// ─── Classification ───────────────────────────────────────────────────────────
202
204 return std::abs(beta.value) < constants::BETA_NEWTONIAN_THRESHOLD;
205}
206
208 return !isNewtonian(beta);
209}
210
212 return LorentzTransform::isValidBeta(beta.value);
213}
214
215// ─── clamp ────────────────────────────────────────────────────────────────────
216
217BetaVelocity BetaCalculator::clamp(double raw_beta) noexcept {
218 // Preserve sign — market direction matters here (unlike speed-only contexts).
219 const double limit = constants::BETA_MAX_SAFE;
220 const double clamped = std::clamp(raw_beta, -limit, limit);
221
222 // If the input was exactly ±BETA_MAX_SAFE, nudge it just inside the boundary
223 // so isValidBeta returns true for the result.
224 if (clamped >= limit) {
225 return BetaVelocity{limit - 1e-15};
226 }
227 if (clamped <= -limit) {
228 return BetaVelocity{-limit + 1e-15};
229 }
230 return BetaVelocity{clamped};
231}
232
233// ─── kineticEnergy ────────────────────────────────────────────────────────────
234
235std::optional<double>
237 double effective_mass,
238 double c_market) noexcept {
239 if (effective_mass <= 0.0) {
240 return std::nullopt;
241 }
242
243 auto g = LorentzTransform::gamma(beta);
244 if (!g) {
245 return std::nullopt;
246 }
247
248 // E_k = (γ − 1) · m_eff · c²_market
249 // At β = 0: E_k = 0 (no kinetic energy at rest).
250 // Newtonian expansion: E_k ≈ ½ m_eff (β·c)² for small β (reproduces
251 // classical ½mv²).
252 return (g->value - 1.0) * effective_mass * c_market * c_market;
253}
254
255// ─── dopplerFactor ────────────────────────────────────────────────────────────
256
257std::optional<double>
259 if (!LorentzTransform::isValidBeta(beta.value)) {
260 return std::nullopt;
261 }
262
263 // D = √((1 + β) / (1 − β))
264 //
265 // β > 0 (approaching): D > 1 — blue-shift (higher observed frequency)
266 // β < 0 (receding): D < 1 — red-shift (lower observed frequency)
267 // β = 0: D = 1 — no shift
268 //
269 // isValidBeta guarantees |β| < BETA_MAX_SAFE < 1, so (1 − β) > 0 always.
270 const double numerator = 1.0 + beta.value;
271 const double denominator = 1.0 - beta.value;
272
273 // Extra guard: both factors must be positive for a real Doppler factor.
274 if (denominator <= 0.0 || numerator <= 0.0) {
275 return std::nullopt;
276 }
277
278 return std::sqrt(numerator / denominator);
279}
280
281} // namespace srfm::lorentz
static std::optional< double > meanAbsVelocity(std::span< const double > prices, double time_delta) noexcept
static std::optional< BetaVelocity > fromReturn(double period_return, double max_return) noexcept
static bool isRelativistic(BetaVelocity beta) noexcept
static std::optional< BetaVelocity > fromRollingWindow(std::span< const double > prices, std::size_t window, double max_velocity, double time_delta) noexcept
static std::optional< double > kineticEnergy(BetaVelocity beta, double effective_mass, double c_market=constants::SPEED_OF_INFORMATION) noexcept
static std::optional< BetaVelocity > fromPriceVelocity(double price_velocity, double max_velocity) noexcept
static bool isNewtonian(BetaVelocity beta) noexcept
static std::optional< std::vector< BetaVelocity > > fromPriceVelocityOnline(std::span< const double > prices, double time_delta) noexcept
static std::optional< double > dopplerFactor(BetaVelocity beta) noexcept
static BetaVelocity clamp(double raw_beta) noexcept
static bool isValid(BetaVelocity beta) noexcept
Return true if β is in the valid safe range (|β| < BETA_MAX_SAFE).
static std::optional< LorentzFactor > gamma(BetaVelocity beta) noexcept
static bool isValidBeta(double beta) noexcept
static constexpr double BETA_NEWTONIAN_THRESHOLD
Below this β, relativistic corrections are negligible (γ ≈ 1 + β²/2).
Definition constants.hpp:20
static constexpr double BETA_MAX_SAFE
Definition constants.hpp:17
BetaCalculator — financial-to-physics velocity mapping (AGT-01).
Lorentz Transform Engine — AGT-01 public header.