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
396 lines
22 KiB
JavaScript
396 lines
22 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, isTransientCoordinatorFailure } = 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');
|
|
});
|
|
|
|
test('a budget failure is transient but a bad key is not', () => {
|
|
const quota = 'Error: coordinator request failed with 403: {"error":{"message":"Key limit exceeded (monthly limit)."}}';
|
|
const credits = 'LLM 402: {"error":{"message":"Insufficient credits. Add more using ..."}}';
|
|
const afford = 'coordinator request failed with 402: can only afford 3921 tokens';
|
|
assert.equal(isTransientCoordinatorFailure(quota), true);
|
|
assert.equal(isTransientCoordinatorFailure(credits), true);
|
|
assert.equal(isTransientCoordinatorFailure(afford), true);
|
|
|
|
// these must stay dead, retrying them forever helps nobody
|
|
assert.equal(isTransientCoordinatorFailure('request failed with 403: invalid api key'), false);
|
|
assert.equal(isTransientCoordinatorFailure('request failed with 401: unauthorized'), false);
|
|
assert.equal(isTransientCoordinatorFailure('proposal references missing evidence'), false);
|
|
|
|
// and the pre-existing transient cases still are
|
|
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);
|
|
});
|