Improve chart trade overlays and paper fill markers.
De-clutter setup labels (left roles, right prices; hide Entry near Now; S/R in-plot), and draw green/red arrows for paper trade entry and exit on the matching sessions.
This commit is contained in:
@@ -2,6 +2,7 @@ import { useRef, useEffect, useCallback, useState } from 'react';
|
|||||||
import type {
|
import type {
|
||||||
GateTargetLevel,
|
GateTargetLevel,
|
||||||
OHLCVBar,
|
OHLCVBar,
|
||||||
|
PaperTrade,
|
||||||
SRLevel,
|
SRLevel,
|
||||||
SRZone,
|
SRZone,
|
||||||
TradeSetup,
|
TradeSetup,
|
||||||
@@ -20,6 +21,69 @@ interface CandlestickChartProps {
|
|||||||
onShowGateTrafficChange?: (visible: boolean) => void;
|
onShowGateTrafficChange?: (visible: boolean) => void;
|
||||||
tradeSetup?: TradeSetup;
|
tradeSetup?: TradeSetup;
|
||||||
currentPrice?: number;
|
currentPrice?: number;
|
||||||
|
/** Paper trades for this ticker — entry/exit arrows on the matching bars. */
|
||||||
|
paperTrades?: PaperTrade[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** YYYY-MM-DD from an ISO timestamp or bare date string. */
|
||||||
|
function dayKey(value: string): string {
|
||||||
|
return value.length >= 10 ? value.slice(0, 10) : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Index of the bar that best represents a paper-trade event time.
|
||||||
|
* Prefer the same calendar day; else the first session on/after that day.
|
||||||
|
*/
|
||||||
|
function barIndexForEvent(data: OHLCVBar[], timestamp: string): number {
|
||||||
|
if (!data.length) return -1;
|
||||||
|
const day = dayKey(timestamp);
|
||||||
|
const exact = data.findIndex((b) => dayKey(b.date) === day);
|
||||||
|
if (exact >= 0) return exact;
|
||||||
|
const after = data.findIndex((b) => dayKey(b.date) >= day);
|
||||||
|
if (after >= 0) return after;
|
||||||
|
// Event is after the last bar we have — pin to the last session.
|
||||||
|
if (dayKey(data[data.length - 1].date) < day) return data.length - 1;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Filled triangle pointing down; tip sits at (x, y). */
|
||||||
|
function drawEntryArrow(
|
||||||
|
ctx: CanvasRenderingContext2D,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
color: string,
|
||||||
|
size = 7,
|
||||||
|
) {
|
||||||
|
ctx.fillStyle = color;
|
||||||
|
ctx.strokeStyle = 'rgba(10, 11, 17, 0.55)';
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x, y);
|
||||||
|
ctx.lineTo(x - size, y - size * 1.35);
|
||||||
|
ctx.lineTo(x + size, y - size * 1.35);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fill();
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Filled triangle pointing up; tip sits at (x, y). */
|
||||||
|
function drawExitArrow(
|
||||||
|
ctx: CanvasRenderingContext2D,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
color: string,
|
||||||
|
size = 7,
|
||||||
|
) {
|
||||||
|
ctx.fillStyle = color;
|
||||||
|
ctx.strokeStyle = 'rgba(10, 11, 17, 0.55)';
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x, y);
|
||||||
|
ctx.lineTo(x - size, y + size * 1.35);
|
||||||
|
ctx.lineTo(x + size, y + size * 1.35);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fill();
|
||||||
|
ctx.stroke();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A horizontal price marker to draw, with an optional band (zone). */
|
/** A horizontal price marker to draw, with an optional band (zone). */
|
||||||
@@ -98,6 +162,7 @@ export function CandlestickChart({
|
|||||||
onShowGateTrafficChange,
|
onShowGateTrafficChange,
|
||||||
tradeSetup,
|
tradeSetup,
|
||||||
currentPrice,
|
currentPrice,
|
||||||
|
paperTrades = [],
|
||||||
}: CandlestickChartProps) {
|
}: CandlestickChartProps) {
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
const overlayCanvasRef = useRef<HTMLCanvasElement>(null);
|
const overlayCanvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
@@ -153,10 +218,10 @@ export function CandlestickChart({
|
|||||||
// Only the nearest support/resistance are drawn — keep the chart legible
|
// Only the nearest support/resistance are drawn — keep the chart legible
|
||||||
const markers = nearestSRMarkers(srLevels, zones, livePrice);
|
const markers = nearestSRMarkers(srLevels, zones, livePrice);
|
||||||
|
|
||||||
// Margins: line labels (Entry / Stop / Support…) on the LEFT, pure price
|
// Margins: trade role labels (Now / Stop / Gate / Entry) outside on the
|
||||||
// ticks on the RIGHT. Packing both on the right made trade overlays unreadable.
|
// LEFT; pure price ticks on the RIGHT. Support/Resist sit *inside* the
|
||||||
const hasLineLabels = Boolean(tradeSetup) || markers.length > 0;
|
// plot on their lines, so they don't need exterior gutter space.
|
||||||
const ml = hasLineLabels ? 78 : 16;
|
const ml = 56;
|
||||||
const mr = 58;
|
const mr = 58;
|
||||||
const mt = 12, mb = 32;
|
const mt = 12, mb = 32;
|
||||||
const cw = W - ml - mr;
|
const cw = W - ml - mr;
|
||||||
@@ -289,22 +354,63 @@ export function CandlestickChart({
|
|||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect left-side line labels; prices live on the right axis only.
|
// Left-side role labels only; prices live on the right axis.
|
||||||
type LineLabel = { y: number; text: string; color: string; weight?: 'normal' | 'bold' };
|
// Priority when levels crowd: Now > Stop > Gate > Entry > S/R.
|
||||||
|
// Best practice: hide Entry when it sits on top of Now — at market, Now
|
||||||
|
// *is* the fill; Entry only matters once price has drifted from the scan.
|
||||||
|
type LineLabel = {
|
||||||
|
y: number;
|
||||||
|
text: string;
|
||||||
|
color: string;
|
||||||
|
weight?: 'normal' | 'bold';
|
||||||
|
priority: number;
|
||||||
|
};
|
||||||
const lineLabels: LineLabel[] = [];
|
const lineLabels: LineLabel[] = [];
|
||||||
|
const LABEL_MIN_GAP_PX = 14;
|
||||||
|
const nowY = yScale(livePrice);
|
||||||
|
|
||||||
// Nearest support/resistance only (band if it came from a zone)
|
// Entry is noise when price hasn't moved off the scan level. ~0.25R or
|
||||||
|
// ~12px, whichever is larger, keeps the chart honest without double lines.
|
||||||
|
const riskDist = tradeSetup
|
||||||
|
? Math.abs(tradeSetup.entry_price - tradeSetup.stop_loss)
|
||||||
|
: 0;
|
||||||
|
const entryDriftPx = tradeSetup
|
||||||
|
? Math.abs(yScale(tradeSetup.entry_price) - nowY)
|
||||||
|
: 0;
|
||||||
|
const entryDriftR = tradeSetup && riskDist > 0
|
||||||
|
? Math.abs(livePrice - tradeSetup.entry_price) / riskDist
|
||||||
|
: 0;
|
||||||
|
const showEntry = Boolean(
|
||||||
|
tradeSetup
|
||||||
|
&& (entryDriftPx >= 12 || entryDriftR >= 0.25),
|
||||||
|
);
|
||||||
|
|
||||||
|
const tradeYs: number[] = [nowY];
|
||||||
|
if (tradeSetup) {
|
||||||
|
tradeYs.push(yScale(tradeSetup.stop_loss), yScale(tradeSetup.target));
|
||||||
|
if (showEntry) tradeYs.push(yScale(tradeSetup.entry_price));
|
||||||
|
}
|
||||||
|
|
||||||
|
// In-plot S/R labels (drawn after candles so they stay readable). Not
|
||||||
|
// exterior margin labels — they ride the line itself, left-aligned.
|
||||||
|
type InPlotLabel = { y: number; text: string; color: string; bg: string };
|
||||||
|
const inPlotLabels: InPlotLabel[] = [];
|
||||||
|
|
||||||
|
// Nearest support/resistance only (band if it came from a zone).
|
||||||
|
// Drop S/R entirely when it sits on a trade line — the trade label wins.
|
||||||
markers.forEach((m) => {
|
markers.forEach((m) => {
|
||||||
const isSupport = m.role === 'support';
|
const isSupport = m.role === 'support';
|
||||||
// Support = rim-cyan, resistance = neutral ink — rose stays reserved for
|
// Support stays cyan; resistance is a soft coral (warmer than neutral ink,
|
||||||
// the stop level so S/R never impersonates trade levels.
|
// cooler/softer than the stop so the two never read as the same thing).
|
||||||
const color = isSupport ? '#2f9db2' : '#9aa0b0';
|
const color = isSupport ? '#2f9db2' : '#e89b8c';
|
||||||
const yMid = yScale(m.price);
|
const yMid = yScale(m.price);
|
||||||
|
const collidesTrade = tradeYs.some((ty) => Math.abs(ty - yMid) < LABEL_MIN_GAP_PX);
|
||||||
|
if (collidesTrade) return;
|
||||||
|
|
||||||
if (m.high > m.low) {
|
if (m.high > m.low) {
|
||||||
const yTop = yScale(m.high);
|
const yTop = yScale(m.high);
|
||||||
const rectHeight = Math.max(yScale(m.low) - yTop, 2);
|
const rectHeight = Math.max(yScale(m.low) - yTop, 2);
|
||||||
ctx.fillStyle = isSupport ? 'rgba(47, 157, 178, 0.12)' : 'rgba(154, 160, 176, 0.10)';
|
ctx.fillStyle = isSupport ? 'rgba(47, 157, 178, 0.12)' : 'rgba(232, 155, 140, 0.12)';
|
||||||
ctx.fillRect(ml, yTop, cw, rectHeight);
|
ctx.fillRect(ml, yTop, cw, rectHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -319,10 +425,11 @@ export function CandlestickChart({
|
|||||||
ctx.setLineDash([]);
|
ctx.setLineDash([]);
|
||||||
ctx.globalAlpha = 1;
|
ctx.globalAlpha = 1;
|
||||||
|
|
||||||
lineLabels.push({
|
inPlotLabels.push({
|
||||||
y: yMid,
|
y: yMid,
|
||||||
text: isSupport ? 'Support' : 'Resist',
|
text: isSupport ? 'Support' : 'Resist',
|
||||||
color,
|
color,
|
||||||
|
bg: isSupport ? 'rgba(12, 18, 28, 0.78)' : 'rgba(28, 14, 14, 0.78)',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -358,7 +465,9 @@ export function CandlestickChart({
|
|||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
ctx.setLineDash([]);
|
ctx.setLineDash([]);
|
||||||
|
|
||||||
// Entry price: dashed horizontal line (neutral ink)
|
// Entry only when it has drifted from Now — otherwise the two lines
|
||||||
|
// stack and fight for the same label slot.
|
||||||
|
if (showEntry) {
|
||||||
ctx.strokeStyle = 'rgba(154, 160, 176, 0.9)';
|
ctx.strokeStyle = 'rgba(154, 160, 176, 0.9)';
|
||||||
ctx.lineWidth = 1.5;
|
ctx.lineWidth = 1.5;
|
||||||
ctx.setLineDash([6, 4]);
|
ctx.setLineDash([6, 4]);
|
||||||
@@ -367,59 +476,56 @@ export function CandlestickChart({
|
|||||||
ctx.lineTo(ml + cw, entryY);
|
ctx.lineTo(ml + cw, entryY);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
ctx.setLineDash([]);
|
ctx.setLineDash([]);
|
||||||
|
lineLabels.push({
|
||||||
|
y: entryY,
|
||||||
|
text: 'Entry',
|
||||||
|
color: 'rgba(154, 160, 176, 0.95)',
|
||||||
|
weight: 'bold',
|
||||||
|
priority: 70,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
lineLabels.push(
|
lineLabels.push(
|
||||||
{ y: entryY, text: 'Entry', color: 'rgba(154, 160, 176, 0.95)', weight: 'bold' },
|
{ y: stopY, text: 'Stop', color: 'rgba(239, 145, 130, 0.95)', weight: 'bold', priority: 90 },
|
||||||
{ y: stopY, text: 'Stop', color: 'rgba(239, 145, 130, 0.95)', weight: 'bold' },
|
{ y: targetY, text: 'Gate', color: 'rgba(196, 181, 253, 0.95)', weight: 'bold', priority: 80 },
|
||||||
{ y: targetY, text: 'Gate', color: 'rgba(196, 181, 253, 0.95)', weight: 'bold' },
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Current price line — the anchor for everything else (drawn on top)
|
// Current price line — the anchor for everything else (drawn on top)
|
||||||
{
|
{
|
||||||
const py = yScale(livePrice);
|
|
||||||
ctx.strokeStyle = 'rgba(226, 232, 240, 0.9)';
|
ctx.strokeStyle = 'rgba(226, 232, 240, 0.9)';
|
||||||
ctx.lineWidth = 1.25;
|
ctx.lineWidth = 1.25;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(ml, py);
|
ctx.moveTo(ml, nowY);
|
||||||
ctx.lineTo(ml + cw, py);
|
ctx.lineTo(ml + cw, nowY);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
|
||||||
lineLabels.push({
|
lineLabels.push({
|
||||||
y: py,
|
y: nowY,
|
||||||
text: 'Now',
|
text: 'Now',
|
||||||
color: 'rgba(226, 232, 240, 0.95)',
|
color: 'rgba(226, 232, 240, 0.95)',
|
||||||
weight: 'bold',
|
weight: 'bold',
|
||||||
|
priority: 100,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Left-side role labels (no prices — those sit on the right axis). Spread
|
// Left-side role labels. Prefer dropping lower-priority labels over shifting
|
||||||
// stacked labels so Entry/Stop/Gate don't paint over each other when close.
|
// them off their lines (shifted labels look like wrong prices).
|
||||||
if (lineLabels.length > 0) {
|
if (lineLabels.length > 0) {
|
||||||
const ordered = [...lineLabels].sort((a, b) => a.y - b.y);
|
const byPriority = [...lineLabels].sort((a, b) => b.priority - a.priority);
|
||||||
const minGap = 13;
|
const kept: LineLabel[] = [];
|
||||||
for (let i = 1; i < ordered.length; i++) {
|
for (const candidate of byPriority) {
|
||||||
if (ordered[i].y - ordered[i - 1].y < minGap) {
|
const collides = kept.some(
|
||||||
ordered[i].y = ordered[i - 1].y + minGap;
|
(k) => Math.abs(k.y - candidate.y) < LABEL_MIN_GAP_PX,
|
||||||
}
|
);
|
||||||
}
|
if (!collides) kept.push(candidate);
|
||||||
// If we overflow the pane bottom, pull the stack back up.
|
|
||||||
const maxY = mt + ch - 4;
|
|
||||||
if (ordered.length > 0 && ordered[ordered.length - 1].y > maxY) {
|
|
||||||
let shift = ordered[ordered.length - 1].y - maxY;
|
|
||||||
for (let i = ordered.length - 1; i >= 0 && shift > 0; i--) {
|
|
||||||
const prevFloor = i === 0 ? mt + 4 : ordered[i - 1].y + minGap;
|
|
||||||
const room = ordered[i].y - prevFloor;
|
|
||||||
const pull = Math.min(shift, Math.max(0, room));
|
|
||||||
ordered[i].y -= pull;
|
|
||||||
shift -= pull;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
kept.sort((a, b) => a.y - b.y);
|
||||||
|
|
||||||
ctx.font = '10px "IBM Plex Mono", ui-monospace, monospace';
|
ctx.font = '10px "IBM Plex Mono", ui-monospace, monospace';
|
||||||
ctx.textAlign = 'right';
|
ctx.textAlign = 'right';
|
||||||
ctx.textBaseline = 'middle';
|
ctx.textBaseline = 'middle';
|
||||||
for (const label of ordered) {
|
for (const label of kept) {
|
||||||
ctx.fillStyle = label.color;
|
ctx.fillStyle = label.color;
|
||||||
if (label.weight === 'bold') {
|
if (label.weight === 'bold') {
|
||||||
ctx.font = '600 10px "IBM Plex Mono", ui-monospace, monospace';
|
ctx.font = '600 10px "IBM Plex Mono", ui-monospace, monospace';
|
||||||
@@ -466,6 +572,72 @@ export function CandlestickChart({
|
|||||||
ctx.fillRect(x - candleW / 2, bodyTop, candleW, bodyH);
|
ctx.fillRect(x - candleW / 2, bodyTop, candleW, bodyH);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Support / Resist: label rides the line inside the plot, left-aligned.
|
||||||
|
// Soft pill so text stays legible over candles without needing the gutter.
|
||||||
|
if (inPlotLabels.length > 0) {
|
||||||
|
ctx.font = '600 10px "IBM Plex Mono", ui-monospace, monospace';
|
||||||
|
ctx.textAlign = 'left';
|
||||||
|
ctx.textBaseline = 'middle';
|
||||||
|
const padX = 5;
|
||||||
|
const padY = 2;
|
||||||
|
for (const label of inPlotLabels) {
|
||||||
|
const tw = ctx.measureText(label.text).width;
|
||||||
|
const bx = ml + 6;
|
||||||
|
const by = label.y;
|
||||||
|
const bw = tw + padX * 2;
|
||||||
|
const bh = 14 + padY;
|
||||||
|
ctx.fillStyle = label.bg;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.roundRect(bx, by - bh / 2, bw, bh, 3);
|
||||||
|
ctx.fill();
|
||||||
|
ctx.fillStyle = label.color;
|
||||||
|
ctx.fillText(label.text, bx + padX, by);
|
||||||
|
}
|
||||||
|
ctx.textBaseline = 'alphabetic';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Paper-trade markers: green ↓ entry above the bar at entry price, red ↑
|
||||||
|
// exit below the bar at close price (classic execution-chart convention).
|
||||||
|
if (paperTrades.length > 0) {
|
||||||
|
const ENTRY_GREEN = '#34d399';
|
||||||
|
const EXIT_RED = '#f87171';
|
||||||
|
for (const pt of paperTrades) {
|
||||||
|
const entryAbs = barIndexForEvent(data, pt.opened_at);
|
||||||
|
if (entryAbs >= start && entryAbs < end) {
|
||||||
|
const local = entryAbs - start;
|
||||||
|
const x = ml + local * barW + barW / 2;
|
||||||
|
const bar = data[entryAbs];
|
||||||
|
// Sit the tip on the entry price, but keep the arrow above the high
|
||||||
|
// so it stays readable when entry is mid-body.
|
||||||
|
const yPrice = yScale(pt.entry_price);
|
||||||
|
const yHigh = yScale(bar.high);
|
||||||
|
const y = Math.min(yPrice, yHigh) - 2;
|
||||||
|
if (y >= mt - 2 && y <= priceBottom + 4) {
|
||||||
|
drawEntryArrow(ctx, x, y, ENTRY_GREEN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
pt.status === 'closed'
|
||||||
|
&& pt.closed_at
|
||||||
|
&& pt.close_price != null
|
||||||
|
) {
|
||||||
|
const closeAbs = barIndexForEvent(data, pt.closed_at);
|
||||||
|
if (closeAbs >= start && closeAbs < end) {
|
||||||
|
const local = closeAbs - start;
|
||||||
|
const x = ml + local * barW + barW / 2;
|
||||||
|
const bar = data[closeAbs];
|
||||||
|
const yPrice = yScale(pt.close_price);
|
||||||
|
const yLow = yScale(bar.low);
|
||||||
|
const y = Math.max(yPrice, yLow) + 2;
|
||||||
|
if (y >= mt - 4 && y <= priceBottom + 6) {
|
||||||
|
drawExitArrow(ctx, x, y, EXIT_RED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Store geometry for hit testing (includes visibleRange offset)
|
// Store geometry for hit testing (includes visibleRange offset)
|
||||||
(canvas as any).__chartMeta = {
|
(canvas as any).__chartMeta = {
|
||||||
ml,
|
ml,
|
||||||
@@ -503,6 +675,7 @@ export function CandlestickChart({
|
|||||||
currentPrice,
|
currentPrice,
|
||||||
data,
|
data,
|
||||||
gateTargetLevels,
|
gateTargetLevels,
|
||||||
|
paperTrades,
|
||||||
showGateTraffic,
|
showGateTraffic,
|
||||||
srLevels,
|
srLevels,
|
||||||
tradeSetup,
|
tradeSetup,
|
||||||
|
|||||||
@@ -145,12 +145,21 @@ export default function TickerDetailPage() {
|
|||||||
// Status labels: is there an open paper trade on this ticker, and is it the
|
// Status labels: is there an open paper trade on this ticker, and is it the
|
||||||
// current top pick (same ranking the dashboard highlights)?
|
// current top pick (same ranking the dashboard highlights)?
|
||||||
const openTrades = usePaperTrades('open');
|
const openTrades = usePaperTrades('open');
|
||||||
|
// Full history for chart entry/exit arrows (open + closed on this symbol).
|
||||||
|
const paperTradeHistory = usePaperTrades();
|
||||||
const allTrades = useTrades();
|
const allTrades = useTrades();
|
||||||
const activation = useActivation();
|
const activation = useActivation();
|
||||||
const hasOpenTrade = useMemo(
|
const hasOpenTrade = useMemo(
|
||||||
() => (openTrades.data ?? []).some((t) => t.symbol.toUpperCase() === symbol.toUpperCase()),
|
() => (openTrades.data ?? []).some((t) => t.symbol.toUpperCase() === symbol.toUpperCase()),
|
||||||
[openTrades.data, symbol],
|
[openTrades.data, symbol],
|
||||||
);
|
);
|
||||||
|
const symbolPaperTrades = useMemo(
|
||||||
|
() =>
|
||||||
|
(paperTradeHistory.data ?? []).filter(
|
||||||
|
(t) => t.symbol.toUpperCase() === symbol.toUpperCase(),
|
||||||
|
),
|
||||||
|
[paperTradeHistory.data, symbol],
|
||||||
|
);
|
||||||
const isTopPick = useMemo(
|
const isTopPick = useMemo(
|
||||||
() => topPickSymbol(allTrades.data, activation.data)?.toUpperCase() === symbol.toUpperCase(),
|
() => topPickSymbol(allTrades.data, activation.data)?.toUpperCase() === symbol.toUpperCase(),
|
||||||
[allTrades.data, activation.data, symbol],
|
[allTrades.data, activation.data, symbol],
|
||||||
@@ -456,11 +465,20 @@ export default function TickerDetailPage() {
|
|||||||
onShowGateTrafficChange={setShowGateTraffic}
|
onShowGateTrafficChange={setShowGateTraffic}
|
||||||
tradeSetup={overlayWithTarget}
|
tradeSetup={overlayWithTarget}
|
||||||
currentPrice={priceInfo?.price}
|
currentPrice={priceInfo?.price}
|
||||||
|
paperTrades={symbolPaperTrades}
|
||||||
/>
|
/>
|
||||||
<p className="mt-2 text-[11px] text-gray-500">
|
<p className="mt-2 text-[11px] text-gray-500">
|
||||||
Only the nearest support & resistance are drawn. Full list in the S/R Levels tab.
|
Only the nearest support & resistance are drawn. Full list in the S/R Levels tab.
|
||||||
{srLevels.isError && ' S/R levels unavailable.'}
|
{srLevels.isError && ' S/R levels unavailable.'}
|
||||||
{gateTargetLadder.isError && ' GTL diagnostic unavailable.'}
|
{gateTargetLadder.isError && ' GTL diagnostic unavailable.'}
|
||||||
|
{symbolPaperTrades.length > 0 && (
|
||||||
|
<>
|
||||||
|
{' '}
|
||||||
|
<span className="text-emerald-400/90">▼</span> paper entry
|
||||||
|
{' · '}
|
||||||
|
<span className="text-red-400/90">▲</span> paper exit
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
<ProductionRankStrip
|
<ProductionRankStrip
|
||||||
setup={longSetup ?? shortSetup}
|
setup={longSetup ?? shortSetup}
|
||||||
|
|||||||
Reference in New Issue
Block a user