feat: wavelets
This commit is contained in:
@@ -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<void> {
|
||||
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)}`)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user