vike
Analytics

Periods

Calendar-period returns, the year-by-month matrix and the top-N drawdown table — bucketed on the same UTC day law the history store partitions by.

crates/vike-analytics/src/periods.rs turns an equity curve plus its timestamps into the three calendar views a tearsheet shows: returns per period, a year × month heatmap, and a table of the worst drawdown episodes. Its header lists exactly those three entry points — periodic_returns, monthly_return_matrix and drawdown_table — and everything on this page is one of them or the label function underneath.

All three take the curve and a parallel timestamps slice in epoch milliseconds, UTC. On a result that is equity_curve and equity_ts, and equity_ts is empty when a run did not track it — so calendar bucketing is only available for a run that carried its timestamps. All three panic when the two slices differ in length; each one's # Panics section states it.

The period label

period_key(ts_ms, period) produces a comparable label for the calendar period containing a timestamp. period is one of five strings and the doc gives the shape of each:

periodlabel
"daily"2024-03-15
"weekly"2024-W11 — ISO week
"monthly"2024-03
"quarterly"2024-Q1
"yearly"2024

Anything else panics. The doc records that as inherited behaviour — the ported module raised a ValueError — and adds that it is provenance only: nothing compares this function against the old implementation any more.

The daily bucket is the store's day

The "daily" arm does not derive year-month-day locally. It delegates to vike_model::time::epoch_ms_to_utc_date, the workspace's one day-law home, so this bucket key can never drift from the history store's date= partition math or the end-of-day day-floor. The output is identical — the same YYYY-MM-DD off the same UTC calendar day — and a test in the module proves the two ways of computing it agree across pre-1970 timestamps, the epoch boundary, a leap day and the millisecond either side of midnight.

Periodic returns

periodic_returns(equity_curve, timestamps, period) returns (label, return_fraction) pairs grouped by calendar period, in chronological order. The doc defines the return as last_equity_in_period / first_equity_at_start_of_period − 1, and the body makes "start of period" precise: a bucket's entry equity is the last equity point before the bucket opened (the previous period's closing value), except for the first bucket, which starts from the curve's first point. A period whose entry equity is zero reports 0.0. Insertion order is kept with an IndexMap, mirroring the insertion-ordered dictionary of the port so the output order is the order the periods were first seen.

The monthly matrix

monthly_return_matrix(equity_curve, timestamps) builds the MonthlyReturnMatrix a heatmap renders from: years (ascending), matrix (year → month → Option<f64>, present only for months the curve touched) and annual (year → return). The annual figure is compounded from the months present — the product of 1 + r over them, minus one — not summed.

This is the structure the tearsheet binary's HTML output renders as its monthly-returns table. crates/vike-report/src/html.rs's header states the arrangement: the table reuses monthly_return_matrix over the same equity curve and timestamps the sheet was built from, nothing is re-derived, the table appears only when timestamps align one to one with the curve, and a non-finite monthly cell renders as the empty marker rather than a +NaN%.

The drawdown table

drawdown_table(equity_curve, timestamps, top_n) finds the top-N drawdown episodes sorted by depth, worst first. Each DrawdownEpisode carries:

  • depth — the fractional drawdown from peak, positive (0.20 = 20%)
  • peak_ts, trough_ts — the timestamps of the peak and the trough
  • recovery_ts — None if the curve never recovered
  • length — bars from peak to trough
  • recovery — bars from trough to recovery, None if not recovered

The scan is a single pass. A new peak (equity at or above the running peak) closes any open episode with that bar as its recovery point; while below the peak the trough is tracked; an episode still open when the curve ends is closed with no recovery and included. Episodes of zero depth are dropped, so a monotonically rising curve yields an empty table, and the result is truncated to top_n after sorting. The single worst episode's depth is the same number metrics::max_drawdown reports on the metrics page; this table adds the where and the how long.

Where it fits

A BacktestReport does not carry any of these — it is deliberately compact, and the report page says so. Calendar views are composed by the caller that has timestamps in hand: the HTML tearsheet today. A backtest that wants them calls these functions over equity_curve and equity_ts directly.

On this page