fix(report): correct periodic returns, injection metrics and live view

- Monthly/yearly returns chain from the previous period's close (the first
  trading day of every period was dropped; up to ~9pp/yr error).
- With cash injections, metrics/drawdowns/periodic returns are time-weighted
  on the no-injection curve; capitalFlows reports invested/profit separately.
- Benchmarks cover exactly the strategies' dates.
- CAGR uses calendar time, so gaps in live records do not inflate it.
- Backtests skip the per-trade fee for rebalances below the live minimum
  order and keep a ruined portfolio at zero.
- live: exact strategy match, ineligible strategies hidden, currency,
  startDate, same-window benchmark, stale-price fallback, equity anchored at
  the initial capital.
- PIT membership falls back to the cached history when the Wikipedia change
  log cannot be parsed (the table is no longer on the page).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-25 01:16:33 +08:00
parent 11f4dd95dd
commit 1697bc1641
5 changed files with 333 additions and 146 deletions

View File

@@ -7,8 +7,16 @@ def total_return(equity: pd.Series) -> float:
def annualized_return(equity: pd.Series, periods: int = 252) -> float:
n = len(equity)
return (1 + total_return(equity)) ** (periods / n) - 1
"""CAGR. Uses elapsed calendar time when the index is dated, so gaps in a
live record (missed days) do not inflate the annualized figure."""
growth = 1 + total_return(equity)
if growth <= 0:
return -1.0
if isinstance(equity.index, pd.DatetimeIndex) and len(equity) > 1:
years = (equity.index[-1] - equity.index[0]).days / 365.25
if years > 0:
return growth ** (1 / years) - 1
return growth ** (periods / len(equity)) - 1
def annualized_volatility(returns: pd.Series, periods: int = 252) -> float: