feat: 4h-Aggregation, EMA, ATR, Donchian-High (TDD)

This commit is contained in:
2026-06-09 20:18:53 +00:00
parent 568e282b07
commit c2f1221eb9
8 changed files with 169 additions and 0 deletions

View File

@@ -0,0 +1,40 @@
import { expect, test } from 'bun:test';
import type { Candle } from '../types';
import { aggregate4h, H4 } from './aggregate';
const M15 = 15 * 60 * 1000;
function c15(ts: number, o: number, h: number, l: number, cl: number, v = 1): Candle {
return { ts, open: o, high: h, low: l, close: cl, volume: v };
}
test('aggregiert 16 15m-Candles zu einer 4h-Candle, verwirft unvollständigen letzten Bucket', () => {
const candles: Candle[] = [];
// Bucket 1: ts 0..15*M15 — Preise 100..115
for (let i = 0; i < 16; i++) candles.push(c15(i * M15, 100 + i, 101 + i, 99 + i, 100.5 + i));
// Bucket 2: nur 2 Candles (unvollständig, wird aber als "letzter" sowieso verworfen,
// sobald kein dritter Bucket folgt)
candles.push(c15(H4, 200, 210, 195, 205));
candles.push(c15(H4 + M15, 205, 220, 204, 218));
const out = aggregate4h(candles);
expect(out).toHaveLength(1);
expect(out[0].ts).toBe(0);
expect(out[0].open).toBe(100);
expect(out[0].high).toBe(101 + 15);
expect(out[0].low).toBe(99);
expect(out[0].close).toBe(100.5 + 15);
expect(out[0].volume).toBe(16);
});
test('Lücken in den Daten erzeugen keine Phantom-Buckets', () => {
const candles: Candle[] = [
c15(0, 1, 2, 0.5, 1.5),
c15(2 * H4, 3, 4, 2.5, 3.5), // Bucket dazwischen fehlt komplett
c15(2 * H4 + M15, 3.5, 5, 3, 4),
c15(3 * H4, 9, 9, 9, 9), // öffnet neuen Bucket → Bucket 2*H4 wird finalisiert
];
const out = aggregate4h(candles);
expect(out.map((c) => c.ts)).toEqual([0, 2 * H4]);
expect(out[1].high).toBe(5);
});

View File

@@ -0,0 +1,26 @@
import type { Candle } from '../types';
export const H4 = 4 * 60 * 60 * 1000;
/**
* Aggregiert 15m-Candles (sortiert, ts aufsteigend) zu 4h-Candles.
* Der letzte Bucket wird verworfen, weil nicht feststellbar ist, ob er
* abgeschlossen ist — der Backtest-Runner arbeitet nur auf geschlossenen Candles.
*/
export function aggregate4h(c15: Candle[]): Candle[] {
const out: Candle[] = [];
let cur: Candle | null = null;
for (const c of c15) {
const bucket = Math.floor(c.ts / H4) * H4;
if (cur && cur.ts === bucket) {
cur.high = Math.max(cur.high, c.high);
cur.low = Math.min(cur.low, c.low);
cur.close = c.close;
cur.volume += c.volume;
} else {
if (cur) out.push(cur);
cur = { ts: bucket, open: c.open, high: c.high, low: c.low, close: c.close, volume: c.volume };
}
}
return out; // letzter Bucket absichtlich nicht gepusht
}