feat: add autonomous paper-trading and calibration pipeline

This commit is contained in:
ImBenji
2026-08-03 14:03:27 +01:00
parent 5a9a2e4c6d
commit c4028cc394
46 changed files with 2246 additions and 115 deletions
+173
View File
@@ -0,0 +1,173 @@
// evaluates predictions older than 11 days against realized stock returns
// runs continuously, batching by ticker so we hit yahoo once per company per cycle
const { getPriceContext } = require("./priceContext");
const https = require("https");
async function runOutcomeWorker(archiveDb, intelligenceDb, config) {
const loopDelay = config.workers?.outcomeLoopDelayMs ?? 60000;
// pull predictions that are old enough to evaluate (>= 11 calendar days) and dont have an outcome yet
const getPending = intelligenceDb.prepare(`
SELECT ep.id, ep.company_id, ep.event_date, ep.direction, tc.ticker
FROM event_predictions ep
JOIN tracked_companies tc ON ep.company_id = tc.id
LEFT JOIN prediction_outcomes po ON po.prediction_id = ep.id
WHERE po.prediction_id IS NULL
AND ep.event_date IS NOT NULL
AND date(ep.event_date) <= date('now', '-11 days')
AND ep.direction IN ('positive', 'negative')
AND tc.ticker IS NOT NULL
AND tc.ticker NOT LIKE '%.%'
AND length(tc.ticker) <= 5
ORDER BY ep.event_date ASC
LIMIT 50
`);
const insertOutcome = intelligenceDb.prepare(`
INSERT OR REPLACE INTO prediction_outcomes
(prediction_id, company_id, ticker, event_date, price_0, price_5d, price_10d, r5, r10, correct_5d, correct_10d)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
while (true) {
try {
const pending = getPending.all();
if (pending.length === 0) {
await sleep(loopDelay);
continue;
}
// group by ticker so we only fetch each company's history once per cycle
const byTicker = new Map();
for (const p of pending) {
if (!byTicker.has(p.ticker)) byTicker.set(p.ticker, []);
byTicker.get(p.ticker).push(p);
}
let evaluated = 0;
for (const [ticker, preds] of byTicker.entries()) {
let history;
try {
history = await fetchYahooHistory(ticker, "1y");
} catch (err) {
console.error(`[outcome] yahoo error for ${ticker}: ${err.message}`);
continue;
}
if (!history || history.length === 0) continue;
for (const pred of preds) {
const eventDate = pred.event_date.slice(0, 10);
const price0 = nearestOnOrAfter(history, eventDate);
if (price0 == null) continue;
const date5 = addTradingDays(eventDate, 5);
const date10 = addTradingDays(eventDate, 10);
const price5 = nearestOnOrAfter(history, date5);
const price10 = nearestOnOrAfter(history, date10);
const r5 = price5 != null ? (price5 - price0) / price0 * 100 : null;
const r10 = price10 != null ? (price10 - price0) / price0 * 100 : null;
const correct5 = r5 == null ? null : (pred.direction === "positive" ? (r5 > 0 ? 1 : 0) : (r5 < 0 ? 1 : 0));
const correct10 = r10 == null ? null : (pred.direction === "positive" ? (r10 > 0 ? 1 : 0) : (r10 < 0 ? 1 : 0));
insertOutcome.run(
pred.id, pred.company_id, ticker, eventDate,
price0, price5, price10, r5, r10, correct5, correct10
);
evaluated++;
}
// small delay between tickers so we dont hammer yahoo
await sleep(800);
}
if (evaluated > 0) {
console.log(`[outcome] evaluated ${evaluated} predictions across ${byTicker.size} tickers`);
}
await sleep(loopDelay);
} catch (err) {
console.error("[outcome] cycle error:", err.message);
await sleep(loopDelay);
}
}
}
async function fetchYahooHistory(ticker, range) {
const url = `https://query1.finance.yahoo.com/v8/finance/chart/${encodeURIComponent(ticker)}?range=${range}&interval=1d`;
const body = await httpGet(url, { "User-Agent": "Mozilla/5.0 (compatible; duriin-intelligence/1.0)" });
const parsed = JSON.parse(body);
const result = parsed?.chart?.result?.[0];
if (!result) return null;
const ts = result.timestamp || [];
const closes = result.indicators?.quote?.[0]?.close || [];
const out = [];
for (let i = 0; i < ts.length; i++) {
if (closes[i] == null) continue;
out.push({
date: new Date(ts[i] * 1000).toISOString().slice(0, 10),
close: closes[i],
});
}
return out;
}
function nearestOnOrAfter(history, dateStr) {
for (const row of history) {
if (row.date >= dateStr) return row.close;
}
return null;
}
function addTradingDays(dateStr, n) {
const dt = new Date(dateStr);
let count = 0;
while (count < n) {
dt.setDate(dt.getDate() + 1);
const dow = dt.getDay();
if (dow >= 1 && dow <= 5) count++;
}
return dt.toISOString().slice(0, 10);
}
function httpGet(url, headers) {
return new Promise((resolve, reject) => {
const u = new URL(url);
const req = https.request({
hostname: u.hostname,
path: u.pathname + u.search,
method: "GET",
headers,
}, (res) => {
let data = "";
res.on("data", chunk => data += chunk);
res.on("end", () => {
if (res.statusCode >= 200 && res.statusCode < 300) resolve(data);
else reject(new Error(`yahoo ${res.statusCode}: ${data.slice(0, 200)}`));
});
});
req.on("error", reject);
req.end();
});
}
function sleep(ms) {
return new Promise(r => setTimeout(r, ms));
}
module.exports = { runOutcomeWorker };