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:
@@ -38,7 +38,13 @@ def _fetch_sp500_tables() -> tuple[pd.DataFrame, pd.DataFrame]:
|
||||
html = resp.read().decode("utf-8")
|
||||
tables = pd.read_html(io.StringIO(html))
|
||||
current = tables[0]
|
||||
changes = tables[1]
|
||||
change_tables = [
|
||||
t for t in tables[1:]
|
||||
if any("Added" in "_".join(map(str, c if isinstance(c, tuple) else (c,))) for c in t.columns)
|
||||
]
|
||||
if not change_tables:
|
||||
raise ValueError("S&P 500 change log table not found on the Wikipedia page")
|
||||
changes = change_tables[0]
|
||||
changes.columns = [
|
||||
"_".join(c).strip() if isinstance(c, tuple) else c
|
||||
for c in changes.columns
|
||||
@@ -161,7 +167,16 @@ def load_sp500_history(force_refresh: bool = False) -> dict[str, list[list[str |
|
||||
except Exception:
|
||||
pass
|
||||
print("--- Rebuilding S&P 500 membership history from Wikipedia ---")
|
||||
intervals = build_sp500_history()
|
||||
try:
|
||||
intervals = build_sp500_history()
|
||||
except Exception as exc: # noqa: BLE001 — page layout changes must not break backtests
|
||||
if not os.path.exists(path):
|
||||
raise
|
||||
with open(path) as f:
|
||||
cached = json.load(f)
|
||||
print(f"--- WARNING: rebuild failed ({exc}); using membership history "
|
||||
f"cached on {cached.get('date')} ---")
|
||||
return cached["intervals"]
|
||||
os.makedirs(CACHE_DIR, exist_ok=True)
|
||||
with open(path, "w") as f:
|
||||
json.dump({"date": str(date.today()), "intervals": intervals}, f)
|
||||
|
||||
Reference in New Issue
Block a user