feat: Trump-Engine in Loop und API verdrahtet (/api/trump, Gap-Fetch neue Pairs)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-12 08:48:54 +00:00
parent a13bca596c
commit f8cb424719
3 changed files with 54 additions and 7 deletions

View File

@@ -1,6 +1,6 @@
import { and, desc, eq, gte } from 'drizzle-orm'; import { and, desc, eq, gte } from 'drizzle-orm';
import { db } from '../db/client'; import { db } from '../db/client';
import { botState, candles, decisionLogs, equitySnapshots, gridLots, gridState, paperTrades, positions } from '../db/schema'; import { botState, candles, decisionLogs, equitySnapshots, gridLots, gridState, paperTrades, positions, trumpEvents, trumpPositions } from '../db/schema';
import { aggregate4h } from '../market/aggregate'; import { aggregate4h } from '../market/aggregate';
import { computeMetrics, type EquityPoint } from '../backtest/metrics'; import { computeMetrics, type EquityPoint } from '../backtest/metrics';
import type { ClosedTrade } from '../engine/portfolio'; import type { ClosedTrade } from '../engine/portfolio';
@@ -9,6 +9,7 @@ import { PAIRS } from '../types';
import type { LiveEngine } from '../live/engine'; import type { LiveEngine } from '../live/engine';
import type { GridEngine } from '../live/grid-engine'; import type { GridEngine } from '../live/grid-engine';
import { GRID_CYCLE_CONFIG } from '../live/grid-engine'; import { GRID_CYCLE_CONFIG } from '../live/grid-engine';
import type { TrumpEngine } from '../live/trump-engine';
function json(data: unknown, status = 200): Response { function json(data: unknown, status = 200): Response {
return new Response(JSON.stringify(data), { status, headers: { 'content-type': 'application/json' } }); return new Response(JSON.stringify(data), { status, headers: { 'content-type': 'application/json' } });
@@ -181,7 +182,7 @@ async function getGrid(gridEngine: GridEngine) {
}; };
} }
export function createServer(engine: LiveEngine, gridEngine: GridEngine, port: number) { export function createServer(engine: LiveEngine, gridEngine: GridEngine, trumpEngine: TrumpEngine, port: number) {
const indexHtml = Bun.file(new URL('../../../public/index.html', import.meta.url)); const indexHtml = Bun.file(new URL('../../../public/index.html', import.meta.url));
return Bun.serve({ return Bun.serve({
@@ -221,6 +222,18 @@ export function createServer(engine: LiveEngine, gridEngine: GridEngine, port: n
return json(await getStats()); return json(await getStats());
case '/api/grid': case '/api/grid':
return json(await getGrid(gridEngine)); return json(await getGrid(gridEngine));
case '/api/trump': {
const [state] = await db.select().from(botState).where(eq(botState.id, 3));
const positions_trump = await db.select().from(trumpPositions);
const events = await db.select().from(trumpEvents).orderBy(desc(trumpEvents.eventTs)).limit(50);
return Response.json({
status: trumpEngine.status,
cash: state?.cash ?? null,
startCapital: state?.startCapital ?? null,
positions: positions_trump,
events,
});
}
case '/api/candles': { case '/api/candles': {
const pair = url.searchParams.get('pair') ?? 'BTC_USDT'; const pair = url.searchParams.get('pair') ?? 'BTC_USDT';
if (!(PAIRS as readonly string[]).includes(pair)) return json({ error: 'unbekanntes Pair' }, 400); if (!(PAIRS as readonly string[]).includes(pair)) return json({ error: 'unbekanntes Pair' }, 400);

View File

@@ -1,21 +1,25 @@
import { env } from './config'; import { env } from './config';
import { LiveEngine } from './live/engine'; import { LiveEngine } from './live/engine';
import { GridEngine } from './live/grid-engine'; import { GridEngine } from './live/grid-engine';
import { TrumpEngine } from './live/trump-engine';
import { createServer } from './api/server'; import { createServer } from './api/server';
const CYCLE_MS = 5 * 60 * 1000; const CYCLE_MS = 5 * 60 * 1000;
const engine = new LiveEngine(); const engine = new LiveEngine();
const gridEngine = new GridEngine(); const gridEngine = new GridEngine();
const trumpEngine = new TrumpEngine();
await engine.init(); await engine.init();
await gridEngine.init(); await gridEngine.init();
createServer(engine, gridEngine, env.PORT); await trumpEngine.init();
console.log(`trade-kuns Live-Paper-Engines (trend + grid) laufen auf :${env.PORT}`); createServer(engine, gridEngine, trumpEngine, env.PORT);
console.log(`trade-kuns Live-Paper-Engines (trend + grid + trump) laufen auf :${env.PORT}`);
// Grid läuft nach der Trend-Engine — deren Gap-Fetch füllt die Candle-DB für beide. // Grid/Trump laufen nach der Trend-Engine — deren Gap-Fetch füllt die Candle-DB für PAIRS.
const cycle = async () => { const cycle = async () => {
await engine.runCycle(); await engine.runCycle();
await gridEngine.runCycle(); await gridEngine.runCycle();
await trumpEngine.runCycle();
}; };
void cycle(); void cycle();
setInterval(() => void cycle(), CYCLE_MS); setInterval(() => void cycle(), CYCLE_MS);

View File

@@ -1,10 +1,11 @@
import { and, eq, isNotNull, isNull } from 'drizzle-orm'; import { and, eq, isNotNull, isNull } from 'drizzle-orm';
import { db } from '../db/client'; import { db } from '../db/client';
import { botState, equitySnapshots, paperTrades, trumpEvents, trumpPositions } from '../db/schema'; import { botState, equitySnapshots, paperTrades, trumpEvents, trumpPositions } from '../db/schema';
import { getCandles } from '../market/candle-store'; import { fetchCandles } from '../market/cryptocom';
import { getCandles, insertCandles } from '../market/candle-store';
import { DEFAULT_EXEC } from '../engine/portfolio'; import { DEFAULT_EXEC } from '../engine/portfolio';
import { pollSignals } from '../signals/poller'; import { pollSignals } from '../signals/poller';
import { TRUMP_PAIRS } from '../types'; import { PAIRS, TRUMP_PAIRS } from '../types';
import type { Candle, Pair } from '../types'; import type { Candle, Pair } from '../types';
import { import {
processTrumpCycle, processTrumpCycle,
@@ -60,11 +61,40 @@ export class TrumpEngine {
this.status.cursorTs = cursor; this.status.cursorTs = cursor;
} }
/** Holt fehlende 15m-Candles für Nicht-Trend-Pairs (TRUMP_CYCLE_CONFIG.pairs minus PAIRS). */
private async fillCandleGaps(): Promise<void> {
const trumpOnlyPairs = TRUMP_CYCLE_CONFIG.pairs.filter(
(p) => !(PAIRS as readonly string[]).includes(p),
);
const state = await db.select().from(botState).where(eq(botState.id, BOT_STATE_ID));
const cursorTs = state[0]?.cursorTs.getTime() ?? Date.now();
const now = Date.now();
for (const pair of trumpOnlyPairs) {
try {
const fresh: Candle[] = [];
let endTs: number | undefined;
for (let i = 0; i < 40; i++) {
const batch = await fetchCandles(pair, '15m', 300, endTs);
if (batch.length === 0) break;
fresh.push(...batch);
const oldest = Math.min(...batch.map((c) => c.ts));
if (oldest <= cursorTs) break;
endTs = oldest - 1;
}
const closed = fresh.filter((c) => c.ts + M15 <= now && c.ts > cursorTs);
if (closed.length > 0) await insertCandles(pair, closed);
} catch (err) {
console.warn(`Trump-Engine Gap-Fetch Fehler für ${pair}:`, err instanceof Error ? err.message : String(err));
}
}
}
async runCycle(): Promise<void> { async runCycle(): Promise<void> {
if (this.cycling) return; if (this.cycling) return;
this.cycling = true; this.cycling = true;
try { try {
await pollSignals(); // non-fatal intern; wirft nicht await pollSignals(); // non-fatal intern; wirft nicht
await this.fillCandleGaps();
const state = await this.loadState(); const state = await this.loadState();
const from = state.cursorTs - WARMUP_BARS_15M * M15; const from = state.cursorTs - WARMUP_BARS_15M * M15;