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
+39
View File
@@ -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('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);
});