diff --git a/frontend/src/lib/waveletTrade.ts b/frontend/src/lib/waveletTrade.ts index c2d926a..a20af92 100644 --- a/frontend/src/lib/waveletTrade.ts +++ b/frontend/src/lib/waveletTrade.ts @@ -364,12 +364,31 @@ export type WaveletOptimizationResult = { analysisMethod?: 'cwt' | 'ssq' } +// Trend-confirmation filter: require a SECONDARY band's slope to align with the +// direction implied by each trigger before it's allowed to fire — e.g. only take band +// 1's trough (buy) if band 3 is sloping up, only take its peak (sell) if band 3 is +// sloping down. Buy/cover expect an upward move so they require a positive slope; +// sell/short expect a downward move so they require a negative slope. `null` disables +// the filter entirely (every buyFilters/etc. array stays empty, unchanged behaviour). +function trendConfirmationFilters(filterCurveId: string | null): { + buy: WaveletSlopeFilter[]; sell: WaveletSlopeFilter[]; short: WaveletSlopeFilter[]; cover: WaveletSlopeFilter[] +} { + if (!filterCurveId) return { buy: [], sell: [], short: [], cover: [] } + return { + buy: [{ curveId: filterCurveId, sign: 'positive' }], + sell: [{ curveId: filterCurveId, sign: 'negative' }], + short: [{ curveId: filterCurveId, sign: 'negative' }], + cover: [{ curveId: filterCurveId, sign: 'positive' }], + } +} + // All grid combos use the SAME trigger kind/band symmetrically for both sides of a // position with each kind's validated default parameters — testing every // combination of *different* kinds for entry vs exit would multiply the grid by // another 5x for little practical benefit. export function buildSymmetricTradeConfig( curveId: string, triggerKind: WaveletTriggerKind, mode: WaveletPositionMode, variant: WaveletExitVariant, + filterCurveId: string | null = null, ): WaveletTradeConfig { const effectiveCurveId = triggerKind === 'ridge_shift' ? 'ridge' : @@ -379,10 +398,11 @@ export function buildSymmetricTradeConfig( // ridge_shift's period series is coarser than a price band, so it rarely crosses // a full standard deviation — the shared default K=1 starves it of trades. if (triggerKind === 'ridge_shift') trigger.levelThresholdK = 0.5 + const filters = trendConfirmationFilters(filterCurveId) return { mode, - buyTrigger: trigger, sellTrigger: trigger, buyFilters: [], sellFilters: [], - shortTrigger: trigger, coverTrigger: trigger, shortFilters: [], coverFilters: [], + buyTrigger: trigger, sellTrigger: trigger, buyFilters: filters.buy, sellFilters: filters.sell, + shortTrigger: trigger, coverTrigger: trigger, shortFilters: filters.short, coverFilters: filters.cover, exitStyle: variant.exitStyle, takeProfitPct: variant.takeProfitPct, stopLossPct: variant.stopLossPct, } } @@ -391,6 +411,7 @@ export function runOptimizationGrid( symbol: string, lookback: number, wavelet: string, rolling: any, bandsToTest: number[], kindsToTest: WaveletTriggerKind[], modesToTest: WaveletPositionMode[], exitVariants: WaveletExitVariant[], + filterBandIndex: number | null = null, ): WaveletOptimizationResult[] { const curves: WaveletCurve[] = rolling.bands.map((band: any) => ({ id: `band_${band.index}`, label: band.label, series: band.series })) if (rolling.ridge_period_days) { @@ -400,6 +421,7 @@ export function runOptimizationGrid( if (band.energy) curves.push({ id: `energy_band_${band.index}`, label: `Energie ${band.label}`, series: band.energy }) }) const availableBandIndices = new Set(rolling.bands.map((band: any) => band.index)) + const filterCurveId = filterBandIndex != null && availableBandIndices.has(filterBandIndex) ? `band_${filterBandIndex}` : null const results: WaveletOptimizationResult[] = [] let ridgeShiftComputed = false for (const bandIndex of bandsToTest) { @@ -409,7 +431,7 @@ export function runOptimizationGrid( if (kind === 'ridge_shift' && ridgeShiftComputed) continue for (const mode of modesToTest) { for (const variant of exitVariants) { - const config = buildSymmetricTradeConfig(curveId, kind, mode, variant) + const config = buildSymmetricTradeConfig(curveId, kind, mode, variant, filterCurveId) const sim = runWaveletTradeSimulation(rolling.dates, rolling.original, curves, config) results.push({ symbol, lookback, wavelet, mode, @@ -438,6 +460,7 @@ export async function runFullOptimization( onProgress: (done: number, total: number, label: string, failures: string[]) => void, onBatchResults: (results: WaveletOptimizationResult[]) => void, method: 'cwt' | 'ssq' = 'cwt', + filterBandIndex: number | null = null, ): Promise { const combos: { symbol: string; lookback: number; wavelet: string }[] = [] for (const symbol of instruments) { @@ -455,7 +478,7 @@ export async function runFullOptimization( const { data: rolling } = await api.get('/wavelet/rolling', { params: { symbol, period, levels, wavelet, lookback, step: 1, method }, }) - onBatchResults(runOptimizationGrid(symbol, lookback, wavelet, rolling, bandsToTest, kindsToTest, modesToTest, exitVariants)) + onBatchResults(runOptimizationGrid(symbol, lookback, wavelet, rolling, bandsToTest, kindsToTest, modesToTest, exitVariants, filterBandIndex)) } catch (error) { failures.push(`${symbol} @ ${lookback}j (${wavelet}): ${error instanceof Error ? error.message : String(error)}`) } diff --git a/frontend/src/pages/WaveletsSimulation.tsx b/frontend/src/pages/WaveletsSimulation.tsx index 2c68784..33fb58a 100644 --- a/frontend/src/pages/WaveletsSimulation.tsx +++ b/frontend/src/pages/WaveletsSimulation.tsx @@ -66,6 +66,7 @@ export default function WaveletsSimulation() { const [modes, setModes] = useState>(new Set(['both'])) const [takeProfits, setTakeProfits] = useState([0]) const [stopLosses, setStopLosses] = useState([0]) + const [filterBand, setFilterBand] = useState(null) const [simName, setSimName] = useState(() => `Run ${new Date().toISOString().slice(0, 16).replace('T', ' ')}`) // ── Run state ──────────────────────────────────────────────────────────────── @@ -103,7 +104,7 @@ export default function WaveletsSimulation() { const form = { instruments: [...selectedInstruments], period, levels, wavelets: [...wavelets], lookbacks, bands: [...bands], kinds: [...kinds], modes: [...modes], - takeProfits, stopLosses, method, + takeProfits, stopLosses, method, filterBand, } const created = await api.post('/wavelet/simulations', { name: simName, form, results: [] }).then(r => r.data) setActiveSimId(created.id) @@ -124,6 +125,7 @@ export default function WaveletsSimulation() { try { await api.patch(`/wavelet/simulations/${created.id}`, { append_results: batch }) } catch { /* non-blocking */ } }, method, + filterBand, ) setRunning(false) qc.invalidateQueries({ queryKey: ['wavelet-simulations'] }) @@ -286,6 +288,22 @@ export default function WaveletsSimulation() { +
+
+ Filtre de tendance (bande de confirmation) +
+
+ + {ALL_BANDS.slice(0, levels).map(b => ( + + ))} +
+
+
Types de déclencheur