Surface current price; flag stale setups; declutter chart
Deploy / lint (push) Successful in 6s
Deploy / test (push) Successful in 36s
Deploy / deploy (push) Successful in 24s

Triggered by MRK: entry 113 shown with no current price (actually ~119).

- Ticker header shows last close + day change % + "last close · Nd ago"
  (the age reveals OHLCV collection lag — why entry looked off)
- Setup cards show Current price and entry drift; flag setups as
  stale (price moved >1/3 toward target) or invalidated (past stop)
- Chart: draw only nearest support below + nearest resistance above
  current price, plus a prominent "Now" price line (full S/R stays in
  the S/R tab)
- Chart overlay is selectable (Auto/Long/Short/None) — only the chosen
  setup's entry/stop/target render, instead of everything at once

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-14 11:30:16 +02:00
parent d3eb8a2b97
commit 33f6baca6b
3 changed files with 204 additions and 88 deletions
@@ -1,18 +1,45 @@
import { useMemo, useRef, useEffect, useCallback, useState } from 'react'; import { useRef, useEffect, useCallback, useState } from 'react';
import type { OHLCVBar, SRLevel, SRZone, TradeSetup } from '../../lib/types'; import type { OHLCVBar, SRLevel, SRZone, TradeSetup } from '../../lib/types';
import { formatPrice, formatDate } from '../../lib/format'; import { formatPrice, formatDate } from '../../lib/format';
interface CandlestickChartProps { interface CandlestickChartProps {
data: OHLCVBar[]; data: OHLCVBar[];
srLevels?: SRLevel[]; srLevels?: SRLevel[];
maxSRLevels?: number;
zones?: SRZone[]; zones?: SRZone[];
tradeSetup?: TradeSetup; tradeSetup?: TradeSetup;
currentPrice?: number;
} }
function filterTopSRLevels(levels: SRLevel[], max: number): SRLevel[] { /** A horizontal price marker to draw, with an optional band (zone). */
if (levels.length <= max) return levels; interface SRMarker {
return [...levels].sort((a, b) => b.strength - a.strength).slice(0, max); price: number;
low: number;
high: number;
strength: number;
role: 'support' | 'resistance';
}
/**
* Pick only the nearest support below and nearest resistance above the current
* price — the two levels that actually matter for a trade right now. Keeps the
* chart readable; the full S/R list lives in the S/R tab.
*/
function nearestSRMarkers(
srLevels: SRLevel[],
zones: SRZone[],
price: number,
): SRMarker[] {
const raw: SRMarker[] = zones.length
? zones.map((z) => ({ price: z.midpoint, low: z.low, high: z.high, strength: z.strength, role: 'support' as const }))
: srLevels.map((l) => ({ price: l.price_level, low: l.price_level, high: l.price_level, strength: l.strength, role: 'support' as const }));
const below = raw.filter((m) => m.high <= price).sort((a, b) => (price - a.price) - (price - b.price));
const above = raw.filter((m) => m.low > price).sort((a, b) => (a.price - price) - (b.price - price));
const out: SRMarker[] = [];
if (below[0]) out.push({ ...below[0], role: 'support' });
if (above[0]) out.push({ ...above[0], role: 'resistance' });
return out;
} }
interface TooltipState { interface TooltipState {
@@ -24,7 +51,7 @@ interface TooltipState {
const MIN_VISIBLE_BARS = 10; const MIN_VISIBLE_BARS = 10;
export function CandlestickChart({ data, srLevels = [], maxSRLevels = 6, zones = [], tradeSetup }: CandlestickChartProps) { export function CandlestickChart({ data, srLevels = [], zones = [], tradeSetup, currentPrice }: CandlestickChartProps) {
const canvasRef = useRef<HTMLCanvasElement>(null); const canvasRef = useRef<HTMLCanvasElement>(null);
const overlayCanvasRef = useRef<HTMLCanvasElement>(null); const overlayCanvasRef = useRef<HTMLCanvasElement>(null);
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
@@ -46,8 +73,6 @@ export function CandlestickChart({ data, srLevels = [], maxSRLevels = 6, zones =
setVisibleRange({ start: 0, end: data.length }); setVisibleRange({ start: 0, end: data.length });
}, [data]); }, [data]);
const topLevels = useMemo(() => filterTopSRLevels(srLevels, maxSRLevels), [srLevels, maxSRLevels]);
const draw = useCallback(() => { const draw = useCallback(() => {
const canvas = canvasRef.current; const canvas = canvasRef.current;
const container = containerRef.current; const container = containerRef.current;
@@ -79,12 +104,16 @@ export function CandlestickChart({ data, srLevels = [], maxSRLevels = 6, zones =
const cw = W - ml - mr; const cw = W - ml - mr;
const ch = H - mt - mb; const ch = H - mt - mb;
// Current price = explicit prop, else latest close
const livePrice = currentPrice ?? visibleData[visibleData.length - 1].close;
// Only the nearest support/resistance are drawn — keep the chart legible
const markers = nearestSRMarkers(srLevels, zones, livePrice);
// Price range from visible data // Price range from visible data
const allPrices = visibleData.flatMap((b) => [b.high, b.low]); const allPrices = visibleData.flatMap((b) => [b.high, b.low]);
const srPrices = topLevels.map((l) => l.price_level); const srPrices = markers.flatMap((m) => [m.low, m.high]);
const zonePrices = zones.flatMap((z) => [z.low, z.high]);
const tradePrices = tradeSetup ? [tradeSetup.entry_price, tradeSetup.stop_loss, tradeSetup.target] : []; const tradePrices = tradeSetup ? [tradeSetup.entry_price, tradeSetup.stop_loss, tradeSetup.target] : [];
const allVals = [...allPrices, ...srPrices, ...zonePrices, ...tradePrices]; const allVals = [...allPrices, ...srPrices, ...tradePrices, livePrice];
const minP = Math.min(...allVals); const minP = Math.min(...allVals);
const maxP = Math.max(...allVals); const maxP = Math.max(...allVals);
const pad = (maxP - minP) * 0.06 || 1; const pad = (maxP - minP) * 0.06 || 1;
@@ -121,73 +150,37 @@ export function CandlestickChart({ data, srLevels = [], maxSRLevels = 6, zones =
ctx.fillText(formatDate(visibleData[i].date), x, H - 6); ctx.fillText(formatDate(visibleData[i].date), x, H - 6);
} }
// S/R levels (only when no zones are provided) // Nearest support/resistance only (band if it came from a zone)
if (zones.length === 0) { markers.forEach((m) => {
topLevels.forEach((level) => { const isSupport = m.role === 'support';
const y = yScale(level.price_level); const color = isSupport ? '#10b981' : '#ef4444';
const isSupport = level.type === 'support'; const yMid = yScale(m.price);
const color = isSupport ? '#10b981' : '#ef4444';
ctx.strokeStyle = color; if (m.high > m.low) {
ctx.lineWidth = 1.5; const yTop = yScale(m.high);
ctx.globalAlpha = 0.55; const rectHeight = Math.max(yScale(m.low) - yTop, 2);
ctx.setLineDash([6, 3]); ctx.fillStyle = isSupport ? 'rgba(16, 185, 129, 0.12)' : 'rgba(239, 68, 68, 0.12)';
ctx.beginPath(); ctx.fillRect(ml, yTop, cw, rectHeight);
ctx.moveTo(ml, y); }
ctx.lineTo(ml + cw, y);
ctx.stroke();
ctx.setLineDash([]);
ctx.globalAlpha = 1;
// Label ctx.strokeStyle = color;
ctx.fillStyle = color; ctx.lineWidth = 1.25;
ctx.font = '10px "IBM Plex Mono", ui-monospace, monospace'; ctx.globalAlpha = 0.6;
ctx.textAlign = 'left'; ctx.setLineDash([6, 3]);
ctx.fillText(
`${level.type[0].toUpperCase()} ${formatPrice(level.price_level)}`,
ml + cw + 4,
y + 3
);
});
}
// S/R Zone rectangles (drawn before candles so candles render on top)
zones.forEach((zone) => {
const isSupport = zone.type === 'support';
const fillColor = isSupport ? 'rgba(16, 185, 129, 0.15)' : 'rgba(239, 68, 68, 0.15)';
const borderColor = isSupport ? 'rgba(16, 185, 129, 0.35)' : 'rgba(239, 68, 68, 0.35)';
const labelColor = isSupport ? '#10b981' : '#ef4444';
const yTop = yScale(zone.high);
const yBottom = yScale(zone.low);
// Handle single-level zones (low == high) as thin 2px-height rectangles
const rectHeight = Math.max(yBottom - yTop, 2);
// Shaded rectangle spanning full chart width
ctx.fillStyle = fillColor;
ctx.fillRect(ml, yTop, cw, rectHeight);
// Border lines at top and bottom of zone
ctx.strokeStyle = borderColor;
ctx.lineWidth = 1;
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(ml, yTop); ctx.moveTo(ml, yMid);
ctx.lineTo(ml + cw, yTop); ctx.lineTo(ml + cw, yMid);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(ml, yTop + rectHeight);
ctx.lineTo(ml + cw, yTop + rectHeight);
ctx.stroke(); ctx.stroke();
ctx.setLineDash([]);
ctx.globalAlpha = 1;
// Label with midpoint price and strength score ctx.fillStyle = color;
const yMid = yTop + rectHeight / 2;
ctx.fillStyle = labelColor;
ctx.font = '10px "IBM Plex Mono", ui-monospace, monospace'; ctx.font = '10px "IBM Plex Mono", ui-monospace, monospace';
ctx.textAlign = 'left'; ctx.textAlign = 'left';
ctx.fillText( ctx.fillText(
`${zone.type[0].toUpperCase()} ${formatPrice(zone.midpoint)} (${zone.strength})`, `${isSupport ? 'Support' : 'Resistance'} ${formatPrice(m.price)} (${m.strength})`,
ml + cw + 4, ml + cw + 4,
yMid + 3 yMid + 3,
); );
}); });
@@ -248,6 +241,28 @@ export function CandlestickChart({ data, srLevels = [], maxSRLevels = 6, zones =
ctx.fillText(`TP ${formatPrice(tradeSetup.target)}`, ml + cw + 4, targetY + 3); ctx.fillText(`TP ${formatPrice(tradeSetup.target)}`, ml + cw + 4, targetY + 3);
} }
// Current price line — the anchor for everything else (drawn on top)
{
const py = yScale(livePrice);
ctx.strokeStyle = 'rgba(226, 232, 240, 0.9)';
ctx.lineWidth = 1.25;
ctx.beginPath();
ctx.moveTo(ml, py);
ctx.lineTo(ml + cw, py);
ctx.stroke();
const label = `Now ${formatPrice(livePrice)}`;
ctx.font = '10px "IBM Plex Mono", ui-monospace, monospace';
const tw = ctx.measureText(label).width;
ctx.fillStyle = 'rgba(226, 232, 240, 0.95)';
ctx.fillRect(ml + 2, py - 8, tw + 8, 16);
ctx.fillStyle = '#0e120f';
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
ctx.fillText(label, ml + 6, py);
ctx.textBaseline = 'alphabetic';
}
// Candles // Candles
visibleData.forEach((bar, i) => { visibleData.forEach((bar, i) => {
const x = ml + i * barW + barW / 2; const x = ml + i * barW + barW / 2;
@@ -285,7 +300,7 @@ export function CandlestickChart({ data, srLevels = [], maxSRLevels = 6, zones =
overlay.style.width = `${W}px`; overlay.style.width = `${W}px`;
overlay.style.height = `${H}px`; overlay.style.height = `${H}px`;
} }
}, [data, topLevels, visibleRange, zones, tradeSetup]); }, [data, srLevels, visibleRange, zones, tradeSetup, currentPrice]);
const drawCrosshair = useCallback(() => { const drawCrosshair = useCallback(() => {
const overlay = overlayCanvasRef.current; const overlay = overlayCanvasRef.current;
@@ -6,6 +6,26 @@ interface RecommendationPanelProps {
symbol: string; symbol: string;
longSetup?: TradeSetup; longSetup?: TradeSetup;
shortSetup?: TradeSetup; shortSetup?: TradeSetup;
currentPrice?: number;
}
/**
* How far current price has drifted from the setup's entry. A setup whose
* entry is far from the live price (price already ran toward target, or fell
* through the stop) is stale — entering now changes the risk/reward.
*/
function entryDrift(setup: TradeSetup, currentPrice?: number) {
if (currentPrice == null || !setup.entry_price) return null;
const pct = ((currentPrice - setup.entry_price) / setup.entry_price) * 100;
const towardTarget = setup.direction === 'long' ? currentPrice >= setup.entry_price : currentPrice <= setup.entry_price;
// Past the stop entirely = invalidated; moved >1/3 of the way to target = stale
const span = Math.abs(setup.target - setup.entry_price);
const moved = Math.abs(currentPrice - setup.entry_price);
const beyondStop = setup.direction === 'long' ? currentPrice <= setup.stop_loss : currentPrice >= setup.stop_loss;
let status: 'fresh' | 'stale' | 'invalidated' = 'fresh';
if (beyondStop) status = 'invalidated';
else if (span > 0 && moved / span > 0.33) status = 'stale';
return { pct, towardTarget, status };
} }
function riskClass(risk: TradeSetup['risk_level']) { function riskClass(risk: TradeSetup['risk_level']) {
@@ -54,7 +74,7 @@ function TargetTable({ setup }: { setup: TradeSetup }) {
); );
} }
function SetupCard({ setup, action }: { setup?: TradeSetup; action?: TradeSetup['recommended_action'] }) { function SetupCard({ setup, action, currentPrice }: { setup?: TradeSetup; action?: TradeSetup['recommended_action']; currentPrice?: number }) {
if (!setup) { if (!setup) {
return ( return (
<div className="glass-sm p-4 text-xs text-gray-500"> <div className="glass-sm p-4 text-xs text-gray-500">
@@ -64,6 +84,7 @@ function SetupCard({ setup, action }: { setup?: TradeSetup; action?: TradeSetup[
} }
const recommended = isRecommended(setup, action); const recommended = isRecommended(setup, action);
const drift = entryDrift(setup, currentPrice);
return ( return (
<div <div
@@ -81,8 +102,20 @@ function SetupCard({ setup, action }: { setup?: TradeSetup; action?: TradeSetup[
<p className="text-[11px] text-amber-400">Alternative setup (ticker bias currently favors the opposite direction).</p> <p className="text-[11px] text-amber-400">Alternative setup (ticker bias currently favors the opposite direction).</p>
)} )}
{drift && drift.status === 'invalidated' && (
<p className="text-[11px] text-red-400">
Price ({formatPrice(currentPrice!)}) is past the stop this setup is invalidated.
</p>
)}
{drift && drift.status === 'stale' && (
<p className="text-[11px] text-amber-400">
Price has moved {drift.pct >= 0 ? '+' : ''}{drift.pct.toFixed(1)}% from entry{drift.towardTarget ? ' toward target' : ' against the setup'} entry may be stale.
</p>
)}
<div className="grid grid-cols-2 gap-2 text-xs"> <div className="grid grid-cols-2 gap-2 text-xs">
<div className="text-gray-500">Entry</div><div className="font-mono text-gray-200">{formatPrice(setup.entry_price)}</div> <div className="text-gray-500">Current</div><div className="font-mono text-gray-200">{currentPrice != null ? formatPrice(currentPrice) : '—'}</div>
<div className="text-gray-500">Entry</div><div className="font-mono text-gray-200">{formatPrice(setup.entry_price)}{drift ? ` (${drift.pct >= 0 ? '+' : ''}${drift.pct.toFixed(1)}%)` : ''}</div>
<div className="text-gray-500">Stop</div><div className="font-mono text-gray-200">{formatPrice(setup.stop_loss)}</div> <div className="text-gray-500">Stop</div><div className="font-mono text-gray-200">{formatPrice(setup.stop_loss)}</div>
<div className="text-gray-500">Primary Target</div><div className="font-mono text-gray-200">{formatPrice(setup.target)}</div> <div className="text-gray-500">Primary Target</div><div className="font-mono text-gray-200">{formatPrice(setup.target)}</div>
<div className="text-gray-500">R:R</div><div className="font-mono text-gray-200">{setup.rr_ratio.toFixed(2)}</div> <div className="text-gray-500">R:R</div><div className="font-mono text-gray-200">{setup.rr_ratio.toFixed(2)}</div>
@@ -99,7 +132,7 @@ function SetupCard({ setup, action }: { setup?: TradeSetup; action?: TradeSetup[
); );
} }
export function RecommendationPanel({ symbol, longSetup, shortSetup }: RecommendationPanelProps) { export function RecommendationPanel({ symbol, longSetup, shortSetup, currentPrice }: RecommendationPanelProps) {
const summary = longSetup?.recommendation_summary ?? shortSetup?.recommendation_summary; const summary = longSetup?.recommendation_summary ?? shortSetup?.recommendation_summary;
const action = (summary?.action ?? 'NEUTRAL') as TradeSetup['recommended_action']; const action = (summary?.action ?? 'NEUTRAL') as TradeSetup['recommended_action'];
const preferredDirection = recommendationActionDirection(action); const preferredDirection = recommendationActionDirection(action);
@@ -143,7 +176,7 @@ export function RecommendationPanel({ symbol, longSetup, shortSetup }: Recommend
{preferredDirection !== 'neutral' && preferredSetup ? ( {preferredDirection !== 'neutral' && preferredSetup ? (
<div className="space-y-3"> <div className="space-y-3">
<SetupCard setup={preferredSetup} action={action} /> <SetupCard setup={preferredSetup} action={action} currentPrice={currentPrice} />
{alternativeSetup && ( {alternativeSetup && (
<details className="glass-sm p-3"> <details className="glass-sm p-3">
@@ -151,15 +184,15 @@ export function RecommendationPanel({ symbol, longSetup, shortSetup }: Recommend
Alternative scenario ({alternativeSetup.direction.toUpperCase()}) Alternative scenario ({alternativeSetup.direction.toUpperCase()})
</summary> </summary>
<div className="mt-3"> <div className="mt-3">
<SetupCard setup={alternativeSetup} action={action} /> <SetupCard setup={alternativeSetup} action={action} currentPrice={currentPrice} />
</div> </div>
</details> </details>
)} )}
</div> </div>
) : ( ) : (
<div className="grid gap-4 lg:grid-cols-2"> <div className="grid gap-4 lg:grid-cols-2">
<SetupCard setup={longSetup} action={action} /> <SetupCard setup={longSetup} action={action} currentPrice={currentPrice} />
<SetupCard setup={shortSetup} action={action} /> <SetupCard setup={shortSetup} action={action} currentPrice={currentPrice} />
</div> </div>
)} )}
</div> </div>
+79 -11
View File
@@ -121,12 +121,30 @@ export default function TickerDetailPage() {
[setupsForSymbol], [setupsForSymbol],
); );
// Use the highest-confidence setup for chart overlay fallback. // Current price = latest close, with day-over-day change
const tradeSetup: TradeSetup | undefined = useMemo(() => { const priceInfo = useMemo(() => {
const bars = ohlcv.data;
if (!bars || bars.length === 0) return null;
const last = bars[bars.length - 1];
const prev = bars.length > 1 ? bars[bars.length - 2] : null;
const change = prev && prev.close ? ((last.close - prev.close) / prev.close) * 100 : null;
return { price: last.close, date: last.date, change };
}, [ohlcv.data]);
// Which setup the chart overlays. 'auto' = the ticker's preferred direction.
const [overlayChoice, setOverlayChoice] = useState<'auto' | 'long' | 'short' | 'none'>('auto');
const action = (longSetup ?? shortSetup)?.recommended_action ?? null;
const overlaySetup: TradeSetup | undefined = useMemo(() => {
if (overlayChoice === 'none') return undefined;
if (overlayChoice === 'long') return longSetup;
if (overlayChoice === 'short') return shortSetup;
// auto: preferred direction's setup, else the highest-confidence available
if (action?.startsWith('LONG') && longSetup) return longSetup;
if (action?.startsWith('SHORT') && shortSetup) return shortSetup;
const candidates = [longSetup, shortSetup].filter(Boolean) as TradeSetup[]; const candidates = [longSetup, shortSetup].filter(Boolean) as TradeSetup[];
if (candidates.length === 0) return undefined;
return candidates.sort((a, b) => (b.confidence_score ?? 0) - (a.confidence_score ?? 0))[0]; return candidates.sort((a, b) => (b.confidence_score ?? 0) - (a.confidence_score ?? 0))[0];
}, [longSetup, shortSetup]); }, [overlayChoice, longSetup, shortSetup, action]);
// Sort visible S/R levels by strength for the table (only levels within chart zones) // Sort visible S/R levels by strength for the table (only levels within chart zones)
const sortedLevels = useMemo(() => { const sortedLevels = useMemo(() => {
@@ -138,9 +156,19 @@ export default function TickerDetailPage() {
<div className="space-y-6 animate-slide-up"> <div className="space-y-6 animate-slide-up">
{/* Header */} {/* Header */}
<div className="flex flex-wrap items-center justify-between gap-4"> <div className="flex flex-wrap items-center justify-between gap-4">
<div> <div className="flex items-baseline gap-4">
<h1 className="text-3xl font-semibold text-gray-100">{symbol.toUpperCase()}</h1> <h1 className="text-3xl font-semibold text-gray-100">{symbol.toUpperCase()}</h1>
<p className="text-sm text-gray-500 mt-0.5">Ticker Detail</p> {priceInfo && (
<div className="flex items-baseline gap-2">
<span className="num text-2xl font-semibold text-gray-100">{formatPrice(priceInfo.price)}</span>
{priceInfo.change !== null && (
<span className={`num text-sm font-medium ${priceInfo.change >= 0 ? 'text-emerald-400' : 'text-red-400'}`}>
{priceInfo.change >= 0 ? '+' : ''}{priceInfo.change.toFixed(2)}%
</span>
)}
<span className="text-xs text-gray-500">last close · {timeAgo(priceInfo.date)}</span>
</div>
)}
</div> </div>
<Button onClick={() => ingestion.mutate(symbol)} loading={ingestion.isPending}> <Button onClick={() => ingestion.mutate(symbol)} loading={ingestion.isPending}>
{ingestion.isPending ? 'Fetching…' : 'Fetch Data'} {ingestion.isPending ? 'Fetching…' : 'Fetch Data'}
@@ -150,7 +178,12 @@ export default function TickerDetailPage() {
{/* Data freshness bar */} {/* Data freshness bar */}
<DataFreshnessBar items={dataStatus} /> <DataFreshnessBar items={dataStatus} />
<RecommendationPanel symbol={symbol} longSetup={longSetup} shortSetup={shortSetup} /> <RecommendationPanel
symbol={symbol}
longSetup={longSetup}
shortSetup={shortSetup}
currentPrice={priceInfo?.price}
/>
{/* Chart — always visible */} {/* Chart — always visible */}
<Section title="Price Chart"> <Section title="Price Chart">
@@ -162,11 +195,46 @@ export default function TickerDetailPage() {
/> />
)} )}
{ohlcv.data && ( {ohlcv.data && (
<div className="glass p-5"> <div className="glass p-5 space-y-3">
<CandlestickChart data={ohlcv.data} srLevels={srLevels.data?.levels} zones={srLevels.data?.zones} tradeSetup={tradeSetup} /> {(longSetup || shortSetup) && (
{srLevels.isError && ( <div className="flex flex-wrap items-center gap-2 text-xs">
<p className="mt-2 text-xs text-amber-500/80">S/R levels unavailable chart shown without overlays</p> <span className="text-gray-500">Overlay setup:</span>
{([
{ key: 'auto', label: 'Auto', show: true },
{ key: 'long', label: 'Long', show: !!longSetup },
{ key: 'short', label: 'Short', show: !!shortSetup },
{ key: 'none', label: 'None', show: true },
] as const).filter((o) => o.show).map((o) => (
<button
key={o.key}
onClick={() => setOverlayChoice(o.key)}
className={`rounded-md px-2.5 py-1 transition-colors ${
overlayChoice === o.key
? 'bg-blue-400/15 text-blue-200 border border-blue-400/30'
: 'text-gray-400 border border-white/[0.08] hover:text-gray-200 hover:bg-white/[0.04]'
}`}
>
{o.label}
</button>
))}
{overlaySetup && (
<span className="num ml-1 text-gray-500">
showing {overlaySetup.direction.toUpperCase()} · entry {formatPrice(overlaySetup.entry_price)} target {formatPrice(overlaySetup.target)}
</span>
)}
</div>
)} )}
<CandlestickChart
data={ohlcv.data}
srLevels={srLevels.data?.levels}
zones={srLevels.data?.zones}
tradeSetup={overlaySetup}
currentPrice={priceInfo?.price}
/>
<p className="text-[11px] text-gray-500">
Only the nearest support &amp; resistance are drawn. Full list in the S/R Levels tab.
{srLevels.isError && ' S/R levels unavailable.'}
</p>
</div> </div>
)} )}
</Section> </Section>