diff --git a/frontend/README.md b/frontend/README.md
new file mode 100644
index 0000000..3ca45cc
--- /dev/null
+++ b/frontend/README.md
@@ -0,0 +1,65 @@
+# CircuitBreaker.ai — Frontend
+
+> **Institutional Terminal** for the autonomous DeFi circuit breaker.
+> High-density, data-centric risk dashboard — Bloomberg/Paradigm aesthetic, zero slop.
+
+Next.js (App Router) · TypeScript · Tailwind · Recharts · Lucide.
+
+## Run
+
+```bash
+cd frontend
+pnpm install
+pnpm dev # http://localhost:3000
+# prod: pnpm build && pnpm start
+```
+
+> Note: another app already listens on :3000 in this repo — run the frontend on a
+> free port with `pnpm exec next start -p 3939` if needed.
+
+## What it shows
+
+The whole terminal replays the **real USDC/SVB depeg (March 2023)** candle-by-candle
+(data extracted by `quant-backtest`). The breaker walks **SAFE → ARMED → TRIGGERED**
+live as CBRI climbs, evacuates at the optimal threshold **τ\* = 66**, exiting at
+**\$1.0000** while the depeg bottomed at **\$0.873** → **+\$126.9k saved** on \$1M.
+
+Four screens (per the brief):
+1. **CBRI Core** + **Risk Evolution** — global 0–100 risk gauge, CBRI ∥ USDC over time.
+2. **Pool Monitor** — target Uniswap v3 pool, TVL, price divergence, order-flow imbalance.
+3. **Backtest Simulator** — interactive τ slider, funds-saved curve, reliable vs false-positive zones, slippage.
+4. **Execution & Status** — breaker state + 1inch best-exec USDC→USDT evacuation log.
+
+Replay transport (bottom bar): play/pause, speed, scrub, **jump-to-trip**.
+
+## 🔌 Backend integration — the ONE file to touch
+
+Everything reads through `lib/data.ts`. Today it returns **bundled seed JSON**
+(`public/seed/*.json`, generated from `quant-backtest/output/*.csv`). To go live,
+replace the three getters with calls to the quant backend / The Graph / 1inch —
+**the return types are the contract** (`lib/types.ts`), nothing else changes:
+
+| Getter (`lib/data.ts`) | Returns | Live source |
+|---|---|---|
+| `getSeries()` | `CbriPoint[]` | quant CBRI stream (The Graph → `features.py`) |
+| `getSweep()` | `SweepRow[]` | `backtest.py` τ-sweep |
+| `getSummary()` | `Summary` | `backtest.py` τ\* selection |
+
+HTTP surface already wired (swap the handlers' data source, keep the shapes):
+
+- `GET /api/cbri/series` → `CbriPoint[]`
+- `GET /api/backtest/sweep` → `{ sweep: SweepRow[], summary: Summary }`
+- `GET /api/summary` → `Summary`
+
+For a **live feed**, point `lib/store.tsx` at a WebSocket/SSE that pushes new
+`CbriPoint`s and flip the replay clock off — the status logic (`deriveStatus`) and
+all panels already consume `current` reactively.
+
+## Design system
+
+Dark-committed "institutional terminal" (`tailwind.config.ts`): functional colors
+only — `risk`/`safe`/`armed` reserved status hues (always icon + label, never
+color-alone), a CVD-validated categorical trio, technical greys. Mono for all data
+(JetBrains Mono), tabular numbers everywhere. No glassmorphism, no neon, no glow.
+Charts follow single-axis discipline (CBRI and price are stacked panels sharing the
+x-axis, never a dual-axis).
diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx
new file mode 100644
index 0000000..f56d8e4
--- /dev/null
+++ b/frontend/app/page.tsx
@@ -0,0 +1,88 @@
+"use client";
+
+import { BreakerProvider } from "@/lib/store";
+import { Panel } from "@/components/ui";
+import { Header } from "@/components/Header";
+import { KpiTape } from "@/components/KpiTape";
+import { RiskGauge } from "@/components/RiskGauge";
+import { CbriChart } from "@/components/CbriChart";
+import { PoolMonitor } from "@/components/PoolMonitor";
+import { DrainChart } from "@/components/DrainChart";
+import { BacktestSimulator } from "@/components/BacktestSimulator";
+import { ExecutionPanel } from "@/components/ExecutionPanel";
+import { ReplayControls } from "@/components/ReplayControls";
+
+export default function Page() {
+ return (
+
+
+
+
+
+
+ {/* LEFT — the core + evolution + pool + drain */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* RIGHT — backtest simulator + execution */}
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/components/BacktestSimulator.tsx b/frontend/components/BacktestSimulator.tsx
new file mode 100644
index 0000000..a37295d
--- /dev/null
+++ b/frontend/components/BacktestSimulator.tsx
@@ -0,0 +1,222 @@
+"use client";
+
+import { useMemo, useState } from "react";
+import {
+ LineChart,
+ Line,
+ XAxis,
+ YAxis,
+ ReferenceLine,
+ ReferenceDot,
+ ResponsiveContainer,
+ Tooltip,
+} from "recharts";
+import { useBreaker } from "@/lib/store";
+import { sweepAt } from "@/lib/data";
+import { usdCompact, bps, price4, stampIso } from "@/lib/format";
+import { Target, AlertTriangle } from "lucide-react";
+
+function SweepTip({ active, payload }: any) {
+ if (!active || !payload?.length) return null;
+ const p = payload[0].payload;
+ return (
+
+
τ = {p.tau}
+
+ {usdCompact(p.fundsSaved)} saved
+
+
{bps(p.exitSlipBps)} slip
+
+ );
+}
+
+export function BacktestSimulator() {
+ const { sweep, summary } = useBreaker();
+ const tauStar = summary?.tauStar ?? 66;
+ const [tau, setTau] = useState(tauStar);
+
+ const data = useMemo(
+ () =>
+ sweep.map((r) => ({
+ ...r,
+ savedK: r.fundsSaved / 1e3,
+ reliable: r.fp === 0 ? r.fundsSaved / 1e3 : null,
+ risky: r.fp === 1 ? r.fundsSaved / 1e3 : null,
+ })),
+ [sweep]
+ );
+
+ const sel = sweepAt(sweep, tau);
+ const star = sweepAt(sweep, tauStar);
+ const delta = sel && star ? sel.fundsSaved - star.fundsSaved : 0;
+ const isOptimal = tau >= (summary?.plateauLo ?? tauStar) && tau <= (summary?.plateauHi ?? tauStar);
+
+ return (
+
+ {/* chart */}
+
+
+
+
+ `${v}k`}
+ />
+
+
+
+
+ {sel && (
+
+ )}
+ } cursor={{ stroke: "#3c424a" }} />
+
+
+
+
+ {/* legend */}
+
+
+ reliable (0 false-positive)
+
+
+ τ too low → false alarm
+
+
+
+ {/* slider */}
+
+
+
Trigger threshold τ
+
+
+ {tau}
+
+
+
+
+
setTau(Number(e.target.value))}
+ />
+
+
+ {/* result readout */}
+
+ {[
+ { l: "Trigger", v: sel ? stampIso(sel.triggerDt).slice(3) : "—", s: "UTC" },
+ { l: "Exit price", v: sel ? price4(sel.exitPrice) : "—", s: "USDC" },
+ { l: "Slippage", v: sel ? bps(sel.exitSlipBps) : "—", s: "1inch-cal" },
+ {
+ l: "Funds saved",
+ v: sel ? usdCompact(sel.fundsSaved) : "—",
+ s: "vs trough",
+ tone: sel && sel.fp ? "risk" : "safe",
+ },
+ ].map((c) => (
+
+
{c.l}
+
+ {c.v}
+
+
{c.s}
+
+ ))}
+
+
+ {/* verdict line */}
+
+ {sel?.fp ? (
+ <>
+
τ={tau} fires during calm market — false
+ evacuation risk.
+ >
+ ) : isOptimal ? (
+ <>
+
τ={tau} on the optimal plateau [{summary?.plateauLo}–
+ {summary?.plateauHi}] · max funds saved, 0 false-positive.
+ >
+ ) : (
+ <>
+ τ={tau} reliable but late — {delta < 0 ? usdCompact(delta) : "+" + usdCompact(delta)} vs τ*.
+ >
+ )}
+
+
+ );
+}
diff --git a/frontend/components/CbriChart.tsx b/frontend/components/CbriChart.tsx
new file mode 100644
index 0000000..009dc20
--- /dev/null
+++ b/frontend/components/CbriChart.tsx
@@ -0,0 +1,201 @@
+"use client";
+
+import {
+ AreaChart,
+ Area,
+ LineChart,
+ Line,
+ XAxis,
+ YAxis,
+ ReferenceLine,
+ ReferenceDot,
+ ResponsiveContainer,
+ Tooltip,
+} from "recharts";
+import { useBreaker } from "@/lib/store";
+import { armThreshold } from "@/lib/data";
+import { hhmm, stamp, price4 } from "@/lib/format";
+
+const MARGIN = { top: 6, right: 16, bottom: 0, left: 34 };
+
+function CbriTooltip({ active, payload }: any) {
+ if (!active || !payload?.length) return null;
+ const p = payload[0].payload;
+ return (
+
+
{stamp(p.t)} UTC
+
+ CBRI
+ {p.cbri.toFixed(1)}
+
+
+ USDC
+ {price4(p.usdc)}
+
+
+ drain
+ {p.drain.toFixed(1)}%/h
+
+
+ );
+}
+
+export function CbriChart() {
+ const { visible, series, summary, triggerIdx, triggered } = useBreaker();
+ const tau = summary?.tauStar ?? 66;
+ const arm = armThreshold(tau);
+ const start = summary?.startTs ?? series[0]?.t;
+ const end = summary?.endTs ?? series[series.length - 1]?.t;
+ const domain: [number, number] = [start ?? 0, end ?? 1];
+
+ const trig = triggerIdx >= 0 ? series[triggerIdx] : null;
+ const showTrig = triggered && trig;
+
+ const xTicks =
+ start && end
+ ? Array.from({ length: 4 }, (_, k) => start + ((end - start) * (k + 1)) / 5)
+ : [];
+
+ return (
+
+ {/* CBRI area */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {showTrig && (
+
+ )}
+ }
+ cursor={{ stroke: "#3c424a", strokeWidth: 1 }}
+ />
+
+
+
+
+
+ USDC / USD · depeg signal
+ {showTrig && (
+
+ ⎇ EVAC @ {price4(trig!.usdc)} · {stamp(trig!.t)}
+
+ )}
+
+
+ {/* Price strip */}
+
+
+
+
+ v.toFixed(2)}
+ tickLine={false}
+ axisLine={false}
+ width={30}
+ tick={{ fontSize: 10, fill: "#626871" }}
+ />
+
+ {summary && (
+
+ )}
+
+ {showTrig && (
+
+ )}
+
+
+
+
+ );
+}
diff --git a/frontend/components/DrainChart.tsx b/frontend/components/DrainChart.tsx
new file mode 100644
index 0000000..de78b86
--- /dev/null
+++ b/frontend/components/DrainChart.tsx
@@ -0,0 +1,107 @@
+"use client";
+
+import {
+ AreaChart,
+ Area,
+ XAxis,
+ YAxis,
+ ReferenceLine,
+ ResponsiveContainer,
+ Tooltip,
+} from "recharts";
+import { useBreaker } from "@/lib/store";
+import { hhmm, stamp } from "@/lib/format";
+
+const DRAIN_THRESHOLD = 6; // %/h (config.DRAIN_THRESHOLD * 100)
+
+function DrainTip({ active, payload }: any) {
+ if (!active || !payload?.length) return null;
+ const p = payload[0].payload;
+ return (
+
+ {stamp(p.t)}{" "}
+ {p.drain.toFixed(1)}%/h
+
+ );
+}
+
+export function DrainChart() {
+ const { visible, series, summary, current } = useBreaker();
+ const start = summary?.startTs ?? series[0]?.t;
+ const end = summary?.endTs ?? series[series.length - 1]?.t;
+ const cur = current?.drain ?? 0;
+ const overThreshold = cur >= DRAIN_THRESHOLD;
+
+ return (
+
+
+ ΔL / Δt · liquidity flight rate
+
+ {cur.toFixed(1)}
+ %/h
+
+
+
+
+
+
+
+
+
+
+
+
+ `${v}`}
+ />
+
+
+ }
+ cursor={{ stroke: "#3c424a", strokeWidth: 1 }}
+ />
+
+
+
+
+ );
+}
diff --git a/frontend/components/ExecutionPanel.tsx b/frontend/components/ExecutionPanel.tsx
new file mode 100644
index 0000000..090249e
--- /dev/null
+++ b/frontend/components/ExecutionPanel.tsx
@@ -0,0 +1,156 @@
+"use client";
+
+import { useBreaker } from "@/lib/store";
+import { armThreshold } from "@/lib/data";
+import { StatusBadge } from "./StatusBadge";
+import { usdCompact, bps, hhmm, addr } from "@/lib/format";
+import { CheckCircle2, ArrowRight } from "lucide-react";
+
+function LogLine({
+ ts,
+ tone = "ink3",
+ children,
+}: {
+ ts: number;
+ tone?: "ink3" | "ink2" | "armed" | "risk" | "safe";
+ children: React.ReactNode;
+}) {
+ const toneClass = {
+ ink3: "text-ink3",
+ ink2: "text-ink2",
+ armed: "text-armed",
+ risk: "text-risk",
+ safe: "text-safe",
+ }[tone];
+ return (
+
+ {hhmm(ts)}
+ {children}
+
+ );
+}
+
+export function ExecutionPanel() {
+ const { status, current, summary, execution, triggered } = useBreaker();
+ const tau = summary?.tauStar ?? 66;
+ const arm = armThreshold(tau);
+ const cbri = current?.cbri ?? 0;
+ const ts = current?.t ?? 0;
+
+ const totalOut = execution.reduce((s, l) => s + l.amountUsd, 0);
+
+ return (
+
+ {/* status header */}
+
+
+
+
Position
+
+ {usdCompact(summary?.position ?? 0)} {summary?.safeAsset ? "USDC" : ""}
+
+
+
+
+ {/* narrative line */}
+
+ {status === "SAFE" && (
+ <>Breaker armed & monitoring. CBRI {cbri.toFixed(0)} — well below trip τ* {tau}.>
+ )}
+ {status === "ARMED" && (
+
+ ⚠ Risk building. CBRI {cbri.toFixed(0)} crossed arm {arm} — staging evacuation route.
+
+ )}
+ {status === "TRIGGERED" && (
+
+ ⚡ TRIP. CBRI {cbri.toFixed(0)} ≥ τ* {tau} — emergency evacuation to {summary?.safeAsset} executed.
+
+ )}
+
+
+ {/* body */}
+
+ {!triggered ? (
+ /* standby log */
+
+ subgraph sync · Uniswap v3 · {summary?.pool}
+ CBRI = {cbri.toFixed(1)} · arm={arm} · trip τ*={tau}
+
+ drain {(current?.drain ?? 0).toFixed(1)}%/h · depeg {(Math.abs(1 - (current?.usdc ?? 1)) * 1e4).toFixed(0)}bps
+
+ {status === "ARMED" && (
+ <>
+ route staged: {summary?.safeAsset} best-exec via 1inch
+ awaiting τ* confirmation…
+ >
+ )}
+ no action · funds retained in position
+
+ ) : (
+ /* execution log */
+
+ {/* summary strip */}
+
+
+
Evacuated
+
+ {usdCompact(totalOut)}
+
+
+
+
Funds saved
+
+ {usdCompact(summary?.fundsSaved ?? 0)}
+
+
+
+
Success fee
+
+ {usdCompact(summary?.successFee ?? 0)}
+
+
+
+
+ {/* legs */}
+
+
+ ⚡ TRIP @ CBRI {tau} — 1inch best-execution split · $1M USDC → {summary?.safeAsset}
+
+ {execution.map((l) => (
+
+
+
+
+ {l.venue}
+
+
+ {l.fromToken} {l.toToken}
+
+
+
+
+ {usdCompact(l.amountUsd)}
+
+
+ {bps(l.priceImpactBps)}
+
+
+ {l.status}
+
+
+
+ ))}
+
+ ✓ evacuation complete · exit @ {summary?.exitPrice.toFixed(4)} · avg slip {summary?.exitSlipBps.toFixed(0)}bps
+
+
+
+ )}
+
+
+ );
+}
diff --git a/frontend/components/Header.tsx b/frontend/components/Header.tsx
new file mode 100644
index 0000000..1773fee
--- /dev/null
+++ b/frontend/components/Header.tsx
@@ -0,0 +1,77 @@
+"use client";
+
+import { useBreaker } from "@/lib/store";
+import { stamp } from "@/lib/format";
+import { StatusBadge } from "./StatusBadge";
+
+function SponsorChip({ name, role }: { name: string; role: string }) {
+ return (
+
+ {name}
+
+ {role}
+
+
+ );
+}
+
+export function Header() {
+ const { current, status, summary, ready } = useBreaker();
+ return (
+
+ );
+}
diff --git a/frontend/components/KpiTape.tsx b/frontend/components/KpiTape.tsx
new file mode 100644
index 0000000..7f63d51
--- /dev/null
+++ b/frontend/components/KpiTape.tsx
@@ -0,0 +1,55 @@
+"use client";
+
+import { useBreaker } from "@/lib/store";
+import { usdCompact, price4, pct } from "@/lib/format";
+
+function Cell({
+ label,
+ value,
+ tone = "ink",
+}: {
+ label: string;
+ value: string;
+ tone?: "ink" | "risk" | "safe" | "armed";
+}) {
+ const t = {
+ ink: "text-ink",
+ risk: "text-risk",
+ safe: "text-safe",
+ armed: "text-armed",
+ }[tone];
+ return (
+
+ {label}
+ {value}
+
+ );
+}
+
+export function KpiTape() {
+ const { current, summary, status } = useBreaker();
+ const depegBps = Math.abs(1 - (current?.usdc ?? 1)) * 1e4;
+ const savedPct = summary ? (summary.fundsSaved / summary.position) * 100 : 0;
+
+ return (
+
+
|
+
120 ? "risk" : "ink"} />
+ |
+ |
+
+ |
+ |
+ |
+ |
+ |
+ );
+}
diff --git a/frontend/components/PoolMonitor.tsx b/frontend/components/PoolMonitor.tsx
new file mode 100644
index 0000000..607706c
--- /dev/null
+++ b/frontend/components/PoolMonitor.tsx
@@ -0,0 +1,132 @@
+"use client";
+
+import { useBreaker } from "@/lib/store";
+import { addr, usdCompact, price2, price4 } from "@/lib/format";
+import { Stat } from "./ui";
+import { ExternalLink, Droplets } from "lucide-react";
+
+export function PoolMonitor() {
+ const { current, summary } = useBreaker();
+ const c = current;
+ const usdc = c?.usdc ?? 1;
+ const depegBps = Math.abs(1 - usdc) * 1e4;
+ const ofi = c?.ofi ?? 0;
+ const draining = (c?.netLiq ?? 0) < 0;
+
+ const depegTone = depegBps > 120 ? "risk" : depegBps > 40 ? "armed" : "safe";
+ const depegColor =
+ depegBps > 120 ? "#e5484d" : depegBps > 40 ? "#f5a623" : "#30a46c";
+
+ return (
+
+ {/* pool identity */}
+
+
+ {/* key stats */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* price divergence bar */}
+
+
+ Price Divergence · |1 − USDC|
+
+ {depegBps.toFixed(0)} bps
+
+
+
+ {/* peg center */}
+
+
+ {/* threshold tick at DEPEG_THRESHOLD 1.2% = 120bps */}
+
+
+
+ peg
+ τ 120bps
+ 13% depeg
+
+
+
+ {/* order-flow imbalance */}
+
+
+ Order-Flow Imbalance |I|
+
+ {draining ? "NET LIQ OUTFLOW" : "NET LIQ INFLOW"}
+
+
+
+
bal
+
+
+ {ofi.toFixed(2)}
+
+
+
+
+ );
+}
diff --git a/frontend/components/ReplayControls.tsx b/frontend/components/ReplayControls.tsx
new file mode 100644
index 0000000..4b532b8
--- /dev/null
+++ b/frontend/components/ReplayControls.tsx
@@ -0,0 +1,99 @@
+"use client";
+
+import { useBreaker } from "@/lib/store";
+import { Chip } from "./ui";
+import { stamp } from "@/lib/format";
+import { Play, Pause, RotateCcw, Zap } from "lucide-react";
+
+const SPEEDS: [string, number][] = [
+ ["1×", 1],
+ ["2×", 3],
+ ["4×", 6],
+ ["8×", 12],
+];
+
+export function ReplayControls() {
+ const {
+ playing,
+ toggle,
+ reset,
+ speed,
+ setSpeed,
+ i,
+ series,
+ scrub,
+ current,
+ triggerIdx,
+ jumpToTrigger,
+ } = useBreaker();
+
+ const len = Math.max(1, series.length - 1);
+ const trigPct = triggerIdx >= 0 ? (triggerIdx / len) * 100 : -1;
+
+ return (
+
+ {/* transport */}
+
+
+
+
+
+
+ {/* speed */}
+
+ speed
+ {SPEEDS.map(([lbl, v]) => (
+ setSpeed(v)}>
+ {lbl}
+
+ ))}
+
+
+ {/* scrub */}
+
+ {trigPct >= 0 && (
+
+ )}
+
scrub(Number(e.target.value))}
+ />
+
+
+ {/* readout */}
+
+ {current ? stamp(current.t) : "—"}
+ · {i}/{len}
+
+
+ );
+}
diff --git a/frontend/components/RiskGauge.tsx b/frontend/components/RiskGauge.tsx
new file mode 100644
index 0000000..a1cdad5
--- /dev/null
+++ b/frontend/components/RiskGauge.tsx
@@ -0,0 +1,155 @@
+"use client";
+
+import { useBreaker } from "@/lib/store";
+import { armThreshold } from "@/lib/data";
+import { ArrowUpRight, ArrowDownRight, Minus } from "lucide-react";
+
+const SEGMENTS = 56;
+
+export function RiskGauge() {
+ const { current, visible, status, summary } = useBreaker();
+ const cbri = current?.cbri ?? 0;
+ const tau = summary?.tauStar ?? 66;
+ const arm = armThreshold(tau);
+
+ // trend over the last ~30 min (6 candles)
+ const prev = visible[visible.length - 7]?.cbri ?? cbri;
+ const delta = cbri - prev;
+
+ const zoneColor = (v: number) =>
+ v >= tau ? "#e5484d" : v >= arm ? "#f5a623" : "#30a46c";
+ const statusInk =
+ status === "TRIGGERED"
+ ? "text-risk"
+ : status === "ARMED"
+ ? "text-armed"
+ : "text-safe";
+
+ return (
+
+
+
+
CBRI · Circuit Breaker Risk Index
+
+ Noisy-OR · drain ∨ imbalance ∨ depeg
+
+
+
0.5 ? "text-risk" : delta < -0.5 ? "text-safe" : "text-ink3"
+ }`}
+ >
+ {delta > 0.5 ? (
+
+ ) : delta < -0.5 ? (
+
+ ) : (
+
+ )}
+ {delta >= 0 ? "+" : ""}
+ {delta.toFixed(1)}
+
/30m
+
+
+
+ {/* Hero number */}
+
+
+ {cbri.toFixed(1)}
+
+
/100
+
+
+ {/* Segmented meter */}
+
+ {/* threshold markers */}
+
+ {[
+ { v: arm, label: "ARM", c: "#f5a623" },
+ { v: tau, label: "τ*", c: "#e5484d" },
+ ].map((m) => (
+
+
+ {m.label} {m.v}
+
+
+
+ ))}
+
+
+
+ {Array.from({ length: SEGMENTS }).map((_, k) => {
+ const v = ((k + 0.5) / SEGMENTS) * 100;
+ const lit = v <= cbri;
+ const isHead = Math.abs(v - cbri) < 100 / SEGMENTS / 1.2;
+ const col = zoneColor(v);
+ return (
+
+ );
+ })}
+
+
+ {/* zone legend */}
+
+ 0
+ SAFE ‹{arm}
+ ARMED {arm}–{tau}
+ TRIP ≥{tau}
+ 100
+
+
+
+ {/* sub-signal contributions */}
+
+ {[
+ { k: "DRAIN ΔL/Δt", v: current?.sDrain ?? 0, w: 1.0 },
+ { k: "IMBALANCE", v: current?.sOfi ?? 0, w: 0.0 },
+ { k: "DEPEG", v: current?.sDepeg ?? 0, w: 1.0 },
+ ].map((s) => (
+
+
+ {s.k}
+ {s.w === 0 && (
+
+ w=0
+
+ )}
+
+
+ {(s.v * 100).toFixed(0)}
+ %
+
+
+
+ ))}
+
+
+ );
+}