Files
Duriin-API/test/autonomy.test.js
T
ImBenjiandClaude Opus 5 859d0719b3 fix: stop discarding live predictions the moment they mature
Six live predictions were marked unresolvable, including MSFT twice, WMT and
ITW. Re-running the calculation against yahoo resolves all six, so they were
never unresolvable, they were scored before the market data existed and then
thrown away permanently.

The due-check counted calendar days while calculateOutcome finds the exit bar by
trading days. A friday horizon-1 prediction therefore looked due on saturday,
when monday's close cannot exist. calculateOutcome returned null and the worker
treated null as permanently dead. This hit short horizons hardest, which is
exactly the cohort that produces the first live evidence.

The sql filter stays loose because it cannot know about weekends, and trading day
arithmetic now decides what is genuinely ready. A null result waits for the
horizon to be properly past before anything is retired, and says so when it
finally gives up.

Separately, at horizon 1 the entry and exit lookups could land on the same bar
and produce an excess return of exactly zero, which was recorded as a real
outcome and scored as a directional miss. ITW and WMT both did this. A horizon
that has not elapsed is no longer a measurement.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WnNxwxfXSbeNtjvtz5gayb
2026-09-03 16:28:57 +01:00

339 lines
18 KiB
JavaScript

const test = require('node:test');
const assert = require('node:assert/strict');
const Database = require('better-sqlite3');
const { initAutonomySchema } = require('../src/autonomy/schema');
const { enqueueJob, leaseNextJob, completeJob } = require('../src/autonomy/jobs');
const { normalizeProposal, acceptProposal } = require('../src/autonomy/coordinator');
const { calibrateOutcomes, cohortKey } = require('../src/autonomy/calibration');
const { decide } = require('../src/autonomy/policy');
const { validatePaperIntent, createSimulator } = require('../src/autonomy/execution');
const { calculateOutcome, addTradingDays } = require('../src/autonomy/outcomes');
const { yahooSymbol } = require('../workers/outcomeAutonomyWorker');
const { createOrderIntent } = require('../src/autonomy/orderIntents');
const { enqueueCoordinatorEvent, reconcileArchiveBatch, reconcileLiveBatch } = require('../workers/autonomyWorker');
const { scheduleNext } = require('../workers/replayWorker');
const { refreshHistoricalCalibration, createDecisions, ensureCalibrationColumns } = require('../workers/calibrationWorker');
test('autonomy schema and leased jobs are restart-safe', () => {
const db = new Database(':memory:');
initAutonomySchema(db);
assert.equal(enqueueJob(db, {
jobType: 'enrich', lane: 'live', priority: 10, entityType: 'article', entityId: 42,
idempotencyKey: 'enrich:42',
}).inserted, true);
assert.equal(enqueueJob(db, {
jobType: 'enrich', lane: 'live', priority: 10, entityType: 'article', entityId: 42,
idempotencyKey: 'enrich:42',
}).inserted, false);
const job = leaseNextJob(db, 'test-worker');
assert.equal(job.lane, 'live');
assert.equal(completeJob(db, job.id, 'test-worker'), true);
assert.equal(db.prepare("SELECT status FROM autonomy_jobs WHERE id = ?").get(job.id).status, 'complete');
});
test('coordinator proposals require evidence and contain no arbitrary numeric confidence', () => {
const normalized = normalizeProposal({ predictions: [{
instrument: 'nvda', direction: 'positive', event_type: 'supply_constraint',
horizon_days: 10, evidence_article_ids: [7],
}] }, { informationCutoff: '2026-01-01T00:00:00Z', model: 'test-model' });
assert.equal(normalized.predictions[0].instrument, 'NVDA');
assert.equal('probability' in normalized.predictions[0], false);
assert.throws(() => normalizeProposal({ predictions: [{ instrument: 'NVDA', direction: 'positive', horizon_days: 10 }] }));
});
test('accepted proposal preserves evidence and creates immutable prediction', () => {
const archive = new Database(':memory:');
archive.exec('CREATE TABLE articles (id INTEGER PRIMARY KEY)');
archive.prepare('INSERT INTO articles (id) VALUES (?)').run(7);
const intelligence = new Database(':memory:');
initAutonomySchema(intelligence);
intelligence.prepare("INSERT INTO autonomy_instruments(symbol, broker, active, tradable) VALUES ('NVDA', 'test', 1, 1)").run();
const result = acceptProposal(intelligence, archive, {
predictions: [{ instrument: 'NVDA', direction: 'positive', event_type: 'earnings', horizon_days: 10, evidence_article_ids: [7] }],
}, { informationCutoff: '2026-01-01T00:00:00Z', strategyVersion: 'test' });
assert.equal(result.predictionCount, 1);
assert.deepEqual(JSON.parse(intelligence.prepare('SELECT evidence_article_ids FROM autonomy_predictions').get().evidence_article_ids), [7]);
});
test('replay evidence cannot look beyond its information cutoff and remains non-executable', () => {
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();
assert.throws(() => acceptProposal(intelligence, archive, {
predictions: [{ instrument: 'NVDA', direction: 'positive', event_type: 'earnings', horizon_days: 10, evidence_article_ids: [7] }],
}, { informationCutoff: '2019-12-31T00:00:00Z', origin: 'replay', replayRunId: 1 }), /missing evidence/);
const proposal = intelligence.prepare("INSERT INTO autonomy_proposals(payload, information_cutoff, status) VALUES ('{}', datetime('now'), 'accepted')").run();
const prediction = intelligence.prepare(`INSERT INTO autonomy_predictions
(proposal_id, instrument, direction, event_type, horizon_days, information_cutoff, evidence_article_ids, strategy_version, origin)
VALUES (?, 'NVDA', 'positive', 'test', 10, datetime('now'), '[7]', 'test', 'replay')`).run(proposal.lastInsertRowid);
intelligence.prepare("INSERT INTO autonomy_decisions(prediction_id, action, rationale, strategy_version) VALUES (?, 'BUY', 'test', 'test')").run(prediction.lastInsertRowid);
const executable = intelligence.prepare(`SELECT d.id FROM autonomy_decisions d JOIN autonomy_predictions p ON p.id=d.prediction_id
WHERE d.action IN ('BUY','SELL') AND p.origin='live'`).all();
assert.equal(executable.length, 0);
});
test('replay scheduler skips terminal replay jobs instead of pinning the cursor', () => {
const archive = new Database(':memory:');
archive.exec(`
CREATE TABLE articles (
id INTEGER PRIMARY KEY,
title TEXT,
description TEXT,
content TEXT,
pub_date_effective TEXT,
is_index_page INTEGER
)
`);
archive.prepare("INSERT INTO articles VALUES (1, 'bad', '', 'content', '2020-01-01T00:00:00Z', 0)").run();
archive.prepare("INSERT INTO articles VALUES (2, 'next', '', 'content', '2020-01-02T00:00:00Z', 0)").run();
const intelligence = new Database(':memory:');
initAutonomySchema(intelligence);
const runId = intelligence.prepare(`
INSERT INTO autonomy_replay_runs (watermark_at, strategy_version, prompt_version, coordinator_model)
VALUES ('2020-01-03T00:00:00Z', 'test', 'test', 'test')
`).run().lastInsertRowid;
enqueueJob(intelligence, {
jobType: 'replay_article', lane: 'historical', priority: 1, entityType: 'article', entityId: 1,
idempotencyKey: `replay:${runId}:article:1`,
});
intelligence.prepare("UPDATE autonomy_jobs SET status='dead_letter', attempts=5, last_error='fetch failed'").run();
const run = intelligence.prepare('SELECT * FROM autonomy_replay_runs WHERE id=?').get(runId);
const next = scheduleNext(intelligence, archive, run);
assert.equal(next.id, 2);
assert.equal(intelligence.prepare('SELECT cursor_article_id FROM autonomy_replay_runs WHERE id=?').get(runId).cursor_article_id, 1);
assert.equal(intelligence.prepare("SELECT COUNT(*) count FROM autonomy_jobs WHERE status='pending' AND entity_id='2'").get().count, 1);
});
test('calibration and policy abstain on insufficient evidence', () => {
const calibration = calibrateOutcomes([
{ excess_return: 0.02, direction_correct: 1 },
{ excess_return: -0.01, direction_correct: 0 },
]);
assert.equal(calibration.sampleSize, 2);
const result = decide({ ...calibration }, { minSampleSize: 30 });
assert.equal(result.action, 'ABSTAIN');
assert.equal(cohortKey({ sector: 'tech', eventType: 'earnings', horizonDays: 10, direction: 'positive' }), 'v2|tech|earnings|medium|positive');
assert.equal(decide({
direction: 'negative', probability: 0.8, expectedExcessReturn: -0.02, lowerReturn: -0.04,
sampleSize: 40, distinctInstruments: 9, topInstrumentShare: 0.25,
}).action, 'SELL');
});
test('historical replay outcomes create replay calibration snapshots', () => {
const db = new Database(':memory:');
initAutonomySchema(db);
const proposal = db.prepare("INSERT INTO autonomy_proposals(payload, information_cutoff, status) VALUES ('{}', '2020-01-01T00:00:00Z', 'accepted')").run();
const prediction = db.prepare(`
INSERT INTO autonomy_predictions
(proposal_id, instrument, direction, event_type, horizon_days, information_cutoff, evidence_article_ids,
learning_eligible, strategy_version, origin, replay_run_id, status)
VALUES (?, 'NVDA', 'positive', 'earnings', 10, '2020-01-01T00:00:00Z', '[1]', 0, 'test', 'replay', 7, 'resolved')
`).run(proposal.lastInsertRowid);
db.prepare(`
INSERT INTO autonomy_outcomes(prediction_id, excess_return, direction_correct)
VALUES (?, 0.04, 1)
`).run(prediction.lastInsertRowid);
// one per-run replay snapshot plus the pooled historical/replay snapshot
assert.equal(refreshHistoricalCalibration(db, 'test-cal'), 2);
const snapshot = db.prepare("SELECT source, replay_run_id, sample_size, directional_probability FROM autonomy_calibration_snapshots WHERE source='replay'").get();
assert.equal(snapshot.source, 'replay');
assert.equal(snapshot.replay_run_id, 7);
assert.equal(snapshot.sample_size, 1);
assert(snapshot.directional_probability > 0.5);
});
test('live decisions map calibration snapshot fields into policy inputs', () => {
const db = new Database(':memory:');
initAutonomySchema(db);
const proposal = db.prepare("INSERT INTO autonomy_proposals(payload, information_cutoff, status) VALUES ('{}', datetime('now'), 'accepted')").run();
const prediction = db.prepare(`
INSERT INTO autonomy_predictions
(proposal_id, instrument, direction, event_type, horizon_days, information_cutoff, evidence_article_ids,
learning_eligible, strategy_version, origin, status)
VALUES (?, 'NVDA', 'positive', 'earnings', 10, datetime('now'), '[1]', 1, 'test', 'live', 'open')
`).run(proposal.lastInsertRowid);
ensureCalibrationColumns(db);
db.prepare(`
INSERT INTO autonomy_calibration_snapshots
(cohort_key, sample_size, effective_sample_size, directional_probability, expected_excess_return,
lower_return, upper_return, parent_cohort_key, version, source)
VALUES ('v2|unknown|earnings|medium|positive', 40, 42, 0.7, 0.02, -0.01, 0.06, NULL, 'test-cal', 'live')
`).run();
db.prepare("UPDATE autonomy_calibration_snapshots SET distinct_instruments = 11, top_instrument_share = 0.2").run();
assert.equal(createDecisions(db), 1);
const decision = db.prepare('SELECT * FROM autonomy_decisions WHERE prediction_id=?').get(prediction.lastInsertRowid);
assert.equal(decision.action, 'BUY');
assert.equal(decision.calibrated_probability, 0.7);
assert.equal(decision.expected_excess_return, 0.02);
});
test('paper execution is allowlisted, bounded and idempotent', () => {
const intent = validatePaperIntent({ decisionId: 12, instrument: 'NVDA', action: 'BUY', notional: 100 }, { tradable: true, maxNotional: 500 });
const broker = createSimulator();
assert.deepEqual(broker.submit(intent), broker.submit(intent));
assert.throws(() => validatePaperIntent({ decisionId: 13, instrument: 'PRIVATE', action: 'BUY', notional: 100 }, { tradable: false }));
assert.throws(() => validatePaperIntent({ decisionId: 14, instrument: 'NVDA', action: 'BUY', notional: 501 }, { tradable: true, maxNotional: 500 }));
});
test('archive reconciliation prioritizes recent usable events and is bounded', () => {
const archive = new Database(':memory:');
archive.exec('CREATE TABLE articles (id INTEGER PRIMARY KEY, event_id INTEGER, ingested_at TEXT, content TEXT, has_embedding INTEGER)');
archive.prepare('INSERT INTO articles VALUES (1, 99, ?, ?, 1)').run(new Date().toISOString(), 'content');
const intelligence = new Database(':memory:');
initAutonomySchema(intelligence);
const result = reconcileArchiveBatch(archive, intelligence, 1);
assert.equal(result.scanned, 1);
const job = intelligence.prepare("SELECT lane, priority FROM autonomy_jobs WHERE job_type='coordinator_event'").get();
assert.equal(job.lane, 'live');
assert.equal(job.priority, 100);
const live = reconcileLiveBatch(archive, intelligence, 1);
assert.equal(live.scanned, 1);
});
test('archive reconciliation recovers transient dead-lettered coordinator jobs', () => {
const intelligence = new Database(':memory:');
initAutonomySchema(intelligence);
enqueueJob(intelligence, {
jobType: 'coordinator_event', lane: 'historical', priority: 10, entityType: 'event', entityId: 99,
idempotencyKey: 'coordinator_event:99',
});
intelligence.prepare(`
UPDATE autonomy_jobs
SET status='dead_letter', attempts=5, last_error='TypeError: fetch failed'
WHERE idempotency_key='coordinator_event:99'
`).run();
const result = enqueueCoordinatorEvent(intelligence, {
event_id: 99,
ingested_at: new Date().toISOString(),
content: 'content',
has_embedding: 1,
});
assert.equal(result.recovered, true);
const job = intelligence.prepare("SELECT status, lane, priority, attempts, last_error FROM autonomy_jobs WHERE idempotency_key='coordinator_event:99'").get();
assert.equal(job.status, 'pending');
assert.equal(job.lane, 'live');
assert.equal(job.priority, 100);
assert.equal(job.attempts, 0);
assert.match(job.last_error, /Recovered transient coordinator failure/);
});
test('archive reconciliation leaves non-transient coordinator dead letters alone', () => {
const intelligence = new Database(':memory:');
initAutonomySchema(intelligence);
enqueueJob(intelligence, {
jobType: 'coordinator_event', lane: 'historical', priority: 10, entityType: 'event', entityId: 100,
idempotencyKey: 'coordinator_event:100',
});
intelligence.prepare(`
UPDATE autonomy_jobs
SET status='dead_letter', attempts=5, last_error='no tradable instruments are allowlisted'
WHERE idempotency_key='coordinator_event:100'
`).run();
const result = enqueueCoordinatorEvent(intelligence, {
event_id: 100,
ingested_at: new Date().toISOString(),
content: 'content',
has_embedding: 1,
});
assert.equal(result.recovered, false);
const job = intelligence.prepare("SELECT status, attempts, last_error FROM autonomy_jobs WHERE idempotency_key='coordinator_event:100'").get();
assert.equal(job.status, 'dead_letter');
assert.equal(job.attempts, 5);
assert.equal(job.last_error, 'no tradable instruments are allowlisted');
});
test('outcome calculation uses benchmark-relative return', () => {
const outcome = calculateOutcome(
{ information_cutoff: '2026-01-02T00:00:00Z', horizon_days: 5, direction: 'positive' },
[{ date: '2026-01-02', close: 100 }, { date: '2026-01-09', close: 110 }],
[{ date: '2026-01-02', close: 100 }, { date: '2026-01-09', close: 105 }]
);
assert.equal(outcome.directionCorrect, 1);
assert.equal(outcome.excessReturn, 0.05);
});
test('order intents require the explicit instrument allowlist', () => {
const db = new Database(':memory:');
initAutonomySchema(db);
db.prepare("INSERT INTO autonomy_instruments(symbol, broker, active, tradable) VALUES ('NVDA', 'sim', 1, 1)").run();
const proposal = db.prepare(`INSERT INTO autonomy_proposals(payload, information_cutoff, status) VALUES ('{}', datetime('now'), 'accepted')`).run();
const prediction = db.prepare(`INSERT INTO autonomy_predictions(proposal_id, instrument, direction, event_type, horizon_days, information_cutoff, evidence_article_ids, learning_eligible, strategy_version) VALUES (?, 'NVDA', 'positive', 'test', 10, datetime('now'), '[1]', 1, 'test')`).run(proposal.lastInsertRowid);
const decision = db.prepare(`INSERT INTO autonomy_decisions(prediction_id, action, rationale, strategy_version) VALUES (?, 'BUY', 'test', 'test')`).run(prediction.lastInsertRowid);
const intent = createOrderIntent(db, decision.lastInsertRowid, 100, { maxNotional: 100 });
assert.equal(intent.side, 'buy');
assert.equal(db.prepare('SELECT status FROM autonomy_order_intents').get().status, 'shadow');
});
test('event_type is held to the closed family enum', () => {
const base = { instrument: 'NVDA', direction: 'positive', horizon_days: 10, evidence_article_ids: [1] };
const typeOf = (eventType) => normalizeProposal({ predictions: [{ ...base, event_type: eventType }] }).predictions[0].eventType;
// the enum itself, in the shapes a model actually emits
assert.equal(typeOf('earnings'), 'earnings');
assert.equal(typeOf('Supply Chain'), 'supply_chain');
assert.equal(typeOf('M_AND_A'), 'm_and_a');
// "none of these fit" is a legitimate answer and has to survive
assert.equal(typeOf('other'), 'other');
// off-enum but placeable: salvaged onto the family calibration would have
// picked anyway, so we dont throw away a usable prediction over a label
assert.equal(typeOf('earnings_beat_q3'), 'earnings');
assert.equal(typeOf('ceo resignation'), 'leadership');
// unplaceable free text is the 201-distinct-values failure, and is rejected
assert.throws(() => typeOf('vibes_shifted'), /event_type must be one of/);
assert.throws(() => typeOf(''), /event_type must be one of/);
});
test('dotted tickers are translated to the format the price feed expects', () => {
// BRK.B 404d forever and the outcome worker retried it in a hot loop
assert.equal(yahooSymbol('BRK.B'), 'BRK-B');
assert.equal(yahooSymbol('ABR.PRD'), 'ABR-PRD');
assert.equal(yahooSymbol('aac.u'), 'AAC-U');
// ordinary symbols must pass through untouched
assert.equal(yahooSymbol('NVDA'), 'NVDA');
assert.equal(yahooSymbol(' spy '), 'SPY');
});
test('an outcome whose horizon has not actually elapsed is not a result', () => {
// ITW and WMT both scored exactly 0.00% excess in production because at
// horizon 1 the entry and exit lookups landed on the same bar. Zero is not a
// measurement, and it was being recorded as a directional miss.
const sameBar = calculateOutcome(
{ information_cutoff: '2026-01-02T00:00:00Z', horizon_days: 1, direction: 'positive' },
[{ date: '2026-01-05', close: 100 }],
[{ date: '2026-01-05', close: 100 }]
);
assert.equal(sameBar, null);
// the exit bar genuinely existing still scores normally
const real = calculateOutcome(
{ information_cutoff: '2026-01-02T00:00:00Z', horizon_days: 1, direction: 'positive' },
[{ date: '2026-01-02', close: 100 }, { date: '2026-01-05', close: 104 }],
[{ date: '2026-01-02', close: 100 }, { date: '2026-01-05', close: 102 }]
);
assert.equal(real.directionCorrect, 1);
assert.ok(Math.abs(real.excessReturn - 0.02) < 1e-9);
});
test('trading day arithmetic steps over weekends', () => {
// friday + 1 trading day is monday, not saturday. the old due-check counted
// calendar days and so called a friday horizon-1 prediction due on saturday,
// when monday's close cannot exist yet.
assert.equal(addTradingDays('2026-01-02', 1), '2026-01-05');
assert.equal(addTradingDays('2026-01-02', 5), '2026-01-09');
assert.equal(addTradingDays('2026-01-02', 0), '2026-01-02');
});