feat: add autonomous paper-trading and calibration pipeline
This commit is contained in:
+76
-9
@@ -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.`;
|
||||
|
||||
Reference in New Issue
Block a user