// frontend/src/components/IntradayZoomChart.tsx import React, { useEffect, useRef } from 'react'; import { createChart, ColorType } from 'lightweight-charts'; import type { IChartApi, SeriesMarker, UTCTimestamp } from 'lightweight-charts'; import type { LedgerItem } from './LedgerTable'; interface CandleData { time: number; open: number; high: number; low: number; close: number; volume: number; vwap: number | null; ema_9: number | null; ema_21: number | null; ema_50: number | null; } interface IntradayZoomChartProps { candles: CandleData[]; tradeItem: LedgerItem; onClose: () => void; } export const IntradayZoomChart: React.FC = ({ candles, tradeItem, onClose }) => { const chartContainerRef = useRef(null); const chartRef = useRef(null); const [renderError, setRenderError] = React.useState(null); useEffect(() => { if (!chartContainerRef.current || candles.length === 0) return; setRenderError(null); let chart: IChartApi | null = null; const handleResize = () => { if (chartContainerRef.current && chartRef.current) { chartRef.current.applyOptions({ width: chartContainerRef.current.clientWidth, }); } }; try { // 1. 创建 TradingView 图表,设定 Robinhood 极简黑夜风样式 chart = createChart(chartContainerRef.current, { width: chartContainerRef.current.clientWidth, height: 350, layout: { background: { type: ColorType.Solid, color: '#09090b' }, textColor: '#8e8e93', }, grid: { vertLines: { visible: false }, horzLines: { visible: false }, }, timeScale: { timeVisible: true, secondsVisible: false, borderVisible: false, }, rightPriceScale: { borderVisible: false, }, }); chartRef.current = chart; // 2. 添加 K 线蜡烛图系列 const candlestickSeries = chart.addCandlestickSeries({ upColor: '#00c805', downColor: '#ff3b30', borderDownColor: '#ff3b30', borderUpColor: '#00c805', wickDownColor: '#ff3b30', wickUpColor: '#00c805', }); // 3. 添加均线序列 const vwapSeries = chart.addLineSeries({ color: '#ffd700', lineWidth: 2, title: 'VWAP', priceLineVisible: false, }); const ema9Series = chart.addLineSeries({ color: '#2196f3', lineWidth: 1, title: 'EMA 9', priceLineVisible: false, }); // 4. 填充数据 const candleData = candles.map((c) => ({ time: c.time as UTCTimestamp, open: c.open, high: c.high, low: c.low, close: c.close, })); candlestickSeries.setData(candleData); const vwapData = candles .filter((c) => c.vwap !== null) .map((c) => ({ time: c.time as UTCTimestamp, value: c.vwap as number })); vwapSeries.setData(vwapData); const ema9Data = candles .filter((c) => c.ema_9 !== null) .map((c) => ({ time: c.time as UTCTimestamp, value: c.ema_9 as number })); ema9Series.setData(ema9Data); // 5. 绑定今日开盘 9:30 的模拟交易标记 (T+1 开盘价成交) // 我们寻找当天 09:30 AM EST 附近的 candle。如果没有正好 09:30 的,我们就用第一个 candle。 let markerTime: UTCTimestamp = candles[0].time as UTCTimestamp; // 查找 09:30 的 K线。由于 Unix 时间戳可以转为当地小时分钟: // 在东部时间 09:30:00,我们可以计算第一个在 09:30 的时间戳: for (const c of candles) { const dateObj = new Date(c.time * 1000); // yfinance 历史分钟级数据在 US/Eastern 时区 // 我们可以直接通过 toLocaleTimeString 或者 getUTCHours/getHours 检查 // 由于 backend 传来的 time 已经是 Unix 时间戳 // 对应美东时间 9:30 约为 UTC 时间 13:30 (标准时) 或 14:30 (夏令时) // 我们可以用本地小时检查,或者直接选择 9:30 标记 const hour = dateObj.getHours(); const min = dateObj.getMinutes(); if (hour === 9 && min === 30) { markerTime = c.time as UTCTimestamp; break; } } const pnl = tradeItem.realized_pnl; const isBuy = tradeItem.action === 'BUY'; const color = isBuy ? '#00c805' : (pnl !== undefined && pnl < 0 ? '#ff3b30' : '#00c805'); const shape = isBuy ? 'arrowUp' : 'arrowDown'; const position = isBuy ? 'belowBar' : 'aboveBar'; const text = isBuy ? `BUY ${tradeItem.shares}股 @ ${tradeItem.execution_price.toFixed(2)}` : `SELL ${tradeItem.shares}股 @ ${tradeItem.execution_price.toFixed(2)} (${pnl !== undefined && pnl >= 0 ? '+' : ''}${pnl?.toFixed(2)})`; const chartMarkers: SeriesMarker[] = [{ time: markerTime, position: position as any, color: color, shape: shape as any, text: text, size: 1.5, }]; candlestickSeries.setMarkers(chartMarkers); // 自动缩放自适应数据范围 chart.timeScale().fitContent(); window.addEventListener('resize', handleResize); } catch (e: any) { console.error("Intraday chart rendering error:", e); setRenderError(e.message || String(e)); } return () => { window.removeEventListener('resize', handleResize); if (chart) { chart.remove(); } }; }, [candles, tradeItem]); if (renderError) { return (

日内图表加载错误

{renderError}
); } return (

🔍 日内交易 1分钟微观透视 (Intraday Trade Inspector)

标的:{tradeItem.ticker} | 交易日期:{tradeItem.timestamp.split(' ')[0]} | 成交动作: {tradeItem.action === 'BUY' ? '买入' : '平仓'}
● VWAP ● EMA 9
); };