The old admin was five separate html pages, so every navigation was a full reload and the operational picture was scattered across all of them. Worth saying: the api was never the problem, every endpoint answers in under 200ms. It felt slow because of the architecture, not the backend. This is a single page console. React and htm from a cdn, no bundler and no babel-in-the-browser, because a runtime transpiler on every load is exactly the slowness we are trying to get rid of. Hash routing, polling that keeps the last good payload on screen instead of flashing a spinner, and stale responses are dropped so a slow request cannot overwrite a newer one. /admin/api/ops/overview answers the whole dashboard in one call rather than making the browser fan out and stitch. It carries the things that actually matter and were not visible anywhere before: when live evidence matures, why a cohort does or does not clear the trade gate, which stage of the pipeline has gone quiet, and what is sitting in dead letters. Controls, all of which change production and all of which ask twice: - requeue dead letters, which only ever moves dead_letter back to pending - execution mode and a kill switch, now read from autonomy_settings on every poll instead of only from AUTONOMY_EXECUTION_MODE, so halting no longer needs a redeploy first - run the reaction analysis and read its output The d3 graph is framed rather than ported. It works, and rewriting it would risk something valuable for nothing the operator can see. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WnNxwxfXSbeNtjvtz5gayb
123 lines
6.1 KiB
JavaScript
123 lines
6.1 KiB
JavaScript
const os = require('os');
|
|
const { openRuntimeDb } = require('../src/db/runtime');
|
|
const { initAutonomySchema } = require('../src/autonomy/schema');
|
|
const { createOrderIntent } = require('../src/autonomy/orderIntents');
|
|
const { createAlpacaPaperClient } = require('../src/brokers/alpacaPaper');
|
|
const { getExecutionControls } = require('../src/autonomy/settings');
|
|
|
|
function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); }
|
|
|
|
async function runExecutionWorker({ intelligencePath, pollMs = 10000, mode = 'shadow', notional = 100, workerId = `execution-${os.hostname()}-${process.pid}` } = {}) {
|
|
if (!['shadow', 'paper'].includes(mode)) throw new Error(`unsupported execution mode: ${mode}`);
|
|
const db = openRuntimeDb(intelligencePath, { schema: 'intelligence' });
|
|
db.pragma('journal_mode = WAL');
|
|
db.pragma('busy_timeout = 5000');
|
|
initAutonomySchema(db);
|
|
|
|
// mode is re-read every poll now. the env var is only the default, so the kill
|
|
// switch actually works during an incident instead of needing a redeploy first.
|
|
let paperClient = null;
|
|
let lastMode = null;
|
|
let lastKill = null;
|
|
|
|
while (true) {
|
|
const controls = getExecutionControls(db, { AUTONOMY_EXECUTION_MODE: mode });
|
|
if (controls.mode !== lastMode) {
|
|
console.log(`[${workerId}] execution mode = ${controls.mode} (from ${controls.modeSource})`);
|
|
lastMode = controls.mode;
|
|
}
|
|
if (controls.killSwitch !== lastKill) {
|
|
console.log(`[${workerId}] kill switch ${controls.killSwitch ? 'ENGAGED, no orders will be placed' : 'released'}`);
|
|
lastKill = controls.killSwitch;
|
|
}
|
|
if (controls.mode === 'paper' && !paperClient) {
|
|
paperClient = createAlpacaPaperClient({ keyId: process.env.ALPACA_PAPER_KEY_ID, secretKey: process.env.ALPACA_PAPER_SECRET_KEY });
|
|
}
|
|
if (controls.mode !== 'paper') paperClient = null;
|
|
if (paperClient) {
|
|
try {
|
|
const [account, positions, orders] = await Promise.all([
|
|
paperClient.getAccount(), paperClient.getPositions(), paperClient.getOrders(),
|
|
]);
|
|
db.prepare(`
|
|
INSERT INTO autonomy_account_snapshots(broker, account_id, equity, cash, buying_power, payload)
|
|
VALUES ('alpaca-paper', ?, ?, ?, ?, ?)
|
|
`).run(account.id || null, Number(account.equity), Number(account.cash), Number(account.buying_power), JSON.stringify(account));
|
|
const insertPosition = db.prepare(`
|
|
INSERT INTO autonomy_position_snapshots(broker, instrument, quantity, market_value, unrealized_pl, payload)
|
|
VALUES ('alpaca-paper', ?, ?, ?, ?, ?)
|
|
`);
|
|
for (const position of positions || []) {
|
|
insertPosition.run(position.symbol, Number(position.qty), Number(position.market_value), Number(position.unrealized_pl), JSON.stringify(position));
|
|
}
|
|
for (const order of orders || []) {
|
|
if (!order.client_order_id) continue;
|
|
const mapped = { accepted: 'submitted', new: 'submitted', pending_new: 'submitted', partially_filled: 'partially_filled', filled: 'filled', canceled: 'cancelled', cancelled: 'cancelled', rejected: 'rejected' }[order.status];
|
|
if (!mapped) continue;
|
|
db.prepare(`
|
|
UPDATE autonomy_order_intents SET status=?, broker_order_id=?, updated_at=datetime('now')
|
|
WHERE client_order_id=?
|
|
`).run(mapped, order.id || null, order.client_order_id);
|
|
db.prepare(`
|
|
INSERT INTO autonomy_broker_events(broker, event_type, broker_id, payload)
|
|
VALUES ('alpaca-paper', ?, ?, ?)
|
|
`).run(order.status, order.id || null, JSON.stringify(order));
|
|
}
|
|
} catch (error) {
|
|
console.error(`[${workerId}] broker reconciliation:`, error.message);
|
|
}
|
|
}
|
|
// Broker reconciliation above still runs while the switch is engaged, we want
|
|
// to keep seeing the account. It is order creation specifically that stops.
|
|
const decisions = controls.killSwitch ? [] : db.prepare(`
|
|
SELECT d.id FROM autonomy_decisions d
|
|
JOIN autonomy_predictions p ON p.id = d.prediction_id
|
|
LEFT JOIN autonomy_order_intents oi ON oi.decision_id = d.id
|
|
WHERE oi.id IS NULL AND d.action IN ('BUY', 'SELL') AND p.origin = 'live'
|
|
ORDER BY d.created_at ASC LIMIT 25
|
|
`).all();
|
|
for (const decision of decisions) {
|
|
try {
|
|
const intent = createOrderIntent(db, decision.id, notional, { tradable: true, maxNotional: notional });
|
|
if (controls.mode === 'paper') db.prepare("UPDATE autonomy_order_intents SET status='pending', updated_at=datetime('now') WHERE client_order_id=?").run(intent.clientOrderId);
|
|
console.log(`[${workerId}] ${controls.mode} intent ${intent.clientOrderId}`);
|
|
} catch (error) {
|
|
console.error(`[${workerId}] decision ${decision.id}:`, error.message);
|
|
}
|
|
}
|
|
if (paperClient) {
|
|
const pending = db.prepare("SELECT * FROM autonomy_order_intents WHERE status='pending' ORDER BY created_at ASC LIMIT 25").all();
|
|
for (const intent of pending) {
|
|
try {
|
|
let order;
|
|
try { order = await paperClient.getOrderByClientId(intent.client_order_id); } catch (error) {
|
|
if (error.status !== 404) throw error;
|
|
}
|
|
if (!order) {
|
|
order = await paperClient.submitOrder({
|
|
symbol: intent.instrument,
|
|
notional: String(intent.notional),
|
|
side: intent.side,
|
|
type: 'market',
|
|
time_in_force: 'day',
|
|
client_order_id: intent.client_order_id,
|
|
});
|
|
}
|
|
db.prepare(`
|
|
UPDATE autonomy_order_intents
|
|
SET status = ?, broker_order_id = ?, updated_at = datetime('now')
|
|
WHERE id = ?
|
|
`).run(order.status === 'filled' ? 'filled' : 'submitted', order.id || null, intent.id);
|
|
} catch (error) {
|
|
console.error(`[${workerId}] paper order ${intent.client_order_id}:`, error.message);
|
|
db.prepare("UPDATE autonomy_order_intents SET attempts=attempts+1, last_error=?, updated_at=datetime('now') WHERE id=?")
|
|
.run(String(error.message).slice(0, 1000), intent.id);
|
|
}
|
|
}
|
|
}
|
|
await sleep(pollMs);
|
|
}
|
|
}
|
|
|
|
module.exports = { runExecutionWorker };
|