feat: Grid-Timeframe parametrisierbar (--tf) — kürzere TFs monoton schlechter
aggregateTf verallgemeinert aggregate4h. Walk-Forward 1h/15m: PF 0.59/0.29 (vs 0.87 auf 4h), 15m MaxDD 97% — Fee-Mathematik bestätigt. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,19 @@
|
|||||||
| B: spacing 1.5×ATR | 1.03 | 514 | 56.0 % | 10.6 % | 1.53 | ❌ PF + Fenster |
|
| B: spacing 1.5×ATR | 1.03 | 514 | 56.0 % | 10.6 % | 1.53 | ❌ PF + Fenster |
|
||||||
| C: ADX<15 (strenger) | 0.94 | 425 | 63.3 % | 6.5 % | 1.24 | ❌ PF + Fenster |
|
| C: ADX<15 (strenger) | 0.94 | 425 | 63.3 % | 6.5 % | 1.24 | ❌ PF + Fenster |
|
||||||
|
|
||||||
|
## Nachtrag: kürzere Timeframes (User-Frage „wird das auf kürzeren Einheiten besser?")
|
||||||
|
|
||||||
|
Entscheidungs-Timeframe parametrisiert (`--tf` Minuten; ATR/ADX/Aktivierung auf 1h bzw. 15m statt 4h):
|
||||||
|
|
||||||
|
| Variante | OOS-PF | Trades | WinRate | MaxDD | Gate |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| D: tf 1h, spacing 1.0×ATR | 0.59 | 4410 | 63.9 % | 57.1 % | ❌ |
|
||||||
|
| E: tf 1h, spacing 1.5×ATR | 0.64 | 2246 | 54.3 % | 37.6 % | ❌ |
|
||||||
|
| F: tf 15m, spacing 1.0×ATR | 0.29 | 19 783 | 47.1 % | **97.2 %** | ❌ |
|
||||||
|
| G: tf 15m, spacing 2.0×ATR | 0.47 | 6259 | 44.7 % | 67.8 % | ❌ |
|
||||||
|
|
||||||
|
**Monoton schlechter, je kürzer der Timeframe** — exakt die Fee-Mathematik: ATR(1h) ≈ ⅓, ATR(15m) ≈ ⅙ von ATR(4h) → das Spacing schrumpft auf die Größenordnung der 0.3 % Round-Trip-Kosten, jeder TP verdient fast nichts, die Breakdown-Verluste bleiben gleich groß. tf 15m ist Totalverlust (MaxDD 97 %). Das war auch der Todesmechanismus des krypto-kuns-v1-Bots (1–15-min-Signale).
|
||||||
|
|
||||||
## Befund
|
## Befund
|
||||||
|
|
||||||
Klassische Grid-Pathologie, durch Regime-Filter abgemildert, aber nicht behoben:
|
Klassische Grid-Pathologie, durch Regime-Filter abgemildert, aber nicht behoben:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { Candle, Pair } from '../types';
|
import type { Candle, Pair } from '../types';
|
||||||
import { PAIRS } from '../types';
|
import { PAIRS } from '../types';
|
||||||
import { aggregate4h, H4 } from '../market/aggregate';
|
import { aggregateTf, H4 } from '../market/aggregate';
|
||||||
import { atr } from '../indicators/atr';
|
import { atr } from '../indicators/atr';
|
||||||
import { adx } from '../indicators/adx';
|
import { adx } from '../indicators/adx';
|
||||||
import type { ClosedTrade, ExecConfig } from '../engine/portfolio';
|
import type { ClosedTrade, ExecConfig } from '../engine/portfolio';
|
||||||
@@ -8,10 +8,11 @@ import type { EquityPoint } from './metrics';
|
|||||||
import type { BacktestResult } from './runner';
|
import type { BacktestResult } from './runner';
|
||||||
|
|
||||||
export interface GridParams {
|
export interface GridParams {
|
||||||
spacingAtrMult: number; // Level-Abstand = mult × ATR(14, 4h) bei Aktivierung
|
spacingAtrMult: number; // Level-Abstand = mult × ATR(atrPeriod, tf) bei Aktivierung
|
||||||
gridLevels: number; // Buy-Levels unterhalb des Centers
|
gridLevels: number; // Buy-Levels unterhalb des Centers
|
||||||
adxMax: number; // Grid nur aktiv im Seitwärtsregime (ADX < adxMax)
|
adxMax: number; // Grid nur aktiv im Seitwärtsregime (ADX < adxMax)
|
||||||
atrPeriod: number;
|
atrPeriod: number;
|
||||||
|
tfMs: number; // Entscheidungs-Timeframe (Aktivierung/Deaktivierung, ATR/ADX-Basis)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_GRID_PARAMS: GridParams = {
|
export const DEFAULT_GRID_PARAMS: GridParams = {
|
||||||
@@ -19,6 +20,7 @@ export const DEFAULT_GRID_PARAMS: GridParams = {
|
|||||||
gridLevels: 4,
|
gridLevels: 4,
|
||||||
adxMax: 20,
|
adxMax: 20,
|
||||||
atrPeriod: 14,
|
atrPeriod: 14,
|
||||||
|
tfMs: H4,
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface GridConfig {
|
export interface GridConfig {
|
||||||
@@ -50,8 +52,8 @@ interface ActiveGrid {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* ATR-Grid mit ADX-Regime-Filter, long-only, je Pair unabhängig.
|
* ATR-Grid mit ADX-Regime-Filter, long-only, je Pair unabhängig.
|
||||||
* 4h-Close: Aktivierung/Deaktivierung; 15m: Fills (Sells vor Buys —
|
* Tf-Close (default 4h): Aktivierung/Deaktivierung; 15m: Fills (Sells vor
|
||||||
* ein im selben Bar gekaufter Lot kann nicht im selben Bar verkaufen).
|
* Buys — ein im selben Bar gekaufter Lot kann nicht im selben Bar verkaufen).
|
||||||
* Fee/Slippage-Mathematik identisch zu Portfolio (pessimistische Fills).
|
* Fee/Slippage-Mathematik identisch zu Portfolio (pessimistische Fills).
|
||||||
*/
|
*/
|
||||||
export function runGridBacktest(candles15ByPair: Map<Pair, Candle[]>, cfg: GridConfig): BacktestResult {
|
export function runGridBacktest(candles15ByPair: Map<Pair, Candle[]>, cfg: GridConfig): BacktestResult {
|
||||||
@@ -106,7 +108,7 @@ export function runGridBacktest(candles15ByPair: Map<Pair, Candle[]>, cfg: GridC
|
|||||||
// --- Kontexte + gemergte 15m-Timeline (wie runner.ts) ---
|
// --- Kontexte + gemergte 15m-Timeline (wie runner.ts) ---
|
||||||
const contexts = PAIRS.filter((pr) => candles15ByPair.has(pr)).map((pair) => {
|
const contexts = PAIRS.filter((pr) => candles15ByPair.has(pr)).map((pair) => {
|
||||||
const c15 = candles15ByPair.get(pair)!;
|
const c15 = candles15ByPair.get(pair)!;
|
||||||
const c4h = aggregate4h(c15);
|
const c4h = aggregateTf(c15, p.tfMs);
|
||||||
return { pair, c4h, atr: atr(c4h, p.atrPeriod), adx: adx(c4h, p.atrPeriod), next4h: 0 };
|
return { pair, c4h, atr: atr(c4h, p.atrPeriod), adx: adx(c4h, p.atrPeriod), next4h: 0 };
|
||||||
});
|
});
|
||||||
const byPair = new Map(contexts.map((c) => [c.pair, c]));
|
const byPair = new Map(contexts.map((c) => [c.pair, c]));
|
||||||
@@ -123,13 +125,13 @@ export function runGridBacktest(candles15ByPair: Map<Pair, Candle[]>, cfg: GridC
|
|||||||
|
|
||||||
for (const { ts, pair, candle } of timeline) {
|
for (const { ts, pair, candle } of timeline) {
|
||||||
const ctx = byPair.get(pair)!;
|
const ctx = byPair.get(pair)!;
|
||||||
const bucket = Math.floor(ts / H4) * H4;
|
const bucket = Math.floor(ts / p.tfMs) * p.tfMs;
|
||||||
|
|
||||||
// 1) Neu abgeschlossene 4h-Bars: Deaktivierung / Aktivierung
|
// 1) Neu abgeschlossene Tf-Bars: Deaktivierung / Aktivierung
|
||||||
while (ctx.next4h < ctx.c4h.length && ctx.c4h[ctx.next4h].ts < bucket) {
|
while (ctx.next4h < ctx.c4h.length && ctx.c4h[ctx.next4h].ts < bucket) {
|
||||||
const i = ctx.next4h++;
|
const i = ctx.next4h++;
|
||||||
const bar = ctx.c4h[i];
|
const bar = ctx.c4h[i];
|
||||||
const barCloseTs = bar.ts + H4;
|
const barCloseTs = bar.ts + p.tfMs;
|
||||||
if (barCloseTs < cfg.tradeFrom || barCloseTs >= cfg.tradeTo) continue;
|
if (barCloseTs < cfg.tradeFrom || barCloseTs >= cfg.tradeTo) continue;
|
||||||
|
|
||||||
const g = grids.get(pair);
|
const g = grids.get(pair);
|
||||||
|
|||||||
@@ -3,15 +3,15 @@ import type { Candle } from '../types';
|
|||||||
export const H4 = 4 * 60 * 60 * 1000;
|
export const H4 = 4 * 60 * 60 * 1000;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Aggregiert 15m-Candles (sortiert, ts aufsteigend) zu 4h-Candles.
|
* Aggregiert 15m-Candles (sortiert, ts aufsteigend) zu Buckets von `tfMs`.
|
||||||
* Der letzte Bucket wird verworfen, weil nicht feststellbar ist, ob er
|
* Der letzte Bucket wird verworfen, weil nicht feststellbar ist, ob er
|
||||||
* abgeschlossen ist — der Backtest-Runner arbeitet nur auf geschlossenen Candles.
|
* abgeschlossen ist — Backtests arbeiten nur auf geschlossenen Candles.
|
||||||
*/
|
*/
|
||||||
export function aggregate4h(c15: Candle[]): Candle[] {
|
export function aggregateTf(c15: Candle[], tfMs: number): Candle[] {
|
||||||
const out: Candle[] = [];
|
const out: Candle[] = [];
|
||||||
let cur: Candle | null = null;
|
let cur: Candle | null = null;
|
||||||
for (const c of c15) {
|
for (const c of c15) {
|
||||||
const bucket = Math.floor(c.ts / H4) * H4;
|
const bucket = Math.floor(c.ts / tfMs) * tfMs;
|
||||||
if (cur && cur.ts === bucket) {
|
if (cur && cur.ts === bucket) {
|
||||||
cur.high = Math.max(cur.high, c.high);
|
cur.high = Math.max(cur.high, c.high);
|
||||||
cur.low = Math.min(cur.low, c.low);
|
cur.low = Math.min(cur.low, c.low);
|
||||||
@@ -24,3 +24,9 @@ export function aggregate4h(c15: Candle[]): Candle[] {
|
|||||||
}
|
}
|
||||||
return out; // letzter Bucket absichtlich nicht gepusht
|
return out; // letzter Bucket absichtlich nicht gepusht
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Aggregiert 15m-Candles zu 4h-Candles. */
|
||||||
|
export function aggregate4h(c15: Candle[]): Candle[] {
|
||||||
|
return aggregateTf(c15, H4);
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,10 +14,11 @@ function argNum(flag: string, def: number): number {
|
|||||||
return i >= 0 ? Number(process.argv[i + 1]) : def;
|
return i >= 0 ? Number(process.argv[i + 1]) : def;
|
||||||
}
|
}
|
||||||
const PARAMS = {
|
const PARAMS = {
|
||||||
...DEFAULT_GRID_PARAMS, // spacing 1×ATR, 4 Levels, ADX < 20
|
...DEFAULT_GRID_PARAMS, // spacing 1×ATR, 4 Levels, ADX < 20, tf 4h
|
||||||
spacingAtrMult: argNum('--spacing', DEFAULT_GRID_PARAMS.spacingAtrMult),
|
spacingAtrMult: argNum('--spacing', DEFAULT_GRID_PARAMS.spacingAtrMult),
|
||||||
gridLevels: argNum('--levels', DEFAULT_GRID_PARAMS.gridLevels),
|
gridLevels: argNum('--levels', DEFAULT_GRID_PARAMS.gridLevels),
|
||||||
adxMax: argNum('--adx', DEFAULT_GRID_PARAMS.adxMax),
|
adxMax: argNum('--adx', DEFAULT_GRID_PARAMS.adxMax),
|
||||||
|
tfMs: argNum('--tf', DEFAULT_GRID_PARAMS.tfMs / 60000) * 60000, // Minuten
|
||||||
};
|
};
|
||||||
const START_CAPITAL = 1000;
|
const START_CAPITAL = 1000;
|
||||||
const EXEC = DEFAULT_EXEC;
|
const EXEC = DEFAULT_EXEC;
|
||||||
@@ -36,7 +37,7 @@ for (const pair of PAIRS) {
|
|||||||
console.log(`${pair}: ${cov.count} Candles (${cov.from.toISOString()} → ${cov.to.toISOString()})`);
|
console.log(`${pair}: ${cov.count} Candles (${cov.from.toISOString()} → ${cov.to.toISOString()})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`\nATR-Grid (fix: spacing ${PARAMS.spacingAtrMult}×ATR, ${PARAMS.gridLevels} Levels, ADX < ${PARAMS.adxMax}, long-only)`);
|
console.log(`\nATR-Grid (fix: spacing ${PARAMS.spacingAtrMult}×ATR, ${PARAMS.gridLevels} Levels, ADX < ${PARAMS.adxMax}, tf ${PARAMS.tfMs / 60000}m, long-only)`);
|
||||||
console.log(`Walk-Forward über ${((dataTo - dataFrom) / 86400000).toFixed(0)} Tage…\n`);
|
console.log(`Walk-Forward über ${((dataTo - dataFrom) / 86400000).toFixed(0)} Tage…\n`);
|
||||||
|
|
||||||
const windows = buildWindows(dataFrom, dataTo);
|
const windows = buildWindows(dataFrom, dataTo);
|
||||||
|
|||||||
Reference in New Issue
Block a user