quant-ai / frontend /src /components /IntradayZoomChart.tsx
Ypeng12's picture
fix(quant): resolve timezone issue, sanitize titles, add live Watchlist batch quotes & auto-start AI trading runner
6a4889b
Raw
History Blame Contribute Delete
8.08 kB
// 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<IntradayZoomChartProps> = ({ candles, tradeItem, onClose }) => {
const chartContainerRef = useRef<HTMLDivElement>(null);
const chartRef = useRef<IChartApi | null>(null);
const [renderError, setRenderError] = React.useState<string | null>(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 历史分钟级数据在 America/New_York 时区
// 我们可以直接通过 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<UTCTimestamp>[] = [{
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 (
<div className="loader-container" style={{ color: 'var(--color-red)', padding: '1rem', border: '1px dashed var(--color-border)' }}>
<h4 style={{ margin: 0 }}>日内图表加载错误</h4>
<div style={{ fontSize: '0.8rem', fontFamily: 'monospace' }}>{renderError}</div>
</div>
);
}
return (
<div className="card" style={{ marginTop: '1.5rem', border: '1px solid rgba(0, 200, 5, 0.3)', background: '#09090b' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px' }}>
<div>
<h3 className="card-title" style={{ margin: 0 }}>🔍 日内交易 1分钟微观透视 (Intraday Trade Inspector)</h3>
<span style={{ fontSize: '0.78rem', color: 'var(--color-text-secondary)' }}>
标的:<strong style={{ color: '#fff' }}>{tradeItem.ticker}</strong> |
交易日期:<strong style={{ color: '#fff' }}>{tradeItem.timestamp.split(' ')[0]}</strong> |
成交动作:<strong style={{ color: tradeItem.action === 'BUY' ? 'var(--color-green)' : 'var(--color-red)' }}>
{tradeItem.action === 'BUY' ? '买入' : '平仓'}
</strong>
</span>
</div>
<button
onClick={onClose}
style={{
background: 'rgba(255,255,255,0.06)',
border: 'none',
color: '#fff',
borderRadius: '50%',
width: '26px',
height: '26px',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '0.9rem'
}}
>
</button>
</div>
<div style={{ position: 'relative', width: '100%' }}>
<div
style={{
position: 'absolute',
top: '8px',
left: '10px',
zIndex: 10,
display: 'flex',
gap: '12px',
fontSize: '0.75rem',
fontWeight: 600,
pointerEvents: 'none',
}}
>
<span style={{ color: '#ffd700' }}>● VWAP</span>
<span style={{ color: '#2196f3' }}>● EMA 9</span>
</div>
<div ref={chartContainerRef} style={{ width: '100%', height: '350px' }} />
</div>
</div>
);
};