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>
212 lines
7.3 KiB
TypeScript
212 lines
7.3 KiB
TypeScript
import { useState } from 'react'
|
|
import { FlaskConical, Loader2 } from 'lucide-react'
|
|
import type { BacktestResult } from '../../lib/api'
|
|
import { runBacktest } from '../../lib/api'
|
|
|
|
interface Props {
|
|
onResult: (result: BacktestResult) => void
|
|
}
|
|
|
|
const INSTRUMENTS = [
|
|
'EUR_USD', 'GBP_USD', 'USD_JPY', 'USD_CHF', 'AUD_USD', 'USD_CAD',
|
|
'GBP_JPY', 'EUR_JPY', 'SPX500_USD', 'NAS100_USD', 'XAU_USD',
|
|
]
|
|
|
|
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)
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
const [form, setForm] = useState({
|
|
instrument: 'EUR_USD',
|
|
granularity: 'H1',
|
|
candle_count: 500,
|
|
initial_balance: 10000,
|
|
risk_percent: 1.0,
|
|
rr_ratio: 2.0,
|
|
swing_strength: 5,
|
|
liquidity_tolerance_pips: 2.0,
|
|
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,
|
|
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'
|
|
setError(msg)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
const Field = ({
|
|
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>
|
|
{options ? (
|
|
<select
|
|
value={form[name as keyof typeof form]}
|
|
onChange={(e) => handleChange(name, e.target.value)}
|
|
className="w-full bg-[#0f1117] border border-[#2a2d3e] rounded-lg px-3 py-2 text-sm text-white focus:outline-none focus:border-[#6366f1]"
|
|
>
|
|
{options.map((o) => <option key={o} value={o}>{o}</option>)}
|
|
</select>
|
|
) : (
|
|
<input
|
|
type={type}
|
|
step="any"
|
|
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">
|
|
Parametres du backtest
|
|
</h2>
|
|
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<Field label="Instrument" name="instrument" options={INSTRUMENTS} />
|
|
<Field label="Timeframe" name="granularity" options={GRANULARITIES} />
|
|
<Field label="Nombre de bougies" name="candle_count" />
|
|
<Field label="Capital initial ($)" name="initial_balance" />
|
|
<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="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}
|
|
</div>
|
|
)}
|
|
|
|
<button
|
|
type="submit"
|
|
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 ? (
|
|
<><Loader2 size={16} className="animate-spin" /> Backtesting...</>
|
|
) : (
|
|
<><FlaskConical size={16} /> Lancer le backtest</>
|
|
)}
|
|
</button>
|
|
</form>
|
|
)
|
|
}
|