feat(chart): 메인 → RSI/MACD crosshair 동기화
토스 차트 인상 — 메인 캔들에 hover 하면 RSI/MACD 보조 패널도 같은 시점을 가리키며 가격 라벨이 뜬다. lightweight-charts setCrosshairPosition / clearCrosshairPosition API 활용. - rsiByTsRef / macdSignalByTsRef: 보조 시계열의 ts→value O(1) lookup. RSI/MACD 데이터 push 할 때 동시에 채움. - crosshair handler: hover 시 보조 차트도 setCrosshairPosition, 마우스가 차트 밖이면 clearCrosshairPosition 으로 양쪽 다 해제. - MACD 는 signal 라인 기준 — 가격 라벨이 안정적으로 보이도록. - 보조 차트 toggle off / 데이터 부족 시점은 ref guard 와 lookup miss 로 자연스럽게 noop.
This commit is contained in:
@@ -216,6 +216,12 @@ export function StockChart({ chart, prediction }: Props) {
|
|||||||
const macdSignalRef = useRef<ISeriesApi<"Line"> | null>(null);
|
const macdSignalRef = useRef<ISeriesApi<"Line"> | null>(null);
|
||||||
const macdHistRef = useRef<ISeriesApi<"Histogram"> | null>(null);
|
const macdHistRef = useRef<ISeriesApi<"Histogram"> | null>(null);
|
||||||
|
|
||||||
|
// 보조 차트 crosshair 동기화용 time→value lookup.
|
||||||
|
// 메인 차트에서 hover 한 시점의 RSI/MACD 값을 O(1) 로 찾아 setCrosshairPosition 에
|
||||||
|
// 넣어줘야 보조 차트의 가격 라벨이 정확히 뜬다.
|
||||||
|
const rsiByTsRef = useRef<Map<number, number>>(new Map());
|
||||||
|
const macdSignalByTsRef = useRef<Map<number, number>>(new Map());
|
||||||
|
|
||||||
// 토글 상태 — 기본 MA20 만 ON. 사용자 컨트롤이 너무 시끄럽지 않게.
|
// 토글 상태 — 기본 MA20 만 ON. 사용자 컨트롤이 너무 시끄럽지 않게.
|
||||||
const [active, setActive] = useState<Set<SmaWindow>>(() => new Set<SmaWindow>([20]));
|
const [active, setActive] = useState<Set<SmaWindow>>(() => new Set<SmaWindow>([20]));
|
||||||
const [rsiOn, setRsiOn] = useState(false);
|
const [rsiOn, setRsiOn] = useState(false);
|
||||||
@@ -484,17 +490,21 @@ export function StockChart({ chart, prediction }: Props) {
|
|||||||
};
|
};
|
||||||
}, [showRsi]);
|
}, [showRsi]);
|
||||||
|
|
||||||
// RSI 데이터 push.
|
// RSI 데이터 push + crosshair sync 용 ts→value map 채우기.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!rsiSeriesRef.current) return;
|
if (!rsiSeriesRef.current) return;
|
||||||
const values = rsi(closes, 14);
|
const values = rsi(closes, 14);
|
||||||
const data: LineData[] = [];
|
const data: LineData[] = [];
|
||||||
|
const lookup = new Map<number, number>();
|
||||||
for (let i = 0; i < values.length; i++) {
|
for (let i = 0; i < values.length; i++) {
|
||||||
const v = values[i];
|
const v = values[i];
|
||||||
if (v == null) continue;
|
if (v == null) continue;
|
||||||
data.push({ time: isoToUtcTs(chart.ohlcv[i].date), value: v });
|
const t = isoToUtcTs(chart.ohlcv[i].date);
|
||||||
|
data.push({ time: t, value: v });
|
||||||
|
lookup.set(t as number, v);
|
||||||
}
|
}
|
||||||
rsiSeriesRef.current.setData(data);
|
rsiSeriesRef.current.setData(data);
|
||||||
|
rsiByTsRef.current = lookup;
|
||||||
}, [closes, chart, showRsi]);
|
}, [closes, chart, showRsi]);
|
||||||
|
|
||||||
// MACD 보조 차트 생성/제거.
|
// MACD 보조 차트 생성/제거.
|
||||||
@@ -554,7 +564,8 @@ export function StockChart({ chart, prediction }: Props) {
|
|||||||
};
|
};
|
||||||
}, [showMacd]);
|
}, [showMacd]);
|
||||||
|
|
||||||
// MACD 데이터 push.
|
// MACD 데이터 push + crosshair sync 용 signal ts→value map 채우기.
|
||||||
|
// signal 라인 기준으로 crosshair 가격 라벨이 뜨도록 — MACD 라인보다 안정적으로 보임.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const hSeries = macdHistRef.current;
|
const hSeries = macdHistRef.current;
|
||||||
const mSeries = macdLineRef.current;
|
const mSeries = macdLineRef.current;
|
||||||
@@ -564,10 +575,14 @@ export function StockChart({ chart, prediction }: Props) {
|
|||||||
const mLine: LineData[] = [];
|
const mLine: LineData[] = [];
|
||||||
const sLine: LineData[] = [];
|
const sLine: LineData[] = [];
|
||||||
const hBars: HistogramData[] = [];
|
const hBars: HistogramData[] = [];
|
||||||
|
const signalLookup = new Map<number, number>();
|
||||||
for (let i = 0; i < closes.length; i++) {
|
for (let i = 0; i < closes.length; i++) {
|
||||||
const t = isoToUtcTs(chart.ohlcv[i].date);
|
const t = isoToUtcTs(chart.ohlcv[i].date);
|
||||||
if (m[i] != null) mLine.push({ time: t, value: m[i] as number });
|
if (m[i] != null) mLine.push({ time: t, value: m[i] as number });
|
||||||
if (sig[i] != null) sLine.push({ time: t, value: sig[i] as number });
|
if (sig[i] != null) {
|
||||||
|
sLine.push({ time: t, value: sig[i] as number });
|
||||||
|
signalLookup.set(t as number, sig[i] as number);
|
||||||
|
}
|
||||||
if (hist[i] != null) {
|
if (hist[i] != null) {
|
||||||
const h = hist[i] as number;
|
const h = hist[i] as number;
|
||||||
// 한국 관행: 양수 막대=상승=빨강, 음수=하락=파랑. 컬러 옅게 (배경 톤).
|
// 한국 관행: 양수 막대=상승=빨강, 음수=하락=파랑. 컬러 옅게 (배경 톤).
|
||||||
@@ -577,6 +592,7 @@ export function StockChart({ chart, prediction }: Props) {
|
|||||||
hSeries.setData(hBars);
|
hSeries.setData(hBars);
|
||||||
mSeries.setData(mLine);
|
mSeries.setData(mLine);
|
||||||
sSeries.setData(sLine);
|
sSeries.setData(sLine);
|
||||||
|
macdSignalByTsRef.current = signalLookup;
|
||||||
}, [closes, chart, showMacd]);
|
}, [closes, chart, showMacd]);
|
||||||
|
|
||||||
// crosshair → 한국어 오버레이.
|
// crosshair → 한국어 오버레이.
|
||||||
@@ -611,15 +627,19 @@ export function StockChart({ chart, prediction }: Props) {
|
|||||||
const c = chartRef.current;
|
const c = chartRef.current;
|
||||||
if (!c) return;
|
if (!c) return;
|
||||||
const handler = (param: MouseEventParams) => {
|
const handler = (param: MouseEventParams) => {
|
||||||
// 차트 영역 밖이면 hover 해제 → lastSummary 가 자동 표시됨.
|
// 차트 영역 밖이면 hover 해제 → lastSummary 가 자동 표시 + 보조 차트 crosshair clear.
|
||||||
if (!param.time || !param.point) {
|
if (!param.time || !param.point) {
|
||||||
setHover(null);
|
setHover(null);
|
||||||
|
rsiChartRef.current?.clearCrosshairPosition();
|
||||||
|
macdChartRef.current?.clearCrosshairPosition();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const t = param.time as UTCTimestamp;
|
const t = param.time as UTCTimestamp;
|
||||||
const p = ohlcvByTs.get(t as number);
|
const p = ohlcvByTs.get(t as number);
|
||||||
if (!p) {
|
if (!p) {
|
||||||
setHover(null);
|
setHover(null);
|
||||||
|
rsiChartRef.current?.clearCrosshairPosition();
|
||||||
|
macdChartRef.current?.clearCrosshairPosition();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const pct =
|
const pct =
|
||||||
@@ -635,6 +655,22 @@ export function StockChart({ chart, prediction }: Props) {
|
|||||||
volume: p.volume,
|
volume: p.volume,
|
||||||
pct,
|
pct,
|
||||||
});
|
});
|
||||||
|
// 보조 차트 crosshair 같은 시점으로 이동 — 그 시점 데이터가 있을 때만.
|
||||||
|
// setCrosshairPosition 은 series 의 price 좌표를 받음.
|
||||||
|
const rsiChart = rsiChartRef.current;
|
||||||
|
const rsiSeries = rsiSeriesRef.current;
|
||||||
|
if (rsiChart && rsiSeries) {
|
||||||
|
const v = rsiByTsRef.current.get(t as number);
|
||||||
|
if (v != null) rsiChart.setCrosshairPosition(v, t, rsiSeries);
|
||||||
|
else rsiChart.clearCrosshairPosition();
|
||||||
|
}
|
||||||
|
const macdChart = macdChartRef.current;
|
||||||
|
const macdSig = macdSignalRef.current;
|
||||||
|
if (macdChart && macdSig) {
|
||||||
|
const v = macdSignalByTsRef.current.get(t as number);
|
||||||
|
if (v != null) macdChart.setCrosshairPosition(v, t, macdSig);
|
||||||
|
else macdChart.clearCrosshairPosition();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
c.subscribeCrosshairMove(handler);
|
c.subscribeCrosshairMove(handler);
|
||||||
return () => {
|
return () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user