← 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
A kalshi-dash/src/components/RiskKnob.jsxM kalshi-dash/src/pages/Settings.jsx
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 →