feat: add autonomous paper-trading and calibration pipeline
This commit is contained in:
+111
-12
@@ -2,6 +2,11 @@ const https = require("https");
|
||||
const http = require("http");
|
||||
|
||||
const { findMatchedCompaniesByEmbedding } = require("./embeddings");
|
||||
const { getPriceContext, formatPriceContext } = require("./priceContext");
|
||||
|
||||
const REPROCESS_MIN_NEW_ARTICLES = 3;
|
||||
const REPROCESS_COOLDOWN_HOURS = 6;
|
||||
|
||||
|
||||
async function runAugorWorker(archiveDb, intelligenceDb, config) {
|
||||
const loopDelay = config.workers?.augorLoopDelayMs ?? 1500;
|
||||
@@ -11,6 +16,26 @@ async function runAugorWorker(archiveDb, intelligenceDb, config) {
|
||||
SELECT * FROM article_queue WHERE status = 'pending' LIMIT 1
|
||||
`);
|
||||
|
||||
const getProcessingState = intelligenceDb.prepare(
|
||||
"SELECT last_processed_at, articles_at_last_run FROM event_processing_state WHERE event_id = ?"
|
||||
);
|
||||
|
||||
const upsertProcessingState = intelligenceDb.prepare(`
|
||||
INSERT INTO event_processing_state (event_id, last_processed_at, articles_at_last_run)
|
||||
VALUES (?, CURRENT_TIMESTAMP, ?)
|
||||
ON CONFLICT(event_id) DO UPDATE SET
|
||||
last_processed_at = CURRENT_TIMESTAMP,
|
||||
articles_at_last_run = excluded.articles_at_last_run
|
||||
`);
|
||||
|
||||
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 recordEvent = intelligenceDb.prepare(
|
||||
`INSERT INTO worker_events (worker) VALUES ('augor')`
|
||||
);
|
||||
@@ -45,8 +70,8 @@ async function runAugorWorker(archiveDb, intelligenceDb, config) {
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`);
|
||||
const insertPrediction = intelligenceDb.prepare(`
|
||||
INSERT INTO event_predictions (event_id, company_id, type, direction, magnitude, timeframe, rationale, event_date)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO event_predictions (event_id, company_id, type, direction, magnitude, timeframe, rationale, probability, event_date)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
const getEventDate = archiveDb.prepare(`
|
||||
@@ -66,7 +91,6 @@ async function runAugorWorker(archiveDb, intelligenceDb, config) {
|
||||
const queueRow = getPending.get();
|
||||
|
||||
if (!queueRow) {
|
||||
await sleep(loopDelay);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -98,12 +122,29 @@ async function runAugorWorker(archiveDb, intelligenceDb, config) {
|
||||
|
||||
const eventArticleIds = eventArticles.map(a => a.id);
|
||||
|
||||
|
||||
// event-level batching guard — skip re-processing if we already ran on this event recently
|
||||
// and not enough new articles have arrived to justify another LLM call
|
||||
const state = getProcessingState.get(eventId);
|
||||
if (state && state.last_processed_at) {
|
||||
const newArticles = eventArticles.length - state.articles_at_last_run;
|
||||
const lastRunMs = new Date(state.last_processed_at + "Z").getTime();
|
||||
const hoursSince = (Date.now() - lastRunMs) / 3_600_000;
|
||||
|
||||
if (newArticles < REPROCESS_MIN_NEW_ARTICLES && hoursSince < REPROCESS_COOLDOWN_HOURS) {
|
||||
for (const r of getEventArticleIds.all(eventId)) setStatusByArticleId.run(r.id);
|
||||
console.log(`[augor] event ${eventId} — only ${newArticles} new articles in ${hoursSince.toFixed(1)}h, skipping re-process`);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const matchedCompanies = findMatchedCompaniesByEmbedding(
|
||||
eventArticleIds, archiveDb, intelligenceDb, config
|
||||
);
|
||||
|
||||
if (matchedCompanies.length === 0) {
|
||||
for (const r of getEventArticleIds.all(eventId)) setStatusByArticleId.run(r.id);
|
||||
upsertProcessingState.run(eventId, eventArticles.length);
|
||||
console.log(`[augor] event ${eventId} — no company match, skipped`);
|
||||
continue;
|
||||
}
|
||||
@@ -114,6 +155,7 @@ async function runAugorWorker(archiveDb, intelligenceDb, config) {
|
||||
|
||||
const eventDateRow = getEventDate.get(eventId);
|
||||
const eventDate = eventDateRow ? eventDateRow.pub_date_effective : null;
|
||||
const eventDateOnly = eventDate ? eventDate.slice(0, 10) : null;
|
||||
|
||||
const articleText = eventArticles.map((a, i) => {
|
||||
const body = (a.content || a.description || "").slice(0, 2000);
|
||||
@@ -130,9 +172,30 @@ async function runAugorWorker(archiveDb, intelligenceDb, config) {
|
||||
factsBlock = `Known facts about ${company.name}:\n${lines}`;
|
||||
}
|
||||
|
||||
const result = await callLlm(llmConfig, buildPrompt(company.name, event.title, articleText, factsBlock));
|
||||
|
||||
// pull live market context for the company at the time of the event
|
||||
let priceBlock = null;
|
||||
if (company.ticker && eventDateOnly) {
|
||||
try {
|
||||
const snapshot = await getPriceContext(intelligenceDb, company.ticker, eventDateOnly);
|
||||
priceBlock = formatPriceContext(snapshot, company.ticker);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
|
||||
// historical accuracy of past predictions for this company
|
||||
let accuracyBlock = null;
|
||||
const acc = getCompanyAccuracy.get(company.id);
|
||||
if (acc && acc.total >= 5) {
|
||||
const pct = (acc.correct / acc.total * 100).toFixed(0);
|
||||
accuracyBlock = `Past prediction accuracy for ${company.name}: ${pct}% over ${acc.total} evaluated calls.`;
|
||||
}
|
||||
|
||||
const result = await callLlm(llmConfig, buildPrompt(company.name, event.title, articleText, factsBlock, priceBlock, accuracyBlock));
|
||||
|
||||
if (result) {
|
||||
const seenPreds = new Set();
|
||||
|
||||
const writeAll = intelligenceDb.transaction(() => {
|
||||
for (const r of (result.knowledge?.relationships || [])) {
|
||||
insertKnowledge.run(eventId, company.id, "relationship", JSON.stringify(r), eventDate);
|
||||
@@ -145,7 +208,21 @@ async function runAugorWorker(archiveDb, intelligenceDb, config) {
|
||||
}
|
||||
|
||||
for (const p of (result.predictions || [])) {
|
||||
insertPrediction.run(eventId, company.id, p.type, p.direction, p.magnitude, p.timeframe, p.rationale, eventDate);
|
||||
// hard filter — neutral predictions are dead weight, skip them
|
||||
if (p.direction === "neutral") continue;
|
||||
if (p.direction !== "positive" && p.direction !== "negative") continue;
|
||||
|
||||
const key = `${p.type}|${p.direction}|${p.magnitude}|${p.timeframe}`;
|
||||
if (seenPreds.has(key)) continue;
|
||||
seenPreds.add(key);
|
||||
|
||||
const prob = typeof p.probability === "number" && p.probability >= 0 && p.probability <= 1
|
||||
? p.probability
|
||||
: null;
|
||||
|
||||
insertPrediction.run(
|
||||
eventId, company.id, p.type, p.direction, p.magnitude, p.timeframe, p.rationale, prob, eventDate
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -158,6 +235,7 @@ async function runAugorWorker(archiveDb, intelligenceDb, config) {
|
||||
}
|
||||
|
||||
for (const r of getEventArticleIds.all(eventId)) setStatusByArticleId.run(r.id);
|
||||
upsertProcessingState.run(eventId, eventArticles.length);
|
||||
recordEvent.run();
|
||||
pruneCounter++;
|
||||
if (pruneCounter >= 100) { pruneEvents.run(); pruneCounter = 0; }
|
||||
@@ -165,23 +243,44 @@ async function runAugorWorker(archiveDb, intelligenceDb, config) {
|
||||
|
||||
} catch (err) {
|
||||
console.error("[augor] error:", err.message);
|
||||
} finally {
|
||||
// Enforce pacing on every path, including the many early `continue`
|
||||
// branches for skipped or already-processed events.
|
||||
await sleep(loopDelay);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function buildPrompt(companyName, eventTitle, articleText, factsBlock) {
|
||||
function buildPrompt(companyName, eventTitle, articleText, factsBlock, priceBlock, accuracyBlock) {
|
||||
const factsPart = factsBlock ? `${factsBlock}\n\n` : "";
|
||||
const pricePart = priceBlock ? `Market context for ${companyName}:\n${priceBlock}\n\n` : "";
|
||||
const accPart = accuracyBlock ? `${accuracyBlock}\n\n` : "";
|
||||
|
||||
return `You are a financial intelligence analyst focused on ${companyName}. Always respond in English regardless of the language of the input articles.
|
||||
return `You are a financial intelligence analyst. Always respond in English.
|
||||
|
||||
${factsPart}Assess the impact of the following news event on ${companyName} given what you already know about the company.
|
||||
|
||||
Event: ${eventTitle}
|
||||
${factsPart}${pricePart}${accPart}Event: ${eventTitle}
|
||||
|
||||
${articleText}
|
||||
|
||||
Return JSON only — no explanation. Shape:
|
||||
Analyze the impact of this event on ${companyName}. Return JSON only — no explanation.
|
||||
|
||||
Strict rules — read carefully:
|
||||
- predictions must be directly caused by THIS specific event — no speculation, no priced-in narrative
|
||||
- only emit a prediction if the evidence is unambiguous AND the effect on ${companyName} is concrete and quantifiable
|
||||
- the default answer is no prediction. an empty predictions array is the correct output for most news. only emit one when the event clearly moves the needle
|
||||
- never emit two predictions that share type+direction+magnitude+timeframe — collapse them into one
|
||||
- direction must be "positive" or "negative" only — no neutral predictions, ever. if the impact is unclear, emit nothing
|
||||
- magnitude:
|
||||
"high" = major revenue/market-share shift, expected >5% stock move
|
||||
"medium" = measurable but limited, expected 1-5% stock move
|
||||
omit any prediction that doesnt clear the medium bar
|
||||
- timeframe:
|
||||
"short" = days to 2 weeks (use sparingly — short-horizon predictions are unreliable)
|
||||
"medium" = 2 weeks to 3 months
|
||||
"long" = 3+ months — preferred when the thesis is structural
|
||||
- probability: your honest calibrated probability that the directional call is correct over the stated timeframe, as a number between 0.5 and 0.95. if you cant honestly assign >= 0.6, dont emit the prediction
|
||||
- if the market context shows the stock has already moved sharply (>10% in 30 days), be sceptical that this event adds new information — the move may already be priced in
|
||||
|
||||
{
|
||||
"knowledge": {
|
||||
"relationships": [
|
||||
@@ -195,7 +294,7 @@ Return JSON only — no explanation. Shape:
|
||||
]
|
||||
},
|
||||
"predictions": [
|
||||
{ "type": "market_share|stock_price|competitive_position|other", "direction": "positive|negative|neutral", "magnitude": "high|medium|low", "timeframe": "short|medium|long", "rationale": "string" }
|
||||
{ "type": "market_share|stock_price|competitive_position|other", "direction": "positive|negative", "magnitude": "high|medium", "timeframe": "short|medium|long", "probability": 0.0, "rationale": "string" }
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user