test: Geld-Pfade im Trump-Cycle (minNotional, Cash-Erschöpfung, Re-Entry) + deutsche Kommentare

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-12 08:38:10 +00:00
parent a297d83849
commit 204c0541a7
2 changed files with 78 additions and 3 deletions

View File

@@ -72,6 +72,77 @@ describe('processTrumpCycle', () => {
expect(r.consumed).toHaveLength(2);
});
test('minNotional-Ablehnung: Budget < minNotionalUsdt → kein Trade, Event konsumiert', () => {
// equityFraction 0.2 × cash 10000 = 2000 < minNotionalUsdt 5000
const cfg = { ...CFG, minNotionalUsdt: 5000 };
const candles = new Map([['BTC_USDT' as Pair, flat('BTC_USDT', 200)]]);
const events = [{ id: 1, instrument: 'BTC_USDT' as Pair, eventTs: T0 + 1 }];
const r = processTrumpCycle(candles, events, fresh(), cfg);
expect(r.positions).toHaveLength(0);
expect(r.closedTrades).toHaveLength(0);
expect(r.consumed).toEqual([{ eventId: 1, consumedAt: T0 + M15 }]);
expect(r.cash).toBe(10_000);
});
test('Cash-Erschöpfung: budget > minNotional aber cash < cost → kein neuer Trade, Event konsumiert', () => {
// Equity ≈ 100 + 100×100 = 10100, budget ≈ 2020 > minNotional 10
// aber cash 100 < cost (2020) → Entry wird abgelehnt
const state: TrumpLiveState = {
cash: 100,
positions: [{
pair: 'ETH_USDT' as Pair,
qty: 100,
entryTs: T0 - 10 * M15,
entryPrice: 99,
entryCost: 9900,
riskAmount: 9900,
exitDueTs: T0 + 1000 * M15,
eventId: 99,
}],
cursorTs: T0,
};
const candles = new Map([
['BTC_USDT' as Pair, flat('BTC_USDT', 200, 100)],
['ETH_USDT' as Pair, flat('ETH_USDT', 200, 100)],
]);
const events = [{ id: 1, instrument: 'BTC_USDT' as Pair, eventTs: T0 + 1 }];
const r = processTrumpCycle(candles, events, state, CFG);
// ETH-Position bleibt, kein neuer BTC-Trade
expect(r.positions).toHaveLength(1);
expect(r.positions[0].pair).toBe('ETH_USDT');
// Event konsumiert
expect(r.consumed).toHaveLength(1);
expect(r.consumed[0].eventId).toBe(1);
// Cash unverändert
expect(r.cash).toBe(100);
});
test('Re-Entry nach Exit im selben Pair am selben Bar (Exit vor Entry)', () => {
// holdHours 1 → holdMs = 4×M15
// Cursor T0, Event id=1 eventTs=T0: Entry an Candle T0+M15, exitDueTs=T0+5*M15
// Exit-Bedingung: ts+M15 >= T0+5*M15 → ts >= T0+4*M15 → Exit-Candle ts=T0+4*M15
// Event id=2 eventTs=T0+4*M15: an selber Candle → Exit läuft zuerst, dann Entry
// Candles auf 6 begrenzen (T0..T0+5*M15): zweite Position hat exitDueTs=T0+8*M15
// → kein zweiter Exit innerhalb der Candle-Fensters → r.positions enthält noch die neue Position
const cfg = { ...CFG, holdHours: 1 };
const candles = new Map([['BTC_USDT' as Pair, flat('BTC_USDT', 6)]]);
const events = [
{ id: 1, instrument: 'BTC_USDT' as Pair, eventTs: T0 },
{ id: 2, instrument: 'BTC_USDT' as Pair, eventTs: T0 + 4 * M15 },
];
const r = processTrumpCycle(candles, events, fresh(), cfg);
// Erster Trade geschlossen: Exit an ts=T0+4*M15
expect(r.closedTrades).toHaveLength(1);
expect(r.closedTrades[0].exitTs).toBe(T0 + 4 * M15);
expect(r.closedTrades[0].exitReason).toBe('trump_hold');
// Re-Entry: neue offene Position mit entryTs=T0+4*M15 und eventId=2
expect(r.positions).toHaveLength(1);
expect(r.positions[0].entryTs).toBe(T0 + 4 * M15);
expect(r.positions[0].eventId).toBe(2);
// Beide Events konsumiert
expect(r.consumed.map((c) => c.eventId).sort()).toEqual([1, 2]);
});
test('Cursor-Idempotenz: gesplittete Zyklen ≡ ein Zyklus (Parität)', () => {
const n = 300;
const wave = (pair: Pair, base: number): Candle[] =>

View File

@@ -53,6 +53,10 @@ export interface TrumpCycleResult {
* Events werden beim Verarbeiten immer konsumiert (verfallen ohne freien Slot).
* Cursor-idempotent: gesplittete Zyklen ergeben exakt dasselbe wie ein Lauf —
* Paritätstest erzwingt das (Aufrufer reicht nur unkonsumierte Events ein).
* Events für Pairs, die in candles15ByPair fehlen oder nach dem Cursor kein Candle
* haben (fehlender Key oder leeres Array), werden nicht besucht und bleiben unkonsumiert —
* der Aufrufer muss sicherstellen, dass solche Events später Candles bekommen
* (Gap-Fetch/Backfill) oder vom Aufrufer selbst verworfen werden.
*/
export function processTrumpCycle(
candles15ByPair: Map<Pair, Candle[]>,
@@ -71,14 +75,14 @@ export function processTrumpCycle(
const pairs = cfg.pairs.filter((p) => candles15ByPair.has(p));
// Group and sort events by pair
// Events nach Pair gruppieren und sortieren
const pending = new Map<Pair, TrumpEventInput[]>();
for (const ev of [...events].sort((a, b) => a.eventTs - b.eventTs || a.id - b.id)) {
if (!pending.has(ev.instrument)) pending.set(ev.instrument, []);
pending.get(ev.instrument)!.push(ev);
}
// Seed lastClose from candles up to and including cursorTs
// lastClose mit Candles bis einschließlich cursorTs vorbelegen
for (const pair of pairs) {
for (const c of candles15ByPair.get(pair)!) {
if (c.ts > state.cursorTs) break;
@@ -92,7 +96,7 @@ export function processTrumpCycle(
return eq;
};
// Build timeline of all candles strictly after cursorTs, sorted by ts then by pairs order
// Timeline aller Candles strikt nach cursorTs, sortiert nach ts dann nach pairs-Reihenfolge
const timeline: { ts: number; pair: Pair; candle: Candle }[] = [];
for (const pair of pairs) {
for (const candle of candles15ByPair.get(pair)!) {