Show production rank below ticker chart
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
import type { TradeSetup } from '../../lib/types';
|
||||
|
||||
const MOMENTUM_WEIGHT = 0.8;
|
||||
const VOLATILITY_WEIGHT = 0.2;
|
||||
|
||||
interface ProductionRankStripProps {
|
||||
setup?: TradeSetup;
|
||||
momentumGate: number;
|
||||
}
|
||||
|
||||
function clampPercent(value: number): number {
|
||||
return Math.min(100, Math.max(0, value));
|
||||
}
|
||||
|
||||
function topShare(value: number): string {
|
||||
return `${Math.max(1, Math.round(100 - clampPercent(value)))}%`;
|
||||
}
|
||||
|
||||
function PercentileRail({
|
||||
value,
|
||||
colorClass,
|
||||
gate,
|
||||
gateLabel,
|
||||
}: {
|
||||
value: number | null;
|
||||
colorClass: string;
|
||||
gate?: number;
|
||||
gateLabel?: string;
|
||||
}) {
|
||||
const normalized = value == null ? 0 : clampPercent(value);
|
||||
const normalizedGate = gate == null ? null : clampPercent(gate);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative mt-2 h-1.5 rounded-full bg-white/[0.07]"
|
||||
role="meter"
|
||||
aria-valuemin={0}
|
||||
aria-valuemax={100}
|
||||
aria-valuenow={value == null ? undefined : normalized}
|
||||
aria-label={value == null ? 'Percentile unavailable' : `${normalized.toFixed(1)} percentile`}
|
||||
>
|
||||
{value != null && (
|
||||
<span
|
||||
className={`absolute inset-y-0 left-0 rounded-full ${colorClass}`}
|
||||
style={{ width: `${normalized}%` }}
|
||||
/>
|
||||
)}
|
||||
{normalizedGate != null && (
|
||||
<span
|
||||
className="absolute -top-1.5 h-4 w-px bg-gray-300/70"
|
||||
style={{ left: `${normalizedGate}%` }}
|
||||
title={gateLabel}
|
||||
>
|
||||
<span className="absolute -top-4 left-1/2 -translate-x-1/2 whitespace-nowrap text-[8px] uppercase tracking-[0.14em] text-gray-500">
|
||||
gate
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ProductionRankStrip({ setup, momentumGate }: ProductionRankStripProps) {
|
||||
const momentum = setup?.momentum_percentile ?? null;
|
||||
const volatility = setup?.volatility_percentile ?? null;
|
||||
const storedRank = setup?.strategy_rank ?? null;
|
||||
const hasBlend = momentum != null && volatility != null;
|
||||
const computedBlend = hasBlend
|
||||
? momentum * MOMENTUM_WEIGHT + volatility * VOLATILITY_WEIGHT
|
||||
: null;
|
||||
const rank = storedRank ?? computedBlend ?? momentum;
|
||||
|
||||
if (rank == null && momentum == null && volatility == null) return null;
|
||||
|
||||
const normalizedRank = clampPercent(rank ?? 0);
|
||||
const momentumContribution = hasBlend ? clampPercent(momentum) * MOMENTUM_WEIGHT : normalizedRank;
|
||||
const volatilityContribution = hasBlend ? clampPercent(volatility) * VOLATILITY_WEIGHT : 0;
|
||||
const gateEnabled = momentumGate > 0;
|
||||
const gatePassed = momentum != null && (!gateEnabled || momentum >= momentumGate);
|
||||
|
||||
return (
|
||||
<section
|
||||
className="mt-4 border-y border-white/[0.07] py-4"
|
||||
aria-label="Production ranking snapshot"
|
||||
>
|
||||
<div className="grid gap-5 lg:grid-cols-[minmax(170px,0.55fr)_minmax(0,1.8fr)] lg:gap-8">
|
||||
<div className="flex items-end justify-between gap-4 lg:block">
|
||||
<div>
|
||||
<p className="num text-[9px] uppercase tracking-[0.22em] text-gray-500">
|
||||
Production rank
|
||||
</p>
|
||||
{rank != null ? (
|
||||
<p className="font-display mt-1 text-3xl font-semibold tracking-tight text-gray-100">
|
||||
{normalizedRank.toFixed(1)}
|
||||
<span className="ml-1 text-sm font-normal text-gray-500">%ile</span>
|
||||
</p>
|
||||
) : (
|
||||
<p className="font-display mt-1 text-2xl font-semibold text-gray-500">Unavailable</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-right lg:mt-2 lg:text-left">
|
||||
{rank != null && (
|
||||
<p className="text-[11px] text-gray-400">top {topShare(normalizedRank)} of the universe</p>
|
||||
)}
|
||||
{momentum != null && gateEnabled && (
|
||||
<p className={`mt-0.5 text-[10px] ${gatePassed ? 'text-blue-300' : 'text-red-300'}`}>
|
||||
momentum gate {gatePassed ? 'passed' : 'not passed'}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="min-w-0">
|
||||
<div className="flex flex-wrap items-baseline justify-between gap-x-4 gap-y-1">
|
||||
<p className="text-[11px] font-medium text-gray-300">80/20 weighted rank</p>
|
||||
<p className="num text-[10px] text-gray-500">
|
||||
{hasBlend
|
||||
? `${momentumContribution.toFixed(1)} momentum + ${volatilityContribution.toFixed(1)} volatility`
|
||||
: 'momentum-only fallback / volatility rank unavailable'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="relative mt-2 h-2.5 overflow-visible rounded-full bg-white/[0.07]"
|
||||
role="meter"
|
||||
aria-label={rank == null ? 'Production rank unavailable' : `Production rank ${normalizedRank.toFixed(1)} percentile`}
|
||||
aria-valuemin={0}
|
||||
aria-valuemax={100}
|
||||
aria-valuenow={rank == null ? undefined : normalizedRank}
|
||||
>
|
||||
<span
|
||||
className={`absolute inset-y-0 left-0 bg-blue-500 ${
|
||||
volatilityContribution > 0 ? 'rounded-l-full' : 'rounded-full'
|
||||
}`}
|
||||
style={{ width: `${momentumContribution}%` }}
|
||||
title={`Momentum contribution ${momentumContribution.toFixed(1)} points`}
|
||||
/>
|
||||
{volatilityContribution > 0 && (
|
||||
<span
|
||||
className="absolute inset-y-0 rounded-r-full bg-amber-400"
|
||||
style={{ left: `${momentumContribution}%`, width: `${volatilityContribution}%` }}
|
||||
title={`Volatility contribution ${volatilityContribution.toFixed(1)} points`}
|
||||
/>
|
||||
)}
|
||||
{rank != null && (
|
||||
<span
|
||||
className="absolute -top-1.5 h-5 w-px bg-gray-100 shadow-[0_0_8px_rgba(237,238,243,0.55)]"
|
||||
style={{ left: `${normalizedRank}%` }}
|
||||
title={`Combined rank ${normalizedRank.toFixed(1)}`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-5 grid gap-x-8 gap-y-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-[11px] font-medium text-blue-200">Residual 12-1 momentum</p>
|
||||
<p className="mt-0.5 text-[9px] text-gray-600">80% weight / activation signal</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="num text-sm text-gray-200">
|
||||
{momentum == null ? '-' : momentum.toFixed(1)}
|
||||
</p>
|
||||
{momentum != null && (
|
||||
<p className="text-[9px] text-gray-600">top {topShare(momentum)}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<PercentileRail
|
||||
value={momentum}
|
||||
colorClass="bg-blue-500"
|
||||
gate={gateEnabled ? momentumGate : undefined}
|
||||
gateLabel={gateEnabled ? `Activation requires at least the ${momentumGate.toFixed(0)}th percentile` : undefined}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-[11px] font-medium text-amber-200">6-month realized volatility</p>
|
||||
<p className="mt-0.5 text-[9px] text-gray-600">20% weight / ordering tilt only</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="num text-sm text-gray-200">
|
||||
{volatility == null ? '-' : volatility.toFixed(1)}
|
||||
</p>
|
||||
{volatility != null && (
|
||||
<p className="text-[9px] text-gray-600">top {topShare(volatility)}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<PercentileRail value={volatility} colorClass="bg-amber-400" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="mt-3 text-[9px] leading-relaxed text-gray-600">
|
||||
Cross-sectional snapshot from the latest setup scan - not a historical price indicator.
|
||||
Volatility can improve ordering, but it never opens the activation gate by itself.
|
||||
</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import { SentimentPanel } from '../components/ticker/SentimentPanel';
|
||||
import { FundamentalsPanel } from '../components/ticker/FundamentalsPanel';
|
||||
import { IndicatorSelector } from '../components/ticker/IndicatorSelector';
|
||||
import { RecommendationPanel } from '../components/ticker/RecommendationPanel';
|
||||
import { ProductionRankStrip } from '../components/ticker/ProductionRankStrip';
|
||||
import { Button } from '../components/ui/Button';
|
||||
import { Callout } from '../components/ui/Callout';
|
||||
import { formatPrice } from '../lib/format';
|
||||
@@ -461,6 +462,10 @@ export default function TickerDetailPage() {
|
||||
{srLevels.isError && ' S/R levels unavailable.'}
|
||||
{gateTargetLadder.isError && ' GTL diagnostic unavailable.'}
|
||||
</p>
|
||||
<ProductionRankStrip
|
||||
setup={longSetup ?? shortSetup}
|
||||
momentumGate={gateMomentum}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{(longSetup || shortSetup) && (
|
||||
|
||||
Reference in New Issue
Block a user