feat: Short-Seite im Runner + Walk-Forward-CLI (--shorts Flag)
- BacktestConfig.allowShort: boolean (required, explizit) - Runner: Short-Entry (stop = close + mult×ATR), updateChandelierShort für Trail, Stop-Check auf High >= stop, Gap-Fill nach oben - Bestehende Runner-Tests: allowShort: false ergänzt (Verhalten byte-identisch) - Neuer E2E-Test: Short-Breakout → trailing_stop; Long-Only-Sanity-Check - walkforward.ts script: --shorts Flag, Ausgabe "Shorts: AN/AUS" - walkforward.test.ts: allowShort: false ergänzt Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -41,7 +41,7 @@ test('Breakout → Entry auf 4h-Close, Crash → Stop-Exit auf 15m', () => {
|
||||
const data = new Map<Pair, Candle[]>([['BTC_USDT', series()]]);
|
||||
const result = runBacktest(data, {
|
||||
startCapital: 1000, risk: DEFAULT_RISK, exec: DEFAULT_EXEC, maxPositions: 4,
|
||||
params: P, tradeFrom: 0, tradeTo: Number.MAX_SAFE_INTEGER,
|
||||
params: P, tradeFrom: 0, tradeTo: Number.MAX_SAFE_INTEGER, allowShort: false,
|
||||
});
|
||||
expect(result.trades).toHaveLength(1);
|
||||
const t = result.trades[0];
|
||||
@@ -57,7 +57,7 @@ test('tradeFrom verhindert Entries im Warmup-Fenster', () => {
|
||||
const data = new Map<Pair, Candle[]>([['BTC_USDT', series()]]);
|
||||
const result = runBacktest(data, {
|
||||
startCapital: 1000, risk: DEFAULT_RISK, exec: DEFAULT_EXEC, maxPositions: 4,
|
||||
params: P, tradeFrom: 100 * H4, tradeTo: Number.MAX_SAFE_INTEGER, // nach Serien-Ende
|
||||
params: P, tradeFrom: 100 * H4, tradeTo: Number.MAX_SAFE_INTEGER, allowShort: false, // nach Serien-Ende
|
||||
});
|
||||
expect(result.trades).toHaveLength(0);
|
||||
});
|
||||
@@ -73,7 +73,7 @@ test('Stop-Order ist ab Entry aktiv: Low der Entry-Candle unter Stop → soforti
|
||||
const data = new Map<Pair, Candle[]>([['BTC_USDT', s]]);
|
||||
const result = runBacktest(data, {
|
||||
startCapital: 1000, risk: DEFAULT_RISK, exec: DEFAULT_EXEC, maxPositions: 4,
|
||||
params: P, tradeFrom: 0, tradeTo: Number.MAX_SAFE_INTEGER,
|
||||
params: P, tradeFrom: 0, tradeTo: Number.MAX_SAFE_INTEGER, allowShort: false,
|
||||
});
|
||||
expect(result.trades).toHaveLength(1);
|
||||
expect(result.trades[0].exitReason).toBe('trailing_stop');
|
||||
@@ -101,7 +101,7 @@ test('maxPositions: bei gleichzeitigen Signalen gewinnt die PAIRS-Reihenfolge',
|
||||
]);
|
||||
const result = runBacktest(data, {
|
||||
startCapital: 1000, risk: DEFAULT_RISK, exec: DEFAULT_EXEC, maxPositions: 1,
|
||||
params: P, tradeFrom: 0, tradeTo: Number.MAX_SAFE_INTEGER,
|
||||
params: P, tradeFrom: 0, tradeTo: Number.MAX_SAFE_INTEGER, allowShort: false,
|
||||
});
|
||||
// beide Pairs haben identische Serien → beide signalisieren; nur BTC (erster in PAIRS) darf
|
||||
expect(result.trades).toHaveLength(1);
|
||||
@@ -112,7 +112,61 @@ test('Determinismus: identischer Input → identisches Ergebnis', () => {
|
||||
const data = new Map<Pair, Candle[]>([['BTC_USDT', series()]]);
|
||||
const cfg = {
|
||||
startCapital: 1000, risk: DEFAULT_RISK, exec: DEFAULT_EXEC, maxPositions: 4,
|
||||
params: P, tradeFrom: 0, tradeTo: Number.MAX_SAFE_INTEGER,
|
||||
params: P, tradeFrom: 0, tradeTo: Number.MAX_SAFE_INTEGER, allowShort: false,
|
||||
};
|
||||
expect(JSON.stringify(runBacktest(data, cfg))).toBe(JSON.stringify(runBacktest(data, cfg)));
|
||||
});
|
||||
|
||||
/**
|
||||
* Synthetische Short-Serie:
|
||||
* - 7 Plateau-Buckets @ 100 (lows=100, highs=100): donchianLow=100, EMA5=100
|
||||
* → close(100) NICHT < donchianLow(100) → kein Short-Signal auf dem Plateau
|
||||
* - Breakdown-Bucket: close=85 < donchianLow(100) UND < EMA5(95) → Short-Signal
|
||||
* - Rallye-Bucket: High 130 reißt den Short-Stop (≈85 + 1×ATR)
|
||||
* - tradeTo = Breakdown-barCloseTs + H4: blockiert Long-Entry nach dem Short-Exit
|
||||
*/
|
||||
function shortSeries(): { candles: Candle[]; breakdownBarCloseTs: number } {
|
||||
const s: Candle[] = [];
|
||||
let b = 0;
|
||||
// 7 Plateau-Buckets mit exakt flachen Candles (low=100=close=high)
|
||||
for (let i = 0; i < 7; i++, b += H4) s.push(...flat4h(b, 100, 100, 100, 100));
|
||||
const breakdownBucketStart = b;
|
||||
// Breakdown: close=85 < donchianLow(100) AND < EMA5(~95)
|
||||
s.push(...flat4h(b, 100, 100, 84, 85)); b += H4;
|
||||
// Rallye: High 130 reißt Short-Stop (Stop ≈ 85+ATR)
|
||||
s.push(...flat4h(b, 85, 130, 85, 120)); b += H4;
|
||||
// Abschluss-Bucket (damit Rallye als abgeschlossen gilt)
|
||||
s.push(...flat4h(b, 120, 121, 119, 120)); b += H4;
|
||||
// tradeTo: Entry-Zeitpunkt des Breakdown = breakdownBucketStart + H4
|
||||
// +H4 dahinter blockiert die Rallye von einem Long-Entry
|
||||
return { candles: s, breakdownBarCloseTs: breakdownBucketStart + H4 };
|
||||
}
|
||||
|
||||
test('Short-Breakout → Short-Entry auf 4h-Close, Rallye → Stop-Exit auf 15m', () => {
|
||||
const { candles, breakdownBarCloseTs } = shortSeries();
|
||||
// tradeTo = breakdownBarCloseTs + H4: Short-Entry (barCloseTs=breakdownBarCloseTs) ist erlaubt
|
||||
// (breakdownBarCloseTs < tradeTo), aber der nächste Bar (barCloseTs=breakdownBarCloseTs+H4=tradeTo)
|
||||
// ist blockiert → kein Long-Entry nach dem Short-Exit
|
||||
const tradeTo = breakdownBarCloseTs + H4;
|
||||
const data = new Map<Pair, Candle[]>([['BTC_USDT', candles]]);
|
||||
const result = runBacktest(data, {
|
||||
startCapital: 1000, risk: DEFAULT_RISK, exec: DEFAULT_EXEC, maxPositions: 4,
|
||||
params: P, tradeFrom: 0, tradeTo, allowShort: true,
|
||||
});
|
||||
expect(result.trades).toHaveLength(1);
|
||||
const t = result.trades[0];
|
||||
// Entry: Short bei Close 85, Fill = 85*(1−slippage) = 85*0.9995
|
||||
expect(t.entryPrice).toBeCloseTo(85 * 0.9995);
|
||||
expect(t.exitReason).toBe('trailing_stop');
|
||||
expect(t.side).toBe('short');
|
||||
// Verlustbringender Short (Preis stieg stark) → pnl < 0
|
||||
expect(t.pnl).toBeLessThan(0);
|
||||
|
||||
// Sanity: gleiche Daten mit allowShort=false → kein Trade
|
||||
// (kein Long-Signal: nach Plateau breakout close=85 ist weit unter EMA → blocked)
|
||||
const resultLongOnly = runBacktest(data, {
|
||||
startCapital: 1000, risk: DEFAULT_RISK, exec: DEFAULT_EXEC, maxPositions: 4,
|
||||
params: P, tradeFrom: 0, tradeTo, allowShort: false,
|
||||
});
|
||||
expect(resultLongOnly.trades).toHaveLength(0);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user