From c3dd80e203a64ff36ea5b275985d0139f5d72b18 Mon Sep 17 00:00:00 2001 From: Dennis Thiessen Date: Fri, 10 Jul 2026 23:07:22 +0200 Subject: [PATCH] Performance chart under positions; remove obsolete files - PerfChart moves into the left column under Open Positions (half-width), balancing the radar column. Cleanup: - Design mockup pages (DesignMockups / DesignHorizon / DesignOrbit) and their routes removed - the redesign has shipped; the mockups live on in git history. Main bundle shrinks ~97 kB and the chunk-size warning goes away. - frontend/tsconfig.tsbuildinfo untracked and gitignored (incremental build artifact that churned in every commit). - Dead npm test / test:watch scripts removed - vitest was never a dependency; CI already skips the step via its vitest guard. Co-Authored-By: Claude Fable 5 --- .gitignore | 1 + frontend/package.json | 4 +- frontend/src/App.tsx | 6 - frontend/src/pages/DashboardPage.tsx | 10 +- frontend/src/pages/DesignHorizonPage.tsx | 1391 ---------------------- frontend/src/pages/DesignMockupsPage.tsx | 680 ----------- frontend/src/pages/DesignOrbitPage.tsx | 134 --- frontend/tsconfig.tsbuildinfo | 1 - 8 files changed, 7 insertions(+), 2220 deletions(-) delete mode 100644 frontend/src/pages/DesignHorizonPage.tsx delete mode 100644 frontend/src/pages/DesignMockupsPage.tsx delete mode 100644 frontend/src/pages/DesignOrbitPage.tsx delete mode 100644 frontend/tsconfig.tsbuildinfo diff --git a/.gitignore b/.gitignore index 58b973d..1a37243 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,7 @@ Thumbs.db # Frontend frontend/node_modules/ frontend/dist/ +frontend/tsconfig.tsbuildinfo # Alembic alembic/versions/__pycache__/ diff --git a/frontend/package.json b/frontend/package.json index b77dc98..31aac73 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,9 +6,7 @@ "scripts": { "dev": "vite", "build": "tsc -b && vite build", - "preview": "vite preview", - "test": "vitest --run", - "test:watch": "vitest" + "preview": "vite preview" }, "dependencies": { "react": "^18.3.1", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 362d374..bc56c16 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -9,18 +9,12 @@ import SignalsPage from './pages/SignalsPage'; import RegimePage from './pages/RegimePage'; import TickerDetailPage from './pages/TickerDetailPage'; import AdminPage from './pages/AdminPage'; -import DesignMockupsPage from './pages/DesignMockupsPage'; -import DesignHorizonPage from './pages/DesignHorizonPage'; -import DesignOrbitPage from './pages/DesignOrbitPage'; export default function App() { return ( } /> } /> - } /> - } /> - } /> }> }> } /> diff --git a/frontend/src/pages/DashboardPage.tsx b/frontend/src/pages/DashboardPage.tsx index 5a6a89a..b0665cb 100644 --- a/frontend/src/pages/DashboardPage.tsx +++ b/frontend/src/pages/DashboardPage.tsx @@ -480,9 +480,12 @@ export default function DashboardPage() { )} - {/* Open positions | Radar — side by side, half width each */} + {/* Open positions + performance | Radar — side by side, half width each */}
- +
+ + +
@@ -572,9 +575,6 @@ export default function DashboardPage() {
- - {/* Performance — the paper book vs the same dollars in SPY */} - ); } diff --git a/frontend/src/pages/DesignHorizonPage.tsx b/frontend/src/pages/DesignHorizonPage.tsx deleted file mode 100644 index 48156a2..0000000 --- a/frontend/src/pages/DesignHorizonPage.tsx +++ /dev/null @@ -1,1391 +0,0 @@ -/** - * Design study "Horizon" — v2 mockup. - * - * Fully self-contained: static mock data, no hooks into the API, no auth. - * View at /design-horizon with `npm run dev` — no backend or DB needed. - * - * Direction: space void + dim Mars horizon (glow stays atmosphere, never data), - * one focal "what should I do today" card, zero classic tables, drill-down - * everywhere. Chart colors validated (CVD + contrast) against the dark surface. - */ -import { useMemo, useRef, useState } from 'react'; - -/* ------------------------------------------------------------------ */ -/* Mock data */ -/* ------------------------------------------------------------------ */ - -const heroSetup = { - symbol: 'VRT', - name: 'Vertiv Holdings', - direction: 'long' as const, - entry: 92.1, - stop: 86.4, - current: 94.3, - target: 107.0, - rr: 2.6, - targetProb: 58, - residualPct: 94, - conviction: 'High', - sizeHint: 'vol-adjusted size 6.2% of book', -}; - -const positions = [ - { - symbol: 'ANET', name: 'Arista Networks', direction: 'long', r: 1.4, - entry: 118.4, current: 131.2, shares: 25, opened: 'May 28', - stop: 121.8, stopNote: 'trailed from 111.10', target: 140.0, - alphaUsd: 310, exit: 'ATR trailing stop', - spark: [118.4, 119.2, 121.5, 120.8, 123.4, 125.1, 124.2, 126.8, 128.3, 127.5, 129.9, 131.2], - stopTrail: [111.1, 111.1, 111.1, 112.6, 114.2, 114.2, 115.8, 117.4, 117.4, 119.5, 121.8, 121.8], - }, - { - symbol: 'VRT', name: 'Vertiv Holdings', direction: 'long', r: 1.1, - entry: 88.3, current: 94.3, shares: 40, opened: 'Jun 12', - stop: 86.9, stopNote: 'trailed from 83.10', target: 103.0, - alphaUsd: 205, exit: 'ATR trailing stop', - spark: [88.3, 89.1, 88.6, 90.2, 91.4, 90.8, 92.1, 93.0, 92.4, 93.6, 94.1, 94.3], - stopTrail: [83.1, 83.1, 84.0, 84.0, 84.9, 84.9, 85.6, 85.6, 86.2, 86.2, 86.9, 86.9], - }, - { - symbol: 'CLS', name: 'Celestica', direction: 'long', r: 0.4, - entry: 61.2, current: 62.9, shares: 60, opened: 'Jun 30', - stop: 57.1, stopNote: 'initial stop', target: 71.5, - alphaUsd: 84, exit: 'ATR trailing stop', - spark: [61.2, 61.8, 61.1, 62.3, 62.0, 62.6, 63.1, 62.7, 62.4, 62.9, 63.2, 62.9], - stopTrail: [57.1, 57.1, 57.1, 57.1, 57.1, 57.1, 57.1, 57.1, 57.1, 57.1, 57.1, 57.1], - }, - { - symbol: 'SMCI', name: 'Super Micro Computer', direction: 'short', r: -0.6, - entry: 44.8, current: 46.1, shares: 80, opened: 'Jul 2', - stop: 47.0, stopNote: 'initial stop', target: 38.5, - alphaUsd: -57, exit: 'ATR trailing stop', - spark: [44.8, 44.2, 45.0, 45.6, 45.1, 45.9, 46.4, 45.8, 46.2, 46.0, 46.3, 46.1], - stopTrail: [47.0, 47.0, 47.0, 47.0, 47.0, 47.0, 47.0, 47.0, 47.0, 47.0, 47.0, 47.0], - }, -]; - -const radar = [ - { - rank: 2, symbol: 'CLS', name: 'Celestica', direction: 'long', - residual: 91, rr: '2.2:1', prob: 61, qualified: true, note: 'clears EV gate', - }, - { - rank: 3, symbol: 'APP', name: 'AppLovin', direction: 'long', - residual: 89, rr: '2.4:1', prob: 55, qualified: true, note: 'clears EV gate', - }, - { - rank: 4, symbol: 'NVDA', name: 'NVIDIA', direction: 'long', - residual: 87, rr: '1.6:1', prob: 63, qualified: false, note: 'EV below gate — payoff too thin', - }, - { - rank: 5, symbol: 'AMD', name: 'Advanced Micro Devices', direction: 'long', - residual: 84, rr: '2.1:1', prob: 48, qualified: false, note: 'extended — 12% above entry zone', - }, - { - rank: 6, symbol: 'PLTR', name: 'Palantir', direction: 'short', - residual: 82, rr: '1.9:1', prob: 44, qualified: false, note: 'counter-trend in neutral regime', - }, -]; - -const watchlist = [ - { symbol: 'VRT', pct: 3.1 }, { symbol: 'ANET', pct: 2.4 }, - { symbol: 'NVDA', pct: 1.8 }, { symbol: 'AVGO', pct: 1.5 }, - { symbol: 'CRWD', pct: 0.9 }, { symbol: 'MSFT', pct: 0.3 }, - { symbol: 'PLTR', pct: -0.9 }, { symbol: 'META', pct: -1.2 }, -]; - -/* Weekly closes since January, indexed to 100 — one axis, common base. */ -const perfPortfolio = [100, 101.2, 100.6, 102.3, 103.8, 103.1, 105.0, 106.4, 105.2, 107.9, 109.6, 108.8, 111.3, 110.1, 112.6, 114.9, 113.8, 116.2, 118.4, 117.1, 119.8, 121.6, 120.4, 123.1, 125.0, 126.4]; -const perfSpy = [100, 100.8, 100.2, 101.5, 102.1, 101.4, 102.6, 103.3, 102.5, 103.9, 104.6, 103.8, 104.9, 104.1, 105.2, 106.0, 105.3, 106.4, 107.2, 106.5, 107.4, 108.1, 107.3, 108.0, 108.6, 108.2]; -const perfStart = new Date(2026, 0, 9); -const perfDates = perfPortfolio.map((_, i) => { - const d = new Date(perfStart); - d.setDate(d.getDate() + i * 7); - return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); -}); - -const alphaSpark = [0, 42, 30, 95, 150, 128, 210, 265, 231, 340, 415, 388, 470, 442, 508, 570, 538, 590, 642]; - -/* ~3 months of daily VRT candles, seeded so the mock is stable across reloads. */ -type Candle = { d: Date; o: number; h: number; l: number; c: number; v: number }; -const vrtCandles: Candle[] = (() => { - let s = 42 >>> 0; - const rnd = () => { s = (Math.imul(s, 1664525) + 1013904223) >>> 0; return s / 4294967296; }; - const days: Date[] = []; - const cursor = new Date(2026, 6, 9); - while (days.length < 64) { - const dow = cursor.getDay(); - if (dow !== 0 && dow !== 6) days.unshift(new Date(cursor)); - cursor.setDate(cursor.getDate() - 1); - } - const drift = (i: number) => (i < 20 ? 0.30 : i < 28 ? -0.38 : i < 46 ? 0.44 : i < 53 ? -0.32 : 0.52); - const out: Candle[] = []; - let close = 73.6; - for (let i = 0; i < 64; i++) { - const chg = drift(i) + (rnd() - 0.5) * 2.4; - const o = close; - const c = o + chg; - out.push({ - d: days[i], o, c, - h: Math.max(o, c) + rnd() * 1.2, - l: Math.min(o, c) - rnd() * 1.2, - v: 1.4e6 * (0.55 + rnd()) * (1 + Math.abs(chg) / 2.5), - }); - close = c; - } - const shift = 94.3 - close; - return out.map((b) => ({ ...b, o: b.o + shift, h: b.h + shift, l: b.l + shift, c: b.c + shift })); -})(); - -const tickerLevels = { - resistance: 96.4, - supportLo: 88.7, - supportHi: 90.1, -}; - -/* Score fingerprint — all axes 0–100 percentile, same order everywhere. */ -const RADAR_AXES = ['Momentum', 'Trend', 'Setup', 'Sentiment', 'Fundam.']; -const RADAR_AXES_FULL = [ - 'Residual momentum', 'Trend strength', 'Setup quality', 'News sentiment', 'Fundamentals', -]; -const heroScores = [94, 88, 85, 71, 62]; -const liveMedian = [68, 61, 57, 52, 55]; -const qualifiedCompare = [ - { symbol: 'VRT', tag: 'top pick', scores: heroScores }, - { symbol: 'CLS', tag: 'rank 2', scores: [91, 82, 72, 58, 74] }, - { symbol: 'APP', tag: 'rank 3', scores: [89, 78, 69, 66, 81] }, -]; - -/* ------------------------------------------------------------------ */ -/* Small building blocks */ -/* ------------------------------------------------------------------ */ - -function fmt(n: number, digits = 2): string { - return n.toLocaleString('en-US', { minimumFractionDigits: digits, maximumFractionDigits: digits }); -} - -function signed(n: number, unit = ''): string { - return `${n > 0 ? '+' : n < 0 ? '−' : ''}${fmt(Math.abs(n))}${unit}`; -} - -function DirTag({ direction }: { direction: string }) { - return ( - - {direction} - - ); -} - -/** 2px sparkline, endpoint dot with surface ring. Single series — no legend. */ -function Spark({ data, color, w = 130, h = 34 }: { data: number[]; color: string; w?: number; h?: number }) { - const min = Math.min(...data); - const max = Math.max(...data); - const span = max - min || 1; - const px = (i: number) => 3 + (i / (data.length - 1)) * (w - 10); - const py = (v: number) => h - 5 - ((v - min) / span) * (h - 10); - const path = data.map((v, i) => `${i === 0 ? 'M' : 'L'}${px(i).toFixed(1)},${py(v).toFixed(1)}`).join(' '); - return ( - - ); -} - -/** Diverging R-multiple bar: grows from a centered zero baseline. */ -function RBar({ r, max = 1.6 }: { r: number; max?: number }) { - const pct = Math.min(Math.abs(r) / max, 1) * 50; - return ( -
- - = 0 ? '50%' : `${50 - pct}%`, - width: `${pct}%`, - background: r >= 0 ? 'var(--up)' : 'var(--down)', - borderRadius: r >= 0 ? '0 4px 4px 0' : '4px 0 0 4px', - }} - /> -
- ); -} - -/* ------------------------------------------------------------------ */ -/* Radar — the score fingerprint (5 axes, 0–100 %ile, fixed order) */ -/* ------------------------------------------------------------------ */ - -function RadarChart({ - scores, reference, size = 110, labels = false, -}: { scores: number[]; reference?: number[]; size?: number; labels?: boolean }) { - const [hover, setHover] = useState(null); - const pad = labels ? 78 : 10; - const vb = size + pad * 2; - const c = vb / 2; - const r = size / 2; - const pt = (v: number, i: number): [number, number] => { - const a = ((-90 + i * 72) * Math.PI) / 180; - return [c + (Math.cos(a) * r * v) / 100, c + (Math.sin(a) * r * v) / 100]; - }; - const poly = (vals: number[]) => - vals.map((v, i) => pt(v, i).map((n) => n.toFixed(1)).join(',')).join(' '); - const rendered = vb * (labels ? 0.95 : 1); - const tip = hover !== null ? pt(scores[hover], hover) : null; - return ( -
- `${ax} ${scores[i]}`).join(', ')} (0–100 percentile)`} - > - {[50, 100].map((ring) => ( - - ))} - {RADAR_AXES.map((ax, i) => { - const [x, y] = pt(100, i); - return ; - })} - {reference && ( - - )} - - {scores.map((v, i) => { - const [x, y] = pt(v, i); - return ( - - ); - })} - {labels && RADAR_AXES.map((ax, i) => { - const a = ((-90 + i * 72) * Math.PI) / 180; - const x = c + Math.cos(a) * (r + 14); - const y = c + Math.sin(a) * (r + 14); - const anchor = Math.cos(a) > 0.3 ? 'start' : Math.cos(a) < -0.3 ? 'end' : 'middle'; - const dy = Math.sin(a) < -0.3 ? -2 : Math.sin(a) > 0.3 ? 9 : 4; - return ( - - {ax} {scores[i]} - - ); - })} - {/* hit targets on top — comfortably larger than the dots, keyboard-reachable */} - {scores.map((v, i) => { - const [x, y] = pt(v, i); - return ( - setHover(i)} - onMouseLeave={() => setHover(null)} - onFocus={() => setHover(i)} - onBlur={() => setHover(null)} - /> - ); - })} - - {hover !== null && tip && ( -
- {RADAR_AXES_FULL[hover]} - {scores[hover]}th %ile - {reference && live-setup median {reference[hover]}} -
- )} -
- ); -} - -/* ------------------------------------------------------------------ */ -/* Trade chart — price since entry with entry / trailing stop / target */ -/* ------------------------------------------------------------------ */ - -type Position = (typeof positions)[number]; - -function TradeChart({ p }: { p: Position }) { - const w = 560; const h = 150; - const padL = 8; const padR = 96; const padT = 10; const padB = 12; - const isShort = p.direction === 'short'; - const vals = [...p.spark, ...p.stopTrail, p.entry]; - let lo = Math.min(...vals); - let hi = Math.max(...vals); - const range = hi - lo || 1; - // Only stretch the scale to the target when it doesn't flatten the price action. - const targetFits = isShort ? lo - p.target <= range * 0.9 : p.target - hi <= range * 0.9; - if (targetFits) { lo = Math.min(lo, p.target); hi = Math.max(hi, p.target); } - const pad = (hi - lo) * 0.07; - lo -= pad; hi += pad; - const plotW = w - padL - padR; - const plotH = h - padT - padB; - const px = (i: number) => padL + (i / (p.spark.length - 1)) * plotW; - const py = (v: number) => padT + plotH - ((v - lo) / (hi - lo)) * plotH; - const pricePath = p.spark.map((v, i) => `${i === 0 ? 'M' : 'L'}${px(i).toFixed(1)},${py(v).toFixed(1)}`).join(' '); - let stopPath = `M${px(0).toFixed(1)},${py(p.stopTrail[0]).toFixed(1)}`; - for (let i = 1; i < p.stopTrail.length; i++) { - stopPath += `H${px(i).toFixed(1)}V${py(p.stopTrail[i]).toFixed(1)}`; - } - const col = p.r >= 0 ? 'var(--up)' : 'var(--down)'; - const labelX = w - padR + 10; - const entryY = py(p.entry); - let stopY = py(p.stopTrail[p.stopTrail.length - 1]); - if (Math.abs(stopY - entryY) < 11) stopY = entryY + (stopY >= entryY ? 11 : -11); - const lastI = p.spark.length - 1; - return ( - - - entry {fmt(p.entry)} - - stop {fmt(p.stop)} - {targetFits ? ( - <> - - target {fmt(p.target)} - - ) : ( - - target {fmt(p.target)} {isShort ? '↓' : '↑'} - - )} - - - - ); -} - -/* ------------------------------------------------------------------ */ -/* The price rail — stop → entry → current → target, laid out spatially */ -/* ------------------------------------------------------------------ */ - -function PriceRail({ s }: { s: typeof heroSetup }) { - const lo = s.stop - (s.target - s.stop) * 0.06; - const hi = s.target + (s.target - s.stop) * 0.06; - const pct = (v: number) => ((v - lo) / (hi - lo)) * 100; - const rNow = (s.current - s.entry) / (s.entry - s.stop); - return ( -
-
-
-
- {/* markers */} -
- - - stop - {fmt(s.stop)} - −1R - -
-
- - - entry - {fmt(s.entry)} - -
-
- - - now - {fmt(s.current)} - {signed(rNow)}R - -
-
- - - target - {fmt(s.target)} - +{fmt(s.rr, 1)}R - -
-
- ); -} - -/* ------------------------------------------------------------------ */ -/* Performance chart — indexed to 100, crosshair + tooltip on hover */ -/* ------------------------------------------------------------------ */ - -const CHART = { w: 760, h: 220, top: 14, right: 64, bottom: 26, left: 40 }; - -function PerfChart() { - const [hover, setHover] = useState(null); - const svgRef = useRef(null); - - const { w, h, top, right, bottom, left } = CHART; - const plotW = w - left - right; - const plotH = h - top - bottom; - const yMin = 95; - const yMax = 130; - const px = (i: number) => left + (i / (perfPortfolio.length - 1)) * plotW; - const py = (v: number) => top + plotH - ((v - yMin) / (yMax - yMin)) * plotH; - const line = (data: number[]) => - data.map((v, i) => `${i === 0 ? 'M' : 'L'}${px(i).toFixed(1)},${py(v).toFixed(1)}`).join(' '); - - const yTicks = [100, 110, 120, 130]; - const xTicks = useMemo( - () => [0, 5, 10, 15, 20, 25].map((i) => ({ i, label: perfDates[i] })), - [], - ); - - const onMove = (e: React.MouseEvent) => { - const rect = svgRef.current?.getBoundingClientRect(); - if (!rect) return; - const x = ((e.clientX - rect.left) / rect.width) * w; - const i = Math.round(((x - left) / plotW) * (perfPortfolio.length - 1)); - setHover(Math.max(0, Math.min(perfPortfolio.length - 1, i))); - }; - - const last = perfPortfolio.length - 1; - - return ( -
-
-
-

Performance

-

Paper book vs S&P 500 · indexed to 100 · since January

-
-
- Book - S&P 500 -
-
- setHover(null)} - role="img" - aria-label="Paper book up 26.4% versus S&P 500 up 8.2% since January, indexed to 100" - > - {yTicks.map((t) => ( - - - {t} - - ))} - {xTicks.map(({ i, label }) => ( - {label} - ))} - - - {hover !== null && ( - - - - - - )} - {/* direct end labels */} - - - {fmt(perfPortfolio[last], 1)} - {fmt(perfSpy[last], 1)} - -
- {hover !== null - ? <>week of {perfDates[hover]} — book {fmt(perfPortfolio[hover], 1)} · S&P 500 {fmt(perfSpy[hover], 1)} - : <>hover for weekly values} -
-
- ); -} - -/* ------------------------------------------------------------------ */ -/* Ticker drill-down — candlesticks + volume + trade levels */ -/* ------------------------------------------------------------------ */ - -function TickerPanel() { - const [hover, setHover] = useState(null); - const svgRef = useRef(null); - - const w = 900; const padT = 10; const padB = 20; const padL = 8; const padR = 96; - const priceH = 290; const volH = 54; const gap = 16; - const H = padT + priceH + gap + volH + padB; - const n = vrtCandles.length; - const slot = (w - padL - padR) / n; - const bodyW = Math.min(7, slot * 0.62); - - const hi = Math.max(...vrtCandles.map((b) => b.h), heroSetup.target + 1.5); - const lo = Math.min(...vrtCandles.map((b) => b.l)) - 1; - const maxV = Math.max(...vrtCandles.map((b) => b.v)); - const px = (i: number) => padL + (i + 0.5) * slot; - const py = (v: number) => padT + priceH - ((v - lo) / (hi - lo)) * priceH; - const vy = (v: number) => padT + priceH + gap + volH - (v / maxV) * volH; - - // Skip ticks whose right-gutter label would collide with a level label. - const levelYs = [ - heroSetup.entry, heroSetup.stop, heroSetup.target, - tickerLevels.resistance, (tickerLevels.supportLo + tickerLevels.supportHi) / 2, - ]; - const yTicks = [75, 85, 95, 105].filter( - (t) => t > lo && t < hi - && levelYs.every((l) => Math.abs(((t - l) / (hi - lo)) * priceH) >= 13), - ); - const monthTicks = vrtCandles - .map((b, i) => ({ b, i })) - .filter(({ b, i }) => i === 0 || b.d.getMonth() !== vrtCandles[i - 1].d.getMonth()) - .map(({ b, i }) => ({ i, label: b.d.toLocaleDateString('en-US', { month: 'short' }) })); - - const onMove = (e: React.MouseEvent) => { - const rect = svgRef.current?.getBoundingClientRect(); - if (!rect) return; - const x = ((e.clientX - rect.left) / rect.width) * w; - const i = Math.floor((x - padL) / slot); - setHover(Math.max(0, Math.min(n - 1, i))); - }; - - const labelX = w - padR + 10; - const hb = hover !== null ? vrtCandles[hover] : null; - - return ( -
-
-
-

Drill-down · Ticker

-

- VRT - Vertiv Holdings -

-

- 94.30 - +1.9% - today -

-
- composite score 82 - ATR 2.90 · 3.1% - earnings Aug 5 -
-
-
- -

- VRT - live-setup median -

-
-
- -
- -
- {(['1M', '3M', '6M', 'YTD', '1Y', 'All'] as const).map((r) => ( - - ))} -
-
- - setHover(null)} - role="img" - aria-label="Daily candlestick chart for VRT with entry, trailing stop, target, support and resistance levels" - > - {yTicks.map((t) => ( - - - {t} - - ))} - {monthTicks.map(({ i, label }) => ( - {label} - ))} - - {/* support zone + resistance (nearest S/R only — full list stays in its tab) */} - - - sup {fmt(tickerLevels.supportLo, 1)}–{fmt(tickerLevels.supportHi, 1)} - - - resist {fmt(tickerLevels.resistance)} - - {/* trade levels */} - - entry {fmt(heroSetup.entry)} - - stop {fmt(heroSetup.stop)} - - target {fmt(heroSetup.target)} - - {/* candles */} - {vrtCandles.map((b, i) => { - const up = b.c >= b.o; - const col = up ? 'var(--up)' : 'var(--down)'; - const bodyY = py(Math.max(b.o, b.c)); - const bodyH = Math.max(1.5, Math.abs(py(b.o) - py(b.c))); - return ( - - - - - - ); - })} - - {hover !== null && ( - - )} - - -
- {hb ? ( - <> - {hb.d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })} — O {fmt(hb.o)} H {fmt(hb.h)} L {fmt(hb.l)} C {fmt(hb.c)} · vol {(hb.v / 1e6).toFixed(1)}M - - ) : ( - <>hover for daily OHLC · volume below - )} -
- -

- prices 2h ago - indicators 2h ago - sentiment 6h ago - fundamentals 3d ago - manage data → -

-
- ); -} - -/* ------------------------------------------------------------------ */ -/* Page */ -/* ------------------------------------------------------------------ */ - -export default function DesignHorizonPage() { - const [expanded, setExpanded] = useState('ANET'); - const [showTicker, setShowTicker] = useState(true); - - return ( -
- - - {/* atmosphere — stays behind everything, never carries data */} -