feat: Event-Study-Script (Forward-Returns je Quelle/Horizont vs. Baseline)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-12 09:08:14 +00:00
parent c3114db05e
commit 0696565840
2 changed files with 70 additions and 1 deletions

View File

@@ -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<string, Map<number, number>>(); // 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<number, number>();
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();