fix: drop untradable predictions instead of the whole proposal

One untradable ticker rejected everything alongside it. In a single day that was
93 proposals discarding 171 predictions, and 76 of those named something we could
trade perfectly well. They were lost because a sibling in the same response said
EURUSD.

Tradability is a filter, so it applies per prediction now. The untradable one is
dropped and logged, its siblings are kept, and the stored payload records what
was removed so the filtering is auditable rather than invisible.

Lookahead deliberately still rejects the entire proposal. Evidence that did not
exist at the proposal's own cutoff means the response is corrupt rather than
merely untradable, and keeping the rest of it would hide the one thing most worth
seeing. Both halves are pinned by tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WnNxwxfXSbeNtjvtz5gayb
This commit is contained in:
ImBenji
2026-09-04 20:16:55 +01:00
co-authored by Claude Opus 5
parent 75e183aaa3
commit f464c94708
2 changed files with 71 additions and 10 deletions
+32 -10
View File
@@ -96,15 +96,37 @@ function verifyEvidence(archiveDb, articleIds, informationCutoff = null) {
function acceptProposal(intelligenceDb, archiveDb, raw, metadata = {}) {
const proposal = normalizeProposal(raw, metadata);
for (const prediction of proposal.predictions) {
// Tradability is a filter, not an integrity failure, so it is applied per
// prediction. One untradable ticker used to reject the entire proposal and take
// its valid siblings down with it: in a single day 93 proposals were rejected
// this way, discarding 171 predictions of which 76 named something we could
// trade perfectly well.
const allowlisted = intelligenceDb.prepare(
"SELECT tradable FROM autonomy_instruments WHERE symbol = ? AND active = 1 AND tradable = 1"
);
const dropped = [];
const tradable = proposal.predictions.filter((prediction) => {
if (allowlisted.get(prediction.instrument)) return true;
dropped.push(prediction.instrument);
return false;
});
if (dropped.length) {
console.warn(`[coordinator] dropped ${dropped.length} untradable instrument(s): ${dropped.join(', ')}`
+ ` (kept ${tradable.length})`);
}
// Lookahead stays all or nothing. A proposal citing evidence that did not exist
// at its own cutoff is corrupt rather than merely untradable, and quietly keeping
// the rest of it would hide exactly the thing we most need to see.
for (const prediction of tradable) {
if (!verifyEvidence(archiveDb, prediction.evidenceArticleIds, proposal.informationCutoff)) {
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}`);
}
// what actually got stored, plus a record of what was filtered and why
const stored = { ...proposal, predictions: tradable, droppedInstruments: dropped };
const insert = intelligenceDb.prepare(`
INSERT INTO autonomy_proposals
(event_id, payload, information_cutoff, coordinator_model, prompt_version, status)
@@ -117,18 +139,18 @@ function acceptProposal(intelligenceDb, archiveDb, raw, metadata = {}) {
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) {
const proposalResult = insert.run(metadata.eventId || null, JSON.stringify(stored), stored.informationCutoff,
stored.coordinatorModel, stored.promptVersion);
for (const prediction of tradable) {
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,
stored.informationCutoff, JSON.stringify(prediction.evidenceArticleIds), prediction.invalidationCondition,
metadata.learningEligible ? 1 : 0, metadata.strategyVersion || 'autonomy-1',
metadata.origin || 'live', metadata.replayRunId || null);
}
return Number(proposalResult.lastInsertRowid);
});
return { proposalId: tx(), predictionCount: proposal.predictions.length };
return { proposalId: tx(), predictionCount: tradable.length, droppedInstruments: dropped };
}
function recordRejectedProposal(intelligenceDb, raw, metadata = {}, reason = 'validation failed') {