From a07bfee6e6b5b7372e31115cb5eb7d8aa7f6c10c Mon Sep 17 00:00:00 2001 From: Dennis Thiessen Date: Fri, 26 Jun 2026 16:14:32 +0200 Subject: [PATCH] feat: regime quadrant plot in place of the combined gauge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The combined score collapsed two distinct signals into one not-very-meaningful number. Replace its gauge with a quadrant scatter that shows both axes directly: x = regime index (coincident), y = early warning (breadth divergence), with a trail of the last 60 sessions and today highlighted. The four quadrants make the readings legible — ① hot & brittle (narrow melt-up, shakeout risk), ② transition, ③ healthy & broad, ④ real downturn — and the trail surfaces the actual tell: the ①→④ move (early warning rolling over as the regime index climbs = divergence resolving downward). Combined still shows as a line in the score-history chart. Frontend-only; reuses the history endpoint. Lazy-loaded. Co-Authored-By: Claude Opus 4.8 --- .../src/components/regime/RegimeQuadrant.tsx | 146 ++++++++++++++++++ frontend/src/pages/RegimePage.tsx | 15 +- 2 files changed, 150 insertions(+), 11 deletions(-) create mode 100644 frontend/src/components/regime/RegimeQuadrant.tsx diff --git a/frontend/src/components/regime/RegimeQuadrant.tsx b/frontend/src/components/regime/RegimeQuadrant.tsx new file mode 100644 index 0000000..36b18a8 --- /dev/null +++ b/frontend/src/components/regime/RegimeQuadrant.tsx @@ -0,0 +1,146 @@ +import { useMemo } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { + ScatterChart, + Scatter, + XAxis, + YAxis, + ZAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, + ReferenceLine, + ReferenceArea, +} from 'recharts'; +import { getRegimeHistory } from '../../api/regime'; +import { Callout } from '../ui/Callout'; +import { SkeletonCard } from '../ui/Skeleton'; + +// Lazy-loaded (see RegimePage) so recharts stays in the regime-tab chunk. + +// Quadrant dividers. Regime < 40 ≈ intact; early-warning > 60 ≈ elevated. +const X_DIV = 40; // regime index +const Y_DIV = 60; // early warning +const TRAIL = 60; // sessions shown + +interface QPoint { + x: number; + y: number; + date: string; +} + +function QuadrantTip({ active, payload }: { active?: boolean; payload?: { payload: QPoint }[] }) { + if (!active || !payload?.length) return null; + const p = payload[0].payload; + return ( +
+
{p.date}
+
+ Regime {Math.round(p.x)} · Early warning{' '} + {Math.round(p.y)} +
+
+ ); +} + +export default function RegimeQuadrant() { + const history = useQuery({ queryKey: ['regime', 'history'], queryFn: () => getRegimeHistory(400) }); + + const points = useMemo(() => { + const data = history.data ?? []; + return data + .filter((p) => p.early_warning != null) + .slice(-TRAIL) + .map((p) => ({ x: p.index, y: p.early_warning as number, date: p.date })); + }, [history.data]); + + const latest = points.length ? points[points.length - 1] : null; + + return ( +
+
+
+ Regime quadrant — last {TRAIL} sessions +
+ {latest && ( +
+ now: regime {Math.round(latest.x)} · warning{' '} + {Math.round(latest.y)} +
+ )} +
+ + {history.isLoading ? ( + + ) : !points.length ? ( + + Not enough history yet — the early-warning fills in as the daily job runs. + + ) : ( + <> +
+ + + {/* Quadrant shading (drawn first, behind everything) */} + + + + + + + + + + + } /> + {/* Trail (chronological path) */} + + {/* Today */} + {latest && ( + ( + + )} + /> + )} + + +
+ +
+ ① Hot & brittle — narrow melt-up, shakeout risk + ② Transition — break may be starting + ③ Healthy & broad — calm uptrend + ④ Real downturn — regime breaking, broad +
+

+ White dot = today; trail = path over the last {TRAIL} sessions. The tell isn't a single spot but the + move ①→④ (early warning rolling over while the regime index climbs = divergence resolving downward). + Observational — not wired into trades. +

+ + )} +
+ ); +} diff --git a/frontend/src/pages/RegimePage.tsx b/frontend/src/pages/RegimePage.tsx index b196d4b..1618515 100644 --- a/frontend/src/pages/RegimePage.tsx +++ b/frontend/src/pages/RegimePage.tsx @@ -18,6 +18,7 @@ import { // Lazy so recharts (heavy) ships in its own chunk, loaded only on this tab. const ScoreHistoryChart = lazy(() => import('../components/regime/ScoreHistoryChart')); +const RegimeQuadrant = lazy(() => import('../components/regime/RegimeQuadrant')); import type { RegimeBand, RegimeSignal, @@ -554,17 +555,9 @@ export default function RegimePage() { } /> - A weighted mean of the index and the early warning — for observation only. Tune the mix via the - regime config. - } - /> + }> + + }>