Recommendation module in Horizon language; Standing/Dimensions as tabs
Deploy / lint (push) Successful in 7s
Deploy / test (push) Successful in 1m4s
Deploy / deploy (push) Successful in 37s

RecommendationPanel redesigned to match the rest of the redesign:

- Setup cards use the spatial price rail (stop -> entry -> now ->
  target with live R-multiples) instead of the label/value text grid.
- One quiet chip row per card (direction, preferred marker, confidence,
  R:R, target probability) replaces scattered header text.
- Position sizing is a single mono line (shares / position / max loss)
  instead of a boxed 3-column mini-table; Mark-as-taken flow kept, in
  the standard cyan button style.
- Full target ladder moves behind a disclosure (the primary target is
  already on the rail); warnings (counter-trend, stale/invalidated
  drift, earnings window, conflict flags) all kept.
- Cards are quiet outlined blocks, no glass-in-glass; the module
  renders frameless inside the unified ticker panel's Analysis tab,
  under the chart.

Ticker panel tabs now: Analysis, Standing, Dimensions, Indicators,
S/R Levels, Sentiment, Fundamentals - Standing and Dimensions were the
last below-the-fold sections; the whole page is now the single panel.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-10 08:32:03 +02:00
co-authored by Claude Fable 5
parent fc0945367a
commit d806ede07e
2 changed files with 260 additions and 221 deletions
+67 -64
View File
@@ -19,7 +19,6 @@ import { IndicatorSelector } from '../components/ticker/IndicatorSelector';
import { RecommendationPanel } from '../components/ticker/RecommendationPanel';
import { Button } from '../components/ui/Button';
import { Callout } from '../components/ui/Callout';
import { Section } from '../components/ui/Section';
import { formatPrice } from '../lib/format';
import type { TradeSetup } from '../lib/types';
import type { FieldPoint } from '../components/ticker/StandingMatrix';
@@ -27,7 +26,7 @@ import type { FieldPoint } from '../components/ticker/StandingMatrix';
// Lazy so recharts (heavy) ships in its own chunk, not the main ticker bundle.
const StandingMatrix = lazy(() => import('../components/ticker/StandingMatrix'));
const detailTabs = ['Analysis', 'Indicators', 'S/R Levels', 'Sentiment', 'Fundamentals'] as const;
const detailTabs = ['Analysis', 'Standing', 'Dimensions', 'Indicators', 'S/R Levels', 'Sentiment', 'Fundamentals'] as const;
type DetailTab = (typeof detailTabs)[number];
function SectionError({ message, onRetry }: { message: string; onRetry?: () => void }) {
@@ -430,6 +429,72 @@ export default function TickerDetailPage() {
</p>
</>
)}
{(longSetup || shortSetup) && (
<div className="mt-6 border-t border-white/[0.06] pt-5">
<RecommendationPanel
frameless
symbol={symbol}
longSetup={longSetup}
shortSetup={shortSetup}
currentPrice={priceInfo?.price}
nextEarningsDate={fundamentals.data?.next_earnings_date}
/>
</div>
)}
</div>
)}
{activeTab === 'Standing' && (
<div className="hz-frameless animate-fade-in">
{scores.isLoading && <SkeletonCard className="h-80" />}
{scores.isError && (
<SectionError message={scores.error instanceof Error ? scores.error.message : 'Failed to load scores'} onRetry={() => scores.refetch()} />
)}
{scores.data && (
<>
<Suspense fallback={<SkeletonCard className="h-80" />}>
<StandingMatrix
symbol={symbol}
composite={scores.data.composite_score}
momentum={myMomentum}
field={standingField}
gateMomentum={gateMomentum}
status={standingStatus}
confidence={myConfidence}
/>
</Suspense>
{(() => {
const cb = scores.data?.composite_breakdown;
const adj = cb?.sentiment_adjustment;
const base = cb?.base_score;
if (adj == null || base == null || Math.abs(adj) < 0.05) return null;
const composite = scores.data?.composite_score ?? base + adj;
return (
<p className="mt-3 text-center text-[11px] text-gray-500">
Composite{' '}
<span className="font-semibold text-gray-300">{Math.round(composite)}</span>
{' '}= Base {Math.round(base)}{' '}
{adj >= 0 ? '+' : ''} Sentiment{' '}
<span className={adj >= 0 ? 'text-emerald-300/80' : 'text-red-300/80'}>
{Math.abs(adj).toFixed(1)}
</span>
</p>
);
})()}
</>
)}
</div>
)}
{activeTab === 'Dimensions' && (
<div className="hz-frameless animate-fade-in">
{scores.isLoading && <SkeletonCard />}
{scores.isError && (
<SectionError message={scores.error instanceof Error ? scores.error.message : 'Failed to load scores'} onRetry={() => scores.refetch()} />
)}
{scores.data && (
<ScoreCard showComposite={false} showRadar={false} compositeScore={scores.data.composite_score} dimensions={scores.data.dimensions} compositeBreakdown={scores.data.composite_breakdown} />
)}
</div>
)}
@@ -504,68 +569,6 @@ export default function TickerDetailPage() {
</div>
</section>
{activeTab === 'Analysis' && (
<div className="space-y-6 animate-fade-in">
<RecommendationPanel
symbol={symbol}
longSetup={longSetup}
shortSetup={shortSetup}
currentPrice={priceInfo?.price}
nextEarningsDate={fundamentals.data?.next_earnings_date}
/>
<div className="grid items-start gap-6 lg:grid-cols-2">
<Section title="Standing" hint="how this ticker ranks vs. the field">
{scores.isLoading && <SkeletonCard className="h-80" />}
{scores.isError && (
<SectionError message={scores.error instanceof Error ? scores.error.message : 'Failed to load scores'} onRetry={() => scores.refetch()} />
)}
{scores.data && (
<>
<Suspense fallback={<SkeletonCard className="h-80" />}>
<StandingMatrix
symbol={symbol}
composite={scores.data.composite_score}
momentum={myMomentum}
field={standingField}
gateMomentum={gateMomentum}
status={standingStatus}
confidence={myConfidence}
/>
</Suspense>
{(() => {
const cb = scores.data?.composite_breakdown;
const adj = cb?.sentiment_adjustment;
const base = cb?.base_score;
if (adj == null || base == null || Math.abs(adj) < 0.05) return null;
const composite = scores.data?.composite_score ?? base + adj;
return (
<p className="mt-3 text-center text-[11px] text-gray-500">
Composite{' '}
<span className="font-semibold text-gray-300">{Math.round(composite)}</span>
{' '}= Base {Math.round(base)}{' '}
{adj >= 0 ? '+' : ''} Sentiment{' '}
<span className={adj >= 0 ? 'text-emerald-400/80' : 'text-red-400/80'}>
{Math.abs(adj).toFixed(1)}
</span>
</p>
);
})()}
</>
)}
</Section>
<Section title="Dimensions" hint="fingerprint values in the header · expand a row for the formula">
{scores.isLoading && <SkeletonCard />}
{scores.isError && (
<SectionError message={scores.error instanceof Error ? scores.error.message : 'Failed to load scores'} onRetry={() => scores.refetch()} />
)}
{scores.data && (
<ScoreCard showComposite={false} showRadar={false} compositeScore={scores.data.composite_score} dimensions={scores.data.dimensions} compositeBreakdown={scores.data.composite_breakdown} />
)}
</Section>
</div>
</div>
)}
</div>
);
}