feat(targets): price target/stop simulator with chart overlay lines
- New lib/targets.ts: per-code localStorage (key `targets:<code>`) with read/write/clear; rejects non-positive/non-finite values. - New PriceTargets sidebar panel: 목표가/손절가 inputs, live %-from-current preview (rose=up, sky=down), save/clear actions. - StockChart accepts optional `targets` prop and draws dashed horizontal price lines on the candle series (rose=target, sky=stop) with axis labels. - Page loads saved targets on mount/code-change and re-renders chart on save. Also fixes view-counter dedupe caveat from reviewer: split into wasRecordedToday() + markRecorded(); the page now marks localStorage only after POST /api/views succeeds, so a failed POST stays retryable on the next visit instead of being silently swallowed for the day. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@ import {
|
||||
type CandlestickData,
|
||||
type HistogramData,
|
||||
type IChartApi,
|
||||
type IPriceLine,
|
||||
type ISeriesApi,
|
||||
type LineData,
|
||||
type LogicalRange,
|
||||
@@ -13,10 +14,13 @@ import {
|
||||
type UTCTimestamp,
|
||||
} from "lightweight-charts";
|
||||
import type { ChartPayload, LatestPredictionResponse } from "../lib/api";
|
||||
import type { Targets } from "../lib/targets";
|
||||
|
||||
type Props = {
|
||||
chart: ChartPayload;
|
||||
prediction?: LatestPredictionResponse | null;
|
||||
// 사용자가 저장한 목표가/손절가 — 메인 캔들 위 가로 라인 오버레이.
|
||||
targets?: Targets | null;
|
||||
};
|
||||
|
||||
// KST naive ISO ('YYYY-MM-DD' 또는 'YYYY-MM-DDTHH:MM:SS') 를 UTCTimestamp 로.
|
||||
@@ -198,7 +202,7 @@ function macd(
|
||||
return { macd: m, signal: sig, hist };
|
||||
}
|
||||
|
||||
export function StockChart({ chart, prediction }: Props) {
|
||||
export function StockChart({ chart, prediction, targets }: Props) {
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const chartRef = useRef<IChartApi | null>(null);
|
||||
const candleRef = useRef<ISeriesApi<"Candlestick"> | null>(null);
|
||||
@@ -215,6 +219,9 @@ export function StockChart({ chart, prediction }: Props) {
|
||||
const macdLineRef = useRef<ISeriesApi<"Line"> | null>(null);
|
||||
const macdSignalRef = useRef<ISeriesApi<"Line"> | null>(null);
|
||||
const macdHistRef = useRef<ISeriesApi<"Histogram"> | null>(null);
|
||||
// 목표가/손절가 가로 라인 — candleRef 의 priceLine 으로 부착.
|
||||
const targetLineRef = useRef<IPriceLine | null>(null);
|
||||
const stopLineRef = useRef<IPriceLine | null>(null);
|
||||
|
||||
// 보조 차트 crosshair 동기화용 time→value lookup.
|
||||
// 메인 차트에서 hover 한 시점의 RSI/MACD 값을 O(1) 로 찾아 setCrosshairPosition 에
|
||||
@@ -430,6 +437,43 @@ export function StockChart({ chart, prediction }: Props) {
|
||||
chartRef.current.timeScale().fitContent();
|
||||
}, [prediction, isIntraday]);
|
||||
|
||||
// 목표가/손절가 가로 라인 — targets prop 또는 캔들 시리즈 재생성 시 동기화.
|
||||
// candleRef 가 바뀌면 이전 라인 ref 는 이미 해제된 상태라 그냥 새로 만들면 됨.
|
||||
useEffect(() => {
|
||||
const candle = candleRef.current;
|
||||
if (!candle) return;
|
||||
// 이전 라인 제거 후 새로 그림 — 입력 바뀔 때마다 가격값 변경하지 않고 그냥 재생성.
|
||||
if (targetLineRef.current) {
|
||||
try { candle.removePriceLine(targetLineRef.current); } catch { /* 이미 해제됨 */ }
|
||||
targetLineRef.current = null;
|
||||
}
|
||||
if (stopLineRef.current) {
|
||||
try { candle.removePriceLine(stopLineRef.current); } catch { /* 이미 해제됨 */ }
|
||||
stopLineRef.current = null;
|
||||
}
|
||||
if (!targets) return;
|
||||
if (targets.target != null && Number.isFinite(targets.target)) {
|
||||
targetLineRef.current = candle.createPriceLine({
|
||||
price: targets.target,
|
||||
color: "#fb7185", // rose-400 (한국 상승색 톤)
|
||||
lineWidth: 1,
|
||||
lineStyle: 2, // dashed
|
||||
axisLabelVisible: true,
|
||||
title: "목표",
|
||||
});
|
||||
}
|
||||
if (targets.stop != null && Number.isFinite(targets.stop)) {
|
||||
stopLineRef.current = candle.createPriceLine({
|
||||
price: targets.stop,
|
||||
color: "#60a5fa", // sky-400 (한국 하락색 톤)
|
||||
lineWidth: 1,
|
||||
lineStyle: 2,
|
||||
axisLabelVisible: true,
|
||||
title: "손절",
|
||||
});
|
||||
}
|
||||
}, [targets, isIntraday]);
|
||||
|
||||
// RSI 보조 차트 생성/제거 — toggle 또는 interval 변경 시. 메인 chart 와 별 인스턴스.
|
||||
// priceScale fix (0-100) + 30/70 가이드 라인.
|
||||
useEffect(() => {
|
||||
|
||||
Reference in New Issue
Block a user