[object Object]

← back to Ken

Ken: live Risk Level knob in Settings — drag to preview, Apply commits min_confidence to /api/trading/mode (real-money knob, explicit apply). Plus standalone risk-knob.html visualizer.

5a71f568897e7ef784a595d6f3fcad4b468c9d57 · 2026-07-15 12:18:29 -0700 · Steve Abrams

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 5a71f568897e7ef784a595d6f3fcad4b468c9d57
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Jul 15 12:18:29 2026 -0700

    Ken: live Risk Level knob in Settings — drag to preview, Apply commits min_confidence to /api/trading/mode (real-money knob, explicit apply). Plus standalone risk-knob.html visualizer.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 kalshi-dash/src/components/RiskKnob.jsx | 170 ++++++++++++++++++++++++++++++++
 kalshi-dash/src/pages/Settings.jsx      |   8 ++
 2 files changed, 178 insertions(+)

diff --git a/kalshi-dash/src/components/RiskKnob.jsx b/kalshi-dash/src/components/RiskKnob.jsx
new file mode 100644
index 0000000..b17147e
--- /dev/null
+++ b/kalshi-dash/src/components/RiskKnob.jsx
@@ -0,0 +1,170 @@
+import React, { useState, useEffect, useRef } from 'react';
+import { post } from '../api';
+
+// ── polar gauge geometry: 270° arc, 135° → 405° (=45°) clockwise ──
+const MIN = 50, MAX = 99, A0 = 135, A1 = 405, CX = 170, CY = 170, R = 132;
+const rad = (d) => (d - 90) * Math.PI / 180;
+const ptOn = (d, r = R) => [CX + r * Math.cos(rad(d)), CY + r * Math.sin(rad(d))];
+const arcPath = (from, to, r = R) => {
+  const [x0, y0] = ptOn(from, r), [x1, y1] = ptOn(to, r);
+  const large = (to - from) > 180 ? 1 : 0;
+  return `M${x0} ${y0} A${r} ${r} 0 ${large} 1 ${x1} ${y1}`;
+};
+const val2ang = (v) => A0 + (v - MIN) / (MAX - MIN) * (A1 - A0);
+const zone = (v) => v < 65 ? { t: 'HIGH RISK', c: '#ef4444' }
+  : v < 75 ? { t: 'ELEVATED', c: '#f59e0b' }
+  : v < 85 ? { t: 'BALANCED', c: '#eab308' }
+  : { t: 'CONSERVATIVE', c: '#22c55e' };
+
+const TICKS = [];
+for (let v = MIN; v <= MAX; v += 5) if (v % 5 === 0) TICKS.push(v);
+
+export default function RiskKnob() {
+  const [val, setVal] = useState(75);          // dialed (preview) value
+  const [committed, setCommitted] = useState(null); // last value confirmed live
+  const [busy, setBusy] = useState(false);
+  const [msg, setMsg] = useState(null);        // {ok, text}
+  const stageRef = useRef(null);
+  const dragging = useRef(false);
+  const raf = useRef(0);
+
+  // fetch current min_confidence, then run the entrance sweep to it
+  useEffect(() => {
+    post('/trading/mode', { action: 'get' }).then((d) => {
+      const mc = d?.trade_config?.min_confidence ?? 75;
+      setCommitted(mc);
+      const t0 = performance.now();
+      const step = (now) => {
+        const t = Math.min(1, (now - t0) / 1300), e = 1 - Math.pow(1 - t, 3);
+        setVal(Math.round(MIN + (mc - MIN) * e));
+        if (t < 1) raf.current = requestAnimationFrame(step); else setVal(mc);
+      };
+      raf.current = requestAnimationFrame(step);
+    }).catch(() => setCommitted(75));
+    return () => cancelAnimationFrame(raf.current);
+  }, []);
+
+  // drag-to-turn
+  useEffect(() => {
+    const el = stageRef.current; if (!el) return;
+    const angOf = (cx, cy, x, y) => { let d = Math.atan2(y - cy, x - cx) * 180 / Math.PI + 90; if (d < 0) d += 360; return d; };
+    const fromEvent = (ev) => {
+      const r = el.getBoundingClientRect();
+      const cx = r.left + r.width / 2, cy = r.top + r.height / 2;
+      const p = ev.touches ? ev.touches[0] : ev;
+      let a = angOf(cx, cy, p.clientX, p.clientY);
+      if (a < A0 - 10 && a < 90) a += 360;
+      a = Math.max(A0, Math.min(A1, a));
+      setVal(Math.round(MIN + (a - A0) / (A1 - A0) * (MAX - MIN)));
+    };
+    const down = (e) => { dragging.current = true; cancelAnimationFrame(raf.current); fromEvent(e); e.preventDefault(); };
+    const move = (e) => { if (dragging.current) fromEvent(e); };
+    const up = () => { dragging.current = false; };
+    el.addEventListener('mousedown', down);
+    el.addEventListener('touchstart', down, { passive: false });
+    window.addEventListener('mousemove', move);
+    window.addEventListener('touchmove', move, { passive: false });
+    window.addEventListener('mouseup', up);
+    window.addEventListener('touchend', up);
+    return () => {
+      el.removeEventListener('mousedown', down); el.removeEventListener('touchstart', down);
+      window.removeEventListener('mousemove', move); window.removeEventListener('touchmove', move);
+      window.removeEventListener('mouseup', up); window.removeEventListener('touchend', up);
+    };
+  }, []);
+
+  async function apply() {
+    setBusy(true); setMsg(null);
+    try {
+      const d = await post('/trading/mode', { action: 'update_trade_config', min_confidence: val });
+      const mc = d?.trade_config?.min_confidence;
+      if (mc == null) throw new Error(d?.error || 'no confirmation from server');
+      setCommitted(mc); setVal(mc);
+      setMsg({ ok: true, text: `Applied — Ken now trades only on ≥${mc}% confidence signals.` });
+    } catch (e) {
+      setMsg({ ok: false, text: 'Failed: ' + e.message });
+    }
+    setBusy(false);
+  }
+
+  const a = val2ang(val), z = zone(val), ang = a - 270;
+  const dirty = committed != null && val !== committed;
+
+  return (
+    <div className="rk-wrap">
+      <style>{`
+        .rk-wrap{display:flex;flex-direction:column;align-items:center;gap:16px}
+        .rk-stage{position:relative;width:300px;height:300px;display:grid;place-items:center;touch-action:none;user-select:none;cursor:grab}
+        .rk-stage:active{cursor:grabbing}
+        .rk-stage svg{position:absolute;inset:0;width:100%;height:100%;overflow:visible}
+        .rk-halo{position:absolute;width:210px;height:210px;border-radius:50%;filter:blur(34px);opacity:.5;
+          transition:background .6s ease;animation:rkBreathe 3.4s ease-in-out infinite;pointer-events:none}
+        @keyframes rkBreathe{0%,100%{transform:scale(.96);opacity:.4}50%{transform:scale(1.05);opacity:.58}}
+        .rk-knob{position:absolute;width:170px;height:170px;border-radius:50%;
+          background:radial-gradient(110px 110px at 38% 30%,#1c2836,#0c131c 70%);
+          border:1px solid rgba(255,255,255,.08);
+          box-shadow:0 16px 34px rgba(0,0,0,.55),inset 0 2px 6px rgba(255,255,255,.06),inset 0 -16px 26px rgba(0,0,0,.5);
+          transition:transform .04s linear}
+        .rk-ptr{position:absolute;top:11px;left:50%;width:6px;height:30px;border-radius:4px;margin-left:-3px;
+          background:linear-gradient(#fff,#9fe8ff);box-shadow:0 0 13px #22d3ee,0 0 4px #fff}
+        .rk-read{position:absolute;text-align:center;pointer-events:none}
+        .rk-val{font-size:56px;font-weight:800;letter-spacing:-1px;line-height:1;font-variant-numeric:tabular-nums;color:var(--text,#e6edf3);text-shadow:0 0 20px rgba(34,211,238,.22)}
+        .rk-unit{font-size:11px;color:var(--text-muted,#7d8da3);letter-spacing:2px;margin-top:3px}
+        .rk-risk{margin-top:7px;font-size:12px;font-weight:800;letter-spacing:2px;transition:color .4s ease}
+        .rk-legend{display:flex;gap:14px;font-size:10.5px;color:var(--text-muted,#7d8da3);letter-spacing:.4px;flex-wrap:wrap;justify-content:center}
+        .rk-legend b{display:inline-block;width:9px;height:9px;border-radius:50%;margin-right:5px;vertical-align:middle}
+        .rk-apply{display:flex;align-items:center;gap:12px}
+        .rk-btn{border-radius:10px;padding:9px 18px;font-size:13px;font-weight:700;letter-spacing:.4px;cursor:pointer;transition:.15s;border:1px solid}
+        .rk-btn.on{background:rgba(34,211,238,.14);border-color:#22d3ee;color:#67e8f9}
+        .rk-btn.on:hover{background:rgba(34,211,238,.22)}
+        .rk-btn.off{background:transparent;border-color:rgba(255,255,255,.12);color:var(--text-muted,#7d8da3);cursor:default}
+        .rk-note{font-size:12px}
+      `}</style>
+
+      <div className="rk-stage" ref={stageRef} title="Drag to set how selective Ken is">
+        <div className="rk-halo" style={{ background: `radial-gradient(circle, ${z.c}66, transparent 70%)` }} />
+        <svg viewBox="0 0 300 300" aria-hidden="true">
+          <defs>
+            <linearGradient id="rkArc" x1="0" y1="1" x2="1" y2="0">
+              <stop offset="0" stopColor="#ef4444" /><stop offset=".38" stopColor="#f59e0b" />
+              <stop offset=".62" stopColor="#eab308" /><stop offset="1" stopColor="#22c55e" />
+            </linearGradient>
+          </defs>
+          <path d={arcPath(A0, A1)} fill="none" stroke="rgba(255,255,255,.07)" strokeWidth="10" strokeLinecap="round" />
+          <path d={arcPath(A0, a)} fill="none" stroke="url(#rkArc)" strokeWidth="10" strokeLinecap="round"
+            style={{ filter: 'drop-shadow(0 0 6px rgba(34,211,238,.35))' }} />
+          <g stroke="rgba(255,255,255,.18)" strokeWidth="2">
+            {TICKS.map((v) => {
+              const ta = val2ang(v), [x0, y0] = ptOn(ta, R + 8), [x1, y1] = ptOn(ta, v % 25 === 0 ? R + 18 : R + 13);
+              return <line key={v} x1={x0} y1={y0} x2={x1} y2={y1} />;
+            })}
+          </g>
+        </svg>
+        <div className="rk-knob" style={{ transform: `rotate(${ang}deg)` }}><div className="rk-ptr" /></div>
+        <div className="rk-read">
+          <div className="rk-val">{val}</div>
+          <div className="rk-unit">MIN CONFIDENCE</div>
+          <div className="rk-risk" style={{ color: z.c }}>{z.t}</div>
+        </div>
+      </div>
+
+      <div className="rk-legend">
+        <span><b style={{ background: '#ef4444' }} />50–64 High risk</span>
+        <span><b style={{ background: '#f59e0b' }} />65–74 Elevated</span>
+        <span><b style={{ background: '#eab308' }} />75–84 Balanced</span>
+        <span><b style={{ background: '#22c55e' }} />85–99 Conservative</span>
+      </div>
+
+      <div className="rk-apply">
+        <button className={'rk-btn ' + (dirty && !busy ? 'on' : 'off')} onClick={dirty && !busy ? apply : undefined} disabled={!dirty || busy}>
+          {busy ? 'Applying…' : dirty ? `Apply ${val} to Ken` : committed == null ? 'Loading…' : `Applied · ${committed}`}
+        </button>
+        {msg && <span className="rk-note" style={{ color: msg.ok ? '#34d399' : '#f87171' }}>{msg.text}</span>}
+      </div>
+      <p className="rk-note" style={{ color: 'var(--text-muted,#7d8da3)', maxWidth: 460, textAlign: 'center', margin: 0 }}>
+        Higher = fewer, higher-conviction real trades (lower risk). Lower = more trades on thinner edges (higher risk).
+        Turning the dial previews; <b>Apply</b> commits it to the live trader.
+      </p>
+    </div>
+  );
+}
diff --git a/kalshi-dash/src/pages/Settings.jsx b/kalshi-dash/src/pages/Settings.jsx
index 24a1155..76a01fc 100644
--- a/kalshi-dash/src/pages/Settings.jsx
+++ b/kalshi-dash/src/pages/Settings.jsx
@@ -1,5 +1,6 @@
 import React, { useState, useEffect } from 'react';
 import { api, post } from '../api';
+import RiskKnob from '../components/RiskKnob';
 
 // ── Interval presets ──
 const INTERVAL_PRESETS = [
@@ -278,6 +279,13 @@ export default function Settings() {
         </div>
       </div>
 
+      {/* ══════════════════════════════
+          RISK LEVEL KNOB (live control)
+          ══════════════════════════════ */}
+      <SettingsPanel title="Risk Level">
+        <RiskKnob />
+      </SettingsPanel>
+
       {/* ══════════════════════════════
           SCAN & PIPELINE CONFIG
           ══════════════════════════════ */}

← 4d35550 auto-save: 2026-07-15T12:06:24 (1 files) — kalshi-dash/risk-  ·  back to Ken  ·  Ken: FIX no-trades root cause — Kalshi API renamed price fie 2066e75 →