feat(metrics): surface realized portfolio turnover in backtest metrics#478
Merged
Merged
Conversation
Compute realized portfolio turnover generically from the executed
target-position frame (0.5*sum|w_t - w_{t-1}| per bar) and expose it as
avg_turnover / total_turnover in the metrics dict, so turnover flows
through the existing run-card scalar-metrics path for any strategy.
Previously the turnover_aware optimizer accumulated turnover on its own
instance, but the engine's optimizer loader discards that instance after
extracting the positions frame, so turnover never reached metrics or the
run card. Deriving it from the position frame the engine already holds
makes the metric available for every optimizer (and the default 1/N),
not just turnover_aware, without coupling the engine to optimizer
internals.
- add pure helper calc_turnover_series(positions)
- calc_metrics gains an optional positions arg; additive, existing
metric values unchanged, zeros on empty/degenerate runs
- _empty_metrics gains the new keys for shape consistency
- wire target_pos into the engine's calc_metrics call site
Refs HKUDS#456
Signed-off-by: Robin1987China <41602358+Robin1987China@users.noreply.github.com>
warren618
added a commit
that referenced
this pull request
Jul 13, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Portfolio Studio follow-up (Refs #456): surface realized portfolio turnover in the backtest metrics/report path, as requested after #466 merged.
After #466, the
turnover_awareoptimizer accumulates realized turnover on its own instance — but the engine's optimizer loader constructs that instance inside a lambda and keeps only the returned positions frame (engines/base.py:155,:132), so therealized_turnoverlist is discarded and never reaches metrics, the run card, or any artifact.This wires turnover into the standard output by computing it generically from the executed position frame the engine already holds:
calc_turnover_series(positions)→ per-bar0.5·Σ|wₜ − wₜ₋₁|calc_metrics(...)gains an optionalpositionsarg and emitsavg_turnover/total_turnover_empty_metricsgains the same zero-valued keystarget_posinto the existingcalc_metricscall siteDesign note (please redirect if you'd prefer otherwise)
Your comment framed this as wiring "the optimizer's
realized_turnoverseries". I implemented it as an engine-level metric computed from the position frame instead, because:turnover_aware;The trade-off: this figure is measured post-normalization, so it can differ slightly from the optimizer's own pre-normalization number. The optimizer's instance-level
realized_turnoveraffordance is left fully intact. Happy to switch to threading the optimizer instance out instead if you prefer the literal approach.Testing
ruff check— cleanpytest agent/tests/test_metrics.py— 44 passed (13 new turnover tests: full rotation = 1.0, constant weights = 0 after entry, entry-from-cash, NaN handling,total ≈ avg×bars, existing metrics unchanged with/without positions, zeros on empty/None)test_factor_gate_fund_prefix,test_bottleneck_available) are pre-existing env failures onmain, unrelated to this changeavg_turnover/total_turnoverappear in the run card scalar metricsNotes
0.0.Refs #456