diff --git a/backend/app/main.py b/backend/app/main.py index 2bc1690..b07f1b8 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -132,3 +132,30 @@ def health_keys() -> dict[str, object]: "dart": dart_mod.ping(), # huggingface 는 모델 다운로드 시점에 확인 (별도 ping 호출 비용 회피) } + + +@app.get("/health/models") +def health_models() -> dict[str, object]: + """Chronos / LGBM 가용성 진단. + + Chronos: lazy 로드 첫 호출이라 30초~수 분 걸릴 수 있음 (HuggingFace 다운로드). + LGBM: 체크포인트 디렉토리 스캔 — retrain 안 돈 cold start 에선 비어있음. + """ + from pathlib import Path + + from app.models import chronos as chronos_mod + + lgbm_dir = Path(os.environ.get("LGBM_MODEL_DIR", "/app/data/models")) + lgbm_files: list[str] = [] + if lgbm_dir.exists(): + lgbm_files = sorted(p.name for p in lgbm_dir.glob("*.pkl")) + + return { + "chronos": chronos_mod.ping(), + "lgbm": { + "model_dir": str(lgbm_dir), + "checkpoint_count": len(lgbm_files), + "samples": lgbm_files[:8], # 너무 많으면 잘라서. + "status": "ok" if lgbm_files else "no_checkpoints (cold start, run retrain_weekly)", + }, + } diff --git a/backend/app/models/ensemble.py b/backend/app/models/ensemble.py index b3a6b23..25f4e03 100644 --- a/backend/app/models/ensemble.py +++ b/backend/app/models/ensemble.py @@ -87,30 +87,41 @@ def predict(code: str, *, horizons: tuple[int, ...] = (1, 3, 5)) -> EnsemblePred sources_used: list[str] = [] cf: ChronosForecast | None = None + chronos_err: str | None = None try: cf = chronos_forecast(closes, horizon=max_h, num_samples=30) sources_used.append("chronos") except Exception as exc: # noqa: BLE001 - logger.warning("chronos forecast failed for %s: %s", code, exc) + chronos_err = f"{type(exc).__name__}: {exc}" + logger.warning("chronos forecast failed for %s: %s", code, chronos_err) steps: list[EnsembleStep] = [] lgbm_raw: dict[int, LgbmForecast] = {} for h in horizons: lf: LgbmForecast | None = None + lgbm_err: str | None = None try: lf = lgbm_predict(code, h) if lf is not None: sources_used.append(f"lgbm_h{h}") lgbm_raw[h] = lf + else: + # predict_one 이 None 반환 = 체크포인트 파일 없음 (cold start). + lgbm_err = "model checkpoint not found (run retrain_weekly)" except Exception as exc: # noqa: BLE001 - logger.warning("lgbm predict failed for %s h=%d: %s", code, h, exc) + lgbm_err = f"{type(exc).__name__}: {exc}" + logger.warning("lgbm predict failed for %s h=%d: %s", code, h, lgbm_err) # 가중치 (DB 없으면 default 0.6/0.4). w = load_weights(code, h) wc, wl = w.w_chronos, w.w_lgbm # 한쪽이 없으면 다른 쪽 전부. if cf is None and lf is None: - raise RuntimeError(f"both chronos & lgbm failed for {code} h={h}") + # 사용자가 브라우저에서 바로 원인을 보게 두 에러를 그대로 노출. + raise RuntimeError( + f"both chronos & lgbm failed for {code} h={h}; " + f"chronos={chronos_err or 'unknown'}; lgbm={lgbm_err or 'unknown'}" + ) if cf is None: wc, wl = 0.0, 1.0 if lf is None: diff --git a/restart-ci.bat b/restart-ci.bat new file mode 100644 index 0000000..f58aa95 --- /dev/null +++ b/restart-ci.bat @@ -0,0 +1,57 @@ +@echo off +REM stock_chart_site - SSH/CI 친화 재시작 스크립트 +REM +REM restart.bat 과의 차이: pause 가 없음. SSH 비대화형 (예: ssh user@host "restart-ci.bat") +REM 에서 멈추지 않고 끝까지 실행. 에러는 종료 코드로만 알린다. +REM +REM 일반 사용 시엔 restart.bat 을 쓰는게 출력 검토에 편하다. + +setlocal enabledelayedexpansion +cd /d "%~dp0" + +echo === stock_chart_site restart-ci === + +where docker >nul 2>&1 +if errorlevel 1 ( + echo [ERROR] docker not found + exit /b 1 +) +docker info >nul 2>&1 +if errorlevel 1 ( + echo [ERROR] Docker Desktop not running + exit /b 1 +) + +set USE_GPU=0 +where nvidia-smi >nul 2>&1 +if not errorlevel 1 ( + nvidia-smi >nul 2>&1 + if not errorlevel 1 set USE_GPU=1 +) + +if "%USE_GPU%"=="1" ( + echo [GPU] using GPU profile + set COMPOSE_FILES=-f docker-compose.yml -f docker-compose.gpu.yml +) else ( + echo [CPU] using CPU profile + set COMPOSE_FILES=-f docker-compose.yml +) + +for /f %%i in ('docker compose %COMPOSE_FILES% ps --status running --quiet backend web 2^>nul ^| find /v /c ""') do set RUN_COUNT=%%i +if "%RUN_COUNT%"=="0" ( + echo [ERROR] backend/web not running. run build.bat first. + exit /b 1 +) + +echo === docker compose up -d --force-recreate --no-deps backend web === +docker compose %COMPOSE_FILES% up -d --force-recreate --no-deps backend web +if errorlevel 1 ( + echo [ERROR] restart failed + exit /b 1 +) + +echo === status === +docker compose %COMPOSE_FILES% ps + +endlocal +exit /b 0