Repository navigation
feat(backtest): banc de référence phase 1 (moteur, coûts, impôt, buy & hold / MA200) - #99
Conversation
… futur, coûts, impôt, buy & hold / MA200) Moteur quotidien exposition cible -> équité (décision à la clôture t, exécution à l'ouverture t+1, coûts par côté, impôt annuel sur plus-values réalisées), métriques (CAGR, Sharpe, Sortino, drawdown, Calmar, turnover), bootstrap par blocs, Sharpe déflaté, détection des séries gelées (CRUDP.PA : 82 % gelé) et rapport généré. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
|
||
| import numpy as np | ||
| import pandas as pd | ||
| from scipy import stats |
There was a problem hiding this comment.
WARNING: scipy is imported here but is not declared in pyproject.toml
scipy is only pulled in transitively (via scikit-learn/statsmodels), so this is an undeclared direct dependency. Any resolver change that drops or renames the transitive edge breaks import src.backtest.metrics at module load, which also breaks tests/test_backtest.py and python -m src.backtest.report. Add "scipy>=1.11" to [project].dependencies.
Reply with @kilocode-bot fix it to have Kilo Code address this issue.
| "time_invested": float((result.exposure > 0.01).mean()), | ||
| "avg_exposure": float(result.exposure.mean()), | ||
| "turnover_per_year": float(tr["notional"].sum() / eq.mean() / years), | ||
| "n_trades": int(len(sells)), |
There was a problem hiding this comment.
SUGGESTION: n_trades counts only SELL rows, but turnover_per_year above sums both BUY and SELL notionals
The report renders this as the column "Trades" (and the published doc quotes "83 trades" for the exit-rules strategy), so the same table mixes two different bases: 83 round-trip exits but ~166 fills. Readers comparing the trade count with the 40x/year turnover will understate the churn by half. Either rename the key/column (n_exits) or count all fills.
Reply with @kilocode-bot fix it to have Kilo Code address this issue.
| for y in years: | ||
| seg = eq[eq.index.year == y] | ||
| prev = eq[eq.index.year < y] | ||
| base = prev.iloc[-1] if len(prev) else seg.iloc[0] |
There was a problem hiding this comment.
SUGGESTION: First calendar year is computed from seg.iloc[0], silently dropping the return of its first bar
For the earliest year there is no prior equity, so the base becomes the equity after the first session of that year, and seg.iloc[-1] / seg.iloc[0] - 1 skips the first day's move. For the 2022 column of the published report that first bar is a large down day for buy & hold, so the annual figure is biased. Use the initial capital (or prepend initial_capital) as the base for the first year.
Reply with @kilocode-bot fix it to have Kilo Code address this issue.
|
|
||
| def frozen_mask(df: pd.DataFrame) -> pd.Series: | ||
| """Ligne gelée : aucun volume ET clôture identique à la veille (placeholder, pas une vraie séance).""" | ||
| return (df["Volume"] == 0) & (df["Close"] == df["Close"].shift(1)) |
There was a problem hiding this comment.
SUGGESTION: frozen_mask hard-requires a Volume column that load_prices never validates
load_prices only checks Open/Close, but every downstream call (coverage, live_segment, and the report's quality table) goes through frozen_mask, which raises KeyError: 'Volume' on a parquet written without volume. Guard the column once in load_prices (raise a clear message, or treat a missing column as "not frozen") so a cache-format change fails at load time instead of mid-report.
Reply with @kilocode-bot fix it to have Kilo Code address this issue.
Code Review SummaryStatus: No Issues Found | Recommendation: Merge All four findings from the previous review are addressed in Files Reviewed (6 files)
Previous Review Summary (commit 922efd7)Current summary above is authoritative. Previous snapshots are kept for context only. Previous review (commit 922efd7)Status: 4 Issues Found | Recommendation: Address before merge Overview
Issue Details (click to expand)WARNING
SUGGESTION
Files Reviewed (8 files)
Reviewed by free · Input: 0 · Output: 0 · Cached: 0 |
… libellé Ventes Review Kilo #99. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Contexte
Phase 1 du plan de passage en réel (#92) : le banc de vérité. Cette première tranche pose le moteur et les références que tout modèle devra battre ; le rejeu des modèles de l'ensemble en walk-forward et l'ablation viennent dans une tranche suivante.
Contenu
src/backtest/engine.py: exposition cible 0..1 → équité. Décision à la clôture t, exécution à l'ouverture t+1 (aucun regard vers le futur), coût par côté, impôt annuel sur plus-values réalisées, option de solde en fin de période.src/backtest/metrics.py: CAGR, Sharpe, Sortino, drawdown, Calmar, temps investi, turnover, gain/perte moyens ; bootstrap par blocs (ΔSharpe avec IC) ; Sharpe déflaté.src/backtest/strategies.py: buy & hold, 50 % fixe, tendance MA (avec hystérésis), momentum 12 mois, et les règles de sortie actuelles det212_executorrejouées sur un signal d'entrée.src/backtest/data.py: chargement du cache et détection des séries gelées.src/backtest/report.py+docs/BACKTEST_BASELINES_2026-09-29.md: rapport généré, avec une lecture rédigée à la main.tests/test_backtest.py: 26 tests (pas de regard vers le futur, coûts, impôt, règles de sortie, métriques, séries gelées).Résultats à retenir (détail dans le document)
Limites
4 ans, un seul cycle, hypothèse de coût (25 pb/côté) et d'impôt (30 %) à confirmer, aucun modèle de l'ensemble encore testé.
Vérification
pytest tests/: 346 passés, 3 ignorés.ruff check(règles de la CI) propre. Aucune modification du code de production.🤖 Generated with Claude Code