feat(chart): 10m 실시간 / 일·주·월 토글 / 오늘 마커 / 예측 거래일 선택

- backend/app/api/chart.py: interval=10m|1d|1w|1mo. 10m 은 ohlcv_1m 을
  time_bucket(10min) 으로 집계, stale(>10분) 이면 KIS 분봉 fetch 후 재조회.
  1w/1mo 는 ohlcv_daily 를 date_trunc 로 집계. today 필드 추가.
- backend/app/fetch/kis.py: fetch_minute_price() 추가 (tr_id FHKST03010200).
  KIS 응답 KST 시각을 tz-aware datetime 으로 변환, 오름차순 정렬.
- web/lib/api.ts: ChartInterval 타입, getChart(interval), predict(horizons[]).
- web/components/StockChart.tsx: 10m 이면 timeVisible. 일·주·월에서 오늘
  화살표 마커 표시. ISO datetime 도 파싱.
- web/components/PredictionPanel.tsx: 단기/중기/장기 프리셋 + 사용자 직접
  지정 (예: 1,2,3,7). API 에 horizons 배열 전달.
- web/app/[code]/page.tsx: interval 칩 (10분/일/주/월). 10m 일 때 60초마다
  폴링. interval 별 기본 lookback (10m=1, 1d=180, 1w=730, 1mo=1825).
This commit is contained in:
claude-owner
2026-05-23 01:34:29 +09:00
parent 928c2160f9
commit 0a5c634680
6 changed files with 503 additions and 81 deletions

View File

@@ -42,6 +42,7 @@ export type SymbolSearch = {
};
export type OhlcvPoint = {
// 1d/1w/1mo: 'YYYY-MM-DD' / 10m: 'YYYY-MM-DDTHH:MM:SS' (KST naive ISO)
date: string;
open: number | null;
high: number | null;
@@ -50,6 +51,8 @@ export type OhlcvPoint = {
volume: number | null;
};
export type ChartInterval = "10m" | "1d" | "1w" | "1mo";
export type SentimentPoint = {
date: string;
n_articles: number;
@@ -68,7 +71,10 @@ export type ChartPayload = {
code: string;
name: string;
market: string;
interval: ChartInterval;
intraday_status: string | null;
range: { from: string; to: string };
today: string;
ohlcv: OhlcvPoint[];
sentiment: SentimentPoint[];
trading_value: TradingValuePoint[];
@@ -178,13 +184,17 @@ export const api = {
`/api/symbols/search?q=${encodeURIComponent(q)}&limit=${limit}&seed_only=${seedOnly}`,
),
getSymbol: (code: string) => getJson<Symbol>(`/api/symbols/${encodeURIComponent(code)}`),
getChart: (code: string, days = 180) =>
getJson<ChartPayload>(`/api/chart/${encodeURIComponent(code)}?days=${days}`),
predict: (code: string, horizons = "1,3,5") =>
getJson<PredictResponse>(
`/api/predict/${encodeURIComponent(code)}?horizons=${encodeURIComponent(horizons)}`,
{ method: "POST" },
getChart: (code: string, days = 180, interval: ChartInterval = "1d") =>
getJson<ChartPayload>(
`/api/chart/${encodeURIComponent(code)}?days=${days}&interval=${encodeURIComponent(interval)}`,
),
predict: (code: string, horizons: string | number[] = "1,3,5") => {
const h = Array.isArray(horizons) ? horizons.join(",") : horizons;
return getJson<PredictResponse>(
`/api/predict/${encodeURIComponent(code)}?horizons=${encodeURIComponent(h)}`,
{ method: "POST" },
);
},
latestPrediction: (code: string) =>
getJson<LatestPredictionResponse>(`/api/predict/${encodeURIComponent(code)}/latest`),
metrics: (code: string, windowDays = 30) =>