feat: add autonomous paper-trading and calibration pipeline
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
function clamp(value, min, max) { return Math.max(min, Math.min(max, value)); }
|
||||
|
||||
function betaMean(wins, total, priorWins = 1, priorLosses = 1) {
|
||||
return (wins + priorWins) / (total + priorWins + priorLosses);
|
||||
}
|
||||
|
||||
function quantile(values, q) {
|
||||
if (!values.length) return null;
|
||||
const sorted = [...values].sort((a, b) => a - b);
|
||||
const position = (sorted.length - 1) * q;
|
||||
const lower = Math.floor(position);
|
||||
const upper = Math.ceil(position);
|
||||
if (lower === upper) return sorted[lower];
|
||||
return sorted[lower] + (sorted[upper] - sorted[lower]) * (position - lower);
|
||||
}
|
||||
|
||||
function cohortKey({ direction, eventType, horizonDays, sector = 'unknown' }) {
|
||||
return [sector, eventType || 'unknown', horizonDays, direction].join('|');
|
||||
}
|
||||
|
||||
function calibrateOutcomes(rows, parent = null) {
|
||||
const clean = rows.filter((row) => Number.isFinite(Number(row.excess_return)));
|
||||
const wins = clean.filter((row) => Number(row.direction_correct) === 1).length;
|
||||
const total = clean.length;
|
||||
const priorProbability = parent ? parent.directionalProbability : 0.5;
|
||||
const priorStrength = parent ? Math.max(2, Math.min(20, parent.effectiveSampleSize / 10)) : 2;
|
||||
const probability = (wins + priorProbability * priorStrength) / (total + priorStrength);
|
||||
const returns = clean.map((row) => Number(row.excess_return));
|
||||
return {
|
||||
sampleSize: total,
|
||||
effectiveSampleSize: total + priorStrength,
|
||||
directionalProbability: clamp(probability, 0.01, 0.99),
|
||||
expectedExcessReturn: returns.length ? returns.reduce((sum, value) => sum + value, 0) / returns.length : null,
|
||||
lowerReturn: quantile(returns, 0.1),
|
||||
upperReturn: quantile(returns, 0.9),
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { betaMean, cohortKey, calibrateOutcomes, quantile };
|
||||
@@ -0,0 +1,90 @@
|
||||
const ALLOWED_DIRECTIONS = new Set(['positive', 'negative']);
|
||||
const ALLOWED_HORIZONS = new Set([1, 5, 10, 20, 30, 60, 90]);
|
||||
|
||||
function normalizeProposal(raw, { informationCutoff, model = 'unknown', promptVersion = 'unknown' } = {}) {
|
||||
if (!raw || typeof raw !== 'object') throw new Error('coordinator output must be an object');
|
||||
const predictions = Array.isArray(raw.predictions) ? raw.predictions : [];
|
||||
const normalized = predictions.map((item) => {
|
||||
const instrument = String(item.instrument || item.ticker || '').trim().toUpperCase();
|
||||
const direction = String(item.direction || '').trim().toLowerCase();
|
||||
const horizonDays = Number(item.horizon_days || item.horizonDays);
|
||||
if (!instrument) throw new Error('prediction instrument is required');
|
||||
if (!ALLOWED_DIRECTIONS.has(direction)) throw new Error(`invalid direction: ${direction}`);
|
||||
if (!ALLOWED_HORIZONS.has(horizonDays)) throw new Error(`invalid horizon_days: ${horizonDays}`);
|
||||
const articleIds = Array.isArray(item.evidence_article_ids)
|
||||
? item.evidence_article_ids.map(Number).filter(Number.isInteger)
|
||||
: [];
|
||||
if (articleIds.length === 0) throw new Error(`prediction for ${instrument} has no evidence`);
|
||||
return {
|
||||
instrument,
|
||||
direction,
|
||||
eventType: String(item.event_type || 'unknown').trim().toLowerCase(),
|
||||
causalChannel: item.causal_channel ? String(item.causal_channel).trim() : null,
|
||||
horizonDays,
|
||||
evidenceArticleIds: [...new Set(articleIds)],
|
||||
invalidationCondition: item.invalidation_condition ? String(item.invalidation_condition).trim() : null,
|
||||
};
|
||||
});
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
informationCutoff: informationCutoff || new Date().toISOString(),
|
||||
coordinatorModel: model,
|
||||
promptVersion,
|
||||
predictions: normalized,
|
||||
};
|
||||
}
|
||||
|
||||
function verifyEvidence(archiveDb, articleIds) {
|
||||
const placeholders = articleIds.map(() => '?').join(',');
|
||||
const rows = archiveDb.prepare(`SELECT id FROM articles WHERE id IN (${placeholders})`).all(...articleIds);
|
||||
const found = new Set(rows.map((row) => row.id));
|
||||
return articleIds.every((id) => found.has(id));
|
||||
}
|
||||
|
||||
function acceptProposal(intelligenceDb, archiveDb, raw, metadata = {}) {
|
||||
const proposal = normalizeProposal(raw, metadata);
|
||||
for (const prediction of proposal.predictions) {
|
||||
if (!verifyEvidence(archiveDb, prediction.evidenceArticleIds)) {
|
||||
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}`);
|
||||
}
|
||||
const insert = intelligenceDb.prepare(`
|
||||
INSERT INTO autonomy_proposals
|
||||
(event_id, payload, information_cutoff, coordinator_model, prompt_version, status)
|
||||
VALUES (?, ?, ?, ?, ?, 'accepted')
|
||||
`);
|
||||
const insertPrediction = intelligenceDb.prepare(`
|
||||
INSERT INTO autonomy_predictions
|
||||
(proposal_id, event_id, instrument, direction, event_type, causal_channel,
|
||||
horizon_days, information_cutoff, evidence_article_ids, invalidation_condition, learning_eligible, strategy_version)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
const tx = intelligenceDb.transaction(() => {
|
||||
const proposalResult = insert.run(metadata.eventId || null, JSON.stringify(proposal), proposal.informationCutoff,
|
||||
proposal.coordinatorModel, proposal.promptVersion);
|
||||
for (const prediction of proposal.predictions) {
|
||||
insertPrediction.run(proposalResult.lastInsertRowid, metadata.eventId || null, prediction.instrument,
|
||||
prediction.direction, prediction.eventType, prediction.causalChannel, prediction.horizonDays,
|
||||
proposal.informationCutoff, JSON.stringify(prediction.evidenceArticleIds), prediction.invalidationCondition,
|
||||
metadata.learningEligible ? 1 : 0, metadata.strategyVersion || 'autonomy-1');
|
||||
}
|
||||
return Number(proposalResult.lastInsertRowid);
|
||||
});
|
||||
return { proposalId: tx(), predictionCount: proposal.predictions.length };
|
||||
}
|
||||
|
||||
function recordRejectedProposal(intelligenceDb, raw, metadata = {}, reason = 'validation failed') {
|
||||
const payload = raw && typeof raw === 'object' ? raw : { raw: String(raw) };
|
||||
return intelligenceDb.prepare(`
|
||||
INSERT INTO autonomy_proposals
|
||||
(event_id, payload, information_cutoff, coordinator_model, prompt_version, status, rejection_reason, reviewed_at)
|
||||
VALUES (?, ?, ?, ?, ?, 'rejected', ?, datetime('now'))
|
||||
`).run(metadata.eventId || null, JSON.stringify(payload), metadata.informationCutoff || new Date().toISOString(),
|
||||
metadata.model || 'unknown', metadata.promptVersion || 'unknown', String(reason).slice(0, 1000)).lastInsertRowid;
|
||||
}
|
||||
|
||||
module.exports = { normalizeProposal, verifyEvidence, acceptProposal, recordRejectedProposal };
|
||||
@@ -0,0 +1,34 @@
|
||||
function makeClientOrderId(decisionId) {
|
||||
return `duriin-${String(decisionId)}`;
|
||||
}
|
||||
|
||||
function validatePaperIntent(intent, constraints = {}) {
|
||||
if (!intent || !intent.decisionId || !intent.instrument) throw new Error('decisionId and instrument are required');
|
||||
if (!['BUY', 'SELL'].includes(intent.action)) throw new Error('only BUY and SELL create order intents');
|
||||
if (!Number.isFinite(Number(intent.notional)) || Number(intent.notional) <= 0) throw new Error('notional must be positive');
|
||||
const maxNotional = Number(constraints.maxNotional ?? 1000);
|
||||
if (Number(intent.notional) > maxNotional) throw new Error('notional exceeds paper risk limit');
|
||||
if (constraints.tradable !== true) throw new Error('instrument is not confirmed tradable');
|
||||
return {
|
||||
clientOrderId: makeClientOrderId(intent.decisionId),
|
||||
instrument: String(intent.instrument).toUpperCase(),
|
||||
side: intent.action === 'BUY' ? 'buy' : 'sell',
|
||||
notional: Number(intent.notional),
|
||||
mode: 'paper',
|
||||
};
|
||||
}
|
||||
|
||||
function createSimulator() {
|
||||
const orders = new Map();
|
||||
return {
|
||||
submit(intent) {
|
||||
if (orders.has(intent.clientOrderId)) return orders.get(intent.clientOrderId);
|
||||
const order = { ...intent, brokerOrderId: `sim-${intent.clientOrderId}`, status: 'accepted' };
|
||||
orders.set(intent.clientOrderId, order);
|
||||
return order;
|
||||
},
|
||||
get(clientOrderId) { return orders.get(clientOrderId) || null; },
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { makeClientOrderId, validatePaperIntent, createSimulator };
|
||||
@@ -0,0 +1,17 @@
|
||||
const { initAutonomySchema } = require('./schema');
|
||||
const jobs = require('./jobs');
|
||||
const coordinator = require('./coordinator');
|
||||
const calibration = require('./calibration');
|
||||
const policy = require('./policy');
|
||||
const execution = require('./execution');
|
||||
const orderIntents = require('./orderIntents');
|
||||
|
||||
module.exports = {
|
||||
initAutonomySchema,
|
||||
...jobs,
|
||||
...coordinator,
|
||||
...calibration,
|
||||
...policy,
|
||||
...execution,
|
||||
...orderIntents,
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
function enqueueJob(db, { jobType, lane = 'historical', priority = 0, entityType, entityId, idempotencyKey }) {
|
||||
const result = db.prepare(`
|
||||
INSERT OR IGNORE INTO autonomy_jobs
|
||||
(job_type, lane, priority, entity_type, entity_id, idempotency_key)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`).run(jobType, lane, priority, entityType, String(entityId), idempotencyKey);
|
||||
return { inserted: result.changes > 0 };
|
||||
}
|
||||
|
||||
function leaseNextJob(db, workerId, leaseSeconds = 60, jobTypes = null) {
|
||||
const tx = db.transaction(() => {
|
||||
const typeClause = Array.isArray(jobTypes) && jobTypes.length
|
||||
? `AND job_type IN (${jobTypes.map(() => '?').join(',')})`
|
||||
: '';
|
||||
const typeParams = Array.isArray(jobTypes) && jobTypes.length ? jobTypes : [];
|
||||
const job = db.prepare(`
|
||||
SELECT * FROM autonomy_jobs
|
||||
WHERE ((status = 'pending' AND datetime(available_at) <= datetime('now'))
|
||||
OR (status = 'leased' AND datetime(lease_expires_at) <= datetime('now')))
|
||||
${typeClause}
|
||||
ORDER BY CASE lane WHEN 'live' THEN 3 WHEN 'maintenance' THEN 2 ELSE 1 END DESC,
|
||||
priority DESC, id ASC
|
||||
LIMIT 1
|
||||
`).get(...typeParams);
|
||||
if (!job) return null;
|
||||
const updated = db.prepare(`
|
||||
UPDATE autonomy_jobs
|
||||
SET status = 'leased', leased_by = ?, lease_expires_at = datetime('now', ?),
|
||||
attempts = attempts + 1
|
||||
WHERE id = ?
|
||||
`).run(workerId, `+${Math.max(1, Math.floor(leaseSeconds))} seconds`, job.id);
|
||||
return updated.changes ? { ...job, status: 'leased', leased_by: workerId } : null;
|
||||
});
|
||||
return tx();
|
||||
}
|
||||
|
||||
function completeJob(db, id, workerId) {
|
||||
return db.prepare(`
|
||||
UPDATE autonomy_jobs
|
||||
SET status = 'complete', completed_at = datetime('now'),
|
||||
leased_by = NULL, lease_expires_at = NULL
|
||||
WHERE id = ? AND leased_by = ?
|
||||
`).run(id, workerId).changes > 0;
|
||||
}
|
||||
|
||||
function failJob(db, id, workerId, error, maxAttempts = 5) {
|
||||
return db.prepare(`
|
||||
UPDATE autonomy_jobs
|
||||
SET status = CASE WHEN attempts >= ? THEN 'dead_letter' ELSE 'pending' END,
|
||||
available_at = datetime('now', '+60 seconds'), last_error = ?,
|
||||
leased_by = NULL, lease_expires_at = NULL
|
||||
WHERE id = ? AND leased_by = ?
|
||||
`).run(maxAttempts, String(error || 'unknown error').slice(0, 2000), id, workerId).changes > 0;
|
||||
}
|
||||
|
||||
module.exports = { enqueueJob, leaseNextJob, completeJob, failJob };
|
||||
@@ -0,0 +1,32 @@
|
||||
function extractJson(text) {
|
||||
const value = String(text || '').trim().replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/, '');
|
||||
try { return JSON.parse(value); } catch (_) {
|
||||
const start = value.indexOf('{');
|
||||
const end = value.lastIndexOf('}');
|
||||
if (start >= 0 && end > start) return JSON.parse(value.slice(start, end + 1));
|
||||
throw new Error('LLM response did not contain valid JSON');
|
||||
}
|
||||
}
|
||||
|
||||
async function callCoordinator(config, prompt) {
|
||||
const apiKey = String(config?.openRouter?.apiKey || '').trim();
|
||||
if (!apiKey) throw new Error('OpenRouter API key is not configured');
|
||||
const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
model: config.openRouter.llmModel,
|
||||
temperature: 0,
|
||||
response_format: { type: 'json_object' },
|
||||
messages: [
|
||||
{ role: 'system', content: 'You are a coordinator. Extract only evidence-backed categorical hypotheses. Never output probabilities, expected returns, confidence scores, position sizes, or trade actions.' },
|
||||
{ role: 'user', content: prompt },
|
||||
],
|
||||
}),
|
||||
});
|
||||
if (!response.ok) throw new Error(`coordinator request failed with ${response.status}`);
|
||||
const body = await response.json();
|
||||
return extractJson(body?.choices?.[0]?.message?.content);
|
||||
}
|
||||
|
||||
module.exports = { extractJson, callCoordinator };
|
||||
@@ -0,0 +1,25 @@
|
||||
const { validatePaperIntent } = require('./execution');
|
||||
|
||||
function createOrderIntent(db, decisionId, notional, constraints = {}) {
|
||||
const row = db.prepare(`
|
||||
SELECT d.id AS decision_id, d.action, p.instrument,
|
||||
COALESCE(ai.active, 0) AS active, COALESCE(ai.tradable, 0) AS tradable
|
||||
FROM autonomy_decisions d
|
||||
JOIN autonomy_predictions p ON p.id = d.prediction_id
|
||||
LEFT JOIN autonomy_instruments ai ON ai.symbol = p.instrument
|
||||
WHERE d.id = ?
|
||||
`).get(decisionId);
|
||||
if (!row) throw new Error(`decision ${decisionId} does not exist`);
|
||||
const intent = validatePaperIntent({ decisionId, instrument: row.instrument, action: row.action, notional }, {
|
||||
...constraints,
|
||||
tradable: row.active === 1 && row.tradable === 1,
|
||||
});
|
||||
const result = db.prepare(`
|
||||
INSERT OR IGNORE INTO autonomy_order_intents
|
||||
(decision_id, client_order_id, instrument, side, notional, status)
|
||||
VALUES (?, ?, ?, ?, ?, 'shadow')
|
||||
`).run(decisionId, intent.clientOrderId, intent.instrument, intent.side, intent.notional);
|
||||
return { ...intent, inserted: result.changes > 0 };
|
||||
}
|
||||
|
||||
module.exports = { createOrderIntent };
|
||||
@@ -0,0 +1,35 @@
|
||||
function addTradingDays(date, days) {
|
||||
const value = new Date(`${date}T00:00:00Z`);
|
||||
let remaining = Math.max(0, Number(days) || 0);
|
||||
while (remaining > 0) {
|
||||
value.setUTCDate(value.getUTCDate() + 1);
|
||||
const weekday = value.getUTCDay();
|
||||
if (weekday !== 0 && weekday !== 6) remaining -= 1;
|
||||
}
|
||||
return value.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
function nearestOnOrAfter(history, date) {
|
||||
return history.find((row) => row.date >= date)?.close ?? null;
|
||||
}
|
||||
|
||||
function calculateOutcome(prediction, instrumentHistory, benchmarkHistory) {
|
||||
const eventDate = String(prediction.information_cutoff).slice(0, 10);
|
||||
const horizonDate = addTradingDays(eventDate, prediction.horizon_days);
|
||||
const price0 = nearestOnOrAfter(instrumentHistory, eventDate);
|
||||
const priceHorizon = nearestOnOrAfter(instrumentHistory, horizonDate);
|
||||
const benchmark0 = nearestOnOrAfter(benchmarkHistory, eventDate);
|
||||
const benchmarkHorizon = nearestOnOrAfter(benchmarkHistory, horizonDate);
|
||||
if (![price0, priceHorizon, benchmark0, benchmarkHorizon].every(Number.isFinite)) return null;
|
||||
const instrumentReturn = (priceHorizon - price0) / price0;
|
||||
const benchmarkReturn = (benchmarkHorizon - benchmark0) / benchmark0;
|
||||
const excessReturn = instrumentReturn - benchmarkReturn;
|
||||
const directionCorrect = prediction.direction === 'positive' ? excessReturn > 0 : excessReturn < 0;
|
||||
return {
|
||||
price0, priceHorizon, benchmark0, benchmarkHorizon,
|
||||
excessReturn, directionCorrect: directionCorrect ? 1 : 0,
|
||||
eventDate, horizonDate,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { addTradingDays, nearestOnOrAfter, calculateOutcome };
|
||||
@@ -0,0 +1,26 @@
|
||||
function decide({ direction = 'positive', probability, expectedExcessReturn, lowerReturn, upperReturn, sampleSize }, rules = {}) {
|
||||
const minSampleSize = Number(rules.minSampleSize ?? 30);
|
||||
const minProbability = Number(rules.minProbability ?? 0.58);
|
||||
const minExpectedReturn = Number(rules.minExpectedReturn ?? 0.005);
|
||||
const maxDownside = Number(rules.maxDownside ?? -0.08);
|
||||
|
||||
if (![probability, expectedExcessReturn].every(Number.isFinite)) {
|
||||
return { action: 'ABSTAIN', rationale: 'calibration unavailable' };
|
||||
}
|
||||
if (sampleSize < minSampleSize) {
|
||||
return { action: 'ABSTAIN', rationale: `insufficient calibration sample (${sampleSize}/${minSampleSize})` };
|
||||
}
|
||||
const signedExpectedReturn = direction === 'negative' ? -expectedExcessReturn : expectedExcessReturn;
|
||||
const signedLowerReturn = direction === 'negative'
|
||||
? (Number.isFinite(upperReturn) ? -upperReturn : null)
|
||||
: (Number.isFinite(lowerReturn) ? lowerReturn : null);
|
||||
if (Number.isFinite(signedLowerReturn) && signedLowerReturn < maxDownside) {
|
||||
return { action: 'HOLD', rationale: 'calibrated downside exceeds policy limit' };
|
||||
}
|
||||
if (probability >= minProbability && signedExpectedReturn >= minExpectedReturn) {
|
||||
return { action: direction === 'negative' ? 'SELL' : 'BUY', rationale: 'calibrated edge clears policy thresholds' };
|
||||
}
|
||||
return { action: 'HOLD', rationale: 'calibrated edge does not clear policy thresholds' };
|
||||
}
|
||||
|
||||
module.exports = { decide };
|
||||
@@ -0,0 +1,192 @@
|
||||
const AUTONOMY_SCHEMA_VERSION = 1;
|
||||
|
||||
function initAutonomySchema(db) {
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS autonomy_schema (
|
||||
version INTEGER PRIMARY KEY,
|
||||
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS autonomy_jobs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
job_type TEXT NOT NULL,
|
||||
lane TEXT NOT NULL CHECK (lane IN ('live', 'historical', 'maintenance')),
|
||||
priority INTEGER NOT NULL DEFAULT 0,
|
||||
entity_type TEXT NOT NULL,
|
||||
entity_id TEXT NOT NULL,
|
||||
idempotency_key TEXT NOT NULL UNIQUE,
|
||||
status TEXT NOT NULL DEFAULT 'pending'
|
||||
CHECK (status IN ('pending', 'leased', 'complete', 'failed', 'dead_letter')),
|
||||
attempts INTEGER NOT NULL DEFAULT 0,
|
||||
available_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
leased_by TEXT,
|
||||
lease_expires_at TEXT,
|
||||
last_error TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
completed_at TEXT
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_autonomy_jobs_claim
|
||||
ON autonomy_jobs(status, lane, priority DESC, available_at);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS autonomy_cursors (
|
||||
key TEXT PRIMARY KEY,
|
||||
value INTEGER NOT NULL DEFAULT 0,
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS autonomy_proposals (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
event_id INTEGER,
|
||||
payload TEXT NOT NULL,
|
||||
information_cutoff TEXT NOT NULL,
|
||||
coordinator_model TEXT,
|
||||
prompt_version TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'candidate'
|
||||
CHECK (status IN ('candidate', 'accepted', 'rejected', 'superseded')),
|
||||
rejection_reason TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
reviewed_at TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS autonomy_instruments (
|
||||
symbol TEXT PRIMARY KEY,
|
||||
broker TEXT NOT NULL,
|
||||
asset_class TEXT NOT NULL DEFAULT 'us_equity',
|
||||
active INTEGER NOT NULL DEFAULT 0,
|
||||
tradable INTEGER NOT NULL DEFAULT 0,
|
||||
shortable INTEGER NOT NULL DEFAULT 0,
|
||||
fractionable INTEGER NOT NULL DEFAULT 0,
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS autonomy_legacy_records (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
source_table TEXT NOT NULL,
|
||||
source_id INTEGER NOT NULL,
|
||||
payload TEXT NOT NULL,
|
||||
calibration_eligible INTEGER NOT NULL DEFAULT 0,
|
||||
imported_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
UNIQUE(source_table, source_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS autonomy_predictions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
proposal_id INTEGER NOT NULL REFERENCES autonomy_proposals(id),
|
||||
event_id INTEGER,
|
||||
instrument TEXT NOT NULL,
|
||||
direction TEXT NOT NULL CHECK (direction IN ('positive', 'negative')),
|
||||
event_type TEXT NOT NULL,
|
||||
causal_channel TEXT,
|
||||
horizon_days INTEGER NOT NULL,
|
||||
information_cutoff TEXT NOT NULL,
|
||||
evidence_article_ids TEXT NOT NULL,
|
||||
invalidation_condition TEXT,
|
||||
learning_eligible INTEGER NOT NULL DEFAULT 0,
|
||||
strategy_version TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'open'
|
||||
CHECK (status IN ('open', 'resolved', 'unresolvable', 'invalidated')),
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_autonomy_predictions_resolution
|
||||
ON autonomy_predictions(status, information_cutoff, instrument);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS autonomy_outcomes (
|
||||
prediction_id INTEGER PRIMARY KEY REFERENCES autonomy_predictions(id),
|
||||
price_0 REAL,
|
||||
price_horizon REAL,
|
||||
benchmark_0 REAL,
|
||||
benchmark_horizon REAL,
|
||||
excess_return REAL,
|
||||
direction_correct INTEGER,
|
||||
error_type TEXT,
|
||||
evaluated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
notes TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS autonomy_calibration_snapshots (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
cohort_key TEXT NOT NULL,
|
||||
sample_size INTEGER NOT NULL,
|
||||
effective_sample_size REAL NOT NULL,
|
||||
directional_probability REAL NOT NULL,
|
||||
expected_excess_return REAL,
|
||||
lower_return REAL,
|
||||
upper_return REAL,
|
||||
parent_cohort_key TEXT,
|
||||
version TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_autonomy_calibration_lookup
|
||||
ON autonomy_calibration_snapshots(cohort_key, created_at DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS autonomy_decisions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
prediction_id INTEGER NOT NULL REFERENCES autonomy_predictions(id),
|
||||
action TEXT NOT NULL CHECK (action IN ('BUY', 'SELL', 'HOLD', 'ABSTAIN')),
|
||||
calibrated_probability REAL,
|
||||
expected_excess_return REAL,
|
||||
rationale TEXT NOT NULL,
|
||||
strategy_version TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS autonomy_order_intents (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
decision_id INTEGER NOT NULL REFERENCES autonomy_decisions(id),
|
||||
client_order_id TEXT NOT NULL UNIQUE,
|
||||
instrument TEXT NOT NULL,
|
||||
side TEXT NOT NULL CHECK (side IN ('buy', 'sell')),
|
||||
notional REAL NOT NULL CHECK (notional > 0),
|
||||
status TEXT NOT NULL DEFAULT 'shadow'
|
||||
CHECK (status IN ('shadow', 'pending', 'submitted', 'filled', 'partially_filled', 'rejected', 'cancelled')),
|
||||
broker_order_id TEXT,
|
||||
attempts INTEGER NOT NULL DEFAULT 0,
|
||||
last_error TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS autonomy_broker_events (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
broker TEXT NOT NULL,
|
||||
event_type TEXT NOT NULL,
|
||||
broker_id TEXT,
|
||||
payload TEXT NOT NULL,
|
||||
occurred_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
UNIQUE(broker, event_type, broker_id, occurred_at)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS autonomy_account_snapshots (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
broker TEXT NOT NULL,
|
||||
account_id TEXT,
|
||||
equity REAL,
|
||||
cash REAL,
|
||||
buying_power REAL,
|
||||
payload TEXT NOT NULL,
|
||||
captured_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS autonomy_position_snapshots (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
broker TEXT NOT NULL,
|
||||
instrument TEXT NOT NULL,
|
||||
quantity REAL,
|
||||
market_value REAL,
|
||||
unrealized_pl REAL,
|
||||
payload TEXT NOT NULL,
|
||||
captured_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
INSERT OR IGNORE INTO autonomy_schema(version) VALUES (${AUTONOMY_SCHEMA_VERSION});
|
||||
`);
|
||||
for (const statement of [
|
||||
'ALTER TABLE autonomy_order_intents ADD COLUMN attempts INTEGER NOT NULL DEFAULT 0',
|
||||
'ALTER TABLE autonomy_order_intents ADD COLUMN last_error TEXT',
|
||||
'ALTER TABLE autonomy_predictions ADD COLUMN learning_eligible INTEGER NOT NULL DEFAULT 0',
|
||||
]) {
|
||||
try { db.exec(statement); } catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { AUTONOMY_SCHEMA_VERSION, initAutonomySchema };
|
||||
Reference in New Issue
Block a user