Special Relativity in Financial Modeling 1.0.0
Lorentz transforms, spacetime classification, and geodesic price paths for quantitative finance
Loading...
Searching...
No Matches
coordinate_normalizer.hpp
Go to the documentation of this file.
1#pragma once
2/**
3 * @file coordinate_normalizer.hpp
4 * @brief Rolling-window z-score normaliser for tick close prices.
5 *
6 * Module: include/srfm/stream/
7 * Owner: AGT-10 (Builder) — 2026-03-01
8 *
9 * Responsibility
10 * --------------
11 * Map raw close prices into a normalised coordinate space using a rolling
12 * Welford online mean/variance estimator over a configurable window.
13 *
14 * normalised = (close − μ_window) / σ_window
15 *
16 * This dimensionless coordinate feeds into BetaCalculator and LorentzTransform,
17 * ensuring the relativistic machinery operates on unit-free quantities.
18 *
19 * Guarantees
20 * ----------
21 * • Online: O(1) per tick, no recomputation over the full window.
22 * • Bounded: memory is exactly window * sizeof(double) + a few scalars.
23 * • Zero-allocation: circular buffer is pre-allocated at construction.
24 * • Non-throwing: update() and normalise() are noexcept.
25 * • Pre-warm: returns 0.0 until window is full (conservative).
26 *
27 * NOT Responsible For
28 * -------------------
29 * • Cross-field normalisation (high/low/volume normalised separately if needed)
30 * • Persistence or serialisation of the rolling state
31 * • Thread safety — single-threaded use only
32 */
33
34#include <algorithm>
35#include <cassert>
36#include <cmath>
37#include <cstddef>
38#include <stdexcept>
39#include <string>
40#include <vector>
41
42namespace srfm::stream {
43
44// ── CoordinateNormalizer ───────────────────────────────────────────────────────
45
46/**
47 * @brief Rolling z-score normaliser using Welford's online algorithm.
48 *
49 * Window size is fixed at construction time. Call update() with each new
50 * close price, then normalise() to obtain the current z-score.
51 *
52 * @code
53 * CoordinateNormalizer norm{20};
54 * for (const auto& tick : ticks) {
55 * norm.update(tick.close);
56 * double z = norm.normalise(tick.close);
57 * // z is 0.0 until the window is full
58 * }
59 * @endcode
60 */
62public:
63 /**
64 * @brief Construct with a given rolling window size.
65 *
66 * @param window Number of ticks in the rolling window. Must be ≥ 2.
67 */
68 explicit CoordinateNormalizer(std::size_t window)
69 : window_{window}
70 , buf_(window, 0.0)
71 {
72 if (window < 2u)
73 throw std::invalid_argument(
74 "CoordinateNormalizer: window must be >= 2, got " +
75 std::to_string(window));
76 }
77
78 // ── State mutation ─────────────────────────────────────────────────────────
79
80 /**
81 * @brief Ingest one new close price into the rolling window.
82 *
83 * Updates the internal circular buffer and the running mean/M2 accumulators
84 * using Welford's online algorithm. O(1) per call.
85 *
86 * @param close Raw close price. Must be finite and positive.
87 * @note noexcept — arithmetic only, no allocation.
88 */
89 void update(double close) noexcept {
90 const double old_val = buf_[pos_];
91 buf_[pos_] = close;
92 pos_ = (pos_ + 1u) % window_;
93
94 if (count_ < window_) {
95 ++count_;
96 // Welford add for growing window.
97 const double delta = close - mean_;
98 mean_ += delta / static_cast<double>(count_);
99 const double delta2 = close - mean_;
100 m2_ += delta * delta2;
101 } else {
102 // Welford update for sliding window (remove old, add new).
103 const double old_mean = mean_;
104 mean_ += (close - old_val) / static_cast<double>(window_);
105 m2_ += (close - old_val) * (close - mean_ + old_val - old_mean);
106 // Clamp M2 to avoid floating-point drift into negatives.
107 if (m2_ < 0.0) m2_ = 0.0;
108 }
109 }
110
111 /**
112 * @brief Return the z-score of @p value relative to the current window.
113 *
114 * Returns 0.0 if the window is not yet full or if the standard deviation
115 * is effectively zero (constant price series).
116 *
117 * @param value Value to normalise (typically the current close price).
118 * @note noexcept — arithmetic only.
119 */
120 [[nodiscard]] double normalise(double close) const noexcept {
121 if (count_ < window_) return 0.0;
122 const double variance = m2_ / static_cast<double>(window_ - 1);
123 const double sigma = std::sqrt(variance);
124 if (sigma < 1e-15) return 0.0;
125 return (close - mean_) / sigma;
126 }
127
128 // ── Diagnostic accessors ───────────────────────────────────────────────────
129
130 /// Rolling mean of the window. 0.0 if window not yet full.
131 [[nodiscard]] double mean() const noexcept { return mean_; }
132
133 /// Rolling sample standard deviation. 0.0 if window not yet full.
134 [[nodiscard]] double sigma() const noexcept {
135 if (count_ < window_) return 0.0;
136 const double v = m2_ / static_cast<double>(window_ - 1);
137 return v < 0.0 ? 0.0 : std::sqrt(v);
138 }
139
140 /// Number of ticks seen so far (saturates at window size).
141 [[nodiscard]] std::size_t count() const noexcept { return count_; }
142
143 /// Whether the window has been filled at least once.
144 [[nodiscard]] bool warmed_up() const noexcept { return count_ >= window_; }
145
146 /// Configured window size.
147 [[nodiscard]] std::size_t window() const noexcept { return window_; }
148
149 // ── Reset ──────────────────────────────────────────────────────────────────
150
151 /**
152 * @brief Reset all state as if no ticks have been seen.
153 *
154 * The window size is preserved.
155 */
156 void reset() noexcept {
157 std::fill(buf_.begin(), buf_.end(), 0.0);
158 pos_ = 0;
159 count_ = 0;
160 mean_ = 0.0;
161 m2_ = 0.0;
162 }
163
164private:
165 std::size_t window_; ///< Configured window length.
166 std::vector<double> buf_; ///< Circular buffer of close prices.
167 std::size_t pos_{0}; ///< Next write position in buf_.
168 std::size_t count_{0}; ///< Ticks ingested (capped at window_).
169 double mean_{0.0}; ///< Running rolling mean.
170 double m2_{0.0}; ///< Running sum of squared deviations.
171};
172
173} // namespace srfm::stream
Rolling z-score normaliser using Welford's online algorithm.
double mean() const noexcept
Rolling mean of the window. 0.0 if window not yet full.
std::size_t window() const noexcept
Configured window size.
std::size_t count() const noexcept
Number of ticks seen so far (saturates at window size).
bool warmed_up() const noexcept
Whether the window has been filled at least once.
double sigma() const noexcept
Rolling sample standard deviation. 0.0 if window not yet full.
void reset() noexcept
Reset all state as if no ticks have been seen.
CoordinateNormalizer(std::size_t window)
Construct with a given rolling window size.
double normalise(double close) const noexcept
Return the z-score of value relative to the current window.
void update(double close) noexcept
Ingest one new close price into the rolling window.