"use client"; import { useState } from "react"; import { api, type LatestPredictionResponse, type LatestPredictionStep, type PredictResponse, } from "../lib/api"; type Props = { code: string; initial?: LatestPredictionResponse | null; onResult: (pred: LatestPredictionResponse) => void; }; function normalizeFromPredictResponse( code: string, resp: PredictResponse, ): LatestPredictionResponse { const steps: LatestPredictionStep[] = resp.steps.map((s) => ({ predicted_at: null, target_date: s.target_date ?? "", horizon: s.horizon, direction: s.direction, prob_up: s.prob_up, prob_flat: s.prob_flat, prob_down: s.prob_down, expected_return: s.expected_return, point_close: s.point_close, ci_low: s.ci_low, ci_high: s.ci_high, user_triggered: resp.user_triggered, features_snapshot: null, })); return { code, found: true, base_date: resp.base_date, base_close: resp.base_close, steps, }; } // 15일/30일/1년 세 프리셋. 각각 그 구간 안에 6~7 step 을 깔아서 차트 캔들이 // 너무 듬성하지 않게 한다. 백엔드는 1..252 거래일 horizon 을 허용 (predict.py 참조). const PREDICT_PRESETS: { id: string; label: string; horizons: number[] }[] = [ { id: "15d", label: "15일", horizons: [1, 3, 5, 7, 10, 15] }, { id: "30d", label: "30일", horizons: [1, 5, 10, 15, 20, 25, 30] }, { id: "1y", label: "1년", horizons: [5, 15, 30, 60, 120, 180, 240] }, ]; export function PredictionPanel({ code, initial, onResult }: Props) { const [pred, setPred] = useState(initial ?? null); const [loading, setLoading] = useState(false); const [err, setErr] = useState(null); const [presetIdx, setPresetIdx] = useState(1); // 30일 기본 async function runPredict() { setLoading(true); setErr(null); try { const horizons = PREDICT_PRESETS[presetIdx].horizons; const r = await api.predict(code, horizons); const normalized = normalizeFromPredictResponse(code, r); setPred(normalized); onResult(normalized); } catch (e) { setErr(e instanceof Error ? e.message : String(e)); } finally { setLoading(false); } } const steps = pred?.steps ?? []; return (
예측 (Chronos + LightGBM 앙상블)
결과는 차트에 옅은 색 캔들로 이어 붙고, 꼬리 길이가 신뢰구간 폭입니다.
예측 기간: {PREDICT_PRESETS.map((p, i) => ( ))} {presetIdx === 2 && ( ※ 30거래일 너머는 모델 학습 범위 밖 — 참고용 )}
{err &&
에러: {err}
} {pred?.found ? (
기준일 {pred.base_date} · 기준종가{" "} {pred.base_close != null ? pred.base_close.toLocaleString() : "-"}
{steps.map((s) => ( ))}
+거래일 매칭일 방향 P(up/flat/down) 기대수익 예측 종가 q10~q90
+{s.horizon} {s.target_date} {s.direction} {fmtPct(s.prob_up)} / {fmtPct(s.prob_flat)} / {fmtPct(s.prob_down)} {fmtSignedPct(s.expected_return)} {s.point_close != null ? s.point_close.toLocaleString() : "-"} {s.ci_low != null ? s.ci_low.toLocaleString() : "-"} ~{" "} {s.ci_high != null ? s.ci_high.toLocaleString() : "-"}
) : (
아직 예측이 없습니다. 예상차트 보기 를 누르면 선택한 기간의 예측 캔들이 현재 차트에 옅은 색으로 이어 붙습니다.
)}
); } function fmtPct(v: number | null): string { if (v == null) return "-"; return `${(v * 100).toFixed(0)}%`; } function fmtSignedPct(v: number | null): string { if (v == null) return "-"; const pct = v * 100; const sign = pct >= 0 ? "+" : ""; return `${sign}${pct.toFixed(2)}%`; }