feat: wavelets
This commit is contained in:
@@ -364,12 +364,31 @@ export type WaveletOptimizationResult = {
|
|||||||
analysisMethod?: 'cwt' | 'ssq'
|
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
|
// 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
|
// position with each kind's validated default parameters — testing every
|
||||||
// combination of *different* kinds for entry vs exit would multiply the grid by
|
// combination of *different* kinds for entry vs exit would multiply the grid by
|
||||||
// another 5x for little practical benefit.
|
// another 5x for little practical benefit.
|
||||||
export function buildSymmetricTradeConfig(
|
export function buildSymmetricTradeConfig(
|
||||||
curveId: string, triggerKind: WaveletTriggerKind, mode: WaveletPositionMode, variant: WaveletExitVariant,
|
curveId: string, triggerKind: WaveletTriggerKind, mode: WaveletPositionMode, variant: WaveletExitVariant,
|
||||||
|
filterCurveId: string | null = null,
|
||||||
): WaveletTradeConfig {
|
): WaveletTradeConfig {
|
||||||
const effectiveCurveId =
|
const effectiveCurveId =
|
||||||
triggerKind === 'ridge_shift' ? 'ridge' :
|
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
|
// 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.
|
// a full standard deviation — the shared default K=1 starves it of trades.
|
||||||
if (triggerKind === 'ridge_shift') trigger.levelThresholdK = 0.5
|
if (triggerKind === 'ridge_shift') trigger.levelThresholdK = 0.5
|
||||||
|
const filters = trendConfirmationFilters(filterCurveId)
|
||||||
return {
|
return {
|
||||||
mode,
|
mode,
|
||||||
buyTrigger: trigger, sellTrigger: trigger, buyFilters: [], sellFilters: [],
|
buyTrigger: trigger, sellTrigger: trigger, buyFilters: filters.buy, sellFilters: filters.sell,
|
||||||
shortTrigger: trigger, coverTrigger: trigger, shortFilters: [], coverFilters: [],
|
shortTrigger: trigger, coverTrigger: trigger, shortFilters: filters.short, coverFilters: filters.cover,
|
||||||
exitStyle: variant.exitStyle, takeProfitPct: variant.takeProfitPct, stopLossPct: variant.stopLossPct,
|
exitStyle: variant.exitStyle, takeProfitPct: variant.takeProfitPct, stopLossPct: variant.stopLossPct,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -391,6 +411,7 @@ export function runOptimizationGrid(
|
|||||||
symbol: string, lookback: number, wavelet: string, rolling: any,
|
symbol: string, lookback: number, wavelet: string, rolling: any,
|
||||||
bandsToTest: number[], kindsToTest: WaveletTriggerKind[], modesToTest: WaveletPositionMode[],
|
bandsToTest: number[], kindsToTest: WaveletTriggerKind[], modesToTest: WaveletPositionMode[],
|
||||||
exitVariants: WaveletExitVariant[],
|
exitVariants: WaveletExitVariant[],
|
||||||
|
filterBandIndex: number | null = null,
|
||||||
): WaveletOptimizationResult[] {
|
): WaveletOptimizationResult[] {
|
||||||
const curves: WaveletCurve[] = rolling.bands.map((band: any) => ({ id: `band_${band.index}`, label: band.label, series: band.series }))
|
const curves: WaveletCurve[] = rolling.bands.map((band: any) => ({ id: `band_${band.index}`, label: band.label, series: band.series }))
|
||||||
if (rolling.ridge_period_days) {
|
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 })
|
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 availableBandIndices = new Set(rolling.bands.map((band: any) => band.index))
|
||||||
|
const filterCurveId = filterBandIndex != null && availableBandIndices.has(filterBandIndex) ? `band_${filterBandIndex}` : null
|
||||||
const results: WaveletOptimizationResult[] = []
|
const results: WaveletOptimizationResult[] = []
|
||||||
let ridgeShiftComputed = false
|
let ridgeShiftComputed = false
|
||||||
for (const bandIndex of bandsToTest) {
|
for (const bandIndex of bandsToTest) {
|
||||||
@@ -409,7 +431,7 @@ export function runOptimizationGrid(
|
|||||||
if (kind === 'ridge_shift' && ridgeShiftComputed) continue
|
if (kind === 'ridge_shift' && ridgeShiftComputed) continue
|
||||||
for (const mode of modesToTest) {
|
for (const mode of modesToTest) {
|
||||||
for (const variant of exitVariants) {
|
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)
|
const sim = runWaveletTradeSimulation(rolling.dates, rolling.original, curves, config)
|
||||||
results.push({
|
results.push({
|
||||||
symbol, lookback, wavelet, mode,
|
symbol, lookback, wavelet, mode,
|
||||||
@@ -438,6 +460,7 @@ export async function runFullOptimization(
|
|||||||
onProgress: (done: number, total: number, label: string, failures: string[]) => void,
|
onProgress: (done: number, total: number, label: string, failures: string[]) => void,
|
||||||
onBatchResults: (results: WaveletOptimizationResult[]) => void,
|
onBatchResults: (results: WaveletOptimizationResult[]) => void,
|
||||||
method: 'cwt' | 'ssq' = 'cwt',
|
method: 'cwt' | 'ssq' = 'cwt',
|
||||||
|
filterBandIndex: number | null = null,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const combos: { symbol: string; lookback: number; wavelet: string }[] = []
|
const combos: { symbol: string; lookback: number; wavelet: string }[] = []
|
||||||
for (const symbol of instruments) {
|
for (const symbol of instruments) {
|
||||||
@@ -455,7 +478,7 @@ export async function runFullOptimization(
|
|||||||
const { data: rolling } = await api.get('/wavelet/rolling', {
|
const { data: rolling } = await api.get('/wavelet/rolling', {
|
||||||
params: { symbol, period, levels, wavelet, lookback, step: 1, method },
|
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) {
|
} catch (error) {
|
||||||
failures.push(`${symbol} @ ${lookback}j (${wavelet}): ${error instanceof Error ? error.message : String(error)}`)
|
failures.push(`${symbol} @ ${lookback}j (${wavelet}): ${error instanceof Error ? error.message : String(error)}`)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ export default function WaveletsSimulation() {
|
|||||||
const [modes, setModes] = useState<Set<WaveletPositionMode>>(new Set(['both']))
|
const [modes, setModes] = useState<Set<WaveletPositionMode>>(new Set(['both']))
|
||||||
const [takeProfits, setTakeProfits] = useState<number[]>([0])
|
const [takeProfits, setTakeProfits] = useState<number[]>([0])
|
||||||
const [stopLosses, setStopLosses] = useState<number[]>([0])
|
const [stopLosses, setStopLosses] = useState<number[]>([0])
|
||||||
|
const [filterBand, setFilterBand] = useState<number | null>(null)
|
||||||
const [simName, setSimName] = useState(() => `Run ${new Date().toISOString().slice(0, 16).replace('T', ' ')}`)
|
const [simName, setSimName] = useState(() => `Run ${new Date().toISOString().slice(0, 16).replace('T', ' ')}`)
|
||||||
|
|
||||||
// ── Run state ────────────────────────────────────────────────────────────────
|
// ── Run state ────────────────────────────────────────────────────────────────
|
||||||
@@ -103,7 +104,7 @@ export default function WaveletsSimulation() {
|
|||||||
const form = {
|
const form = {
|
||||||
instruments: [...selectedInstruments], period, levels, wavelets: [...wavelets],
|
instruments: [...selectedInstruments], period, levels, wavelets: [...wavelets],
|
||||||
lookbacks, bands: [...bands], kinds: [...kinds], modes: [...modes],
|
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)
|
const created = await api.post('/wavelet/simulations', { name: simName, form, results: [] }).then(r => r.data)
|
||||||
setActiveSimId(created.id)
|
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 */ }
|
try { await api.patch(`/wavelet/simulations/${created.id}`, { append_results: batch }) } catch { /* non-blocking */ }
|
||||||
},
|
},
|
||||||
method,
|
method,
|
||||||
|
filterBand,
|
||||||
)
|
)
|
||||||
setRunning(false)
|
setRunning(false)
|
||||||
qc.invalidateQueries({ queryKey: ['wavelet-simulations'] })
|
qc.invalidateQueries({ queryKey: ['wavelet-simulations'] })
|
||||||
@@ -286,6 +288,22 @@ export default function WaveletsSimulation() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="text-slate-500 text-xs mb-1" title="Un retournement (extremum, seuil...) d'une bande à tester ne se déclenche que si cette bande secondaire confirme la même direction (pente positive pour un achat/cover, négative pour une vente/short) — ex. creux de bande 1 achète seulement si bande 3 monte.">
|
||||||
|
Filtre de tendance (bande de confirmation)
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2 flex-wrap text-xs">
|
||||||
|
<label className="flex items-center gap-1 cursor-pointer">
|
||||||
|
<input type="radio" checked={filterBand === null} onChange={() => setFilterBand(null)} /> Aucun
|
||||||
|
</label>
|
||||||
|
{ALL_BANDS.slice(0, levels).map(b => (
|
||||||
|
<label key={b} className="flex items-center gap-1 cursor-pointer">
|
||||||
|
<input type="radio" checked={filterBand === b} onChange={() => setFilterBand(b)} /> Bande {b}
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div className="text-slate-500 text-xs mb-1">Types de déclencheur</div>
|
<div className="text-slate-500 text-xs mb-1">Types de déclencheur</div>
|
||||||
<div className="flex gap-3 flex-wrap text-xs">
|
<div className="flex gap-3 flex-wrap text-xs">
|
||||||
|
|||||||
Reference in New Issue
Block a user