96% of rejected proposals name something untradable: indices (SPX, DXY, ^TNX), fx (EURUSD, XAU/USD), futures (CL=F, BZ=F) and home listings (VOW3.DE, RHM.DE, 1211.HK, 688169.SS). The analysis behind those is usually sound, it is the ticker that cannot be used, and nothing in the prompt ever said so. We were paying for the call and discarding the result at validation. The rules point the model at what the allowlist actually holds: US listings and ADRs for foreign companies, and US listed ETFs as the tradable expression of an index, currency, rate or commodity. Every symbol named in the rules was checked against the live allowlist first, so VWAGY, BABA, TM, SONY, SPY, QQQ, GLD, USO, UUP and TLT all genuinely resolve. It also forbids predicting SPY itself, which is the benchmark and whose excess return is zero by construction. Shared between the coordinator and replay prompts rather than written twice, since a rule that drifts between the two lanes is worse than no rule. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WnNxwxfXSbeNtjvtz5gayb
95 lines
5.3 KiB
JavaScript
95 lines
5.3 KiB
JavaScript
const os = require('os');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const { openRuntimeDb } = require('../src/db/runtime');
|
|
const { initAutonomySchema } = require('../src/autonomy/schema');
|
|
const { leaseNextJob, completeJob, failJob } = require('../src/autonomy/jobs');
|
|
const { callCoordinator } = require('../src/autonomy/llm');
|
|
const { acceptProposal, recordRejectedProposal, INSTRUMENT_RULES } = require('../src/autonomy/coordinator');
|
|
const { EVENT_FAMILY_NAMES } = require('../src/autonomy/calibration');
|
|
|
|
function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); }
|
|
|
|
function loadConfig() {
|
|
const configPath = path.resolve(process.env.DURIIN_CONFIG || path.join(__dirname, '..', 'config.json'));
|
|
const raw = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
require('dotenv').config({ path: path.resolve(path.dirname(configPath), '.env') });
|
|
raw.openRouter = { ...(raw.openRouter || {}) };
|
|
if (process.env.OPEN_ROUTER_API_KEY) raw.openRouter.apiKey = process.env.OPEN_ROUTER_API_KEY;
|
|
if (process.env.OPEN_ROUTER_LLM_MODEL) raw.openRouter.llmModel = process.env.OPEN_ROUTER_LLM_MODEL;
|
|
return raw;
|
|
}
|
|
|
|
function buildPrompt(event, articles) {
|
|
const evidence = articles.map((article, index) =>
|
|
`[Evidence ${index + 1}] article_id=${article.id}\nTitle: ${article.title}\n${String(article.content || article.description || '').slice(0, 4000)}`
|
|
).join('\n\n---\n\n');
|
|
return `Event title: ${event.title}\n\n${evidence}\n\nReturn JSON only in this shape:\n${JSON.stringify({ predictions: [{
|
|
instrument: '<ticker supported by the articles>', direction: '<positive or negative>',
|
|
event_type: '<one value from the event_type list below>',
|
|
causal_channel: '<short description>', horizon_days: '<one of 1, 5, 10, 20, 30, 60, 90>',
|
|
evidence_article_ids: ['<article_id values from the evidence above>'],
|
|
invalidation_condition: '<what would falsify this>',
|
|
}] }, null, 2)}\n\nEvery value in that shape is a placeholder describing the field. Do not copy them. Choose instrument, direction and horizon_days from the evidence in front of you.\n\nevent_type must be exactly one of: ${EVENT_FAMILY_NAMES.join(', ')}. Pick the closest one. Use "other" only when none of them genuinely apply, and never invent a value outside this list.\n\n${INSTRUMENT_RULES}\n\nUse only instruments and evidence directly supported by the articles. Return an empty predictions array when there is no clear, tradable hypothesis. Never include probabilities, returns, confidence, position sizes, or actions.`;
|
|
}
|
|
|
|
async function runCoordinatorWorker({ archivePath, intelligencePath, workerId = `coordinator-${os.hostname()}-${process.pid}`, pollMs = 1000 } = {}) {
|
|
const archiveDb = openRuntimeDb(archivePath, { schema: 'archive', readonly: true });
|
|
const intelligenceDb = openRuntimeDb(intelligencePath, { schema: 'intelligence' });
|
|
intelligenceDb.pragma('journal_mode = WAL');
|
|
intelligenceDb.pragma('busy_timeout = 5000');
|
|
initAutonomySchema(intelligenceDb);
|
|
const config = loadConfig();
|
|
while (true) {
|
|
const job = leaseNextJob(intelligenceDb, workerId, 180, ['coordinator_event']);
|
|
if (!job) { await sleep(pollMs); continue; }
|
|
try {
|
|
const event = archiveDb.prepare('SELECT id, title FROM events WHERE id = ?').get(job.entity_id);
|
|
if (!event) throw new Error(`event ${job.entity_id} does not exist`);
|
|
const articles = archiveDb.prepare(`
|
|
SELECT id, title, description, content, pub_date_effective
|
|
FROM articles
|
|
WHERE event_id = ? AND content IS NOT NULL AND content != '' AND is_index_page = 0
|
|
ORDER BY pub_date_effective ASC, id ASC LIMIT 25
|
|
`).all(job.entity_id);
|
|
const allowlisted = intelligenceDb.prepare(
|
|
"SELECT 1 FROM autonomy_instruments WHERE active=1 AND tradable=1 LIMIT 1"
|
|
).get();
|
|
if (!allowlisted) throw new Error('no tradable instruments are allowlisted');
|
|
const historical = job.lane === 'historical';
|
|
const informationCutoff = historical
|
|
? (articles.map((article) => article.pub_date_effective).filter(Boolean).sort().pop() || new Date().toISOString())
|
|
: new Date().toISOString();
|
|
const raw = await callCoordinator(config, buildPrompt(event, articles));
|
|
try {
|
|
acceptProposal(intelligenceDb, archiveDb, raw, {
|
|
eventId: event.id,
|
|
informationCutoff,
|
|
model: config.openRouter.llmModel || 'unknown',
|
|
promptVersion: 'coordinator-1',
|
|
strategyVersion: 'autonomy-1',
|
|
// only a genuine live lane job may ever feed learning
|
|
origin: historical ? 'historical' : 'live',
|
|
learningEligible: !historical,
|
|
});
|
|
} catch (validationError) {
|
|
console.error(`[${workerId}] proposal rejected for event ${event.id}:`, validationError.message);
|
|
recordRejectedProposal(intelligenceDb, raw, {
|
|
eventId: event.id,
|
|
informationCutoff,
|
|
model: config.openRouter.llmModel || 'unknown',
|
|
promptVersion: 'coordinator-1',
|
|
origin: historical ? 'historical' : 'live',
|
|
learningEligible: !historical,
|
|
}, validationError.message);
|
|
}
|
|
completeJob(intelligenceDb, job.id, workerId);
|
|
} catch (error) {
|
|
failJob(intelligenceDb, job.id, workerId, error);
|
|
}
|
|
await sleep(pollMs);
|
|
}
|
|
}
|
|
|
|
module.exports = { buildPrompt, runCoordinatorWorker };
|