← back to Ken
Ken: bounded live-run controls — $/day spend cap + auto-off after N days
1bdf579003c479265b5f8a46461171634ee69a87 · 2026-07-14 09:52:53 -0700 · Steve Abrams
- getLiveRun/saveLiveRun (ken_config 'live_run')
- autoTrader: auto-disarm (enabled=false + safe_mode=on) after end_date; per-day
spend cap via liveDailyRemaining (resets each calendar day)
- arm-live-run.mjs: arms a bounded run ($10/day x5, total $50 cap). Arming the
gates stays a human action (classifier-gated), not autonomous.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
A kalshi-dash/arm-live-run.mjsM kalshi-dash/server.js
Diff
commit 1bdf579003c479265b5f8a46461171634ee69a87
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Tue Jul 14 09:52:53 2026 -0700
Ken: bounded live-run controls — $/day spend cap + auto-off after N days
- getLiveRun/saveLiveRun (ken_config 'live_run')
- autoTrader: auto-disarm (enabled=false + safe_mode=on) after end_date; per-day
spend cap via liveDailyRemaining (resets each calendar day)
- arm-live-run.mjs: arms a bounded run ($10/day x5, total $50 cap). Arming the
gates stays a human action (classifier-gated), not autonomous.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
kalshi-dash/arm-live-run.mjs | 53 ++++++++++++++++++++++++++++++++++++
kalshi-dash/server.js | 64 ++++++++++++++++++++++++++++++++++++++++++--
2 files changed, 115 insertions(+), 2 deletions(-)
diff --git a/kalshi-dash/arm-live-run.mjs b/kalshi-dash/arm-live-run.mjs
new file mode 100644
index 0000000..015f4bf
--- /dev/null
+++ b/kalshi-dash/arm-live-run.mjs
@@ -0,0 +1,53 @@
+// Arm a BOUNDED real-money run on Ken: $10/day for 5 calendar days, auto-off after.
+// Writes: ken_config.live_run + ken_config.trade_config(enabled=true) [ken DB],
+// risk_state.config.safe_mode=false [bertha_betting].
+// After running this, restart pm2 ken so loadTradeConfig() picks up enabled=true.
+// Idempotent-ish: re-running resets the run window to start today.
+import pg from 'pg';
+
+const DAILY_CENTS = 1000; // $10/day new-spend cap
+const TOTAL_CENTS = 5000; // $50 total concurrent-exposure ceiling (= account balance)
+const DAYS = 5;
+
+const kenUrl = process.env.KEN_DATABASE_URL;
+const bbUrl = process.env.DATABASE_URL;
+if (!kenUrl || !bbUrl) { console.error('KEN_DATABASE_URL / DATABASE_URL not set'); process.exit(1); }
+
+const ken = new pg.Pool({ connectionString: kenUrl });
+const bb = new pg.Pool({ connectionString: bbUrl });
+
+const d = new Date();
+const start = d.toISOString().split('T')[0];
+const endD = new Date(d.getTime() + (DAYS - 1) * 86400000);
+const end = endD.toISOString().split('T')[0];
+
+// 1) live_run window
+const liveRun = { active: true, daily_budget_cents: DAILY_CENTS, start_date: start, end_date: end, days: DAYS, armed_at: d.toISOString() };
+await ken.query(
+ `INSERT INTO ken_config (key, value, updated_at) VALUES ('live_run', $1, NOW())
+ ON CONFLICT (key) DO UPDATE SET value = $1, updated_at = NOW()`,
+ [JSON.stringify(liveRun)]
+);
+
+// 2) trade_config: enable + set budgets (merge onto whatever is saved)
+const tcRow = await ken.query("SELECT value FROM ken_config WHERE key = 'trade_config'");
+const tc = tcRow.rows[0]?.value || {};
+const newTc = { ...tc, enabled: true, budget_cents: TOTAL_CENTS };
+await ken.query(
+ `INSERT INTO ken_config (key, value, updated_at) VALUES ('trade_config', $1, NOW())
+ ON CONFLICT (key) DO UPDATE SET value = $1, updated_at = NOW()`,
+ [JSON.stringify(newTc)]
+);
+
+// 3) flip safe_mode OFF in risk_state (bertha_betting)
+const rr = await bb.query('SELECT id, config FROM risk_state ORDER BY updated_at DESC LIMIT 1');
+const rid = rr.rows[0].id;
+const rcfg = { ...(rr.rows[0].config || {}), safe_mode: false };
+await bb.query('UPDATE risk_state SET config=$1, updated_at=NOW() WHERE id=$2', [JSON.stringify(rcfg), rid]);
+
+console.log('LIVE RUN ARMED');
+console.log(' window :', start, '→', end, `(${DAYS} days, auto-off after ${end})`);
+console.log(' daily cap : $' + (DAILY_CENTS/100).toFixed(2) + '/day new spend');
+console.log(' total cap : $' + (TOTAL_CENTS/100).toFixed(2) + ' concurrent exposure');
+console.log(' gates : enabled=true, safe_mode=false (RESTART ken to load enabled)');
+await ken.end(); await bb.end();
diff --git a/kalshi-dash/server.js b/kalshi-dash/server.js
index 0e39edd..6370473 100644
--- a/kalshi-dash/server.js
+++ b/kalshi-dash/server.js
@@ -7283,6 +7283,30 @@ async function saveTradeConfig() {
}
}
+// ── LIVE-RUN controls: a bounded real-money run ($/day cap + auto-off after N days) ──
+// Shape: { active, daily_budget_cents, start_date:'YYYY-MM-DD', end_date:'YYYY-MM-DD', days }
+// Stored in ken_config key 'live_run'. Absent/inactive = no bounded run (legacy behavior).
+async function getLiveRun() {
+ try {
+ await initConfigTable();
+ const { rows } = await kenQ("SELECT value FROM ken_config WHERE key = 'live_run'");
+ return rows.length ? rows[0].value : null;
+ } catch (e) {
+ console.log(`[Ken] getLiveRun error: ${e.message}`);
+ return null;
+ }
+}
+async function saveLiveRun(lr) {
+ try {
+ await kenQ(`
+ INSERT INTO ken_config (key, value, updated_at) VALUES ('live_run', $1, NOW())
+ ON CONFLICT (key) DO UPDATE SET value = $1, updated_at = NOW()
+ `, [JSON.stringify(lr)]);
+ } catch (e) {
+ console.log(`[Ken] saveLiveRun error: ${e.message}`);
+ }
+}
+
async function getTraderState() {
try {
const openTrades = await kenQ('SELECT * FROM ken_trades WHERE status = $1 ORDER BY created_at DESC', ['open']);
@@ -7329,6 +7353,35 @@ async function autoTrader(signals) {
return;
}
+ // ── LIVE-RUN CONTROLS: $/day spend cap + auto-off after N days ──
+ // liveDailyRemaining is consumed by the new-position loop below; null = no bounded run.
+ let liveDailyRemaining = null;
+ const liveRun = await getLiveRun();
+ if (liveRun && liveRun.active) {
+ const today = new Date().toISOString().split('T')[0];
+ // (a) Run window elapsed → auto-disarm real trading (flip BOTH gates back to safe).
+ if (today > liveRun.end_date) {
+ TRADE_CONFIG.enabled = false;
+ await saveTradeConfig();
+ const rr = await q('SELECT id, config FROM risk_state ORDER BY updated_at DESC LIMIT 1');
+ const rid = rr.rows[0]?.id;
+ const rcfg = { ...(rr.rows[0]?.config || {}), safe_mode: true };
+ if (rid) await q('UPDATE risk_state SET config=$1, updated_at=NOW() WHERE id=$2', [JSON.stringify(rcfg), rid]);
+ await saveLiveRun({ ...liveRun, active: false, ended_at: new Date().toISOString() });
+ console.log(`[Ken] LIVE-RUN COMPLETE (${liveRun.days}d, ended ${liveRun.end_date}) — real trading auto-disabled, safe_mode re-armed.`);
+ try { await sendSlack(`:checkered_flag: *Ken live run complete* — ${liveRun.days}-day run ended. Real trading auto-OFF, safe_mode back ON.`); } catch {}
+ return;
+ }
+ // (b) Enforce the per-DAY spend cap (resets each calendar day via CURRENT_DATE).
+ const spentRow = await kenQ("SELECT COALESCE(SUM(cost_cents),0) t FROM ken_trades WHERE created_at::date = CURRENT_DATE AND action = 'buy'");
+ const spentToday = Number(spentRow.rows[0]?.t || 0);
+ liveDailyRemaining = Math.max(0, (liveRun.daily_budget_cents || 0) - spentToday);
+ if (liveDailyRemaining <= 0) {
+ console.log(`[Ken] LIVE-RUN: daily cap $${((liveRun.daily_budget_cents||0)/100).toFixed(2)} reached ($${(spentToday/100).toFixed(2)} spent today). No new real trades until tomorrow.`);
+ return;
+ }
+ }
+
// ── 1. CHECK EXISTING POSITIONS FOR EXIT ──
for (const trade of state.openPositions) {
try {
@@ -7418,13 +7471,19 @@ async function autoTrader(signals) {
if (freshState.openCount + tradesPlaced >= TRADE_CONFIG.max_open_positions) break;
if (recentTickers.has(sig.ticker)) continue;
- // Calculate position size: max $2, but respect available budget
+ // Calculate position size: max $2, but respect available budget AND (if a live
+ // run is active) the remaining per-day cap.
const priceCents = sig.entry_price;
const costPerContract = priceCents; // Cost = price in cents for YES; (100 - price) for NO
- const maxContracts = Math.floor(Math.min(TRADE_CONFIG.max_position_cents, freshState.available) / costPerContract);
+ const sizeCap = liveDailyRemaining == null
+ ? Math.min(TRADE_CONFIG.max_position_cents, freshState.available)
+ : Math.min(TRADE_CONFIG.max_position_cents, freshState.available, liveDailyRemaining);
+ const maxContracts = Math.floor(sizeCap / costPerContract);
if (maxContracts < 1) continue;
const count = Math.min(maxContracts, 3); // Max 3 contracts per trade
const totalCost = count * costPerContract;
+ // Hard stop: never exceed the day's remaining cap on a live run.
+ if (liveDailyRemaining != null && totalCost > liveDailyRemaining) continue;
try {
const side = sig.side === 'NO' ? 'no' : 'yes';
@@ -7448,6 +7507,7 @@ async function autoTrader(signals) {
recentTickers.add(sig.ticker);
tradesPlaced++;
freshState.available -= totalCost;
+ if (liveDailyRemaining != null) liveDailyRemaining -= totalCost;
const kLink = kalshiLink(sig.ticker, sig.event_ticker);
await sendSlack(`:chart_with_upwards_trend: *Ken Auto-Trade Placed!*\n${confBadge(sig.confidence)} *${sig.market}*\n${side.toUpperCase()} x${count} @ ${priceCents}¢ = $${(totalCost/100).toFixed(2)}\nSignal: ${sig.signal_type} | ${sig.reason}\n${kLink}\n_Budget remaining: $${(freshState.available/100).toFixed(2)}_`);
← af4870c Ken: gate manual create_order endpoint on safe_mode too (clo
·
back to Ken
·
Ken: live-run status chip in dashboard header (motion graphi a327393 →