"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, }; } const HORIZON_PRESETS: { label: string; value: number[] }[] = [ { label: "단기 (1·3·5)", value: [1, 3, 5] }, { label: "중기 (1·5·10)", value: [1, 5, 10] }, { label: "장기 (5·10·20)", value: [5, 10, 20] }, ]; 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(0); const [customRaw, setCustomRaw] = useState(""); const [useCustom, setUseCustom] = useState(false); function effectiveHorizons(): number[] { if (useCustom) { const parsed = customRaw .split(",") .map((s) => Number(s.trim())) .filter((n) => Number.isFinite(n) && n >= 1 && n <= 60); if (parsed.length > 0) return Array.from(new Set(parsed)).sort((a, b) => a - b); } return HORIZON_PRESETS[presetIdx].value; } async function runPredict() { setLoading(true); setErr(null); try { const horizons = effectiveHorizons(); 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 앙상블)
클릭한 종목은 자동 저장 후 다음 거래일 장 종료 시 실제 가격과 비교됩니다.
예측 거래일: {HORIZON_PRESETS.map((p, i) => ( ))}
{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() : "-"}
) : (
아직 예측이 없습니다. 예상차트 보기 버튼을 누르면 1·3·5거래일 후 예측을 생성하고 차트에 점선으로 이어 붙입니다.
)}
); } 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)}%`; }