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:
87
src/server/scripts/trump-backfill.ts
Normal file
87
src/server/scripts/trump-backfill.ts
Normal 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();
|
||||
Reference in New Issue
Block a user