From 32d30c01f2eb4d862cc019e3900d2ccbd039a406 Mon Sep 17 00:00:00 2001 From: OpenSquared Date: Sun, 19 Jul 2026 19:01:49 +0200 Subject: [PATCH] feat: wavelets simulation --- frontend/src/lib/waveletTrade.ts | 28 +++++++++++++++++++++++ frontend/src/pages/WaveletsSimulation.tsx | 10 +++++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/frontend/src/lib/waveletTrade.ts b/frontend/src/lib/waveletTrade.ts index a20af92..4f462ad 100644 --- a/frontend/src/lib/waveletTrade.ts +++ b/frontend/src/lib/waveletTrade.ts @@ -362,6 +362,16 @@ export type WaveletOptimizationResult = { winRate: number tradeCount: number analysisMethod?: 'cwt' | 'ssq' + // Per-trade distribution — a strong totalReturnPct can hide one outlier trade + // carrying the whole result while every other trade is flat or losing; these expose + // that shape instead of just the aggregate. null when there are no wins/losses to + // measure (e.g. tradeCount === 0, or every trade was a win). + avgGainPct: number | null + maxGainPct: number | null + minGainPct: number | null + avgLossPct: number | null + maxLossPct: number | null // most negative — the single worst trade + minLossPct: number | null // least negative — the smallest losing trade } // Trend-confirmation filter: require a SECONDARY band's slope to align with the @@ -407,6 +417,22 @@ export function buildSymmetricTradeConfig( } } +function tradeDistributionStats(trades: WaveletTrade[]): { + avgGainPct: number | null; maxGainPct: number | null; minGainPct: number | null + avgLossPct: number | null; maxLossPct: number | null; minLossPct: number | null +} { + const gains = trades.map(t => t.returnPct).filter(r => r > 0) + const losses = trades.map(t => t.returnPct).filter(r => r <= 0) + return { + avgGainPct: gains.length ? gains.reduce((a, b) => a + b, 0) / gains.length : null, + maxGainPct: gains.length ? Math.max(...gains) : null, + minGainPct: gains.length ? Math.min(...gains) : null, + avgLossPct: losses.length ? losses.reduce((a, b) => a + b, 0) / losses.length : null, + maxLossPct: losses.length ? Math.min(...losses) : null, // most negative = the single worst trade + minLossPct: losses.length ? Math.max(...losses) : null, // least negative = the smallest losing trade + } +} + export function runOptimizationGrid( symbol: string, lookback: number, wavelet: string, rolling: any, bandsToTest: number[], kindsToTest: WaveletTriggerKind[], modesToTest: WaveletPositionMode[], @@ -433,6 +459,7 @@ export function runOptimizationGrid( for (const variant of exitVariants) { const config = buildSymmetricTradeConfig(curveId, kind, mode, variant, filterCurveId) const sim = runWaveletTradeSimulation(rolling.dates, rolling.original, curves, config) + const dist = tradeDistributionStats(sim.trades) results.push({ symbol, lookback, wavelet, mode, triggerKind: kind, @@ -440,6 +467,7 @@ export function runOptimizationGrid( exitStyle: variant.exitStyle, takeProfitPct: variant.takeProfitPct, stopLossPct: variant.stopLossPct, totalReturnPct: sim.totalReturnPct, winRate: sim.winRate, tradeCount: sim.trades.length, analysisMethod: rolling.method ?? 'cwt', + ...dist, }) } } diff --git a/frontend/src/pages/WaveletsSimulation.tsx b/frontend/src/pages/WaveletsSimulation.tsx index 33fb58a..04bbb37 100644 --- a/frontend/src/pages/WaveletsSimulation.tsx +++ b/frontend/src/pages/WaveletsSimulation.tsx @@ -396,6 +396,8 @@ export default function WaveletsSimulation() { Retour Win rate Trades + Gains (moy/max/min) + Pertes (moy/max/min) @@ -414,10 +416,16 @@ export default function WaveletsSimulation() { {(r.winRate * 100).toFixed(0)}% {r.tradeCount} + + {r.avgGainPct != null ? `${r.avgGainPct.toFixed(1)} / ${r.maxGainPct!.toFixed(1)} / ${r.minGainPct!.toFixed(1)}` : '—'} + + + {r.avgLossPct != null ? `${r.avgLossPct.toFixed(1)} / ${r.maxLossPct!.toFixed(1)} / ${r.minLossPct!.toFixed(1)}` : '—'} + {expandedRow === i && ( - +
Détail par instrument (même config)
{perSymbolBreakdown(r).map((d, j) => (