feat: 4h-Aggregation, EMA, ATR, Donchian-High (TDD)
This commit is contained in:
40
src/server/market/aggregate.test.ts
Normal file
40
src/server/market/aggregate.test.ts
Normal 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);
|
||||
});
|
||||
26
src/server/market/aggregate.ts
Normal file
26
src/server/market/aggregate.ts
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user