diff --git a/src/server/api/server.ts b/src/server/api/server.ts index 8a806a1..d583f47 100644 --- a/src/server/api/server.ts +++ b/src/server/api/server.ts @@ -1,6 +1,6 @@ import { and, desc, eq, gte } from 'drizzle-orm'; 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 { computeMetrics, type EquityPoint } from '../backtest/metrics'; import type { ClosedTrade } from '../engine/portfolio'; @@ -9,6 +9,7 @@ import { PAIRS } from '../types'; import type { LiveEngine } from '../live/engine'; import type { GridEngine } 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 { 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)); return Bun.serve({ @@ -221,6 +222,18 @@ export function createServer(engine: LiveEngine, gridEngine: GridEngine, port: n return json(await getStats()); case '/api/grid': 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': { const pair = url.searchParams.get('pair') ?? 'BTC_USDT'; if (!(PAIRS as readonly string[]).includes(pair)) return json({ error: 'unbekanntes Pair' }, 400); diff --git a/src/server/index.ts b/src/server/index.ts index ee80ad9..a162519 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -1,21 +1,25 @@ import { env } from './config'; import { LiveEngine } from './live/engine'; import { GridEngine } from './live/grid-engine'; +import { TrumpEngine } from './live/trump-engine'; import { createServer } from './api/server'; const CYCLE_MS = 5 * 60 * 1000; const engine = new LiveEngine(); const gridEngine = new GridEngine(); +const trumpEngine = new TrumpEngine(); await engine.init(); await gridEngine.init(); -createServer(engine, gridEngine, env.PORT); -console.log(`trade-kuns Live-Paper-Engines (trend + grid) laufen auf :${env.PORT}`); +await trumpEngine.init(); +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 () => { await engine.runCycle(); await gridEngine.runCycle(); + await trumpEngine.runCycle(); }; void cycle(); setInterval(() => void cycle(), CYCLE_MS); diff --git a/src/server/live/trump-engine.ts b/src/server/live/trump-engine.ts index e2ae75f..31d1590 100644 --- a/src/server/live/trump-engine.ts +++ b/src/server/live/trump-engine.ts @@ -1,10 +1,11 @@ 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 { fetchCandles } from '../market/cryptocom'; +import { getCandles, insertCandles } from '../market/candle-store'; import { DEFAULT_EXEC } from '../engine/portfolio'; import { pollSignals } from '../signals/poller'; -import { TRUMP_PAIRS } from '../types'; +import { PAIRS, TRUMP_PAIRS } from '../types'; import type { Candle, Pair } from '../types'; import { processTrumpCycle, @@ -60,11 +61,40 @@ export class TrumpEngine { this.status.cursorTs = cursor; } + /** Holt fehlende 15m-Candles für Nicht-Trend-Pairs (TRUMP_CYCLE_CONFIG.pairs minus PAIRS). */ + private async fillCandleGaps(): Promise { + 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 { if (this.cycling) return; this.cycling = true; try { await pollSignals(); // non-fatal intern; wirft nicht + await this.fillCandleGaps(); const state = await this.loadState(); const from = state.cursorTs - WARMUP_BARS_15M * M15;