feat: TrumpEngine als dritte Paper-Engine (bot_state id=3, Poller im Zyklus)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
172
src/server/live/trump-engine.ts
Normal file
172
src/server/live/trump-engine.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
import { and, eq, isNotNull, isNull } from 'drizzle-orm';
|
||||
import { db } from '../db/client';
|
||||
import { botState, equitySnapshots, paperTrades, trumpEvents, trumpPositions } from '../db/schema';
|
||||
import { getCandles } from '../market/candle-store';
|
||||
import { DEFAULT_EXEC } from '../engine/portfolio';
|
||||
import { pollSignals } from '../signals/poller';
|
||||
import { TRUMP_PAIRS } from '../types';
|
||||
import type { Candle, Pair } from '../types';
|
||||
import {
|
||||
processTrumpCycle,
|
||||
type TrumpCycleConfig,
|
||||
type TrumpCycleResult,
|
||||
type TrumpEventInput,
|
||||
type TrumpLiveState,
|
||||
} from './trump-cycle';
|
||||
|
||||
const M15 = 15 * 60 * 1000;
|
||||
const BOT_STATE_ID = 3; // 1 = Trend, 2 = Grid
|
||||
const START_CAPITAL = 10_000;
|
||||
/** Keine Indikatoren — Warmup nur für lastClose-Seed (Equity offener Positionen). */
|
||||
const WARMUP_BARS_15M = 8;
|
||||
|
||||
export const TRUMP_CYCLE_CONFIG: TrumpCycleConfig = {
|
||||
exec: DEFAULT_EXEC,
|
||||
holdHours: 60, // Default lt. Spec; finaler Wert kommt aus der Event-Study
|
||||
equityFraction: 0.2,
|
||||
maxPositions: 5,
|
||||
minNotionalUsdt: 10,
|
||||
pairs: [...TRUMP_PAIRS],
|
||||
};
|
||||
|
||||
export interface TrumpEngineStatus {
|
||||
lastCycleAt: number | null;
|
||||
lastCycleOk: boolean;
|
||||
lastError: string | null;
|
||||
cursorTs: number | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dritte Paper-Engine (Trump-Copy). Läuft NACH Trend+Grid im Zyklus;
|
||||
* Candle-Gap-Fetch für die Nicht-Trend-Pairs kommt in Task 10.
|
||||
*/
|
||||
export class TrumpEngine {
|
||||
status: TrumpEngineStatus = { lastCycleAt: null, lastCycleOk: true, lastError: null, cursorTs: null };
|
||||
private cycling = false;
|
||||
|
||||
async init(): Promise<void> {
|
||||
const [row] = await db.select().from(botState).where(eq(botState.id, BOT_STATE_ID));
|
||||
if (row) {
|
||||
this.status.cursorTs = row.cursorTs.getTime();
|
||||
return;
|
||||
}
|
||||
const cursor = Math.floor(Date.now() / M15) * M15 - M15;
|
||||
await db.insert(botState).values({
|
||||
id: BOT_STATE_ID,
|
||||
cash: START_CAPITAL,
|
||||
startCapital: START_CAPITAL,
|
||||
cursorTs: new Date(cursor),
|
||||
});
|
||||
this.status.cursorTs = cursor;
|
||||
}
|
||||
|
||||
async runCycle(): Promise<void> {
|
||||
if (this.cycling) return;
|
||||
this.cycling = true;
|
||||
try {
|
||||
await pollSignals(); // non-fatal intern; wirft nicht
|
||||
|
||||
const state = await this.loadState();
|
||||
const from = state.cursorTs - WARMUP_BARS_15M * M15;
|
||||
const candles15 = new Map<Pair, Candle[]>();
|
||||
for (const pair of TRUMP_CYCLE_CONFIG.pairs) {
|
||||
const cs = await getCandles(pair, from);
|
||||
// Pairs ohne Candle-Daten NICHT in die Map: deren Events bleiben pending,
|
||||
// bis Gap-Fetch/Backfill liefert (Vertrag von processTrumpCycle)
|
||||
if (cs.length > 0) candles15.set(pair, cs);
|
||||
else console.warn(`Trump-Engine: keine Candles für ${pair} — Pair in diesem Zyklus übersprungen`);
|
||||
}
|
||||
const events = await this.loadOpenEvents();
|
||||
const result = processTrumpCycle(candles15, events, state, TRUMP_CYCLE_CONFIG);
|
||||
await this.persist(result);
|
||||
this.status.lastCycleAt = Date.now();
|
||||
this.status.lastCycleOk = true;
|
||||
this.status.lastError = null;
|
||||
this.status.cursorTs = result.cursorTs;
|
||||
} catch (err) {
|
||||
this.status.lastCycleAt = Date.now();
|
||||
this.status.lastCycleOk = false;
|
||||
this.status.lastError = err instanceof Error ? err.message : String(err);
|
||||
console.error('Trump-Zyklus fehlgeschlagen:', err);
|
||||
} finally {
|
||||
this.cycling = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async loadState(): Promise<TrumpLiveState> {
|
||||
const [row] = await db.select().from(botState).where(eq(botState.id, BOT_STATE_ID));
|
||||
if (!row) throw new Error('bot_state (trump) fehlt — init() nicht gelaufen?');
|
||||
const posRows = await db.select().from(trumpPositions);
|
||||
return {
|
||||
cash: row.cash,
|
||||
cursorTs: row.cursorTs.getTime(),
|
||||
positions: posRows.map((p) => ({
|
||||
pair: p.pair as Pair,
|
||||
qty: p.qty,
|
||||
entryTs: p.entryTs.getTime(),
|
||||
entryPrice: p.entryPrice,
|
||||
entryCost: p.entryCost,
|
||||
riskAmount: p.riskAmount,
|
||||
exitDueTs: p.exitDueTs.getTime(),
|
||||
eventId: p.eventId,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
private async loadOpenEvents(): Promise<TrumpEventInput[]> {
|
||||
const rows = await db
|
||||
.select()
|
||||
.from(trumpEvents)
|
||||
.where(and(isNull(trumpEvents.consumedAt), isNotNull(trumpEvents.instrument)));
|
||||
return rows.map((r) => ({ id: r.id, instrument: r.instrument as Pair, eventTs: r.eventTs.getTime() }));
|
||||
}
|
||||
|
||||
private async persist(result: TrumpCycleResult): Promise<void> {
|
||||
await db.transaction(async (tx) => {
|
||||
await tx.delete(trumpPositions);
|
||||
for (const p of result.positions) {
|
||||
await tx.insert(trumpPositions).values({
|
||||
pair: p.pair,
|
||||
qty: p.qty,
|
||||
entryTs: new Date(p.entryTs),
|
||||
entryPrice: p.entryPrice,
|
||||
entryCost: p.entryCost,
|
||||
riskAmount: p.riskAmount,
|
||||
exitDueTs: new Date(p.exitDueTs),
|
||||
eventId: p.eventId,
|
||||
});
|
||||
}
|
||||
if (result.closedTrades.length > 0) {
|
||||
await tx.insert(paperTrades).values(
|
||||
result.closedTrades.map((t) => ({
|
||||
bot: 'trump',
|
||||
pair: t.pair,
|
||||
side: t.side,
|
||||
entryTs: new Date(t.entryTs),
|
||||
entryPrice: t.entryPrice,
|
||||
exitTs: new Date(t.exitTs),
|
||||
exitPrice: t.exitPrice,
|
||||
qty: t.qty,
|
||||
pnl: t.pnl,
|
||||
r: t.r,
|
||||
exitReason: t.exitReason,
|
||||
})),
|
||||
);
|
||||
}
|
||||
for (const c of result.consumed) {
|
||||
await tx.update(trumpEvents).set({ consumedAt: new Date(c.consumedAt) }).where(eq(trumpEvents.id, c.eventId));
|
||||
}
|
||||
for (const s of result.equitySnapshots) {
|
||||
const row = { bot: 'trump', ts: new Date(s.ts), equity: s.equity, cash: s.cash };
|
||||
await tx
|
||||
.insert(equitySnapshots)
|
||||
.values(row)
|
||||
.onConflictDoUpdate({ target: [equitySnapshots.bot, equitySnapshots.ts], set: row });
|
||||
}
|
||||
await tx
|
||||
.update(botState)
|
||||
.set({ cash: result.cash, cursorTs: new Date(result.cursorTs), updatedAt: new Date() })
|
||||
.where(eq(botState.id, BOT_STATE_ID));
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user