algotradelib 0.0.0.9000
C++ header-only library for algorithmic trading
Loading...
Searching...
No Matches
metrics.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 <stdexcept>
8
9namespace algotradelib {
10namespace analytics {
11
19
20namespace metric_detail {
21
23struct Summary {
24 std::size_t count_{0};
25 Scalar mean_{0};
26 Scalar squared_{0};
27 // Whether at least two observations are present and all equal, as far as rounding can hide it
28 // (`constant_detail::rounded_constant`); an exactly zero deviation is the caller's own test.
29 bool constant_{false};
30};
31
40inline Summary summarize(math::SeriesView values) {
41 const auto parts = math::observed_parts(values);
42 Summary result;
43 const Scalar count = parts.present_.sum();
44 result.count_ = static_cast<std::size_t>(count);
45 if (result.count_ == 0) {
46 return result;
47 }
48 result.mean_ = parts.values_.sum() / count;
49 result.squared_ = (parts.values_ - result.mean_ * parts.present_).squaredNorm();
50 if (result.count_ >= 2) {
51 const Scalar deviation = std::sqrt(result.squared_ / static_cast<Scalar>(result.count_ - 1));
52 result.constant_ = constant_detail::rounded_constant(
53 deviation, result.mean_, result.count_, parts.values_, parts.present_, 0, parts.values_.size()
54 );
55 }
56 return result;
57}
58
59} // namespace metric_detail
60
70
84 MaxDrawdown result;
85 result.magnitude_ = missing();
86 Scalar peak{0};
87 math::Index peak_row = -1;
88 std::size_t observations = 0;
89 for (math::Index row = 0; row < levels.size(); ++row) {
90 if (!observed(levels[row])) {
91 continue;
92 }
93 ++observations;
94 if (peak_row < 0 || peak < levels[row]) {
95 peak = levels[row];
96 peak_row = row;
97 }
98 if (peak <= Scalar{0}) {
99 continue;
100 }
101 const Scalar decline = (peak - levels[row]) / peak;
102 if (!observed(result.magnitude_) || result.magnitude_ < decline) {
103 result.magnitude_ = decline;
104 result.peak_row_ = peak_row;
105 result.trough_row_ = row;
106 }
107 }
108 // Levels were present but no ratio could be formed from them -- every running peak was at or
109 // below zero. That is "undefined", not "no data".
110 if (observations > 0 && !observed(result.magnitude_)) {
111 result.magnitude_ = math::undefined();
112 }
113 return result;
114}
115
127inline Scalar sharpe(math::SeriesView period_returns, Scalar risk_free = Scalar{0}) {
128 const auto summary = metric_detail::summarize(period_returns);
129 if (summary.count_ < 2) {
130 return missing();
131 }
132 const Scalar deviation =
133 std::sqrt(summary.squared_ / static_cast<Scalar>(summary.count_ - 1));
134 if (deviation == Scalar{0} || summary.constant_) {
135 // Returns were present and never varied. `undefined()` rather than `missing()`, because a
136 // caller ranking strategies should be able to tell this from "not enough history".
137 return math::undefined();
138 }
139 return (summary.mean_ - risk_free) / deviation;
140}
141
153inline Scalar sortino(math::SeriesView period_returns, Scalar target = Scalar{0}) {
154 std::size_t count = 0;
155 std::size_t below = 0;
156 Scalar sum{0};
157 Scalar shortfall{0};
158 for (math::Index row = 0; row < period_returns.size(); ++row) {
159 const Scalar value = period_returns[row];
160 if (!observed(value)) {
161 continue;
162 }
163 ++count;
164 sum += value;
165 if (value < target) {
166 ++below;
167 const Scalar gap = target - value;
168 shortfall += gap * gap;
169 }
170 }
171 if (count < 2) {
172 return missing();
173 }
174 if (below == 0) {
175 // Nothing fell short of the target. There is no downside risk to divide by, and an infinity
176 // would survive every later comparison as the best strategy ever measured. `undefined()`
177 // says "computed, and does not exist" rather than "no data", so a caller ranking strategies
178 // can treat the two differently.
179 return math::undefined();
180 }
181 // Divided by the whole count, not by `below`. That is the definition, and the difference
182 // is a systematic flattery of strategies with rare losses.
183 const Scalar downside = std::sqrt(shortfall / static_cast<Scalar>(count));
184 if (downside == Scalar{0}) {
185 return math::undefined();
186 }
187 return (sum / static_cast<Scalar>(count) - target) / downside;
188}
189
200inline Scalar annualize_ratio(Scalar ratio, Scalar periods_per_year) {
201 if (!(periods_per_year > Scalar{0})) {
202 throw std::invalid_argument("periods per year must be positive");
203 }
204 return observed(ratio) ? ratio * std::sqrt(periods_per_year) : ratio;
205}
206
214
227 math::MatrixView weights,
229) {
230 // Masked once for the whole panel, so each period is a plain vectorized row difference.
231 const math::Matrix held =
232 (weights.array() == weights.array()).select(weights, Scalar{0});
233 math::Vector result = math::unobserved(static_cast<std::size_t>(weights.rows()));
234 for (math::Index row = 0; row < held.rows(); ++row) {
235 if (row == 0) {
236 if (start == TurnoverStart::SKIP) {
237 continue;
238 }
239 result[row] = Scalar{0.5} * held.row(0).cwiseAbs().sum();
240 continue;
241 }
242 result[row] = Scalar{0.5} * (held.row(row) - held.row(row - 1)).cwiseAbs().sum();
243 }
244 return result;
245}
246
258 math::MatrixView weights,
260) {
261 return Scalar{2} * one_way_turnover(weights, start);
262}
263
264} // namespace analytics
265} // 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 one_way_turnover(math::MatrixView weights, TurnoverStart start=TurnoverStart::FROM_CASH)
Computes the one-way turnover of each period, 0.5 * sum(|w[i] - w[i - 1]|).
Definition metrics.hpp:226
TurnoverStart
How turnover treats the first period.
Definition metrics.hpp:208
@ FROM_CASH
Row 0 measures the first positions against an all-cash start. Use it to cost one strategy.
Definition metrics.hpp:210
@ SKIP
Row 0 is missing(). Use it to compare strategies whose histories start on different dates.
Definition metrics.hpp:212
Scalar sortino(math::SeriesView period_returns, Scalar target=Scalar{0})
Computes the per-period Sortino ratio, (mean(returns) - target) / downside_deviation.
Definition metrics.hpp:153
Scalar annualize_ratio(Scalar ratio, Scalar periods_per_year)
Annualizes a per-period ratio by multiplying it by sqrt(periods_per_year).
Definition metrics.hpp:200
math::Vector two_way_turnover(math::MatrixView weights, TurnoverStart start=TurnoverStart::FROM_CASH)
Computes the two-way turnover of each period, sum(|w[i] - w[i - 1]|).
Definition metrics.hpp:257
MaxDrawdown max_drawdown(math::SeriesView levels)
Finds the worst peak-to-trough decline of a level series.
Definition metrics.hpp:83
Scalar missing()
Returns the value that stands for "no observation"; see math::missing.
Definition scalar.hpp:28
Scalar sharpe(math::SeriesView period_returns, Scalar risk_free=Scalar{0})
Computes the per-period Sharpe ratio, (mean(returns) - risk_free) / stdev(returns).
Definition metrics.hpp:127
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::Matrix< Scalar, Eigen::Dynamic, Eigen::Dynamic, Eigen::ColMajor > Matrix
A dense, column-major matrix of Scalar.
Definition eigen.hpp:45
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::Ref< const Matrix > MatrixView
A read-only reference to a whole matrix.
Definition eigen.hpp:62
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.
The worst peak-to-trough decline of a level series, and where it happened.
Definition metrics.hpp:62
math::Index peak_row_
The row of the peak the decline fell from, or -1 if there is none.
Definition metrics.hpp:66
math::Index trough_row_
The row of the low the decline reached, or -1 if there is none.
Definition metrics.hpp:68
Scalar magnitude_
The decline as a non-negative fraction of the peak (0.2 is 20% below it).
Definition metrics.hpp:64