feat: InstrumentDashboard — event timeline strip + crosshair date-aware cards
- InstrumentChart: onDateHover callback via subscribeCrosshairMove (useRef pattern) - EventTimelineStrip: 3 rows LT/MT/CT with CSS-% bars aligned to chart X axis - Cards date-aware: crosshair drives selectedDate; dateTrend + dateSignals computed client-side from lookup maps (priceMap/indMap/sortedDates) without extra API calls - TrendCard: price, RSI, ATR, slopes, momentum, 52W range all at selected date - RegimeCard: 5 signals recomputed at selected date; regime label from server - Date badge above cards; blue tint when browsing history, grey on last date - instrument_service.py: end_date in events; price_data built before events block Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -18,21 +18,14 @@ interface ChartEvent {
|
||||
date: string
|
||||
title: string
|
||||
level: string
|
||||
impact_score?: number
|
||||
}
|
||||
|
||||
interface Props {
|
||||
priceData: PriceCandle[]
|
||||
indicators: {
|
||||
ma20?: LinePoint[]
|
||||
ma50?: LinePoint[]
|
||||
ma100?: LinePoint[]
|
||||
ma200?: LinePoint[]
|
||||
bb_upper?: LinePoint[]
|
||||
bb_lower?: LinePoint[]
|
||||
}
|
||||
events?: ChartEvent[]
|
||||
height?: number
|
||||
priceData: PriceCandle[]
|
||||
indicators: Record<string, LinePoint[]>
|
||||
events?: ChartEvent[]
|
||||
height?: number
|
||||
onDateHover?: (date: string | null) => void
|
||||
}
|
||||
|
||||
const MA_COLORS: Record<string, string> = {
|
||||
@@ -48,9 +41,13 @@ const LEVEL_COLORS: Record<string, string> = {
|
||||
short: '#10b981',
|
||||
}
|
||||
|
||||
export default function InstrumentChart({ priceData, indicators, events = [], height = 420 }: Props) {
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const cleanupRef = useRef<(() => void) | null>(null)
|
||||
export default function InstrumentChart({ priceData, indicators, events = [], height = 420, onDateHover }: Props) {
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const cleanupRef = useRef<(() => void) | null>(null)
|
||||
const onHoverRef = useRef(onDateHover)
|
||||
|
||||
// Keep callback ref fresh without triggering chart rebuild
|
||||
useEffect(() => { onHoverRef.current = onDateHover }, [onDateHover])
|
||||
|
||||
useEffect(() => {
|
||||
cleanupRef.current?.()
|
||||
@@ -90,8 +87,8 @@ export default function InstrumentChart({ priceData, indicators, events = [], he
|
||||
handleScale: true,
|
||||
})
|
||||
|
||||
// Volume histogram (bottom 18% of chart)
|
||||
const totalVol = priceData.reduce((s, d) => s + d.volume, 0)
|
||||
// Volume (bottom 18%)
|
||||
const totalVol = priceData.reduce((s, d) => s + (d.volume || 0), 0)
|
||||
if (totalVol > 0) {
|
||||
const volSeries = chart.addHistogramSeries({
|
||||
color: 'rgba(148,163,184,0.2)',
|
||||
@@ -106,29 +103,28 @@ export default function InstrumentChart({ priceData, indicators, events = [], he
|
||||
})))
|
||||
}
|
||||
|
||||
// Candlestick series
|
||||
// Candlesticks
|
||||
const candleSeries = chart.addCandlestickSeries({
|
||||
upColor: '#10b981',
|
||||
downColor: '#ef4444',
|
||||
borderUpColor: '#10b981',
|
||||
borderDownColor:'#ef4444',
|
||||
wickUpColor: '#6ee7b7',
|
||||
wickDownColor: '#fca5a5',
|
||||
upColor: '#10b981',
|
||||
downColor: '#ef4444',
|
||||
borderUpColor: '#10b981',
|
||||
borderDownColor: '#ef4444',
|
||||
wickUpColor: '#6ee7b7',
|
||||
wickDownColor: '#fca5a5',
|
||||
})
|
||||
candleSeries.setData(priceData)
|
||||
|
||||
// MA lines
|
||||
for (const [key, color] of Object.entries(MA_COLORS)) {
|
||||
const data = indicators[key as keyof typeof indicators]
|
||||
const data = indicators[key]
|
||||
if (data?.length) {
|
||||
const s = chart.addLineSeries({
|
||||
chart.addLineSeries({
|
||||
color,
|
||||
lineWidth: key === 'ma200' ? 1.5 : 1,
|
||||
priceLineVisible: false,
|
||||
lastValueVisible: true,
|
||||
crosshairMarkerVisible: false,
|
||||
})
|
||||
s.setData(data)
|
||||
}).setData(data)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,7 +151,7 @@ export default function InstrumentChart({ priceData, indicators, events = [], he
|
||||
position: 'aboveBar' as const,
|
||||
color: LEVEL_COLORS[ev.level] ?? '#f59e0b',
|
||||
shape: 'arrowDown' as const,
|
||||
text: ev.title.slice(0, 20),
|
||||
text: ev.title.slice(0, 18),
|
||||
}))
|
||||
.sort((a, b) => a.time.localeCompare(b.time))
|
||||
|
||||
@@ -163,6 +159,15 @@ export default function InstrumentChart({ priceData, indicators, events = [], he
|
||||
|
||||
chart.timeScale().fitContent()
|
||||
|
||||
// Crosshair → date callback
|
||||
chart.subscribeCrosshairMove((param: any) => {
|
||||
if (param?.time && typeof param.time === 'string') {
|
||||
onHoverRef.current?.(param.time)
|
||||
} else if (!param?.point) {
|
||||
onHoverRef.current?.(null) // mouse left chart
|
||||
}
|
||||
})
|
||||
|
||||
const ro = new ResizeObserver(() => {
|
||||
if (containerRef.current) chart.applyOptions({ width: containerRef.current.clientWidth })
|
||||
})
|
||||
@@ -187,9 +192,8 @@ export default function InstrumentChart({ priceData, indicators, events = [], he
|
||||
{k.toUpperCase()}
|
||||
</span>
|
||||
))}
|
||||
<span className="flex items-center gap-1.5">
|
||||
<span className="inline-block w-5 h-0.5 rounded opacity-40 border-t border-dashed border-slate-400" />
|
||||
BB(20,2)
|
||||
<span className="flex items-center gap-1.5 opacity-40">
|
||||
<span className="inline-block w-5 border-t border-dashed border-slate-400" />BB(20,2)
|
||||
</span>
|
||||
<span className="ml-auto flex items-center gap-3">
|
||||
{Object.entries(LEVEL_COLORS).map(([l, c]) => (
|
||||
|
||||
Reference in New Issue
Block a user