[object Object]

← 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

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 →