feat: Short-Seite — Indikator, Strategie, Chandelier, Sizing, Portfolio
- donchianLow: Spiegelbild von donchianHigh (tiefstes Tief der N Candles vor i) - evaluateAt(allowShort=false): Short-Signal wenn close < donchianLow && close < trendEma - IndicatorSet + Evaluation erweitert um donchianLow; signal: 'long'|'short'|null - updateChandelierShort: ll + mult×ATR, wandert nur abwärts - sizePosition(side): Short ignoriert Cash-Cap, stopDist = stop − entry - Portfolio: highestHigh → trailExtreme; open/close/equity für Shorts; side auf Position + ClosedTrade Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { expect, test } from 'bun:test';
|
||||
import { updateChandelier } from './chandelier';
|
||||
import { updateChandelier, updateChandelierShort } from './chandelier';
|
||||
|
||||
test('Stop steigt mit neuem Hoch', () => {
|
||||
// hh 100 → 110, ATR 2, Mult 3 → Stop 110−6 = 104
|
||||
@@ -27,3 +27,32 @@ test('Stop ratchtet hoch wenn ATR schrumpft, auch ohne neues Hoch', () => {
|
||||
expect(r.highestHigh).toBe(110);
|
||||
expect(r.stop).toBe(107);
|
||||
});
|
||||
|
||||
// --- Short-Chandelier-Tests (Spiegelbild) ---
|
||||
|
||||
test('Short-Stop fällt mit neuem Tief', () => {
|
||||
// ll 100 → 90, ATR 2, Mult 3 → Stop 90+6 = 96
|
||||
const r = updateChandelierShort({ lowestLow: 100, stop: 106 }, 90, 2, 3);
|
||||
expect(r.lowestLow).toBe(90);
|
||||
expect(r.stop).toBe(96);
|
||||
});
|
||||
|
||||
test('Short-Stop steigt NIE — auch wenn ATR explodiert', () => {
|
||||
// ll bleibt 90, ATR springt auf 10 → Kandidat 90+30=120, aber Stop bleibt 96
|
||||
const r = updateChandelierShort({ lowestLow: 90, stop: 96 }, 95, 10, 3);
|
||||
expect(r.lowestLow).toBe(90);
|
||||
expect(r.stop).toBe(96);
|
||||
});
|
||||
|
||||
test('Short NaN-ATR lässt Stop unverändert', () => {
|
||||
const r = updateChandelierShort({ lowestLow: 90, stop: 96 }, 80, NaN, 3);
|
||||
expect(r.lowestLow).toBe(80);
|
||||
expect(r.stop).toBe(96);
|
||||
});
|
||||
|
||||
test('Short-Stop ratchtet runter wenn ATR schrumpft, auch ohne neues Tief', () => {
|
||||
// ll bleibt 90, ATR schrumpft von 3 auf 1 → Stop 90+3 = 93 < vorher 96
|
||||
const r = updateChandelierShort({ lowestLow: 90, stop: 96 }, 95, 1, 3);
|
||||
expect(r.lowestLow).toBe(90);
|
||||
expect(r.stop).toBe(93);
|
||||
});
|
||||
|
||||
@@ -9,3 +9,15 @@ export function updateChandelier(state: TrailState, barHigh: number, atrValue: n
|
||||
const candidate = Number.isNaN(atrValue) ? -Infinity : highestHigh - mult * atrValue;
|
||||
return { highestHigh, stop: Math.max(state.stop, candidate) };
|
||||
}
|
||||
|
||||
export interface ShortTrailState {
|
||||
lowestLow: number;
|
||||
stop: number;
|
||||
}
|
||||
|
||||
/** Spiegelbild für Shorts: ll + mult×ATR, wandert nur abwärts. */
|
||||
export function updateChandelierShort(state: ShortTrailState, barLow: number, atrValue: number, mult: number): ShortTrailState {
|
||||
const lowestLow = Math.min(state.lowestLow, barLow);
|
||||
const candidate = Number.isNaN(atrValue) ? Infinity : lowestLow + mult * atrValue;
|
||||
return { lowestLow, stop: Math.min(state.stop, candidate) };
|
||||
}
|
||||
|
||||
@@ -60,3 +60,71 @@ test('blockiert bei zu wenig Daten', () => {
|
||||
test('DEFAULT_PARAMS entsprechen der Spec', () => {
|
||||
expect(DEFAULT_PARAMS).toEqual({ donchianPeriod: 20, atrPeriod: 14, atrMultiplier: 3, trendEmaPeriod: 200 });
|
||||
});
|
||||
|
||||
/** Abwärtstrend, letzte Candle bricht unter das 3er-Tief aus. */
|
||||
function breakdownSeries(): Candle[] {
|
||||
const s: Candle[] = [];
|
||||
// Fallende Reihe: Starts bei 20, fällt um je 1 pro Candle
|
||||
for (let i = 0; i < 7; i++) s.push(c(20 - i, 21 - i, 19 - i, 19.5 - i, i));
|
||||
// bisheriges 3er-Tief: min(low[4..6]) = min(15,14,13) = 13 → Close 11 bricht aus, weit unter EMA5
|
||||
s.push(c(13, 14, 10.5, 11, 7));
|
||||
return s;
|
||||
}
|
||||
|
||||
test('Short-Signal bei Donchian-Breakdown unter Trend-EMA (allowShort=true)', () => {
|
||||
const c4h = breakdownSeries();
|
||||
const ev = evaluateAt(c4h, computeIndicators(c4h, P), c4h.length - 1, true);
|
||||
expect(ev.signal).toBe('short');
|
||||
expect(ev.blockedBy).toBeNull();
|
||||
expect(ev.donchianLow).toBe(13); // min(low[4..6]) = min(15,14,13) = 13
|
||||
});
|
||||
|
||||
test('Gleiche Daten mit allowShort=false → kein Short, blockiert', () => {
|
||||
const c4h = breakdownSeries();
|
||||
const ev = evaluateAt(c4h, computeIndicators(c4h, P), c4h.length - 1, false);
|
||||
expect(ev.signal).toBeNull();
|
||||
// close=11 <= donchianHigh (whatever it is) → below_donchian
|
||||
expect(ev.blockedBy).toBe('below_donchian');
|
||||
});
|
||||
|
||||
test('Breakdown aber Close > EMA → kein Short (Aufwärtstrend-Filter)', () => {
|
||||
// Struktur: hohe EMA durch früheren Bullenmarkt, dann kurzer Pullback
|
||||
const s: Candle[] = [];
|
||||
// Hohe Ausgangswerte damit EMA5 hoch bleibt
|
||||
s.push(c(200, 201, 199, 200, 0));
|
||||
s.push(c(200, 201, 199, 200, 1));
|
||||
s.push(c(200, 201, 199, 200, 2));
|
||||
s.push(c(200, 201, 199, 200, 3));
|
||||
s.push(c(200, 201, 199, 200, 4));
|
||||
s.push(c(200, 201, 199, 200, 5));
|
||||
s.push(c(200, 201, 199, 200, 6));
|
||||
// Index 7: donchianLow[7] = min(low[4..6]) = min(199,199,199) = 199
|
||||
// Close 198 < donchianLow(199), aber EMA5 ≈ 200 → Close < donchianLow aber > ... wait
|
||||
// Wir brauchen Close < donchianLow AND Close > trendEma — unmöglich da donchianLow <= ema hier
|
||||
// Stattdessen: close ist ZWISCHEN donchianLow und trendEma → kein Short weil close >= donchianLow
|
||||
// Einfacherer Fall: close < donchianLow aber close > trendEma
|
||||
// Erzwingen: trendEma sehr niedrig (kurze Serie), aber donchianLow noch höher als close
|
||||
// Nutze P.trendEmaPeriod=5: nach 7 Candles @ 200 ist EMA5 ≈ 200
|
||||
// Close 150 < donchianLow(199), aber EMA5 ≈ 200 > Close → das wäre short-Signal (close < ema)
|
||||
// Für "close OVER ema" → no short: wir brauchen close < donchianLow UND close > trendEma
|
||||
// Das geht mit schnell fallender EMA: viele niedrige Werte, dann ein einzelner Ausreißer unten
|
||||
// Einfacher: nutze Initialwerte die EMA unter donchianLow drücken
|
||||
// Serie: low values für EMA, dann hohe Lows für donchian (damit donchianLow hoch ist)
|
||||
const t: Candle[] = [];
|
||||
// 4 Candles bei 10 → EMA5 startet warm bei 10
|
||||
t.push(c(10, 11, 9, 10, 0));
|
||||
t.push(c(10, 11, 9, 10, 1));
|
||||
t.push(c(10, 11, 9, 10, 2));
|
||||
// 3 Candles bei 50 → EMA5 steigt auf ~27, Highs/Lows bei 51/49
|
||||
t.push(c(50, 51, 49, 50, 3));
|
||||
t.push(c(50, 51, 49, 50, 4));
|
||||
t.push(c(50, 51, 49, 50, 5));
|
||||
// 1 Candle: donchianLow[6] = min(low[3..5]) = 49, EMA5 ≈ 37 < 49
|
||||
// Close 45: < donchianLow(49) aber > EMA5(≈37) → kein Short
|
||||
t.push(c(45, 50, 44, 45, 6));
|
||||
const ev = evaluateAt(t, computeIndicators(t, P), t.length - 1, true);
|
||||
// close(45) < donchianLow(49) aber close(45) > trendEma(≈37) → kein Short
|
||||
expect(ev.signal).toBeNull();
|
||||
expect(ev.donchianLow).toBe(49);
|
||||
expect(ev.trendEma).toBeLessThan(45); // EMA ist unter Close
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Candle } from '../types';
|
||||
import { ema } from '../indicators/ema';
|
||||
import { atr } from '../indicators/atr';
|
||||
import { donchianHigh } from '../indicators/donchian';
|
||||
import { donchianHigh, donchianLow } from '../indicators/donchian';
|
||||
|
||||
export interface StrategyParams {
|
||||
donchianPeriod: number;
|
||||
@@ -20,15 +20,17 @@ export const DEFAULT_PARAMS: StrategyParams = {
|
||||
export interface IndicatorSet {
|
||||
trendEma: number[];
|
||||
donchianHigh: number[];
|
||||
donchianLow: number[];
|
||||
atr: number[];
|
||||
}
|
||||
|
||||
export interface Evaluation {
|
||||
signal: 'long' | null;
|
||||
signal: 'long' | 'short' | null;
|
||||
blockedBy: 'insufficient_data' | 'below_donchian' | 'below_trend_ema' | null;
|
||||
close: number;
|
||||
atr: number;
|
||||
donchianHigh: number;
|
||||
donchianLow: number;
|
||||
trendEma: number;
|
||||
}
|
||||
|
||||
@@ -37,24 +39,35 @@ export function computeIndicators(c4h: Candle[], p: StrategyParams): IndicatorSe
|
||||
return {
|
||||
trendEma: ema(c4h.map((c) => c.close), p.trendEmaPeriod),
|
||||
donchianHigh: donchianHigh(c4h, p.donchianPeriod),
|
||||
donchianLow: donchianLow(c4h, p.donchianPeriod),
|
||||
atr: atr(c4h, p.atrPeriod),
|
||||
};
|
||||
}
|
||||
|
||||
/** Bewertet die (abgeschlossene) 4h-Candle an Index i. */
|
||||
export function evaluateAt(c4h: Candle[], ind: IndicatorSet, i: number): Evaluation {
|
||||
export function evaluateAt(c4h: Candle[], ind: IndicatorSet, i: number, allowShort = false): Evaluation {
|
||||
const close = c4h[i]?.close ?? NaN;
|
||||
const base = { close, atr: ind.atr[i], donchianHigh: ind.donchianHigh[i], trendEma: ind.trendEma[i] };
|
||||
const base = {
|
||||
close, atr: ind.atr[i], donchianHigh: ind.donchianHigh[i],
|
||||
donchianLow: ind.donchianLow[i], trendEma: ind.trendEma[i],
|
||||
};
|
||||
if (
|
||||
i < 0 ||
|
||||
i >= c4h.length ||
|
||||
Number.isNaN(ind.trendEma[i]) ||
|
||||
Number.isNaN(ind.donchianHigh[i]) ||
|
||||
Number.isNaN(ind.donchianLow[i]) ||
|
||||
Number.isNaN(ind.atr[i])
|
||||
) {
|
||||
return { signal: null, blockedBy: 'insufficient_data', ...base };
|
||||
}
|
||||
if (close > ind.donchianHigh[i] && close > ind.trendEma[i]) {
|
||||
return { signal: 'long', blockedBy: null, ...base };
|
||||
}
|
||||
if (allowShort && close < ind.donchianLow[i] && close < ind.trendEma[i]) {
|
||||
return { signal: 'short', blockedBy: null, ...base };
|
||||
}
|
||||
// Blocked reasons from the long perspective
|
||||
if (close <= ind.donchianHigh[i]) return { signal: null, blockedBy: 'below_donchian', ...base };
|
||||
if (close <= ind.trendEma[i]) return { signal: null, blockedBy: 'below_trend_ema', ...base };
|
||||
return { signal: 'long', blockedBy: null, ...base };
|
||||
return { signal: null, blockedBy: 'below_trend_ema', ...base };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user