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:
2026-03-01 01:23:42 +01:00
parent 4df8d53b1a
commit adbc41102e
10 changed files with 698 additions and 95 deletions

View File

@@ -12,7 +12,11 @@ const INSTRUMENTS = [
'GBP_JPY', 'EUR_JPY', 'SPX500_USD', 'NAS100_USD', 'XAU_USD',
]
const GRANULARITIES = ['M15', 'M30', 'H1', 'H4', 'D']
const GRANULARITIES = ['M1', 'M5', 'M15', 'M30', 'H1', 'H4', 'D']
const HTF_MAP: Record<string, string> = {
M1: 'M5', M5: 'M15', M15: 'H1', M30: 'H1', H1: 'H4', H4: 'D', D: 'D',
}
export default function BacktestForm({ onResult }: Props) {
const [loading, setLoading] = useState(false)
@@ -30,16 +34,38 @@ export default function BacktestForm({ onResult }: Props) {
spread_pips: 1.5,
})
const [strategies, setStrategies] = useState<string[]>(['order_block_sweep'])
const [filters, setFilters] = useState<string[]>([])
const [htfEmaFast, setHtfEmaFast] = useState(20)
const [htfEmaSlow, setHtfEmaSlow] = useState(50)
const handleChange = (key: string, value: string | number) => {
setForm((prev) => ({ ...prev, [key]: value }))
}
const toggleStrategy = (name: string) => {
setStrategies((prev) =>
prev.includes(name) ? prev.filter((s) => s !== name) : [...prev, name]
)
}
const toggleFilter = (name: string) => {
setFilters((prev) =>
prev.includes(name) ? prev.filter((f) => f !== name) : [...prev, name]
)
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setLoading(true)
setError(null)
try {
const result = await runBacktest(form)
const result = await runBacktest({
...form,
strategies,
filters,
...(filters.includes('htf_trend_filter') ? { htf_ema_fast: htfEmaFast, htf_ema_slow: htfEmaSlow } : {}),
})
onResult(result)
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : 'Erreur inconnue'
@@ -50,9 +76,10 @@ export default function BacktestForm({ onResult }: Props) {
}
const Field = ({
label, name, type = 'number', options
label, name, type = 'number', options, value, onChange,
}: {
label: string; name: string; type?: string; options?: string[]
value?: number; onChange?: (v: number) => void
}) => (
<div>
<label className="block text-xs text-[#64748b] mb-1">{label}</label>
@@ -68,18 +95,41 @@ export default function BacktestForm({ onResult }: Props) {
<input
type={type}
step="any"
value={form[name as keyof typeof form]}
onChange={(e) => handleChange(name, parseFloat(e.target.value) || 0)}
value={value ?? form[name as keyof typeof form]}
onChange={(e) => {
const v = parseFloat(e.target.value) || 0
if (onChange) onChange(v)
else handleChange(name, v)
}}
className="w-full bg-[#0f1117] border border-[#2a2d3e] rounded-lg px-3 py-2 text-sm text-white focus:outline-none focus:border-[#6366f1]"
/>
)}
</div>
)
const Checkbox = ({
label, checked, onChange, description,
}: {
label: string; checked: boolean; onChange: () => void; description?: string
}) => (
<label className="flex items-start gap-2 cursor-pointer group">
<input
type="checkbox"
checked={checked}
onChange={onChange}
className="mt-0.5 accent-[#6366f1] w-3.5 h-3.5"
/>
<div>
<span className="text-sm text-white group-hover:text-[#6366f1] transition-colors">{label}</span>
{description && <p className="text-[10px] text-[#64748b] mt-0.5">{description}</p>}
</div>
</label>
)
return (
<form onSubmit={handleSubmit} className="bg-[#1a1d27] border border-[#2a2d3e] rounded-xl p-5 space-y-4">
<h2 className="text-sm font-semibold text-[#64748b] uppercase tracking-wider mb-4">
Paramètres du backtest
Parametres du backtest
</h2>
<div className="grid grid-cols-2 gap-3">
@@ -90,10 +140,55 @@ export default function BacktestForm({ onResult }: Props) {
<Field label="Risque par trade (%)" name="risk_percent" />
<Field label="Ratio R:R" name="rr_ratio" />
<Field label="Swing strength" name="swing_strength" />
<Field label="Tolérance liquidité (pips)" name="liquidity_tolerance_pips" />
<Field label="Tolerance liquidite (pips)" name="liquidity_tolerance_pips" />
<Field label="Spread (pips)" name="spread_pips" />
</div>
{/* Limite données info */}
{(form.granularity === 'M1' || form.granularity === 'M5') && (
<div className="text-[10px] text-[#64748b] bg-[#0f1117] rounded-lg px-3 py-2 border border-[#2a2d3e]">
{form.granularity === 'M1' ? 'M1 : donnees limitees a 7 jours' : 'M5 : donnees limitees a 60 jours'}
</div>
)}
{/* Stratégies */}
<div className="border-t border-[#2a2d3e] pt-3">
<h3 className="text-[10px] text-[#64748b] uppercase tracking-wider mb-2">Strategies</h3>
<Checkbox
label="Order Block + Liquidity Sweep"
description="ICT : Equal H/L, sweep et entree sur OB"
checked={strategies.includes('order_block_sweep')}
onChange={() => toggleStrategy('order_block_sweep')}
/>
</div>
{/* Filtres */}
<div className="border-t border-[#2a2d3e] pt-3">
<h3 className="text-[10px] text-[#64748b] uppercase tracking-wider mb-2">Filtres</h3>
<Checkbox
label="Filtre tendance HTF"
description={`Trades uniquement dans le sens de la tendance ${HTF_MAP[form.granularity] ?? ''} (EMA cross)`}
checked={filters.includes('htf_trend_filter')}
onChange={() => toggleFilter('htf_trend_filter')}
/>
{filters.includes('htf_trend_filter') && (
<div className="grid grid-cols-2 gap-3 mt-2 ml-5">
<Field
label="EMA rapide"
name="htf_ema_fast"
value={htfEmaFast}
onChange={setHtfEmaFast}
/>
<Field
label="EMA lente"
name="htf_ema_slow"
value={htfEmaSlow}
onChange={setHtfEmaSlow}
/>
</div>
)}
</div>
{error && (
<div className="text-sm text-[#ef5350] bg-[#ef535020] border border-[#ef535033] rounded-lg px-3 py-2">
{error}
@@ -102,7 +197,7 @@ export default function BacktestForm({ onResult }: Props) {
<button
type="submit"
disabled={loading}
disabled={loading || strategies.length === 0}
className="w-full flex items-center justify-center gap-2 py-2.5 bg-[#6366f1] hover:bg-[#4f46e5] text-white text-sm font-medium rounded-lg transition-colors disabled:opacity-60"
>
{loading ? (

View File

@@ -1,5 +1,6 @@
import { createChart, LineSeries, ColorType, type Time } from 'lightweight-charts'
import { useEffect, useRef } from 'react'
import React, { useEffect, useRef, useState } from 'react'
import { ChevronDown, ChevronUp } from 'lucide-react'
import type { BacktestResult } from '../../lib/api'
interface Props {
@@ -18,6 +19,7 @@ function MetricCard({ label, value, color }: { label: string; value: string; col
export default function BacktestResults({ result }: Props) {
const { metrics, equity_curve, trades } = result
const chartRef = useRef<HTMLDivElement>(null)
const [expandedRow, setExpandedRow] = useState<number | null>(null)
useEffect(() => {
if (!chartRef.current || equity_curve.length === 0) return
@@ -115,44 +117,95 @@ export default function BacktestResults({ result }: Props) {
</thead>
<tbody>
{trades.map((t, i) => (
<tr key={i} className="border-b border-[#2a2d3e20] hover:bg-[#2a2d3e20]">
<td className="px-3 py-2">
<span className={`px-1.5 py-0.5 rounded font-semibold ${
t.direction === 'buy'
? 'bg-[#26a69a20] text-[#26a69a]'
: 'bg-[#ef535020] text-[#ef5350]'
<React.Fragment key={i}>
<tr
className="border-b border-[#2a2d3e20] hover:bg-[#2a2d3e20] cursor-pointer"
onClick={() => setExpandedRow(expandedRow === i ? null : i)}
>
<td className="px-3 py-2">
<span className={`px-1.5 py-0.5 rounded font-semibold ${
t.direction === 'buy'
? 'bg-[#26a69a20] text-[#26a69a]'
: 'bg-[#ef535020] text-[#ef5350]'
}`}>
{t.direction.toUpperCase()}
</span>
</td>
<td className="px-3 py-2 text-[#94a3b8] text-[10px] max-w-[140px] truncate" title={t.signal_type}>
<span className="flex items-center gap-1">
{t.signal_type.replace('LiquiditySweep_', 'Sweep ').replace('+OrderBlock', ' + OB')}
{t.reason && (
expandedRow === i
? <ChevronUp size={12} className="text-[#6366f1] shrink-0" />
: <ChevronDown size={12} className="text-[#64748b] shrink-0" />
)}
</span>
</td>
<td className="px-3 py-2 text-right font-mono">{t.entry_price.toFixed(5)}</td>
<td className="px-3 py-2 text-right font-mono text-[#64748b]">
{t.exit_price?.toFixed(5) ?? ''}
</td>
<td className={`px-3 py-2 text-right font-mono ${
(t.pnl_pips ?? 0) > 0 ? 'text-[#26a69a]' : 'text-[#ef5350]'
}`}>
{t.direction.toUpperCase()}
</span>
</td>
<td className="px-3 py-2 text-[#94a3b8] text-[10px] max-w-[140px] truncate" title={t.signal_type}>
{t.signal_type.replace('LiquiditySweep_', 'Sweep ').replace('+OrderBlock', ' + OB')}
</td>
<td className="px-3 py-2 text-right font-mono">{t.entry_price.toFixed(5)}</td>
<td className="px-3 py-2 text-right font-mono text-[#64748b]">
{t.exit_price?.toFixed(5) ?? ''}
</td>
<td className={`px-3 py-2 text-right font-mono ${
(t.pnl_pips ?? 0) > 0 ? 'text-[#26a69a]' : 'text-[#ef5350]'
}`}>
{t.pnl_pips !== null
? `${t.pnl_pips > 0 ? '+' : ''}${t.pnl_pips.toFixed(1)}`
: ''}
</td>
<td className="px-3 py-2">
<span className={t.status === 'win' ? 'text-[#26a69a]' : 'text-[#ef5350]'}>
{t.status === 'win' ? ' Win' : ' Loss'}
</span>
</td>
<td className="px-3 py-2 text-[#64748b] whitespace-nowrap">
{new Date(t.entry_time).toLocaleString('fr-FR', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' })}
</td>
<td className="px-3 py-2 text-[#64748b] whitespace-nowrap">
{t.exit_time
? new Date(t.exit_time).toLocaleString('fr-FR', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' })
: ''}
</td>
</tr>
{t.pnl_pips !== null
? `${t.pnl_pips > 0 ? '+' : ''}${t.pnl_pips.toFixed(1)}`
: ''}
</td>
<td className="px-3 py-2">
<span className={t.status === 'win' ? 'text-[#26a69a]' : 'text-[#ef5350]'}>
{t.status === 'win' ? ' Win' : ' Loss'}
</span>
</td>
<td className="px-3 py-2 text-[#64748b] whitespace-nowrap">
{new Date(t.entry_time).toLocaleString('fr-FR', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' })}
</td>
<td className="px-3 py-2 text-[#64748b] whitespace-nowrap">
{t.exit_time
? new Date(t.exit_time).toLocaleString('fr-FR', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' })
: ''}
</td>
</tr>
{expandedRow === i && t.reason && (
<tr className="bg-[#0f1117]">
<td colSpan={8} className="px-4 py-3">
<div className="text-[11px] space-y-1.5">
<div className="text-white font-medium">{t.reason.summary}</div>
<div className="flex flex-wrap gap-x-6 gap-y-1 text-[#94a3b8]">
{t.reason.swept_level_price != null && (
<span>
Liquidite : <span className={t.reason.swept_level_direction === 'high' ? 'text-[#ef5350]' : 'text-[#26a69a]'}>
{t.reason.swept_level_direction === 'high' ? 'Equal High' : 'Equal Low'} @ {t.reason.swept_level_price.toFixed(5)}
</span>
</span>
)}
{t.reason.ob_direction && (
<span>
Order Block : <span className={t.reason.ob_direction === 'bullish' ? 'text-[#26a69a]' : 'text-[#ef5350]'}>
{t.reason.ob_direction === 'bullish' ? 'Bullish' : 'Bearish'} [{t.reason.ob_bottom?.toFixed(5)} - {t.reason.ob_top?.toFixed(5)}]
</span>
</span>
)}
{t.reason.ob_origin_time && (
<span>
OB forme le : {new Date(t.reason.ob_origin_time).toLocaleString('fr-FR', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' })}
</span>
)}
</div>
{t.reason.filters_applied.length > 0 && (
<div className="flex gap-2 mt-1">
{t.reason.filters_applied.map((f, fi) => (
<span key={fi} className="px-2 py-0.5 rounded bg-[#6366f120] text-[#6366f1] border border-[#6366f133] text-[10px]">
{f}
</span>
))}
</div>
)}
</div>
</td>
</tr>
)}
</React.Fragment>
))}
</tbody>
</table>

View File

@@ -1,6 +1,8 @@
import {
createChart,
CandlestickSeries,
AreaSeries,
LineSeries,
createSeriesMarkers,
type IChartApi,
type ISeriesApi,
@@ -8,29 +10,15 @@ import {
type SeriesMarker,
type Time,
ColorType,
LineStyle,
} from 'lightweight-charts'
import { useEffect, useRef, useState } from 'react'
import type { Candle, BacktestTrade, Trade } from '../../lib/api'
interface OrderBlock {
direction: 'bullish' | 'bearish'
top: number
bottom: number
origin_time: string
mitigated: boolean
}
interface LiquidityLevel {
direction: 'high' | 'low'
price: number
origin_time: string
swept: boolean
}
import { useEffect, useMemo, useRef, useState } from 'react'
import type { Candle, BacktestTrade, Trade, OrderBlockData, LiquidityLevelData } from '../../lib/api'
interface Props {
candles: Candle[]
orderBlocks?: OrderBlock[]
liquidityLevels?: LiquidityLevel[]
orderBlocks?: OrderBlockData[]
liquidityLevels?: LiquidityLevelData[]
trades?: (BacktestTrade | Trade)[]
height?: number
}
@@ -62,13 +50,53 @@ function fmt(v: number): string {
export default function CandlestickChart({
candles,
orderBlocks = [],
liquidityLevels = [],
trades = [],
height = 500,
}: Props) {
const containerRef = useRef<HTMLDivElement>(null)
const chartRef = useRef<IChartApi | null>(null)
const candleSeriesRef = useRef<ISeriesApi<'Candlestick'> | null>(null)
const overlaySeriesRef = useRef<(ISeriesApi<'Area'> | ISeriesApi<'Line'>)[]>([])
const [legend, setLegend] = useState<OhlcLegend | null>(null)
const [hoverPrice, setHoverPrice] = useState<number | null>(null)
// Annotation la plus proche du curseur (OB ou LL)
const hoverAnnotation = useMemo(() => {
if (hoverPrice === null || (!orderBlocks.length && !liquidityLevels.length)) return null
const pip = hoverPrice >= 100 ? 0.01 : hoverPrice >= 10 ? 0.001 : 0.0001
const threshold = pip * 15
let best: { label: string; detail: string; color: string } | null = null
let minDist = threshold
for (const ob of orderBlocks) {
const mid = (ob.top + ob.bottom) / 2
const dist = Math.abs(hoverPrice - mid)
if (dist < minDist) {
minDist = dist
best = {
label: ob.direction === 'bullish' ? 'Bullish OB' : 'Bearish OB',
detail: `${ob.bottom.toFixed(5)} ${ob.top.toFixed(5)}`,
color: ob.direction === 'bullish' ? '#26a69a' : '#ef5350',
}
}
}
for (const ll of liquidityLevels) {
const dist = Math.abs(hoverPrice - ll.price)
if (dist < minDist) {
minDist = dist
best = {
label: ll.direction === 'high' ? 'EQH' : 'EQL',
detail: ll.price.toFixed(5),
color: ll.direction === 'high' ? '#ef5350' : '#26a69a',
}
}
}
return best
}, [hoverPrice, orderBlocks, liquidityLevels])
useEffect(() => {
if (!containerRef.current) return
@@ -111,6 +139,7 @@ export default function CandlestickChart({
chart.subscribeCrosshairMove((param) => {
if (!param.seriesData.size) {
setLegend(null)
setHoverPrice(null)
return
}
const d = param.seriesData.get(candleSeries) as CandlestickData | undefined
@@ -123,6 +152,9 @@ export default function CandlestickChart({
low: d.low,
close: d.close,
})
if (param.point) {
setHoverPrice(candleSeries.coordinateToPrice(param.point.y))
}
})
const resizeObserver = new ResizeObserver(() => {
@@ -195,6 +227,100 @@ export default function CandlestickChart({
chartRef.current?.timeScale().fitContent()
}, [candles, trades])
// Overlay: Order Blocks (zones bornées) + Liquidity Levels (lignes bornées dans le temps)
useEffect(() => {
const chart = chartRef.current
const candleSeries = candleSeriesRef.current
if (!chart || !candleSeries) return
// Cleanup toujours en premier, même quand candles est vide
for (const s of overlaySeriesRef.current) {
try { chart.removeSeries(s) } catch { /* already removed */ }
}
overlaySeriesRef.current = []
if (candles.length === 0) return
const lastTime = toTimestamp(candles[candles.length - 1].time)
// Maps ob_id / ll_id → temps de fin (depuis les trades)
const obEndTimes = new Map<string, Time>()
const llEndTimes = new Map<string, Time>()
for (const trade of trades) {
if ('order_block' in trade) {
const bt = trade as BacktestTrade
if (bt.order_block?.id && bt.exit_time) {
obEndTimes.set(bt.order_block.id, toTimestamp(bt.exit_time))
}
if (bt.liquidity_level?.id && bt.entry_time) {
llEndTimes.set(bt.liquidity_level.id, toTimestamp(bt.entry_time))
}
}
}
// Order Blocks — uniquement ceux utilisés dans un trade, de origin_time à exit_time
const sortedObs = [...orderBlocks]
.filter((ob) => obEndTimes.has(ob.id))
.sort((a, b) => (a.mitigated === b.mitigated ? 0 : a.mitigated ? 1 : -1))
for (const ob of sortedObs) {
const alpha = 0.14
const isBullish = ob.direction === 'bullish'
const color = isBullish ? `rgba(38, 166, 154, ${alpha})` : `rgba(239, 83, 80, ${alpha})`
const lineColor = isBullish ? 'rgba(38, 166, 154, 0.4)' : 'rgba(239, 83, 80, 0.4)'
try {
const startTime = toTimestamp(ob.origin_time)
const endTime = obEndTimes.get(ob.id) ?? lastTime
if ((startTime as number) >= (endTime as number)) continue
const areaSeries = chart.addSeries(AreaSeries, {
topColor: color,
bottomColor: 'transparent',
lineColor,
lineWidth: 1,
lineStyle: LineStyle.Dashed,
crosshairMarkerVisible: false,
priceScaleId: 'right',
baseValue: { type: 'price' as const, price: ob.bottom },
lastValueVisible: false,
priceLineVisible: false,
})
areaSeries.setData([
{ time: startTime, value: ob.top },
{ time: endTime, value: ob.top },
])
overlaySeriesRef.current.push(areaSeries)
} catch { /* skip si problème de temps */ }
}
// Liquidity Levels — uniquement ceux sweepés pour déclencher un trade, de origin_time à entry_time
for (const level of liquidityLevels.filter((l) => llEndTimes.has(l.id))) {
try {
const startTime = toTimestamp(level.origin_time)
const endTime = llEndTimes.get(level.id)!
if ((startTime as number) >= (endTime as number)) continue
const isHigh = level.direction === 'high'
const color = isHigh ? 'rgba(239, 83, 80, 0.7)' : 'rgba(38, 166, 154, 0.7)'
const lineSeries = chart.addSeries(LineSeries, {
color,
lineWidth: 1,
lineStyle: LineStyle.Dashed,
crosshairMarkerVisible: false,
lastValueVisible: false,
priceLineVisible: false,
})
lineSeries.setData([
{ time: startTime, value: level.price },
{ time: endTime, value: level.price },
])
overlaySeriesRef.current.push(lineSeries)
} catch { /* skip */ }
}
}, [candles, orderBlocks, liquidityLevels, trades])
return (
<div className="relative w-full rounded-xl overflow-hidden border border-[#2a2d3e]">
{/* Légende OHLC */}
@@ -206,17 +332,34 @@ export default function CandlestickChart({
<span className="text-slate-300">H <span className="text-[#26a69a]">{fmt(legend.high)}</span></span>
<span className="text-slate-300">L <span className="text-[#ef5350]">{fmt(legend.low)}</span></span>
<span className="text-slate-300">C <span className="text-white">{fmt(legend.close)}</span></span>
{hoverAnnotation && (
<span
className="ml-1 px-2 py-0.5 rounded border text-xs font-mono"
style={{
color: hoverAnnotation.color,
borderColor: hoverAnnotation.color + '55',
backgroundColor: hoverAnnotation.color + '18',
}}
>
{hoverAnnotation.label}: {hoverAnnotation.detail}
</span>
)}
</>
) : (
<div className="flex gap-2">
{orderBlocks.filter((o) => o.direction === 'bullish').length > 0 && (
{trades.filter((t) => 'order_block' in t && (t as BacktestTrade).order_block?.direction === 'bullish').length > 0 && (
<span className="px-2 py-0.5 rounded bg-[#26a69a33] text-[#26a69a] border border-[#26a69a55]">
{orderBlocks.filter((o) => o.direction === 'bullish').length} Bullish OB
{trades.filter((t) => 'order_block' in t && (t as BacktestTrade).order_block?.direction === 'bullish').length} Bullish OB
</span>
)}
{orderBlocks.filter((o) => o.direction === 'bearish').length > 0 && (
{trades.filter((t) => 'order_block' in t && (t as BacktestTrade).order_block?.direction === 'bearish').length > 0 && (
<span className="px-2 py-0.5 rounded bg-[#ef535033] text-[#ef5350] border border-[#ef535055]">
{orderBlocks.filter((o) => o.direction === 'bearish').length} Bearish OB
{trades.filter((t) => 'order_block' in t && (t as BacktestTrade).order_block?.direction === 'bearish').length} Bearish OB
</span>
)}
{trades.filter((t) => 'liquidity_level' in t && (t as BacktestTrade).liquidity_level).length > 0 && (
<span className="px-2 py-0.5 rounded bg-[#6366f133] text-[#6366f1] border border-[#6366f155]">
{trades.filter((t) => 'liquidity_level' in t && (t as BacktestTrade).liquidity_level).length} Sweeps
</span>
)}
</div>

View File

@@ -49,6 +49,34 @@ export interface BacktestMetrics {
profit_factor: number
}
export interface TradeReason {
summary: string
swept_level_price: number | null
swept_level_direction: string | null
ob_direction: string | null
ob_top: number | null
ob_bottom: number | null
ob_origin_time: string | null
filters_applied: string[]
}
export interface OrderBlockData {
id: string
direction: 'bullish' | 'bearish'
top: number
bottom: number
origin_time: string
mitigated: boolean
}
export interface LiquidityLevelData {
id: string
direction: 'high' | 'low'
price: number
origin_time: string
swept: boolean
}
export interface BacktestResult {
backtest_id: number
instrument: string
@@ -57,6 +85,10 @@ export interface BacktestResult {
metrics: BacktestMetrics
equity_curve: { time: string; balance: number }[]
trades: BacktestTrade[]
candles: Candle[]
order_blocks: OrderBlockData[]
liquidity_levels: LiquidityLevelData[]
htf_trend?: { htf_granularity: string; trend: string; ema_fast: number; ema_slow: number } | null
}
export interface BacktestTrade {
@@ -70,6 +102,9 @@ export interface BacktestTrade {
pnl_pips: number | null
status: string
signal_type: string
reason: TradeReason | null
order_block: OrderBlockData | null
liquidity_level: LiquidityLevelData | null
}
export interface BotStatus {
@@ -124,6 +159,10 @@ export const runBacktest = (params: {
swing_strength: number
liquidity_tolerance_pips: number
spread_pips: number
strategies: string[]
filters: string[]
htf_ema_fast?: number
htf_ema_slow?: number
}) =>
api.post<BacktestResult>('/backtest', params).then((r) => r.data)

View File

@@ -2,18 +2,15 @@ import { useState } from 'react'
import BacktestForm from '../components/Backtest/BacktestForm'
import BacktestResults from '../components/Backtest/BacktestResults'
import CandlestickChart from '../components/Chart/CandlestickChart'
import type { BacktestResult, Candle } from '../lib/api'
import { fetchCandles } from '../lib/api'
import type { BacktestResult } from '../lib/api'
export default function Backtest() {
const [result, setResult] = useState<BacktestResult | null>(null)
const [candles, setCandles] = useState<Candle[]>([])
const [backtestKey, setBacktestKey] = useState(0)
const handleResult = async (r: BacktestResult) => {
const handleResult = (r: BacktestResult) => {
setResult(r)
// Charger les candles pour la période du backtest
const c = await fetchCandles(r.instrument, r.granularity, 500)
setCandles(c)
setBacktestKey(k => k + 1)
}
return (
@@ -25,14 +22,30 @@ export default function Backtest() {
{/* Right: results */}
<div className="flex-1 flex flex-col gap-4 min-w-0 overflow-auto">
<h1 className="text-base font-bold text-white">Backtesting Order Block + Liquidity Sweep</h1>
<div className="flex items-center gap-3">
<h1 className="text-base font-bold text-white">
Backtesting{result ? `${result.instrument} ${result.granularity}` : ''}
</h1>
{result?.htf_trend && (
<span className={`text-xs px-2 py-0.5 rounded border ${
result.htf_trend.trend === 'bullish'
? 'bg-[#26a69a20] text-[#26a69a] border-[#26a69a55]'
: 'bg-[#ef535020] text-[#ef5350] border-[#ef535055]'
}`}>
HTF {result.htf_trend.htf_granularity}: {result.htf_trend.trend}
</span>
)}
</div>
{result ? (
<>
{/* Chart avec les trades superposés */}
{/* Chart avec trades, Order Blocks et niveaux de liquidité */}
<CandlestickChart
candles={candles}
key={backtestKey}
candles={result.candles}
trades={result.trades}
orderBlocks={result.order_blocks}
liquidityLevels={result.liquidity_levels}
height={360}
/>
<BacktestResults result={result} />