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:
+32
-10
@@ -96,15 +96,37 @@ function verifyEvidence(archiveDb, articleIds, informationCutoff = null) {
|
|||||||
|
|
||||||
function acceptProposal(intelligenceDb, archiveDb, raw, metadata = {}) {
|
function acceptProposal(intelligenceDb, archiveDb, raw, metadata = {}) {
|
||||||
const proposal = normalizeProposal(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)) {
|
if (!verifyEvidence(archiveDb, prediction.evidenceArticleIds, proposal.informationCutoff)) {
|
||||||
throw new Error(`proposal references missing evidence for ${prediction.instrument}`);
|
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(`
|
const insert = intelligenceDb.prepare(`
|
||||||
INSERT INTO autonomy_proposals
|
INSERT INTO autonomy_proposals
|
||||||
(event_id, payload, information_cutoff, coordinator_model, prompt_version, status)
|
(event_id, payload, information_cutoff, coordinator_model, prompt_version, status)
|
||||||
@@ -117,18 +139,18 @@ function acceptProposal(intelligenceDb, archiveDb, raw, metadata = {}) {
|
|||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
`);
|
`);
|
||||||
const tx = intelligenceDb.transaction(() => {
|
const tx = intelligenceDb.transaction(() => {
|
||||||
const proposalResult = insert.run(metadata.eventId || null, JSON.stringify(proposal), proposal.informationCutoff,
|
const proposalResult = insert.run(metadata.eventId || null, JSON.stringify(stored), stored.informationCutoff,
|
||||||
proposal.coordinatorModel, proposal.promptVersion);
|
stored.coordinatorModel, stored.promptVersion);
|
||||||
for (const prediction of proposal.predictions) {
|
for (const prediction of tradable) {
|
||||||
insertPrediction.run(proposalResult.lastInsertRowid, metadata.eventId || null, prediction.instrument,
|
insertPrediction.run(proposalResult.lastInsertRowid, metadata.eventId || null, prediction.instrument,
|
||||||
prediction.direction, prediction.eventType, prediction.causalChannel, prediction.horizonDays,
|
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.learningEligible ? 1 : 0, metadata.strategyVersion || 'autonomy-1',
|
||||||
metadata.origin || 'live', metadata.replayRunId || null);
|
metadata.origin || 'live', metadata.replayRunId || null);
|
||||||
}
|
}
|
||||||
return Number(proposalResult.lastInsertRowid);
|
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') {
|
function recordRejectedProposal(intelligenceDb, raw, metadata = {}, reason = 'validation failed') {
|
||||||
|
|||||||
@@ -354,3 +354,42 @@ test('a budget failure is transient but a bad key is not', () => {
|
|||||||
assert.equal(isTransientCoordinatorFailure('TypeError: fetch failed'), true);
|
assert.equal(isTransientCoordinatorFailure('TypeError: fetch failed'), true);
|
||||||
assert.equal(isTransientCoordinatorFailure('request failed with 503'), true);
|
assert.equal(isTransientCoordinatorFailure('request failed with 503'), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('an untradable instrument drops itself, not its valid siblings', () => {
|
||||||
|
const archive = new Database(':memory:');
|
||||||
|
archive.exec('CREATE TABLE articles (id INTEGER PRIMARY KEY, pub_date_effective TEXT, pub_date TEXT, ingested_at TEXT)');
|
||||||
|
archive.prepare("INSERT INTO articles VALUES (7, '2020-01-01T00:00:00Z', NULL, '2020-01-01T00:00:00Z')").run();
|
||||||
|
const intelligence = new Database(':memory:');
|
||||||
|
initAutonomySchema(intelligence);
|
||||||
|
intelligence.prepare("INSERT INTO autonomy_instruments(symbol, broker, active, tradable) VALUES ('NVDA','test',1,1)").run();
|
||||||
|
|
||||||
|
const pred = (instrument) => ({
|
||||||
|
instrument, direction: 'positive', event_type: 'earnings',
|
||||||
|
horizon_days: 10, evidence_article_ids: [7],
|
||||||
|
});
|
||||||
|
|
||||||
|
// EURUSD used to take NVDA down with it and lose the whole proposal
|
||||||
|
const result = acceptProposal(intelligence, archive,
|
||||||
|
{ predictions: [pred('NVDA'), pred('EURUSD')] },
|
||||||
|
{ informationCutoff: '2026-01-01T00:00:00Z', strategyVersion: 'test' });
|
||||||
|
|
||||||
|
assert.equal(result.predictionCount, 1);
|
||||||
|
assert.deepEqual(result.droppedInstruments, ['EURUSD']);
|
||||||
|
const stored = intelligence.prepare('SELECT instrument FROM autonomy_predictions').all();
|
||||||
|
assert.deepEqual(stored.map((r) => r.instrument), ['NVDA']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('lookahead still rejects the whole proposal, not just one prediction', () => {
|
||||||
|
const archive = new Database(':memory:');
|
||||||
|
archive.exec('CREATE TABLE articles (id INTEGER PRIMARY KEY, pub_date_effective TEXT, pub_date TEXT, ingested_at TEXT)');
|
||||||
|
archive.prepare("INSERT INTO articles VALUES (7, '2020-06-01T00:00:00Z', NULL, '2020-06-01T00:00:00Z')").run();
|
||||||
|
const intelligence = new Database(':memory:');
|
||||||
|
initAutonomySchema(intelligence);
|
||||||
|
intelligence.prepare("INSERT INTO autonomy_instruments(symbol, broker, active, tradable) VALUES ('NVDA','test',1,1)").run();
|
||||||
|
|
||||||
|
// evidence postdates the cutoff: corrupt, not merely untradable
|
||||||
|
assert.throws(() => acceptProposal(intelligence, archive, {
|
||||||
|
predictions: [{ instrument: 'NVDA', direction: 'positive', event_type: 'earnings', horizon_days: 10, evidence_article_ids: [7] }],
|
||||||
|
}, { informationCutoff: '2020-01-01T00:00:00Z' }), /missing evidence/);
|
||||||
|
assert.equal(intelligence.prepare('SELECT COUNT(*) AS n FROM autonomy_predictions').get().n, 0);
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user