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:
OpenSquared
2026-06-24 22:21:04 +02:00
parent d47fc8f50d
commit 418d03254d
3 changed files with 447 additions and 231 deletions

View File

@@ -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]) => (