algotradelib 0.0.0.9000
C++ header-only library for algorithmic trading
Loading...
Searching...
No Matches
rolling.hpp
Go to the documentation of this file.
1#pragma once
2
3#include "./numeric.hpp"
4
5#include <cmath>
6#include <cstddef>
7#include <limits>
8#include <stdexcept>
9#include <string>
10
11namespace algotradelib {
12namespace analytics {
13
29
30namespace rolling_detail {
31
33struct Reduction {
34 std::size_t count_{0};
35 Scalar sum_{0};
36};
37
39inline Reduction reduce(const math::Observed& parts, math::Index first, math::Index length) {
40 Reduction result;
41 result.count_ = static_cast<std::size_t>(parts.present_.segment(first, length).sum());
42 result.sum_ = parts.values_.segment(first, length).sum();
43 return result;
44}
45
47struct Dispersion {
48 bool reported_{false};
49 std::size_t count_{0};
50 Scalar mean_{0};
51 Scalar deviation_{0};
52};
53
69inline Dispersion dispersion(
70 const math::Observed& parts,
71 math::Index first,
72 math::Index length,
73 std::size_t needed
74) {
75 Dispersion result;
76 const auto reduced = reduce(parts, first, length);
77 result.count_ = reduced.count_;
78 if (reduced.count_ < needed || reduced.count_ < 2) {
79 return result;
80 }
81 result.mean_ = reduced.sum_ / static_cast<Scalar>(reduced.count_);
82 const Scalar squared = (parts.values_.segment(first, length) -
83 result.mean_ * parts.present_.segment(first, length)).squaredNorm();
84 result.deviation_ = std::sqrt(squared / static_cast<Scalar>(reduced.count_ - 1));
85 result.reported_ = true;
86 return result;
87}
88
96inline void check(std::size_t window, std::size_t min_observed) {
97 if (window == 0) {
98 throw std::invalid_argument("rolling window must be at least one row");
99 }
100 if (min_observed == 0) {
101 throw std::invalid_argument("a window must require at least one observation");
102 }
103 if (min_observed > window) {
104 throw std::invalid_argument(
105 "rolling window of " + std::to_string(window) + " rows cannot hold " +
106 std::to_string(min_observed) + " observations"
107 );
108 }
109}
110
112inline math::Index window_start(math::Index row, std::size_t window) {
113 const auto span = static_cast<math::Index>(window);
114 return row + 1 >= span ? row + 1 - span : math::Index{0};
115}
116
117} // namespace rolling_detail
118
129 math::SeriesView values,
130 std::size_t window,
131 std::size_t min_observed
132) {
133 rolling_detail::check(window, min_observed);
134 const auto parts = math::observed_parts(values);
135 math::Vector result = math::unobserved(static_cast<std::size_t>(values.size()));
136 for (math::Index row = 0; row < values.size(); ++row) {
137 const math::Index first = rolling_detail::window_start(row, window);
138 const auto reduced = rolling_detail::reduce(parts, first, row - first + 1);
139 if (reduced.count_ >= min_observed) {
140 result[row] = reduced.sum_;
141 }
142 }
143 return result;
144}
145
154inline math::Vector rolling_sum(math::SeriesView values, std::size_t window) {
155 return rolling_sum(values, window, window);
156}
157
170 math::SeriesView values,
171 std::size_t window,
172 std::size_t min_observed
173) {
174 rolling_detail::check(window, min_observed);
175 const auto parts = math::observed_parts(values);
176 math::Vector result = math::unobserved(static_cast<std::size_t>(values.size()));
177 for (math::Index row = 0; row < values.size(); ++row) {
178 const math::Index first = rolling_detail::window_start(row, window);
179 const auto reduced = rolling_detail::reduce(parts, first, row - first + 1);
180 if (reduced.count_ >= min_observed) {
181 result[row] = reduced.sum_ / static_cast<Scalar>(reduced.count_);
182 }
183 }
184 return result;
185}
186
195inline math::Vector rolling_mean(math::SeriesView values, std::size_t window) {
196 return rolling_mean(values, window, window);
197}
198
212 math::SeriesView values,
213 std::size_t window,
214 std::size_t min_observed
215) {
216 rolling_detail::check(window, min_observed);
217 const auto parts = math::observed_parts(values);
218 math::Vector result = math::unobserved(static_cast<std::size_t>(values.size()));
219 for (math::Index row = 0; row < values.size(); ++row) {
220 const math::Index first = rolling_detail::window_start(row, window);
221 const auto spread =
222 rolling_detail::dispersion(parts, first, row - first + 1, min_observed);
223 if (spread.reported_) {
224 result[row] = spread.deviation_;
225 }
226 }
227 return result;
228}
229
239inline math::Vector rolling_stdev(math::SeriesView values, std::size_t window) {
240 return rolling_stdev(values, window, window);
241}
242
253 math::SeriesView values,
254 std::size_t window,
255 std::size_t min_observed
256) {
257 rolling_detail::check(window, min_observed);
258 const auto parts = math::observed_parts(values);
259 // Absent rows become `+inf` so they lose every comparison without needing a mask inside the
260 // window. A window with no observation at all is rejected by the count, never read from here.
261 const math::Vector ceiling =
262 math::observed_or(values, std::numeric_limits<Scalar>::infinity());
263 math::Vector result = math::unobserved(static_cast<std::size_t>(values.size()));
264 for (math::Index row = 0; row < values.size(); ++row) {
265 const math::Index first = rolling_detail::window_start(row, window);
266 const math::Index length = row - first + 1;
267 if (parts.present_.segment(first, length).sum() >= static_cast<Scalar>(min_observed)) {
268 result[row] = ceiling.segment(first, length).minCoeff();
269 }
270 }
271 return result;
272}
273
282inline math::Vector rolling_min(math::SeriesView values, std::size_t window) {
283 return rolling_min(values, window, window);
284}
285
296 math::SeriesView values,
297 std::size_t window,
298 std::size_t min_observed
299) {
300 rolling_detail::check(window, min_observed);
301 const auto parts = math::observed_parts(values);
302 const math::Vector floor =
303 math::observed_or(values, -std::numeric_limits<Scalar>::infinity());
304 math::Vector result = math::unobserved(static_cast<std::size_t>(values.size()));
305 for (math::Index row = 0; row < values.size(); ++row) {
306 const math::Index first = rolling_detail::window_start(row, window);
307 const math::Index length = row - first + 1;
308 if (parts.present_.segment(first, length).sum() >= static_cast<Scalar>(min_observed)) {
309 result[row] = floor.segment(first, length).maxCoeff();
310 }
311 }
312 return result;
313}
314
323inline math::Vector rolling_max(math::SeriesView values, std::size_t window) {
324 return rolling_max(values, window, window);
325}
326
336inline math::Vector sma(math::SeriesView values, std::size_t window, std::size_t min_observed) {
337 return rolling_mean(values, window, min_observed);
338}
339
348inline math::Vector sma(math::SeriesView values, std::size_t window) {
349 return rolling_mean(values, window, window);
350}
351
365 math::SeriesView values,
366 std::size_t window,
367 std::size_t min_observed
368) {
369 rolling_detail::check(window, min_observed);
370 const auto parts = math::observed_parts(values);
371 math::Vector weights(static_cast<math::Index>(window));
372 for (math::Index at = 0; at < weights.size(); ++at) {
373 weights[at] = static_cast<Scalar>(at + 1);
374 }
375
376 math::Vector result = math::unobserved(static_cast<std::size_t>(values.size()));
377 for (math::Index row = 0; row < values.size(); ++row) {
378 const math::Index first = rolling_detail::window_start(row, window);
379 const math::Index length = row - first + 1;
380 if (parts.present_.segment(first, length).sum() < static_cast<Scalar>(min_observed)) {
381 continue;
382 }
383 const auto used = weights.head(length);
384 const Scalar divisor = parts.present_.segment(first, length).dot(used);
385 if (divisor > Scalar{0}) {
386 result[row] = parts.values_.segment(first, length).dot(used) / divisor;
387 }
388 }
389 return result;
390}
391
400inline math::Vector wma(math::SeriesView values, std::size_t window) {
401 return wma(values, window, window);
402}
403
415inline Scalar alpha_from_span(std::size_t span) {
416 if (span == 0) {
417 throw std::invalid_argument("an ema span must be at least one row");
418 }
419 return Scalar{2} / (static_cast<Scalar>(span) + Scalar{1});
420}
421
422namespace rolling_detail {
423
425inline void check_alpha(Scalar alpha) {
426 if (!(Scalar{0} < alpha && alpha <= Scalar{1})) {
427 throw std::invalid_argument("an ema smoothing factor must be in (0, 1]");
428 }
429}
430
431} // namespace rolling_detail
432
450inline Scalar ema_step(Scalar level, Scalar value, Scalar alpha) {
451 rolling_detail::check_alpha(alpha);
452 if (!observed(value)) {
453 return level;
454 }
455 return observed(level) ? level + alpha * (value - level) : value;
456}
457
463Scalar ema_step(Scalar level, Scalar value, std::size_t span) = delete;
464
484inline math::Vector ema(math::SeriesView values, Scalar alpha, std::size_t min_observed = 1) {
485 rolling_detail::check_alpha(alpha);
486 if (min_observed == 0) {
487 throw std::invalid_argument("ema must require at least one observation");
488 }
489 math::Vector result = math::unobserved(static_cast<std::size_t>(values.size()));
490 Scalar level{0};
491 std::size_t count = 0;
492 for (math::Index row = 0; row < values.size(); ++row) {
493 const Scalar value = values[row];
494 if (!observed(value)) {
495 continue;
496 }
497 // Seeded by count rather than by an unobserved level, so a level an infinity turned into `NaN`
498 // stays `NaN` instead of being mistaken for "no level yet".
499 level = count == 0 ? value : level + alpha * (value - level);
500 ++count;
501 if (count >= min_observed) {
502 result[row] = level;
503 }
504 }
505 return result;
506}
507
513math::Vector ema(math::SeriesView values, std::size_t span, std::size_t min_observed = 1) = delete;
514
515} // namespace analytics
516} // namespace algotradelib
Returns, rolling statistics, indicators and performance metrics over numeric series.
math::Vector wma(math::SeriesView values, std::size_t window, std::size_t min_observed)
Computes the linearly weighted moving average.
Definition rolling.hpp:364
math::Vector rolling_mean(math::SeriesView values, std::size_t window, std::size_t min_observed)
Computes the trailing mean of the observed values.
Definition rolling.hpp:169
bool observed(Scalar value)
Tests whether a value is an observation; see math::observed.
Definition scalar.hpp:36
math::Vector rolling_min(math::SeriesView values, std::size_t window, std::size_t min_observed)
Computes the trailing minimum of the observed values.
Definition rolling.hpp:252
Scalar alpha_from_span(std::size_t span)
Converts an EMA span to its smoothing factor, 2 / (span + 1).
Definition rolling.hpp:415
math::Vector rolling_stdev(math::SeriesView values, std::size_t window, std::size_t min_observed)
Computes the trailing sample standard deviation (divisor count - 1).
Definition rolling.hpp:211
math::Vector sma(math::SeriesView values, std::size_t window, std::size_t min_observed)
Computes the simple moving average; identical to rolling_mean.
Definition rolling.hpp:336
math::Vector ema(math::SeriesView values, Scalar alpha, std::size_t min_observed=1)
Computes the exponentially weighted moving average of a series.
Definition rolling.hpp:484
math::Vector rolling_max(math::SeriesView values, std::size_t window, std::size_t min_observed)
Computes the trailing maximum of the observed values.
Definition rolling.hpp:295
Scalar ema_step(Scalar level, Scalar value, Scalar alpha)
Advances an exponential moving average by one value.
Definition rolling.hpp:450
math::Vector rolling_sum(math::SeriesView values, std::size_t window, std::size_t min_observed)
Computes the trailing sum of the observed values.
Definition rolling.hpp:128
Observed observed_parts(SeriesView values)
Splits a series into zero-filled values and a presence mask.
Definition eigen.hpp:113
double Scalar
The floating-point type the analytics and math layers compute in (double).
Definition scalar.hpp:18
Eigen::Index Index
The signed index type of rows and columns.
Definition eigen.hpp:65
Vector unobserved(std::size_t count)
Makes a vector filled with missing().
Definition eigen.hpp:73
Vector observed_or(SeriesView values, Scalar filler)
Replaces every unobserved entry of a series.
Definition eigen.hpp:130
Eigen::Matrix< Scalar, Eigen::Dynamic, 1 > Vector
A dense column vector of Scalar.
Definition eigen.hpp:48
Eigen::Ref< const Vector > SeriesView
A read-only reference to one series.
Definition eigen.hpp:56
The library: broker-neutral types (core), and the modules below.
Re-exports math::Scalar, math::missing and math::observed into analytics.