- 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).
토큰 발급 1분/1일 제한 (EGW00133 등 403) 회피:
- _load_disk_cache / _save_disk_cache 로 /app/.cache/kis_token.json 영속화
· ./backend:/app 바인드 마운트로 호스트 backend/.cache/ 에 저장
· backend/.cache/ 는 .gitignore 에 이미 포함됨 (secret 비커밋)
· app_key prefix 캐시 무효화 키 (.env 갱신 시 자동 폐기)
· atomic write (tmp → rename) + 0600 권한
- get_token: 메모리 → 디스크 → 신규 발급 순으로 fallback
- 컨테이너 재기동해도 24시간 유효 토큰 재사용 → 발급 폭주 방지
restart.bat:
- restart → up -d --force-recreate --no-deps backend web
· restart 는 env_file 재로드 안 함 (.env 의 KIS_APP_KEY 변경이 무시됨)
· up -d 는 새 인스턴스 생성하며 env_file 다시 읽음
· --no-deps 로 db 는 절대 건드리지 않음 (postgres_data 영속)
리뷰어 지적: docker-compose 가 NEXT_PUBLIC_API_BASE=http://localhost:8000 을
주입해서 클라이언트 번들에 localhost 가 inline 됨. 사금향 게임컴 (192.168.10.13)
브라우저에선 동작하지만, 같은 내부망의 다른 PC 또는 외부 검증자가
http://192.168.10.13:3000 에 접속하면 fetch 가 그 PC 의 localhost:8000 으로 가서
연결 실패. 백엔드는 정상인데도 '검색 결과 없음' 으로 보임.
해석 우선순위:
1) NEXT_PUBLIC_API_BASE 가 비 localhost 값 → 그대로 (프로덕션 도메인 케이스)
2) 브라우저 → window.location.hostname:8000 (LAN/localhost 자동 대응)
3) SSR 폴백 → localhost:8000
localhost/127.0.0.1 판별은 //(localhost|127.0.0.1)(?::|$) 정규식 — 'localhost.evil.com'
같은 서브도메인 우회는 매치 안 됨. node 로 7케이스 검증 완료.
web/app 코드만이라 사금향 PC 는 restart.bat 으로 반영 (next dev hot-reload).
이전: MODEL_DEVICE: cuda (하드코딩) → .env 에 MODEL_DEVICE=cpu 두어도
compose environment 가 env_file 보다 우선이라 무시됨.
수정: MODEL_DEVICE: ${MODEL_DEVICE:-cuda} — 셸 env 또는 .env 의 값이
있으면 그 값, 없으면 기본 cuda.
용도: FinBERT/Chronos 가 GPU 에서 'no kernel image is available for
execution on the device' (PyTorch cu121 / RTX 3070 Ti 호환 이슈) 가 날 때
.env 에 MODEL_DEVICE=cpu 두고 backend 만 recreate 하면 CPU 폴백.
echo MODEL_DEVICE=cpu>> .env
docker compose -f docker-compose.yml -f docker-compose.gpu.yml up -d backend
증상 — 사금향 게임컴 (192.168.10.13) 백엔드 진단으로 확정:
POST /api/refresh/005930 → pykrx_ohlcv: failed
error: 'No module named pkg_resources'
root cause: setuptools 80.0.0 (2025-04) 이 pkg_resources 모듈을 제거함.
Dockerfile 의 'pip install --upgrade ... setuptools ...' 가 최신 (80+) 을
설치 → pykrx 의 'import pkg_resources' 가 import 실패.
해결:
1) bootstrap 단계에서 'pip install setuptools<80' 명시 핀
2) reqs.txt 설치 후에도 동일 핀 반복 (transitive upgrade 차단)
3) 빌드 단계에서 'import pkg_resources' 확인 — 깨지면 빌드 실패시켜
런타임에서 발견되는 일 없도록 함
이 패치 후 KIS (이미 ok 였음) 와 함께 pykrx ohlcv/trading_value 가 정상
동작 → chart 자동 충전 (fa817b3) 이 의도대로 데이터 채워 넣음.
별개 이슈 (이 커밋 범위 밖): FinBERT/Chronos 가 RTX 3070 Ti 에서
'no kernel image is available for execution on the device' 발생.
임시 회피: .env 에 MODEL_DEVICE=cpu 추가.
문제: symbols 시드 후 첫 방문 시 ohlcv_daily 가 비어있어서
- 차트가 빈 캔버스로 렌더링됨
- 예상차트 보기 → 409 'no ohlcv_daily for {code}; refresh first'
daily_batch (16:00 KST) 또는 수동 POST /api/refresh/{code} 만이 OHLCV 를
채우는 구조였는데, 첫 방문 사용자는 둘 다 트리거 안 됨.
수정: GET /api/chart/{code} 가 ohlcv 0행 + symbol 존재 케이스에서
refresh_code 를 동기 호출 (pykrx OHLCV + trading_value + news + finbert)
후 재조회. idempotent 라 다음 방문부터는 캐시 히트.
lookback_days = max(days, 365) — 차트 요청 범위 + Chronos/LightGBM
학습용 과거 시계열 마진. 첫 호출 ~10-30s, 이후는 즉시.
KRX 가 도커망에서 막힌 환경이면 refresh 가 실패 status 로 끝나고
ohlcv 는 여전히 비어있음 — 기존 동작과 동일 (graceful).
리뷰어 지적: docker compose restart 는 서비스명 생략시 db 까지 같이 재시작한다.
db 까지 같이 내려가면 backend lifespan 부팅 시드가 db healthcheck 와 무관하게
실행돼서 race 로 seed 가 다시 실패할 수 있다 (restart 는 depends_on.condition:
service_healthy 를 지키지 않음). db 는 postgres_data 볼륨에 상태가 영속이라
재시작할 이유도 없음.
추가: 실행중 컨테이너 체크도 backend/web 기준으로 좁힘 — db 만 떠있고
backend 가 down 인 경우 통과되던 버그 수정.
build.bat 은 이미지 재빌드 포함 — Dockerfile / pyproject.toml / package*.json /
compose 설정 같은 의존성/이미지 구성이 바뀌었을 때 사용.
restart.bat 은 docker compose restart 만 호출 — backend/app/, web/app/ 안의
코드만 바뀐 경우 사용. 바인드 마운트 (./backend:/app, ./web:/app) 덕에 새 코드가
즉시 컨테이너에서 보이고, 재시작으로 lifespan 부팅 시드도 재실행된다.
사용 가이드:
git pull 후 변경 파일이
- pyproject.toml / Dockerfile / package*.json → build.bat
- app/ 코드만 → restart.bat
문제: 빌드/기동은 성공했는데 종목 검색 결과 빈 화면. 원인은 시드 트랜잭션
구조 — `_fetch_market_listing(KRX)` 가 `with engine.begin()` 블록 *밖*에서
호출되고, 컨테이너에서 KRX 서버 접근이 실패하거나 단일 ticker 처리 중
한 곳이라도 예외가 나면 전체 트랜잭션이 롤백되어 SEED_TICKERS 10개조차
들어가지 않음.
수정:
- `_upsert_seed_tickers()` 분리. SEED 10종목 전용 트랜잭션. 네트워크 무관.
- `seed_symbols()` 가 (1) SEED 먼저 → (2) KRX 시장별 fetch (try-per-market)
→ (3) 전 종목 bulk upsert (별도 트랜잭션) 순서로 동작. KRX fetch 실패해도
SEED 는 살아남음.
- `main.py` lifespan bootstrap 이 SEED upsert 를 매 부팅마다 무조건 실행
(10회, ms 단위, idempotent). count <= 10 일 때만 KRX 전 종목 fetch.
- 새 admin 엔드포인트 `POST /api/refresh/seed/symbols` — 컨테이너 재기동
없이 시드 재시도 가능.
이번 사례 (사금향 PC) 의 복구 절차:
curl -X POST http://localhost:8000/api/refresh/seed/symbols
이후 검색에 SEED 10 종목은 즉시 떠야 함. KRX 가 막힌 환경이면 markets={0,0}
가 응답에 찍히고, 그래도 SEED 만으로 핵심 동작은 가능.
Ubuntu 22.04 의 python3-pip 패키지는 python3.10 용이라, /usr/local/bin/python
만 3.11 로 심볼릭 링크해도 `pip`/`pip3` 명령은 여전히 3.10 의 site-packages
에 의존성을 설치한다. pyproject.toml 이 >=3.11 을 선언했으므로 런타임
인터프리터 불일치 위험.
수정:
- apt 에서 python3-pip 제거. get-pip.py 로 python3.11 전용 pip 를 부트스트랩.
- 모든 pip 호출을 `python -m pip` 로 통일.
- 직후 `RUN python -V && python -m pip -V` sanity check 추가 — 빌드 로그에
인터프리터 버전이 박혀서 추후 불일치 의심 시 즉시 확인 가능.
- CMD 도 `python -m uvicorn` 으로 변경 — 콘솔 스크립트 shebang 이 아니라
명시적으로 3.11 인터프리터에서 실행됨을 보장.
검증 (별도 컨테이너에서 nvidia/cuda:12.1.1 베이스로 동일 레이어 실행):
Python 3.11.0rc1
pip 26.1.1 from /usr/local/lib/python3.11/dist-packages/pip (python 3.11)
기존 `pip install -e .` 두 줄은 두 가지 이유로 실패:
1. `app/` 가 COPY 되기 전 단계라 setuptools packages.find 결과가
비어 editable 빌드가 무의미.
2. Ubuntu 22.04 의 기본 pip 가 PEP 660 build_editable hook 을
요구하지만, build isolation 환경의 setuptools 가 노출 안 함
("build backend is missing the 'build_editable' hook").
수정:
- editable install 자체를 제거. 런타임은 PYTHONPATH=/app 으로
`app.*` import 가 동작하므로 프로젝트를 wheel 로 설치할 필요 없음.
- 의존성은 tomllib 로 pyproject.toml 에서 직접 추출해 단일 `pip
install -r` 로 설치. ML 휠 (chronos/lgbm/transformers ...) 레이어
캐싱이 pyproject.toml 단위로 유지됨 → app 코드 변경시 재빌드 회피.
- COPY app ./app 은 deps 설치 뒤로 유지 (캐시 분리).
`web/package.json` 의 next/eslint-config-next ^14.2.33 상향 (6c79230) 에
맞춰 lockfile 갱신. npm 이 resolve 한 실제 버전은 14.2.35 (14.2.x 최신
patched). 자동 생성되는 next-env.d.ts 의 문서 URL 한 줄도 함께 동기화.
검증:
- `npm install --package-lock-only` 통과
- `npm run typecheck` 통과
- `npm run build` 통과 (Next.js 14.2.35, 4 페이지 정상 생성)
- ensemble.predict() 가 chronos_raw / lgbm_raw 를 함께 반환
- predict_and_store() 가 매 호출마다 3종 행 적재:
model='ensemble' (user_triggered=인자)
model='chronos' (user_triggered=FALSE, shadow)
model='lgbm' (user_triggered=FALSE, shadow)
- retrain_weekly.adjust_weights(): 최근 30일 prediction_outcomes 의
chronos vs lgbm hit_rate 로 ensemble_weights upsert
w_chronos = clamp(0.1, hr_c/(hr_c+hr_l), 0.9), w_lgbm = 1 - w_chronos
모델별 표본 < 10 이면 기본값(0.6/0.4) 유지
- API 응답에 saved_shadow_ids 추가 (TS 타입도 동기화)
- README: 동작 모델 메모 섹션을 실제 구현과 일치하도록 갱신
리뷰어 지적 3번 (ensemble_weights 가 영원히 갱신 안됨, upsert_weights 미호출) 해결.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- match_for_date(d) → match_up_to(today) 로 시맨틱 변경: target_date == d
대신 target_date <= today AND outcomes 미존재 전부 후보로
- 각 후보마다 ohlcv_daily 에서 target_date 이상 today 이하 범위의 최초
거래일 행을 actual_close 로 매칭 → 주말/공휴일 자동 이월
- user_triggered 필터 제거: chronos/lgbm shadow 행도 함께 매칭됨
- prediction_outcomes.target_date 에는 실제 매칭된 거래일을 기록
- 하위 호환: match_for_date(d) 는 match_up_to(d) 별칭으로 유지
리뷰어 지적 2번 (공휴일/주말이면 target_date 일치 행이 영원히 미매칭) 해결.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- main.py 의 lifespan 시작 시 idempotent migration 적용 + symbols 비어있으면 pykrx 로 전 종목 시드
- BOOTSTRAP_DISABLED=1 / SCHEDULER_DISABLED=1 env 로 비활성 가능 (테스트 용)
- 실패해도 서버는 뜨고 /health/db 가 진단 제공
리뷰어 지적 1번 (cold-start 시 /api/refresh 404) 해결.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- GET /api/symbols/search?q=...&seed_only= : trigram + prefix + ILIKE 합산 정렬
- GET /api/symbols/{code} : 메타
- GET /api/chart/{code}?days=N&include_* : OHLCV + 일별 감성 + 외인기관거래대금
- POST /api/predict/{code}?horizons=1,3,5 : on-demand 앙상블 예측 + DB 적재
(user_triggered=TRUE)
- GET /api/predict/{code}/latest : 최신 base_date 의 예측 묶음 + base_close
(UI 가 차트 마지막 점에 이어 붙임)
- GET /api/metrics/{code}?window_days=N : 종목 단위 hit_rate / mae (model, horizon 별)
- GET /api/metrics?window_days=N : 전체 누적
- GET /api/news/{code}?source=&limit= : 최신순 뉴스/공시 목록 (감성 점수 포함)
main.py 에 6개 라우터 모두 include.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- backend/app/models/lgbm.py: 종목 × horizon 별 LightGBM 회귀(y_ret_h)
+ 다중분류(y_dir_h, 3-class). joblib 으로 backend/data/models/{code}_h{H}_*.pkl
저장. early_stopping(30). predict_one() 으로 최신 영업일 피처에 추론.
- backend/app/models/weights.py: ensemble_weights 테이블 IO,
default w_chronos=0.6 / w_lgbm=0.4 (DB 행 없으면 fallback).
- backend/app/models/ensemble.py: Chronos sample 분포 + LGBM regression+cls
결합. point/q10/q90 + prob_up/flat/down + direction 라벨. 한쪽 모델
실패 시 다른 쪽 단독 fallback (cold start: chronos 단독).
- backend/app/pipelines/predict_one.py: predict_and_store(). 결과를
predictions 테이블에 UPSERT, user_triggered 누적 OR. base_date = 마지막
ohlcv 거래일, target_date = base_date + H 영업일(주말 스킵, 공휴일은
매칭잡에서 자연 보정).
- backend/app/pipelines/match_outcomes.py: target_date == d 인
user_triggered=TRUE 예측을 d 의 실제 종가와 매칭 → prediction_outcomes
적재. direction_hit(±0.3% flat band) + abs_error. 실제 종가 없으면
자연 skip.
- backend/app/pipelines/retrain_weekly.py: 시드 10종목 × H 재학습 +
최근 30일 model_performance 적재.
- backend/app/db/migrations/003_ensemble_weights.sql: (code, horizon) →
(w_chronos, w_lgbm, hit_rate_*, sample_count).
- backend/app/pipelines/scheduler.py:
daily_batch : 평일 16:00 KST
match_outcomes : 평일 16:30 KST ← 사용자가 확정한 매칭 시점
retrain_weekly : 일요일 02:00 KST
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- backend/app/nlp/finbert.py: snunlp/KR-FinBert-SC 어댑터.
- score = P(pos) - P(neg) ∈ [-1, +1], label = argmax (neg/neu/pos)
- 768d mean-pooled last hidden state → news.embedding (VECTOR) 저장
- settings.huggingface_token 인증, lazy singleton, cuda/cpu auto
- backend/app/nlp/score_news.py: news 테이블에서 sentiment_score IS NULL
행을 배치 스코어 → UPDATE (... embedding=(:e)::vector). 종목 필터 + limit 옵션.
- backend/app/db/migrations/002_sentiment_view.sql: v_sentiment_daily 뷰.
종목·KST 일별 n_articles, mean_score, pos/neg/neu_ratio, weighted_score
(naver_finance 1.0 / google_rss 0.7 / dart 0.5).
- backend/app/db/migrate.py: 이미 실행 중인 DB 에 새 SQL 마이그레이션 적용용
CLI. 모든 SQL 파일은 idempotent.
- refresh_one.py: refresh 끝에 종목당 200건까지 finbert 스코어, finbert
SourceStatus 를 RefreshReport 에 추가.
- daily_batch.py: 모든 종목 처리 후 score_pending_news(limit=2000) 로 mop-up.
모델 캐시는 docker-compose hf_cache 볼륨(/root/.cache/huggingface).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
사용자 확정: 예측 차트와 실제 차트 매칭은 다음 거래일 장이 끝나는 시점으로.
- README.md: 매칭 배치 시각을 KRX 정규장 마감(15:30) 후 종가 확정 시점
(16:00 ~ 16:30 KST 사이) 으로 명시. 주말/공휴일은 다음 거래일로 이월.
- scheduler.py docstring: Phase 4 슬롯을 "16:30 KST 평일 prediction_outcomes
매칭 배치" 로 구체화. 추론은 on-demand 만 사용한다는 점도 명시.
코드 동작 변화 없음(스케줄러는 아직 daily_batch 1개 잡만 등록).
Phase 4 진입 시 이 정책대로 매칭 잡을 추가.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
10종목 시드 + pykrx OHLCV / 외인·기관 거래대금, KIS read-only EOD, OpenDART
공시, 네이버 금융 뉴스 스크레이퍼, 구글 뉴스 RSS, yfinance 거시(KOSPI/KOSDAQ/
USDKRW/US10Y) fetcher 를 추가하고 refresh_one / daily_batch / backfill /
APScheduler(16:00 KST) 파이프라인으로 묶음.
- backend/app/seed: 10종목 시드 (대형/고변동/테마/플랫폼/방어)
- backend/app/fetch: pykrx, kis, dart, news, macro, symbols_seed
- backend/app/pipelines: refresh_one, daily_batch, backfill(CLI), scheduler
- backend/app/api/refresh.py: POST /api/refresh/{code}?lookback_days=N
- backend/app/main.py: lifespan 으로 스케줄러 기동/종료, /health/keys 추가
- README: .env 복사 안내 보강
스모크 테스트 (실제 키 사용) 결과:
KIS token : ok (token 346자 발급)
KIS daily : 005930 11rows
DART list : 005930 30일 10건
Naver news : 005930 12건
Google RSS : "삼성전자" 92건
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>