feat: Trump-Event-Backfill (on-chain History + Truth-Archiv best effort)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-12 09:04:25 +00:00
parent 52cd31bf42
commit 5f634452c3
2 changed files with 89 additions and 1 deletions

View File

@@ -10,7 +10,8 @@
"rotation": "bun run src/server/scripts/rotation-walkforward.ts", "rotation": "bun run src/server/scripts/rotation-walkforward.ts",
"grid": "bun run src/server/scripts/grid-walkforward.ts", "grid": "bun run src/server/scripts/grid-walkforward.ts",
"db:generate": "bunx drizzle-kit generate", "db:generate": "bunx drizzle-kit generate",
"db:migrate": "bun run src/server/db/migrate.ts" "db:migrate": "bun run src/server/db/migrate.ts",
"trump:backfill": "bun run src/server/scripts/trump-backfill.ts"
}, },
"dependencies": { "dependencies": {
"drizzle-orm": "^0.44.0", "drizzle-orm": "^0.44.0",

View File

@@ -0,0 +1,87 @@
import { parseArgs } from 'util';
import { sql, db } from '../db/client';
import { trumpEvents } from '../db/schema';
import { fetchTransfers, getBlockNumber, getBlockTs } from '../signals/onchain';
import { dedupeTruthEvents, passesNotional } from '../signals/poller';
import { matchCoins } from '../signals/truth';
import { COIN_KEYWORDS } from '../signals/watchlist';
import { getCandles } from '../market/candle-store';
const M15 = 15 * 60 * 1000;
const { values: args } = parseArgs({
options: {
'from-block': { type: 'string', default: '20600000' }, // ≈ Sep 2024, vor den ersten WLFI-Treasury-Käufen
'truth-pages': { type: 'string', default: '300' },
},
});
// ── On-chain ──
const fromBlock = Number(args['from-block']);
const head = await getBlockNumber();
console.log(`On-chain-Scan Block ${fromBlock}${head} (${head - fromBlock} Blöcke, ~${Math.ceil((head - fromBlock) / 5000)} Requests)`);
let onchainCount = 0;
const STEP = 5000;
for (let from = fromBlock; from <= head; from += STEP) {
const to = Math.min(from + STEP - 1, head);
const transfers = await fetchTransfers(from, to, STEP);
for (const t of transfers) {
const ts = await getBlockTs(t.blockNumber);
let price: number | null = null;
if (t.instrument) {
const cs = await getCandles(t.instrument, ts - 24 * 3600_000, ts + M15);
price = cs.length > 0 ? cs[cs.length - 1].close : null;
}
if (!passesNotional(t.amount, price)) continue;
await db.insert(trumpEvents).values({
source: 'onchain', token: t.symbol, instrument: t.instrument,
eventTs: new Date(ts), ref: t.txHash, notionalUsd: t.amount * price!,
}).onConflictDoNothing();
onchainCount++;
console.log(` ${new Date(ts).toISOString()} ${t.symbol} ${t.amount.toFixed(2)} (~$${Math.round(t.amount * price!)}) ${t.txHash}`);
}
if ((from - fromBlock) % (STEP * 20) === 0) console.log(` … Block ${to}`);
await Bun.sleep(150);
}
console.log(`On-chain: ${onchainCount} Events`);
// ── Truth (best effort, letzte N Archiv-Seiten à 10 Posts) ──
// Markup verifiziert 2026-06-12: Seite hat 10 Posts, je ein <div class="status"> (exakt).
// Timestamp als menschenlesbarer Text in class="status-info__meta-item" (kein datetime=-Attribut!).
// Post-Text in class="status__content". URL via href="https://trumpstruth.org/statuses/\d+".
const pages = Number(args['truth-pages']);
const candidates: { symbol: string; ts: number; url: string }[] = [];
let oldestTs = Infinity;
for (let p = 1; p <= pages; p++) {
const res = await fetch(`https://trumpstruth.org/?page=${p}`, { signal: AbortSignal.timeout(20_000) });
if (!res.ok) { console.warn(`Seite ${p}: HTTP ${res.status} — Truth-Scan endet hier`); break; }
const html = await res.text();
// Split auf exaktes class="status" (nicht \b) — gibt genau 10 Post-Blöcke je Seite.
// Vorsicht: class="status\b würde auch status__header, status__body etc. treffen (138 statt 10).
const blocks = html.split(/class="status"/).slice(1);
if (blocks.length === 0) { console.warn(`Seite ${p}: 0 Blöcke — möglicherweise letzte Seite`); break; }
for (const block of blocks) {
const url = block.match(/href="(https:\/\/trumpstruth\.org\/statuses\/\d+)"/)?.[1];
// Timestamp als menschenlesbarer Text z. B. "June 11, 2026, 9:49 PM" (kein datetime=-Attribut)
const timeRaw = block.match(/class="status-info__meta-item">([A-Z][a-z]+ \d+, \d{4}, \d+:\d+ [AP]M)<\/a>/)?.[1];
// Post-Text aus status__content (nicht status-info__body — das ist der Autor-/Meta-Bereich)
const body = block.match(/status__content[^>]*>([\s\S]*?)<\/div>/)?.[1] ?? '';
if (!url || !timeRaw) continue;
const ts = Date.parse(timeRaw);
if (Number.isNaN(ts)) continue;
oldestTs = Math.min(oldestTs, ts);
const text = body.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
for (const symbol of matchCoins(text)) candidates.push({ symbol, ts, url });
}
await Bun.sleep(300);
}
console.log(`Truth-Scan: ${candidates.length} Kandidaten, ältester Post ${oldestTs === Infinity ? '—' : new Date(oldestTs).toISOString()}`);
let truthCount = 0;
for (const e of dedupeTruthEvents(candidates, new Map())) {
const kw = COIN_KEYWORDS.find((c) => c.symbol === e.symbol)!;
await db.insert(trumpEvents).values({
source: 'truth', token: e.symbol, instrument: kw.instrument, eventTs: new Date(e.ts), ref: e.url,
}).onConflictDoNothing();
truthCount++;
}
console.log(`Truth: ${truthCount} Events (Historie nur letzte ${pages} Seiten — Lücke davor ist bekannt und akzeptiert)`);
await sql.end();