diff --git a/package.json b/package.json index ecf23bf..7ac4d0b 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,8 @@ "grid": "bun run src/server/scripts/grid-walkforward.ts", "db:generate": "bunx drizzle-kit generate", "db:migrate": "bun run src/server/db/migrate.ts", - "trump:backfill": "bun run src/server/scripts/trump-backfill.ts" + "trump:backfill": "bun run src/server/scripts/trump-backfill.ts", + "trump:study": "bun run src/server/scripts/trump-event-study.ts" }, "dependencies": { "drizzle-orm": "^0.44.0", diff --git a/src/server/scripts/trump-event-study.ts b/src/server/scripts/trump-event-study.ts new file mode 100644 index 0000000..1302a25 --- /dev/null +++ b/src/server/scripts/trump-event-study.ts @@ -0,0 +1,68 @@ +import { isNotNull } from 'drizzle-orm'; +import { db, sql } from '../db/client'; +import { trumpEvents } from '../db/schema'; +import { getCandles } from '../market/candle-store'; +import { DEFAULT_EXEC } from '../engine/portfolio'; +import type { Pair } from '../types'; + +const M15 = 15 * 60 * 1000; +const HORIZONS_H = [24, 48, 60, 72, 120]; +const COST = (1 - DEFAULT_EXEC.slippage) * (1 - DEFAULT_EXEC.feeRate) / ((1 + DEFAULT_EXEC.slippage) * (1 + DEFAULT_EXEC.feeRate)); // Round-Trip-Faktor + +const events = await db.select().from(trumpEvents).where(isNotNull(trumpEvents.instrument)); +type Row = { source: string; horizon: number; ret: number }; +const rows: Row[] = []; +const baselines = new Map>(); // instrument → horizon → mean ret + +for (const ev of events) { + const instrument = ev.instrument as Pair; + const evTs = ev.eventTs.getTime(); + const candles = await getCandles(instrument, evTs - M15, evTs + 130 * 3600_000); + const entryIdx = candles.findIndex((c) => c.ts >= evTs); + if (entryIdx < 0) continue; + const entry = candles[entryIdx].open; + for (const h of HORIZONS_H) { + const exitTs = candles[entryIdx].ts + h * 3600_000; + const exit = candles.findLast((c) => c.ts <= exitTs); + if (!exit || exit.ts < exitTs - 2 * M15) continue; // Horizont nicht abgedeckt + rows.push({ source: ev.source, horizon: h, ret: (exit.close / entry) * COST - 1 }); + } + // Baseline je Instrument einmalig: unbedingter Mean-Forward-Return über alle 15m-Starts + if (!baselines.has(instrument)) { + const all = await getCandles(instrument); + const byH = new Map(); + for (const h of HORIZONS_H) { + const stepIdx = (h * 3600_000) / M15; + let s = 0, n = 0; + for (let i = 0; i + stepIdx < all.length; i += 16) { // jede 4h ein Sample, deterministisch + s += (all[i + stepIdx].close / all[i].open) * COST - 1; + n++; + } + byH.set(h, n > 0 ? s / n : NaN); + } + baselines.set(instrument, byH); + } +} + +const fmt = (x: number) => (100 * x).toFixed(2) + '%'; +const lines: string[] = ['# Event-Study Trump-Copy — ' + new Date().toISOString().slice(0, 10), '']; +for (const source of ['onchain', 'truth']) { + lines.push(`## Quelle: ${source}`, '', '| Horizont | n | Mean | Median | Hit-Rate |', '|---|---|---|---|---|'); + for (const h of HORIZONS_H) { + const rs = rows.filter((r) => r.source === source && r.horizon === h).map((r) => r.ret).sort((a, b) => a - b); + if (rs.length === 0) { lines.push(`| ${h}h | 0 | — | — | — |`); continue; } + const mean = rs.reduce((a, b) => a + b, 0) / rs.length; + const median = rs[Math.floor(rs.length / 2)]; + const hit = rs.filter((r) => r > 0).length / rs.length; + lines.push(`| ${h}h | ${rs.length} | ${fmt(mean)} | ${fmt(median)} | ${(100 * hit).toFixed(0)}% |`); + } + lines.push(''); +} +lines.push('## Baselines (unbedingter Mean-Forward-Return, gleiche Kosten)', ''); +for (const [inst, byH] of baselines) { + lines.push(`- ${inst}: ` + HORIZONS_H.map((h) => `${h}h ${fmt(byH.get(h)!)}`).join(' · ')); +} +const out = lines.join('\n') + '\n'; +await Bun.write('docs/event-study-trump-2026-06-12.md', out); +console.log(out); +await sql.end();