Horizon redesign: space theme, visual dashboard, chart reskin
Deploy / lint (push) Successful in 9s
Deploy / test (push) Successful in 1m9s
Deploy / deploy (push) Successful in 36s

Replaces the citron/green glass theme with the Horizon direction
(mockup at /design-horizon): space-void background, starfield, dim
Mars horizon, rim-cyan accent, rose for negative. Palette validated
for CVD separation and contrast on the dark surface.

- tailwind.config: gray -> cool space neutrals, blue/emerald -> cyan
  scale, red -> rose; display font Space Grotesk
- globals.css: Horizon tokens, atmosphere, denser glass, cyan
  buttons/inputs, price-rail / R-bar / radar chart CSS
- Dashboard rebuilt: verdict hero, top-pick card with spatial price
  rail, KPI tiles, open positions as diverging R-bars with expandable
  detail + trade chart (OHLCV since entry, entry/stop/target levels),
  radar list with per-setup disqualify reason, watchlist chips
- OpenTradesPanel: table replaced by R-bar rows; all fields kept in
  the drill-down (shares, P&L, alpha, trailing stop, sell)
- qualification: disqualifyReason() mirrors qualifiesSetup rule order
- CandlestickChart: canvas colors moved to Horizon palette; S/R now
  cyan/neutral so rose stays reserved for the stop level
- ScoreCard: radar score fingerprint with hover values
- Design mockup pages included (routed in App.tsx)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-10 07:24:17 +02:00
co-authored by Claude Fable 5
parent 744ea4ddc4
commit 20f6981712
15 changed files with 3283 additions and 370 deletions
+36
View File
@@ -63,6 +63,42 @@ export function qualifiesSetup(setup: TradeSetup, config: ActivationConfig): boo
return true;
}
/**
* Why a setup does NOT clear the gate — the first failing rule, phrased for the
* dashboard's radar list. Returns null when the setup qualifies. Mirrors
* qualifiesSetup rule-for-rule (keep the order in sync).
*/
export function disqualifyReason(setup: TradeSetup, config: ActivationConfig): string | null {
if (setup.rr_ratio < config.min_rr) {
return `R:R ${setup.rr_ratio.toFixed(1)} below gate ${config.min_rr.toFixed(1)}`;
}
if (setup.current_price != null && liveRiskReward(setup, setup.current_price) < config.min_rr) {
return 'price has run — live R:R below gate';
}
const targetProbability = primaryTargetProbability(setup);
if (targetProbability == null || targetProbability <= 0) return 'no target probability';
if ((setup.confidence_score ?? 0) < config.min_confidence) {
return `confidence below ${config.min_confidence.toFixed(0)}%`;
}
if (config.min_momentum_percentile > 0) {
if (setup.direction === 'short') return 'short — momentum gate is long-only';
if (setup.momentum_percentile == null) return 'no residual momentum rank';
if (setup.momentum_percentile < config.min_momentum_percentile) {
return `momentum below ${config.min_momentum_percentile.toFixed(0)}th %ile`;
}
}
if (config.exclude_neutral) {
const actionDir = actionDirection(setup.recommended_action);
if (actionDir === 'neutral') return 'model action neutral';
if (actionDir !== setup.direction) return 'counter to model bias';
}
if (config.require_high_conviction && !HIGH_CONVICTION_ACTIONS.has(setup.recommended_action ?? '')) {
return 'conviction below High';
}
if (config.exclude_conflicts && (setup.risk_level ?? '') !== 'Low') return 'risk flags not clean';
return null;
}
/**
* Symbol of the current single 'top pick' — the #1 row the dashboard highlights:
* the highest residual 12-1 momentum percentile among qualified setups. Returns