Recommendation module in Horizon language; Standing/Dimensions as tabs
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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user