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
+76 -9
View File
@@ -1,10 +1,14 @@
const https = require("https");
const http = require("http");
const { getPriceContext, formatPriceContext } = require("./priceContext");
const CONCURRENCY = 4;
const PREDICTION_WINDOW_DAYS = 21;
async function runSignalWorker(archiveDb, intelligenceDb, config) {
const loopDelay = config.workers?.signalLoopDelayMs ?? 1000;
const llmConfig = config.openRouter || {};
// add as_of column if it doesnt exist yet
@@ -28,15 +32,28 @@ async function runSignalWorker(archiveDb, intelligenceDb, config) {
LIMIT 1
`);
// decay window — only feed recent predictions into the signal prompt.
// backtest showed signal degrades sharply after ~10 days, so use 21d as a soft window
const getPredictions = intelligenceDb.prepare(`
SELECT type, direction, magnitude, timeframe, rationale, event_date, id
SELECT type, direction, magnitude, timeframe, rationale, probability, event_date, id
FROM event_predictions
WHERE company_id = ?
AND substr(event_date, 1, 10) <= ?
AND date(substr(event_date, 1, 10)) >= date(?, '-${PREDICTION_WINDOW_DAYS} days')
AND timeframe != 'short'
AND direction IN ('positive', 'negative')
ORDER BY event_date DESC
LIMIT 50
`);
const getCompanyAccuracy = intelligenceDb.prepare(`
SELECT
COUNT(*) as total,
SUM(correct_10d) as correct
FROM prediction_outcomes
WHERE company_id = ? AND correct_10d IS NOT NULL
`);
const getFacts = intelligenceDb.prepare(`
SELECT claim, type, confidence, confirmation_count
FROM company_facts
@@ -118,11 +135,31 @@ async function runSignalWorker(archiveDb, intelligenceDb, config) {
continue;
}
const predictions = getPredictions.all(company_id, checkpoint_date);
const predictions = getPredictions.all(company_id, checkpoint_date, checkpoint_date);
const facts = getFacts.all(company_id, checkpoint_date);
const relationships = getRelationships.all(company_id, checkpoint_date);
const prompt = buildPrompt(company.name, facts, relationships, predictions, checkpoint_date);
// skip if the decay window left us with nothing useful
if (predictions.length === 0) {
inFlight.delete(key);
continue;
}
// pull market context + historical accuracy for this company
let priceBlock = null;
if (company.ticker) {
try {
const snapshot = await getPriceContext(intelligenceDb, company.ticker, checkpoint_date);
priceBlock = formatPriceContext(snapshot, company.ticker);
} catch (_) {}
}
const acc = getCompanyAccuracy.get(company_id);
const accuracyBlock = (acc && acc.total >= 5)
? `Past prediction accuracy for ${company.name}: ${(acc.correct / acc.total * 100).toFixed(0)}% over ${acc.total} evaluated calls.`
: null;
const prompt = buildPrompt(company.name, facts, relationships, predictions, checkpoint_date, priceBlock, accuracyBlock);
let result;
try {
@@ -165,6 +202,10 @@ async function runSignalWorker(archiveDb, intelligenceDb, config) {
} catch (err) {
console.error(`[signal:${id}] cycle error:`, err.message);
} finally {
// Successful and early-exit paths must yield too; otherwise an invalid
// checkpoint can turn this into a tight synchronous SQLite loop.
await sleep(loopDelay);
}
}
}
@@ -179,7 +220,7 @@ async function runSignalWorker(archiveDb, intelligenceDb, config) {
}
function buildPrompt(companyName, facts, relationships, predictions, asOf) {
function buildPrompt(companyName, facts, relationships, predictions, asOf, priceBlock, accuracyBlock) {
const factsBlock = facts.length > 0
? facts.map(f => `- ${f.claim} (confirmed ${f.confirmation_count}x)`).join("\n")
: "No known facts yet.";
@@ -188,9 +229,30 @@ function buildPrompt(companyName, facts, relationships, predictions, asOf) {
? relationships.map(r => `- ${r.relationship_type}: ${r.to_entity} (${r.confidence})`).join("\n")
: "No known relationships.";
const predBlock = predictions.map((p, i) =>
`${i + 1}. [${p.type}] ${p.direction} / ${p.magnitude} / ${p.timeframe}${p.rationale || "no rationale"}`
).join("\n");
// recency-weighted prediction block — newer predictions get a [RECENT] tag,
// and high-magnitude + long-timeframe gets [HIGH CONFIDENCE].
// probability is surfaced when present so the LLM can weight by it.
const asOfMs = new Date(asOf + "T00:00:00Z").getTime();
const predBlock = predictions.map((p, i) => {
const tags = [];
if (p.magnitude === "high" && p.timeframe === "long") tags.push("HIGH CONFIDENCE");
if (p.event_date) {
const ageDays = Math.round((asOfMs - new Date(p.event_date.slice(0, 10) + "T00:00:00Z").getTime()) / 86_400_000);
if (ageDays <= 7) tags.push(`RECENT ${ageDays}d`);
else tags.push(`${ageDays}d old`);
}
const probStr = (typeof p.probability === "number") ? ` p=${p.probability.toFixed(2)}` : "";
const tagStr = tags.length ? ` [${tags.join(", ")}]` : "";
return `${i + 1}. [${p.type}]${tagStr}${probStr} ${p.direction} / ${p.magnitude} / ${p.timeframe}${p.rationale || "no rationale"}`;
}).join("\n");
const pricePart = priceBlock ? `\nMarket context for ${companyName}:\n${priceBlock}\n` : "";
const accPart = accuracyBlock ? `\n${accuracyBlock}\n` : "";
return `You are a financial intelligence analyst generating a trade signal for ${companyName} as of ${asOf}.
@@ -199,10 +261,12 @@ ${factsBlock}
Known relationships:
${relBlock}
Event predictions up to ${asOf}:
${pricePart}${accPart}
Recent event predictions (last 21 days):
${predBlock}
Weight RECENT and HIGH CONFIDENCE predictions more heavily. Discount older predictions and any that lack a probability score. Predictions that disagree with the recent price trajectory are weaker — be sceptical of bullish predictions on a name that has already rallied 20% in 30 days, and vice versa.
Generate a trade signal as JSON with this exact shape:
{
"signal": "BUY | HOLD | SELL",
@@ -214,11 +278,14 @@ Generate a trade signal as JSON with this exact shape:
"summary": "2-3 sentence plain English summary"
}
Default to HOLD when the predictions are mixed, stale, or low-probability. Reserve BUY/SELL for cases where the weight of high-confidence recent evidence is unambiguous.
Risk factors should be derived from:
- Supply chain concentration (heavy dependence on single suppliers)
- Geopolitical exposure (relationships with entities in sensitive regions)
- Competitive threats (strong competitors gaining ground)
- Regulatory exposure (themes mentioning regulation or export controls)
- Stretched valuation given recent price moves
- Negative prediction patterns in recent events
Only output valid JSON. Always respond in English.`;