feat: add autonomous paper-trading and calibration pipeline
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
const ALLOWED_DIRECTIONS = new Set(['positive', 'negative']);
|
||||
const ALLOWED_HORIZONS = new Set([1, 5, 10, 20, 30, 60, 90]);
|
||||
|
||||
function normalizeProposal(raw, { informationCutoff, model = 'unknown', promptVersion = 'unknown' } = {}) {
|
||||
if (!raw || typeof raw !== 'object') throw new Error('coordinator output must be an object');
|
||||
const predictions = Array.isArray(raw.predictions) ? raw.predictions : [];
|
||||
const normalized = predictions.map((item) => {
|
||||
const instrument = String(item.instrument || item.ticker || '').trim().toUpperCase();
|
||||
const direction = String(item.direction || '').trim().toLowerCase();
|
||||
const horizonDays = Number(item.horizon_days || item.horizonDays);
|
||||
if (!instrument) throw new Error('prediction instrument is required');
|
||||
if (!ALLOWED_DIRECTIONS.has(direction)) throw new Error(`invalid direction: ${direction}`);
|
||||
if (!ALLOWED_HORIZONS.has(horizonDays)) throw new Error(`invalid horizon_days: ${horizonDays}`);
|
||||
const articleIds = Array.isArray(item.evidence_article_ids)
|
||||
? item.evidence_article_ids.map(Number).filter(Number.isInteger)
|
||||
: [];
|
||||
if (articleIds.length === 0) throw new Error(`prediction for ${instrument} has no evidence`);
|
||||
return {
|
||||
instrument,
|
||||
direction,
|
||||
eventType: String(item.event_type || 'unknown').trim().toLowerCase(),
|
||||
causalChannel: item.causal_channel ? String(item.causal_channel).trim() : null,
|
||||
horizonDays,
|
||||
evidenceArticleIds: [...new Set(articleIds)],
|
||||
invalidationCondition: item.invalidation_condition ? String(item.invalidation_condition).trim() : null,
|
||||
};
|
||||
});
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
informationCutoff: informationCutoff || new Date().toISOString(),
|
||||
coordinatorModel: model,
|
||||
promptVersion,
|
||||
predictions: normalized,
|
||||
};
|
||||
}
|
||||
|
||||
function verifyEvidence(archiveDb, articleIds) {
|
||||
const placeholders = articleIds.map(() => '?').join(',');
|
||||
const rows = archiveDb.prepare(`SELECT id FROM articles WHERE id IN (${placeholders})`).all(...articleIds);
|
||||
const found = new Set(rows.map((row) => row.id));
|
||||
return articleIds.every((id) => found.has(id));
|
||||
}
|
||||
|
||||
function acceptProposal(intelligenceDb, archiveDb, raw, metadata = {}) {
|
||||
const proposal = normalizeProposal(raw, metadata);
|
||||
for (const prediction of proposal.predictions) {
|
||||
if (!verifyEvidence(archiveDb, prediction.evidenceArticleIds)) {
|
||||
throw new Error(`proposal references missing evidence for ${prediction.instrument}`);
|
||||
}
|
||||
const instrument = intelligenceDb.prepare(
|
||||
"SELECT tradable FROM autonomy_instruments WHERE symbol = ? AND active = 1 AND tradable = 1"
|
||||
).get(prediction.instrument);
|
||||
if (!instrument) throw new Error(`instrument is not currently allowlisted: ${prediction.instrument}`);
|
||||
}
|
||||
const insert = intelligenceDb.prepare(`
|
||||
INSERT INTO autonomy_proposals
|
||||
(event_id, payload, information_cutoff, coordinator_model, prompt_version, status)
|
||||
VALUES (?, ?, ?, ?, ?, 'accepted')
|
||||
`);
|
||||
const insertPrediction = intelligenceDb.prepare(`
|
||||
INSERT INTO autonomy_predictions
|
||||
(proposal_id, event_id, instrument, direction, event_type, causal_channel,
|
||||
horizon_days, information_cutoff, evidence_article_ids, invalidation_condition, learning_eligible, strategy_version)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
const tx = intelligenceDb.transaction(() => {
|
||||
const proposalResult = insert.run(metadata.eventId || null, JSON.stringify(proposal), proposal.informationCutoff,
|
||||
proposal.coordinatorModel, proposal.promptVersion);
|
||||
for (const prediction of proposal.predictions) {
|
||||
insertPrediction.run(proposalResult.lastInsertRowid, metadata.eventId || null, prediction.instrument,
|
||||
prediction.direction, prediction.eventType, prediction.causalChannel, prediction.horizonDays,
|
||||
proposal.informationCutoff, JSON.stringify(prediction.evidenceArticleIds), prediction.invalidationCondition,
|
||||
metadata.learningEligible ? 1 : 0, metadata.strategyVersion || 'autonomy-1');
|
||||
}
|
||||
return Number(proposalResult.lastInsertRowid);
|
||||
});
|
||||
return { proposalId: tx(), predictionCount: proposal.predictions.length };
|
||||
}
|
||||
|
||||
function recordRejectedProposal(intelligenceDb, raw, metadata = {}, reason = 'validation failed') {
|
||||
const payload = raw && typeof raw === 'object' ? raw : { raw: String(raw) };
|
||||
return intelligenceDb.prepare(`
|
||||
INSERT INTO autonomy_proposals
|
||||
(event_id, payload, information_cutoff, coordinator_model, prompt_version, status, rejection_reason, reviewed_at)
|
||||
VALUES (?, ?, ?, ?, ?, 'rejected', ?, datetime('now'))
|
||||
`).run(metadata.eventId || null, JSON.stringify(payload), metadata.informationCutoff || new Date().toISOString(),
|
||||
metadata.model || 'unknown', metadata.promptVersion || 'unknown', String(reason).slice(0, 1000)).lastInsertRowid;
|
||||
}
|
||||
|
||||
module.exports = { normalizeProposal, verifyEvidence, acceptProposal, recordRejectedProposal };
|
||||
Reference in New Issue
Block a user