feat: add autonomous paper-trading and calibration pipeline

This commit is contained in:
ImBenji
2026-08-03 14:03:27 +01:00
parent 5a9a2e4c6d
commit c4028cc394
46 changed files with 2246 additions and 115 deletions
+39
View File
@@ -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 };
+90
View File
@@ -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 };
+34
View File
@@ -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 };
+17
View File
@@ -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,
};
+56
View File
@@ -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 };
+32
View File
@@ -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 };
+25
View File
@@ -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 };
+35
View File
@@ -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 };
+26
View File
@@ -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 };
+192
View File
@@ -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 };