← back to Ken

kalshi-dash/trading-switch.html

155 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Ken — Trading Switch</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Space+Grotesk:wght@500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet" />
<style>
  :root{ --bg:#0b0e14; --card:#141a24; --line:#232c3a; --ink:#e6ebf2; --dim:#8a97a8;
         --green:#22c55e; --red:#ef4444; --amber:#f59e0b; }
  *{box-sizing:border-box} html,body{margin:0;background:var(--bg);color:var(--ink);
    font-family:Inter,system-ui,sans-serif}
  .wrap{max-width:560px;margin:0 auto;padding:28px 18px}
  h1{font-family:'Space Grotesk',sans-serif;font-size:20px;letter-spacing:.02em;margin:0 0 2px}
  .sub{color:var(--dim);font-size:13px;margin-bottom:22px}
  .card{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:22px;margin-bottom:16px}
  .switchrow{display:flex;flex-direction:column;align-items:center;gap:20px;padding:6px 0 2px}
  .statewrap{display:flex;align-items:center}
  .state{font-family:'Space Grotesk',sans-serif;font-size:26px;font-weight:700}
  .state.on{color:var(--green)} .state.off{color:var(--dim)}

  /* ── Large brass AB bat-switch ── */
  .brass-switch{cursor:pointer;user-select:none;outline:none}
  .brass-switch:focus-visible .plate{box-shadow:inset 0 2px 4px rgba(255,255,255,.5),inset 0 -3px 8px rgba(0,0,0,.45),0 0 0 3px rgba(245,224,140,.5),0 10px 24px rgba(0,0,0,.55)}
  .brass-switch[data-busy="true"]{opacity:.6;pointer-events:none}
  .plate{position:relative;width:176px;height:250px;border-radius:16px;margin:0 auto;
    padding:18px 0;display:flex;flex-direction:column;align-items:center;justify-content:space-between;
    background:linear-gradient(150deg,#7a5c12 0%,#c9a53a 16%,#f6e79a 32%,#d9b64a 48%,#a8801f 64%,#e6cf78 82%,#7f6115 100%);
    border:1px solid #5c460f;
    box-shadow:inset 0 2px 4px rgba(255,255,255,.55),inset 0 -4px 10px rgba(0,0,0,.5),0 12px 26px rgba(0,0,0,.6),0 2px 4px rgba(0,0,0,.4)}
  .screw{position:absolute;width:17px;height:17px;border-radius:50%;
    background:radial-gradient(circle at 38% 32%,#f9efb8,#bf9a34 58%,#6f550f);
    box-shadow:inset 0 1px 1px rgba(255,255,255,.7),0 1px 2px rgba(0,0,0,.55)}
  .screw::after{content:"";position:absolute;left:3px;right:3px;top:7px;height:2px;background:#5c460f;opacity:.75;transform:rotate(38deg)}
  .screw.tl{top:9px;left:9px}.screw.tr{top:9px;right:9px}.screw.bl{bottom:9px;left:9px}.screw.br{bottom:9px;right:9px}
  .monogram{font-family:'Space Grotesk',sans-serif;font-weight:800;font-size:19px;letter-spacing:.16em;
    color:#4f3d0d;text-shadow:0 1px 0 rgba(255,255,255,.55),0 -1px 1px rgba(0,0,0,.4)}
  .engrave{font-family:'Space Grotesk',sans-serif;font-weight:700;font-size:14px;letter-spacing:.24em;
    color:#5c4610;text-shadow:0 1px 0 rgba(255,255,255,.5),0 -1px 0 rgba(0,0,0,.35);transition:color .2s,text-shadow .2s}
  .brass-switch[data-on="true"] .on-lbl{color:#1c7a34;text-shadow:0 0 9px rgba(34,197,94,.75),0 1px 0 rgba(255,255,255,.3)}
  .brass-switch[data-on="false"] .off-lbl{color:#8a1f1f;text-shadow:0 1px 0 rgba(255,255,255,.4)}
  .throw{position:relative;width:60px;height:118px;border-radius:32px;
    background:linear-gradient(90deg,#4a380b,#7a5c12 32%,#2f240700 50%,#5a460f 70%,#332703);
    box-shadow:inset 0 4px 9px rgba(0,0,0,.75),inset 0 -2px 3px rgba(255,255,255,.14)}
  .pivot{position:absolute;left:50%;top:50%;width:46px;height:46px;border-radius:50%;transform:translate(-50%,-50%);
    background:radial-gradient(circle at 40% 34%,#f6e79a,#bf9a34 56%,#7a5c12);
    box-shadow:inset 0 1px 2px rgba(255,255,255,.65),0 2px 5px rgba(0,0,0,.55);z-index:1}
  .lever{position:absolute;left:50%;bottom:50%;transform-origin:bottom center;z-index:2;
    transform:translateX(-50%) rotate(178deg);transition:transform .26s cubic-bezier(.34,1.5,.5,1)}
  .brass-switch[data-on="true"] .lever{transform:translateX(-50%) rotate(0deg)}
  .stem{width:22px;height:60px;border-radius:11px;margin:0 auto;
    background:linear-gradient(90deg,#7a5c12,#e6cf78 44%,#f6e79a 52%,#a8801f 78%,#6f550f);
    box-shadow:inset 1px 0 1px rgba(255,255,255,.55),inset -2px 0 3px rgba(0,0,0,.45),0 2px 5px rgba(0,0,0,.4)}
  .ball{position:absolute;top:-16px;left:50%;transform:translateX(-50%);width:38px;height:38px;border-radius:50%;
    background:radial-gradient(circle at 36% 30%,#f9efb8,#c19b33 56%,#7a5c12);
    box-shadow:inset 0 2px 3px rgba(255,255,255,.65),0 4px 8px rgba(0,0,0,.55);transition:background .2s,box-shadow .2s}
  .brass-switch[data-on="true"] .ball{background:radial-gradient(circle at 36% 30%,#d9ffe4,#22c55e 60%,#0f7a34);
    box-shadow:0 0 16px rgba(34,197,94,.85),inset 0 2px 3px rgba(255,255,255,.5)}
  .grid{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:6px}
  .kv{background:#0f141c;border:1px solid var(--line);border-radius:9px;padding:10px 12px}
  .kv .k{color:var(--dim);font-size:11px;text-transform:uppercase;letter-spacing:.04em}
  .kv .v{font-family:'JetBrains Mono',monospace;font-size:15px;margin-top:3px}
  .warn{background:rgba(239,68,68,.08);border:1px solid rgba(239,68,68,.35);color:#fca5a5;
    border-radius:10px;padding:11px 13px;font-size:13px;margin-top:14px;display:none}
  .warn.show{display:block}
  .note{background:rgba(245,158,11,.08);border:1px solid rgba(245,158,11,.3);color:#fcd34d;
    border-radius:10px;padding:11px 13px;font-size:12.5px;margin-top:12px;display:none}
  .note.show{display:block}
  .foot{color:var(--dim);font-size:11.5px;margin-top:18px;line-height:1.6}
  a{color:#7dd3fc}
  .pulse{width:9px;height:9px;border-radius:50%;display:inline-block;margin-right:7px;vertical-align:middle}
  .pulse.on{background:var(--green);box-shadow:0 0 0 0 rgba(34,197,94,.6);animation:p 1.6s infinite}
  .pulse.off{background:var(--dim)}
  @keyframes p{0%{box-shadow:0 0 0 0 rgba(34,197,94,.5)}70%{box-shadow:0 0 0 9px rgba(34,197,94,0)}100%{box-shadow:0 0 0 0 rgba(34,197,94,0)}}
</style>
</head>
<body>
<div class="wrap">
  <h1>Ken — Trading Switch</h1>
  <div class="sub">The single source of truth for real-money trading. The <code>safe_mode</code> guard now holds whatever this toggle sets.</div>

  <div class="card">
    <div class="switchrow">
      <div class="statewrap"><span class="pulse off" id="pulse"></span><span class="state off" id="stateLabel">…</span></div>
      <div class="brass-switch" id="toggle" data-on="false" role="switch" aria-checked="false" tabindex="0" title="Throw the switch to arm / disarm real-money trading">
        <div class="plate">
          <span class="screw tl"></span><span class="screw tr"></span><span class="screw bl"></span><span class="screw br"></span>
          <div class="monogram">AB</div>
          <div class="engrave on-lbl">ON</div>
          <div class="throw">
            <div class="pivot"></div>
            <div class="lever"><div class="ball"></div><div class="stem"></div></div>
          </div>
          <div class="engrave off-lbl">OFF</div>
        </div>
      </div>
    </div>
    <div class="warn" id="warn">⚠ <b>Real money armed.</b> The engine will place real Kalshi orders when ≥2 proven winner-bots agree, capped at $2/position · $10/day · $4 daily-loss.</div>
    <div class="note" id="note">Armed, but <b>no winner-consensus yet</b> — the fresh sim needs ~50 resolved trades/bot before real fills can fire. Armed ≠ trading until then.</div>
    <div class="grid" id="grid"></div>
  </div>

  <div class="foot" id="foot"></div>
</div>
<script>
const api = (body)=>fetch('/api/trading/mode',{method:'POST',credentials:'same-origin',
  headers:{'Content-Type':'application/json'},body:JSON.stringify(body)}).then(r=>r.json());
const liveRun = ()=>fetch('/api/live-run',{credentials:'same-origin'}).then(r=>r.json()).catch(()=>({}));
const $ = id=>document.getElementById(id);
let state = {};

function render(s, lr){
  const on = s.safe_mode === false;                 // trading ON == safe_mode OFF
  const tog = $('toggle'); tog.dataset.on = on; tog.setAttribute('aria-checked', on);
  $('stateLabel').textContent = on ? 'TRADING ON' : 'TRADING OFF';
  $('stateLabel').className = 'state ' + (on?'on':'off');
  $('pulse').className = 'pulse ' + (on?'on':'off');
  $('warn').className = 'warn' + (on?' show':'');
  const tc = s.trade_config || {}, ts = s.trader_state || {};
  const kv = [
    ['Master brake (safe_mode)', on ? 'OFF (armed)' : 'ON (safe)'],
    ['Auto-trader', s.auto_trader_enabled ? 'enabled' : 'disabled'],
    ['Kalshi', (s.env||'?').toUpperCase() + (s.kalshi_connected?' · connected':' · no keys')],
    ['Live run', lr && lr.active ? ('day '+lr.day+'/'+lr.days) : 'inactive'],
    ['Spent today', lr ? ('$'+((lr.spent_today_cents||0)/100).toFixed(2)+' / $'+((lr.daily_budget_cents||0)/100).toFixed(2)) : '—'],
    ['Open positions', (ts.openCount!=null?ts.openCount:'—') + ' / ' + (tc.max_open_positions||'—')],
    ['Per-position cap', '$'+(((tc.max_position_cents)||0)/100).toFixed(2)],
    ['Daily-loss limit', '$'+(((tc.daily_loss_limit_cents)||0)/100).toFixed(2)],
  ];
  $('grid').innerHTML = kv.map(([k,v])=>`<div class="kv"><div class="k">${k}</div><div class="v">${v}</div></div>`).join('');
  // consensus caveat only matters when armed
  $('note').className = 'note' + (on ? ' show' : '');
}

async function refresh(){
  try{ const [s,lr] = await Promise.all([api({action:'get'}), liveRun()]); state=s; render(s,lr);
       $('foot').innerHTML = 'Guard: source-of-truth mode — flips inconsistent with this switch are auto-healed + alerted. '+
         'Real trades so far: <b>'+((state.trader_state&&state.trader_state.realTrades)||0)+'</b>. Auto-refresh 5s.'; }
  catch(e){ $('foot').textContent = 'load error: '+e.message; }
}

async function setTrading(on){
  if(on && !confirm('Turn REAL-MONEY trading ON?\n\nThe $50 live run will place real Kalshi orders once ≥2 winner-bots agree (capped $2/pos · $10/day). The safe_mode guard will HOLD it on until you toggle off.')) return;
  const tog=$('toggle'); tog.dataset.busy='true';
  try{ await api({action:'set_trading', on}); await refresh(); }
  catch(e){ alert('failed: '+e.message); }
  tog.dataset.busy='false';
}
$('toggle').addEventListener('click', ()=> setTrading(state.safe_mode !== false ? true : false));
$('toggle').addEventListener('keydown', e=>{ if(e.key===' '||e.key==='Enter'){e.preventDefault(); $('toggle').click();} });
refresh(); setInterval(refresh, 5000);
</script>
</body>
</html>