Metrics and rolling windows
This guide explains how the metric families fit together, which conventions the numbers follow, and what each family returns when the input is degenerate. It is a map, not a substitute for the API reference: every function carries the exact formula and failure rule in its own documentation.
Inputs and units
Metric functions take a fincore::dataframe, a column name (defaulting to
"returns" for most of them) and, where relevant, a risk-free rate. Values are
decimal returns (0.01 is one percent). Timestamps are UTC nanosecond instants
and are used for ordering, window boundaries and date-based grouping, not for
arithmetic on the values.
NaN means “missing” everywhere in this library. It is not silently replaced by
zero, and it is not dropped from a frame. Each function states whether it drops
missing observations pairwise, per column or not at all; the default for the
scalar ratios is to drop NaN and work with the remaining values.
An empty or mis-typed column is a structural error and raises input_error.
A column that exists but is too short usually yields NaN rather than an
exception, because “not enough data” is a value, not a programming mistake.
Annualization
annualization_factor(frequency) returns the number of periods per year:
Frequency |
Factor |
|---|---|
|
252 |
|
52 |
|
12 |
|
4 |
|
1 |
Passing an explicit value as the second argument returns it unchanged, including
zero or a negative number — validation belongs to the caller. Ratios that scale
by sqrt(annualization) use this factor directly, so switching frequency changes
a Sharpe ratio but never the sign of the mean.
Return and ratio family
Group |
Functions |
Notes |
|---|---|---|
Level and excess |
|
|
Risk-adjusted |
|
Each has its own deviation divisor and its own degenerate rule |
Tail |
|
The tail is defined by a cutoff quantile, not by a fixed count |
Benchmark-relative |
|
Pairwise alignment against a benchmark column |
Two rules explain most surprises:
Sample versus population.
sharpe_ratiodivides by the sample standard deviation (one degree of freedom), while the downside deviation insidesortino_ratiodivides squared deviations by the number of usable observations. The two ratios are therefore not interchangeable denominators.Zero is not special-cased. A zero deviation is left to IEEE arithmetic rather than replaced by
NaNor infinity, except where a function says otherwise.calmar_ratioandmar_ratioreturnNaNwhen the maximum drawdown is zero or positive, because the quotient would be meaningless.
adjusted_sharpe_ratio clamps its skew/kurtosis adjustment to [0.9, 1.1] for
fewer than 20 raw observations and to [0.8, 1.3] otherwise, and returns NaN
for fewer than four raw values. deflated_sharpe_ratio treats a trial count
below one as one and returns exactly 0 or 1 when its estimated Sharpe variance is
non-positive.
Drawdown family
Function |
Returns |
|---|---|
|
A frame with the running drawdown for each observation |
|
A single depth as a negative or zero fraction |
|
The peak, valley and optional recovery timestamps of the deepest episode |
|
Depth lists, or rows with peak/valley/recovery and duration |
|
Depth of the worst drawdown measured over that calendar bucket |
|
Time to recover the worst drawdown |
When several highs are equal, the window starts at the latest one, so it
covers the episode that produced the trough rather than everything since the
first equal high. A series that never declines has no drawdown window at all,
which is why the window functions return std::optional.
Distribution and stability statistics
skewness, kurtosis, serial_correlation, hurst_exponent, stutzer_index,
r_cubed, r_cubed_turtle, win_rate, loss_rate, relative_win_rate,
safe_correlation and the performance-statistics family in metrics/perf_stats.hpp
answer “what shape is this series?” rather than “what did it earn?”.
They share two conventions: non-finite observations are excluded from the statistic and reported through the usable count, and the divisor is the one stated in each function’s documentation. Some use the biased (population) form to match a reference implementation; do not assume consistency across functions.
Rolling windows and tables
rolling_engine computes a requested set of metrics over a fixed window and
returns one frame per metric. Windows are complete-window only: the first
window - 1 observations produce no row, so the output index is shorter than the
input index. metrics/rolling.hpp exposes the individual rolling primitives for
callers that need a single series.
Table builders (metrics/yearly.hpp, metrics/frequencies.hpp and the quantile
tables in factor/performance.hpp) group by date or period. They preserve the
ordering rule stated in their own documentation — some order by calendar year and
ISO week label rather than by bucket start, which can place a January observation
after a December row of the same year.
Worked example
#include <fincore/dataframe.hpp>
#include <fincore/metrics/drawdown.hpp>
#include <fincore/metrics/ratios.hpp>
#include <cstdio>
int main() {
const auto returns = fincore::make_series(
{fincore::utc_timestamp(2025, 1, 2),
fincore::utc_timestamp(2025, 1, 3),
fincore::utc_timestamp(2025, 1, 6),
fincore::utc_timestamp(2025, 1, 7)},
"returns", {0.012, -0.02, 0.005, 0.03});
std::printf("sharpe %.4f\n", fincore::sharpe_ratio(returns, "returns"));
std::printf("sortino %.4f\n", fincore::sortino_ratio(returns, "returns"));
std::printf("max dd %.4f\n", fincore::max_drawdown(returns, "returns"));
if (const auto window = fincore::max_drawdown_underwater(returns, "returns")) {
std::printf("peak %lld valley %lld\n",
static_cast<long long>(window->peak),
static_cast<long long>(window->valley));
}
return 0;
}
Four observations are enough for a Sharpe ratio but not enough to be representative; the example shows the calling shape, not a research result.
Choosing a metric
Reporting a headline risk-adjusted number? Use
sharpe_ratioand say which annualization you used.Penalising downside only?
sortino_ratiowith an explicitrequired_return.Judging a short track record?
adjusted_sharpe_ratioordeflated_sharpe_ratio; both try to discount luck, and both say so in their clamped ranges.Comparing against a benchmark?
alpha/betaandinformation_ratio, and remember the alignment rule: the two frames are paired on the timestamps they share.Summarising a path? Drawdown windows and recovery lengths are more informative than a single maximum, because they say how long a reader would have suffered.
The API reference lists every function with its exact formula and failure rule; when a number surprises you, the function’s own documentation is the authority.