fix: aligner les candles du chart avec la période du backtest
Bug: les markers BUY/SELL du chart utilisaient les timestamps des trades du backtest mais les candles étaient fetchées séparément (500 candles récentes), causant un désalignement visuel. - Backend: /backtest retourne désormais les candles exactes du DataFrame analysé - Frontend: Backtest.tsx utilise result.candles directement (suppression du fetchCandles séparé) - Ajout: sérialisation reason/OB/LL sur les trades, overlays OB/LL bornés dans le temps, trade reasons expandables, filtre HTF, badge tendance HTF Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
@@ -8,6 +7,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.core.backtester import Backtester
|
||||
from app.core.database import get_db
|
||||
from app.core.strategy.order_block_sweep import OrderBlockSweepParams, OrderBlockSweepStrategy
|
||||
from app.core.strategy.htf_trend_filter import HTFTrendFilter, HTFTrendFilterParams, HTF_MAP
|
||||
from app.models.backtest_result import BacktestResult
|
||||
from app.services.market_data import MarketDataService
|
||||
|
||||
@@ -24,6 +24,11 @@ class BacktestRequest(BaseModel):
|
||||
swing_strength: int = Field(default=5, ge=2, le=20)
|
||||
liquidity_tolerance_pips: float = Field(default=2.0, ge=0.5)
|
||||
spread_pips: float = Field(default=1.5, ge=0)
|
||||
# Sélection des stratégies et filtres
|
||||
strategies: list[str] = Field(default=["order_block_sweep"])
|
||||
filters: list[str] = Field(default=[])
|
||||
htf_ema_fast: int = Field(default=20, ge=5, le=100)
|
||||
htf_ema_slow: int = Field(default=50, ge=10, le=200)
|
||||
|
||||
|
||||
@router.post("")
|
||||
@@ -42,13 +47,45 @@ async def run_backtest(
|
||||
if len(df) < 150:
|
||||
raise HTTPException(400, "Pas assez de données (min 150 bougies)")
|
||||
|
||||
# Configurer la stratégie
|
||||
params = OrderBlockSweepParams(
|
||||
swing_strength=req.swing_strength,
|
||||
liquidity_tolerance_pips=req.liquidity_tolerance_pips,
|
||||
rr_ratio=req.rr_ratio,
|
||||
)
|
||||
strategy = OrderBlockSweepStrategy(params)
|
||||
# Construire la stratégie selon la sélection
|
||||
strategies = []
|
||||
if "order_block_sweep" in req.strategies:
|
||||
params = OrderBlockSweepParams(
|
||||
swing_strength=req.swing_strength,
|
||||
liquidity_tolerance_pips=req.liquidity_tolerance_pips,
|
||||
rr_ratio=req.rr_ratio,
|
||||
)
|
||||
strategies.append(OrderBlockSweepStrategy(params))
|
||||
|
||||
if not strategies:
|
||||
raise HTTPException(400, "Au moins une stratégie doit être sélectionnée")
|
||||
|
||||
# Pour l'instant une seule stratégie supportée
|
||||
strategy = strategies[0]
|
||||
|
||||
# Construire les filtres
|
||||
active_filters = []
|
||||
htf_trend_info: Optional[dict] = None
|
||||
|
||||
if "htf_trend_filter" in req.filters:
|
||||
htf_gran = HTF_MAP.get(req.granularity, req.granularity)
|
||||
try:
|
||||
htf_df = await service.get_candles(req.instrument, htf_gran, 200)
|
||||
except Exception as e:
|
||||
raise HTTPException(502, f"Erreur fetch données HTF ({htf_gran}): {e}")
|
||||
|
||||
htf_filter = HTFTrendFilter(HTFTrendFilterParams(
|
||||
ema_fast=req.htf_ema_fast,
|
||||
ema_slow=req.htf_ema_slow,
|
||||
))
|
||||
trend = htf_filter.determine_trend(htf_df)
|
||||
active_filters.append(htf_filter)
|
||||
htf_trend_info = {
|
||||
"htf_granularity": htf_gran,
|
||||
"trend": trend,
|
||||
"ema_fast": req.htf_ema_fast,
|
||||
"ema_slow": req.htf_ema_slow,
|
||||
}
|
||||
|
||||
# Lancer le backtest
|
||||
backtester = Backtester(
|
||||
@@ -56,10 +93,15 @@ async def run_backtest(
|
||||
initial_balance=req.initial_balance,
|
||||
risk_percent=req.risk_percent,
|
||||
spread_pips=req.spread_pips,
|
||||
filters=active_filters,
|
||||
)
|
||||
metrics = backtester.run(df)
|
||||
|
||||
# Sauvegarder en BDD
|
||||
strategy_params = strategy.get_params()
|
||||
if htf_trend_info:
|
||||
strategy_params["htf_trend_filter"] = htf_trend_info
|
||||
|
||||
result = BacktestResult(
|
||||
instrument=req.instrument,
|
||||
granularity=req.granularity,
|
||||
@@ -76,7 +118,7 @@ async def run_backtest(
|
||||
sharpe_ratio=metrics.sharpe_ratio,
|
||||
expectancy=metrics.expectancy,
|
||||
equity_curve=metrics.equity_curve,
|
||||
strategy_params=strategy.get_params(),
|
||||
strategy_params=strategy_params,
|
||||
)
|
||||
db.add(result)
|
||||
await db.commit()
|
||||
@@ -95,10 +137,18 @@ async def run_backtest(
|
||||
"pnl_pips": t.pnl_pips,
|
||||
"status": t.status,
|
||||
"signal_type": t.signal_type,
|
||||
"reason": t.reason,
|
||||
"order_block": t.order_block,
|
||||
"liquidity_level": t.liquidity_level,
|
||||
}
|
||||
for t in metrics.trades
|
||||
]
|
||||
|
||||
# Candles utilisées pour le backtest (pour le chart frontend)
|
||||
candles_out = df[["time", "open", "high", "low", "close", "volume"]].copy()
|
||||
candles_out["time"] = candles_out["time"].astype(str)
|
||||
candles_list = candles_out.to_dict(orient="records")
|
||||
|
||||
return {
|
||||
"backtest_id": result.id,
|
||||
"instrument": req.instrument,
|
||||
@@ -123,6 +173,31 @@ async def run_backtest(
|
||||
},
|
||||
"equity_curve": metrics.equity_curve,
|
||||
"trades": trades_out,
|
||||
"candles": candles_list,
|
||||
"order_blocks": metrics.all_order_blocks,
|
||||
"liquidity_levels": metrics.all_liquidity_levels,
|
||||
"htf_trend": htf_trend_info,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/strategies")
|
||||
async def list_strategies():
|
||||
"""Retourne les stratégies et filtres disponibles."""
|
||||
return {
|
||||
"strategies": [
|
||||
{
|
||||
"name": "order_block_sweep",
|
||||
"label": "Order Block + Liquidity Sweep",
|
||||
"description": "Stratégie ICT : détection Equal H/L, sweep et entrée sur OB",
|
||||
},
|
||||
],
|
||||
"filters": [
|
||||
{
|
||||
"name": "htf_trend_filter",
|
||||
"label": "Filtre tendance HTF",
|
||||
"description": "Ne prendre que les trades dans le sens de la tendance du timeframe supérieur (EMA crossover)",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user