feat: Donchian-Trend-Strategie (Entry-Evaluation)
This commit is contained in:
62
src/server/strategy/donchian-trend.test.ts
Normal file
62
src/server/strategy/donchian-trend.test.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { expect, test } from 'bun:test';
|
||||
import type { Candle } from '../types';
|
||||
import { computeIndicators, evaluateAt, DEFAULT_PARAMS, type StrategyParams } from './donchian-trend';
|
||||
|
||||
const P: StrategyParams = { donchianPeriod: 3, atrPeriod: 3, atrMultiplier: 3, trendEmaPeriod: 5 };
|
||||
|
||||
function c(o: number, h: number, l: number, cl: number, ts = 0): Candle {
|
||||
return { ts, open: o, high: h, low: l, close: cl, volume: 1 };
|
||||
}
|
||||
|
||||
/** Aufwärtstrend, letzte Candle bricht über das 3er-Hoch aus. */
|
||||
function breakoutSeries(): Candle[] {
|
||||
const s: Candle[] = [];
|
||||
for (let i = 0; i < 7; i++) s.push(c(10 + i, 11 + i, 9 + i, 10.5 + i, i));
|
||||
// bisheriges 3er-Hoch: max(high[4..6]) = 17 → Close 18 bricht aus, weit über EMA5
|
||||
s.push(c(17, 18.5, 16.5, 18, 7));
|
||||
return s;
|
||||
}
|
||||
|
||||
test('Long-Signal bei Donchian-Breakout über Trend-EMA', () => {
|
||||
const c4h = breakoutSeries();
|
||||
const ev = evaluateAt(c4h, computeIndicators(c4h, P), c4h.length - 1, P);
|
||||
expect(ev.signal).toBe('long');
|
||||
expect(ev.blockedBy).toBeNull();
|
||||
expect(ev.donchianHigh).toBe(17);
|
||||
expect(Number.isNaN(ev.atr)).toBe(false);
|
||||
});
|
||||
|
||||
test('blockiert unter Donchian-High', () => {
|
||||
const c4h = breakoutSeries();
|
||||
c4h[c4h.length - 1].close = 16.9; // unter 17
|
||||
const ev = evaluateAt(c4h, computeIndicators(c4h, P), c4h.length - 1, P);
|
||||
expect(ev.signal).toBeNull();
|
||||
expect(ev.blockedBy).toBe('below_donchian');
|
||||
});
|
||||
|
||||
test('blockiert unter Trend-EMA (Bärenmarkt-Filter)', () => {
|
||||
// Hoher Trend, dann Stufen-Abfall, dann Mini-Breakout: Close > Donchian aber < EMA
|
||||
const s: Candle[] = [];
|
||||
s.push(c(100, 101, 99, 100, 0));
|
||||
s.push(c(100, 101, 99, 100, 1));
|
||||
s.push(c(100, 101, 99, 100, 2));
|
||||
s.push(c(75, 76, 74, 75, 3));
|
||||
s.push(c(75, 76, 74, 75, 4));
|
||||
s.push(c(75, 76, 74, 75, 5));
|
||||
s.push(c(75, 76, 74, 75, 6));
|
||||
// Index 7: Donchian[7] = max(76,76,76) = 76, EMA5 ≈ 80, Close 77 > Donchian aber < EMA
|
||||
s.push(c(77, 80, 76, 77, 7));
|
||||
const ev = evaluateAt(s, computeIndicators(s, P), s.length - 1, P);
|
||||
expect(ev.signal).toBeNull();
|
||||
expect(ev.blockedBy).toBe('below_trend_ema');
|
||||
});
|
||||
|
||||
test('blockiert bei zu wenig Daten', () => {
|
||||
const c4h = breakoutSeries().slice(0, 4);
|
||||
const ev = evaluateAt(c4h, computeIndicators(c4h, P), c4h.length - 1, P);
|
||||
expect(ev.blockedBy).toBe('insufficient_data');
|
||||
});
|
||||
|
||||
test('DEFAULT_PARAMS entsprechen der Spec', () => {
|
||||
expect(DEFAULT_PARAMS).toEqual({ donchianPeriod: 20, atrPeriod: 14, atrMultiplier: 3, trendEmaPeriod: 200 });
|
||||
});
|
||||
59
src/server/strategy/donchian-trend.ts
Normal file
59
src/server/strategy/donchian-trend.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import type { Candle } from '../types';
|
||||
import { ema } from '../indicators/ema';
|
||||
import { atr } from '../indicators/atr';
|
||||
import { donchianHigh } from '../indicators/donchian';
|
||||
|
||||
export interface StrategyParams {
|
||||
donchianPeriod: number;
|
||||
atrPeriod: number;
|
||||
atrMultiplier: number;
|
||||
trendEmaPeriod: number;
|
||||
}
|
||||
|
||||
export const DEFAULT_PARAMS: StrategyParams = {
|
||||
donchianPeriod: 20,
|
||||
atrPeriod: 14,
|
||||
atrMultiplier: 3,
|
||||
trendEmaPeriod: 200,
|
||||
};
|
||||
|
||||
export interface IndicatorSet {
|
||||
trendEma: number[];
|
||||
donchianHigh: number[];
|
||||
atr: number[];
|
||||
}
|
||||
|
||||
export interface Evaluation {
|
||||
signal: 'long' | null;
|
||||
blockedBy: 'insufficient_data' | 'below_donchian' | 'below_trend_ema' | null;
|
||||
close: number;
|
||||
atr: number;
|
||||
donchianHigh: number;
|
||||
trendEma: number;
|
||||
}
|
||||
|
||||
/** Indikatoren einmal über die ganze Serie — Index i nutzt nur Daten ≤ i (kein Lookahead). */
|
||||
export function computeIndicators(c4h: Candle[], p: StrategyParams): IndicatorSet {
|
||||
return {
|
||||
trendEma: ema(c4h.map((c) => c.close), p.trendEmaPeriod),
|
||||
donchianHigh: donchianHigh(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, p: StrategyParams): Evaluation {
|
||||
const close = c4h[i]?.close ?? NaN;
|
||||
const base = { close, atr: ind.atr[i], donchianHigh: ind.donchianHigh[i], trendEma: ind.trendEma[i] };
|
||||
if (
|
||||
i < 0 ||
|
||||
Number.isNaN(ind.trendEma[i]) ||
|
||||
Number.isNaN(ind.donchianHigh[i]) ||
|
||||
Number.isNaN(ind.atr[i])
|
||||
) {
|
||||
return { signal: null, blockedBy: 'insufficient_data', ...base };
|
||||
}
|
||||
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 };
|
||||
}
|
||||
Reference in New Issue
Block a user