← 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>