algotradelib 0.0.0.9000
C++ header-only library for algorithmic trading
Loading...
Searching...
No Matches
indicators.hpp
Go to the documentation of this file.
1#pragma once
2
3#include "./rolling.hpp"
4
5#include <algorithm>
6#include <cmath>
7#include <cstddef>
8#include <stdexcept>
9
10namespace algotradelib {
11namespace analytics {
12
23
24namespace indicator_detail {
25
38inline math::Vector wilder(math::SeriesView values, std::size_t period) {
39 const auto span = static_cast<Scalar>(period);
40 math::Vector result = math::unobserved(static_cast<std::size_t>(values.size()));
41 Scalar average{0};
42 Scalar seed{0};
43 std::size_t count = 0;
44 for (math::Index row = 0; row < values.size(); ++row) {
45 const Scalar value = values[row];
46 if (!observed(value)) {
47 continue;
48 }
49 ++count;
50 if (count < period) {
51 seed += value;
52 continue;
53 }
54 if (count == period) {
55 average = (seed + value) / span;
56 } else {
57 average = (average * (span - Scalar{1}) + value) / span;
58 }
59 result[row] = average;
60 }
61 return result;
62}
63
73struct Paired {
74 math::Vector left_;
75 math::Vector right_;
76 math::Vector present_;
77};
78
80inline Paired pair_series(math::SeriesView left, math::SeriesView right) {
81 const auto left_parts = math::observed_parts(left);
82 const auto right_parts = math::observed_parts(right);
83 Paired paired;
84 paired.present_ = math::both_present(left_parts.present_, right_parts.present_);
85 paired.left_ = left_parts.values_.cwiseProduct(paired.present_);
86 paired.right_ = right_parts.values_.cwiseProduct(paired.present_);
87 return paired;
88}
89
91struct Pairing {
92 bool reported_{false};
93 std::size_t count_{0};
94 Scalar mean_left_{0};
95 Scalar mean_right_{0};
96};
97
99inline Pairing reduce_pair(
100 const Paired& paired,
101 math::Index first,
102 math::Index length,
103 std::size_t needed
104) {
105 Pairing result;
106 const Scalar count = paired.present_.segment(first, length).sum();
107 result.count_ = static_cast<std::size_t>(count);
108 if (result.count_ < needed || result.count_ < 2) {
109 return result;
110 }
111 result.mean_left_ = paired.left_.segment(first, length).sum() / count;
112 result.mean_right_ = paired.right_.segment(first, length).sum() / count;
113 result.reported_ = true;
114 return result;
115}
116
118inline math::Vector centred(
119 const math::Vector& values,
120 const math::Vector& present,
121 math::Index first,
122 math::Index length,
123 Scalar mean
124) {
125 return values.segment(first, length) - mean * present.segment(first, length);
126}
127
134inline void require_same_rows(math::SeriesView left, math::SeriesView right) {
135 if (left.size() != right.size()) {
136 throw std::invalid_argument("paired series must have the same number of rows");
137 }
138}
139
140} // namespace indicator_detail
141
155 math::SeriesView values,
156 std::size_t window,
157 std::size_t min_observed
158) {
159 rolling_detail::check(window, min_observed);
160 const auto parts = math::observed_parts(values);
161 math::Vector result = math::unobserved(static_cast<std::size_t>(values.size()));
162 for (math::Index row = 0; row < values.size(); ++row) {
163 if (!observed(values[row])) {
164 continue;
165 }
166 const math::Index first = rolling_detail::window_start(row, window);
167 const auto spread = rolling_detail::dispersion(parts, first, row - first + 1, min_observed);
168 if (!spread.reported_) {
169 continue;
170 }
171 if (spread.deviation_ == Scalar{0} ||
172 constant_detail::rounded_constant(
173 spread.deviation_, spread.mean_, spread.count_, parts.values_, parts.present_, first, row - first + 1
174 )) {
175 // The data was there and the statistic does not exist for it, which is a different
176 // answer from "no data yet" and now says so. Equal values that are not binary fractions
177 // leave a deviation of a few ulps rather than zero; dividing by it would not be a z-score.
178 result[row] = math::undefined();
179 continue;
180 }
181 result[row] = (values[row] - spread.mean_) / spread.deviation_;
182 }
183 return result;
184}
185
194inline math::Vector zscore(math::SeriesView values, std::size_t window) {
195 return zscore(values, window, window);
196}
197
207
225 math::SeriesView values,
226 std::size_t window,
227 Scalar multiplier,
228 std::size_t min_observed
229) {
230 rolling_detail::check(window, min_observed);
231 const auto parts = math::observed_parts(values);
232 const auto rows = static_cast<std::size_t>(values.size());
234 for (math::Index row = 0; row < values.size(); ++row) {
235 const math::Index first = rolling_detail::window_start(row, window);
236 const auto spread = rolling_detail::dispersion(parts, first, row - first + 1, min_observed);
237 if (!spread.reported_) {
238 continue;
239 }
240 bands.middle_[row] = spread.mean_;
241 bands.upper_[row] = spread.mean_ + multiplier * spread.deviation_;
242 bands.lower_[row] = spread.mean_ - multiplier * spread.deviation_;
243 }
244 return bands;
245}
246
259 math::SeriesView values,
260 std::size_t window = 20,
261 Scalar multiplier = Scalar{2}
262) {
263 return bollinger(values, window, multiplier, window);
264}
265
279inline math::Vector rsi(math::SeriesView prices, std::size_t period = 14) {
280 if (period == 0) {
281 throw std::invalid_argument("rsi period must be at least one row");
282 }
283 const auto rows = static_cast<std::size_t>(prices.size());
284 math::Vector gains = math::unobserved(rows);
285 math::Vector losses = math::unobserved(rows);
286 for (math::Index row = 1; row < prices.size(); ++row) {
287 if (!observed(prices[row - 1]) || !observed(prices[row])) {
288 continue;
289 }
290 const Scalar change = prices[row] - prices[row - 1];
291 gains[row] = std::max(change, Scalar{0});
292 losses[row] = std::max(-change, Scalar{0});
293 }
294
295 const math::Vector average_gain = indicator_detail::wilder(gains, period);
296 const math::Vector average_loss = indicator_detail::wilder(losses, period);
297
298 math::Vector result = math::unobserved(rows);
299 for (math::Index row = 0; row < prices.size(); ++row) {
300 if (!observed(average_gain[row]) || !observed(average_loss[row])) {
301 continue;
302 }
303 if (average_loss[row] == Scalar{0}) {
304 result[row] = average_gain[row] == Scalar{0} ? Scalar{50} : Scalar{100};
305 continue;
306 }
307 const Scalar strength = average_gain[row] / average_loss[row];
308 result[row] = Scalar{100} - Scalar{100} / (Scalar{1} + strength);
309 }
310 return result;
311}
312
326 math::SeriesView highs,
327 math::SeriesView lows,
328 math::SeriesView closes
329) {
330 indicator_detail::require_same_rows(highs, lows);
331 indicator_detail::require_same_rows(highs, closes);
332
333 math::Vector result = math::unobserved(static_cast<std::size_t>(highs.size()));
334 for (math::Index row = 0; row < highs.size(); ++row) {
335 if (!observed(highs[row]) || !observed(lows[row])) {
336 continue;
337 }
338 Scalar range = highs[row] - lows[row];
339 if (row > 0 && observed(closes[row - 1])) {
340 const Scalar previous = closes[row - 1];
341 range = std::max(range, std::abs(highs[row] - previous));
342 range = std::max(range, std::abs(lows[row] - previous));
343 }
344 result[row] = range;
345 }
346 return result;
347}
348
363 math::SeriesView highs,
364 math::SeriesView lows,
365 math::SeriesView closes,
366 std::size_t period = 14
367) {
368 if (period == 0) {
369 throw std::invalid_argument("atr period must be at least one bar");
370 }
371 return indicator_detail::wilder(true_range(highs, lows, closes), period);
372}
373
383
400 math::SeriesView values,
401 std::size_t fast = 12,
402 std::size_t slow = 26,
403 std::size_t signal_span = 9
404) {
405 if (fast >= slow) {
406 throw std::invalid_argument("macd fast span must be shorter than the slow span");
407 }
408 const math::Vector quick = ema(values, alpha_from_span(fast));
409 const math::Vector slower = ema(values, alpha_from_span(slow));
410
411 const auto rows = static_cast<std::size_t>(values.size());
413 for (math::Index row = 0; row < values.size(); ++row) {
414 if (observed(quick[row]) && observed(slower[row])) {
415 lines.macd_[row] = quick[row] - slower[row];
416 }
417 }
418 lines.signal_ = ema(lines.macd_, alpha_from_span(signal_span));
419 for (math::Index row = 0; row < values.size(); ++row) {
420 if (observed(lines.macd_[row]) && observed(lines.signal_[row])) {
421 lines.histogram_[row] = lines.macd_[row] - lines.signal_[row];
422 }
423 }
424 return lines;
425}
426
442 math::SeriesView left,
443 math::SeriesView right,
444 std::size_t window,
445 std::size_t min_observed
446) {
447 indicator_detail::require_same_rows(left, right);
448 rolling_detail::check(window, min_observed);
449 const auto paired = indicator_detail::pair_series(left, right);
450 math::Vector result = math::unobserved(static_cast<std::size_t>(left.size()));
451 for (math::Index row = 0; row < left.size(); ++row) {
452 const math::Index first = rolling_detail::window_start(row, window);
453 const math::Index length = row - first + 1;
454 const auto reduced = indicator_detail::reduce_pair(paired, first, length, min_observed);
455 if (!reduced.reported_) {
456 continue;
457 }
458 const math::Vector left_centred = indicator_detail::centred(
459 paired.left_, paired.present_, first, length, reduced.mean_left_
460 );
461 const math::Vector right_centred = indicator_detail::centred(
462 paired.right_, paired.present_, first, length, reduced.mean_right_
463 );
464 result[row] = left_centred.dot(right_centred) / static_cast<Scalar>(reduced.count_ - 1);
465 }
466 return result;
467}
468
479 math::SeriesView left,
480 math::SeriesView right,
481 std::size_t window
482) {
483 return rolling_cov(left, right, window, window);
484}
485
502 math::SeriesView left,
503 math::SeriesView right,
504 std::size_t window,
505 std::size_t min_observed
506) {
507 indicator_detail::require_same_rows(left, right);
508 rolling_detail::check(window, min_observed);
509 const auto paired = indicator_detail::pair_series(left, right);
510 math::Vector result = math::unobserved(static_cast<std::size_t>(left.size()));
511 for (math::Index row = 0; row < left.size(); ++row) {
512 const math::Index first = rolling_detail::window_start(row, window);
513 const math::Index length = row - first + 1;
514 const auto reduced = indicator_detail::reduce_pair(paired, first, length, min_observed);
515 if (!reduced.reported_) {
516 continue;
517 }
518 const math::Vector left_centred = indicator_detail::centred(
519 paired.left_, paired.present_, first, length, reduced.mean_left_
520 );
521 const math::Vector right_centred = indicator_detail::centred(
522 paired.right_, paired.present_, first, length, reduced.mean_right_
523 );
524 const Scalar squared_left = left_centred.squaredNorm();
525 const Scalar squared_right = right_centred.squaredNorm();
526 const auto spread = [&](Scalar squared) {
527 return std::sqrt(squared / static_cast<Scalar>(reduced.count_ - 1));
528 };
529 if (squared_left == Scalar{0} || squared_right == Scalar{0} ||
530 constant_detail::rounded_constant(
531 spread(squared_left), reduced.mean_left_, reduced.count_, paired.left_, paired.present_, first, length
532 ) ||
533 constant_detail::rounded_constant(
534 spread(squared_right), reduced.mean_right_, reduced.count_, paired.right_, paired.present_, first, length
535 )) {
536 // A constant series. Correlation with it is undefined, not zero.
537 result[row] = math::undefined();
538 continue;
539 }
540 // The root of the product, which keeps a collinear pair at exactly 1 or -1; two roots only where the
541 // product of two finite squared sums underflows (tiny dispersions) or overflows (large ones).
542 const Scalar product = squared_left * squared_right;
543 const Scalar denominator =
544 std::isnormal(product) ? std::sqrt(product) : std::sqrt(squared_left) * std::sqrt(squared_right);
545 result[row] = left_centred.dot(right_centred) / denominator;
546 }
547 return result;
548}
549
560 math::SeriesView left,
561 math::SeriesView right,
562 std::size_t window
563) {
564 return rolling_corr(left, right, window, window);
565}
566
582 math::SeriesView asset,
583 math::SeriesView market,
584 std::size_t window,
585 std::size_t min_observed
586) {
587 indicator_detail::require_same_rows(asset, market);
588 rolling_detail::check(window, min_observed);
589 const auto paired = indicator_detail::pair_series(asset, market);
590 math::Vector result = math::unobserved(static_cast<std::size_t>(asset.size()));
591 for (math::Index row = 0; row < asset.size(); ++row) {
592 const math::Index first = rolling_detail::window_start(row, window);
593 const math::Index length = row - first + 1;
594 const auto reduced = indicator_detail::reduce_pair(paired, first, length, min_observed);
595 if (!reduced.reported_) {
596 continue;
597 }
598 const math::Vector market_centred = indicator_detail::centred(
599 paired.right_, paired.present_, first, length, reduced.mean_right_
600 );
601 const Scalar squared_market = market_centred.squaredNorm();
602 if (squared_market == Scalar{0} ||
603 constant_detail::rounded_constant(
604 std::sqrt(squared_market / static_cast<Scalar>(reduced.count_ - 1)),
605 reduced.mean_right_,
606 reduced.count_,
607 paired.right_,
608 paired.present_,
609 first,
610 length
611 )) {
612 // No market movement to attribute the asset's movement to.
613 result[row] = math::undefined();
614 continue;
615 }
616 const math::Vector asset_centred = indicator_detail::centred(
617 paired.left_, paired.present_, first, length, reduced.mean_left_
618 );
619 result[row] = asset_centred.dot(market_centred) / squared_market;
620 }
621 return result;
622}
623
634 math::SeriesView asset,
635 math::SeriesView market,
636 std::size_t window
637) {
638 return beta(asset, market, window, window);
639}
640
641} // namespace analytics
642} // namespace algotradelib
Returns, rolling statistics, indicators and performance metrics over numeric series.
bool observed(Scalar value)
Tests whether a value is an observation; see math::observed.
Definition scalar.hpp:36
math::Vector beta(math::SeriesView asset, math::SeriesView market, std::size_t window, std::size_t min_observed)
Computes the trailing beta of an asset against a market, cov(asset, market) / var(market).
math::Vector rsi(math::SeriesView prices, std::size_t period=14)
Computes Wilder's relative strength index.
math::Vector zscore(math::SeriesView values, std::size_t window, std::size_t min_observed)
Computes how many trailing sample standard deviations each value lies from its trailing mean.
Scalar alpha_from_span(std::size_t span)
Converts an EMA span to its smoothing factor, 2 / (span + 1).
Definition rolling.hpp:415
BollingerBands bollinger(math::SeriesView values, std::size_t window, Scalar multiplier, std::size_t min_observed)
Computes Bollinger bands: the trailing mean and bands multiplier sample standard deviations either si...
math::Vector rolling_cov(math::SeriesView left, math::SeriesView right, std::size_t window, std::size_t min_observed)
Computes the trailing sample covariance of two series (divisor count - 1).
MacdLines macd(math::SeriesView values, std::size_t fast=12, std::size_t slow=26, std::size_t signal_span=9)
Computes the MACD (moving-average convergence/divergence) lines.
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 true_range(math::SeriesView highs, math::SeriesView lows, math::SeriesView closes)
Computes each bar's true range, max(high - low, |high - prev close|, |low - prev close|).
math::Vector atr(math::SeriesView highs, math::SeriesView lows, math::SeriesView closes, std::size_t period=14)
Computes the average true range with Wilder's smoothing.
math::Vector rolling_corr(math::SeriesView left, math::SeriesView right, std::size_t window, std::size_t min_observed)
Computes the trailing Pearson correlation of two series.
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
Vector both_present(const Vector &left, const Vector &right)
Combines two presence masks: 1 only where both series are observed.
Definition eigen.hpp:144
Eigen::Index Index
The signed index type of rows and columns.
Definition eigen.hpp:65
Scalar undefined()
Returns the value that stands for "the data was there, but the statistic does not exist".
Definition scalar.hpp:51
Vector unobserved(std::size_t count)
Makes a vector filled with missing().
Definition eigen.hpp:73
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.
Trailing-window statistics: sums, means, deviations, extremes and moving averages.
The three lines of Bollinger bands, each as long as the input.
math::Vector upper_
middle_ plus multiplier trailing sample standard deviations.
math::Vector lower_
middle_ minus multiplier trailing sample standard deviations.
math::Vector middle_
The trailing mean.
The three lines of a MACD, each as long as the input.
math::Vector macd_
The fast EMA minus the slow EMA.
math::Vector histogram_
macd_ minus signal_.
math::Vector signal_
The EMA of macd_.