From 5f634452c321bce9893ad717b66b8ebf78457ead Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 12 Jun 2026 09:04:25 +0000 Subject: [PATCH] feat: Trump-Event-Backfill (on-chain History + Truth-Archiv best effort) Co-Authored-By: Claude Fable 5 --- package.json | 3 +- src/server/scripts/trump-backfill.ts | 87 ++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 src/server/scripts/trump-backfill.ts diff --git a/package.json b/package.json index baa96ff..ecf23bf 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "rotation": "bun run src/server/scripts/rotation-walkforward.ts", "grid": "bun run src/server/scripts/grid-walkforward.ts", "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": { "drizzle-orm": "^0.44.0", diff --git a/src/server/scripts/trump-backfill.ts b/src/server/scripts/trump-backfill.ts new file mode 100644 index 0000000..5f38e62 --- /dev/null +++ b/src/server/scripts/trump-backfill.ts @@ -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
(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();