Stock Chart Site
++ Phase 0 scaffold. 종목 검색 UI는 Phase 6에서 추가됩니다. +
+GET {process.env.NEXT_PUBLIC_API_BASE}/health
+ diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..5517d07 --- /dev/null +++ b/.env.example @@ -0,0 +1,39 @@ +# Copy to .env and fill in. .env is gitignored. + +# --- Database --- +POSTGRES_USER=stock +POSTGRES_PASSWORD=stockpw +POSTGRES_DB=stock +POSTGRES_HOST=db +POSTGRES_PORT=5432 +DATABASE_URL=postgresql+psycopg://stock:stockpw@db:5432/stock + +# --- Timezone --- +TZ=Asia/Seoul + +# --- API keys (모두 무료 발급. 없어도 pykrx만으로 일단 동작) --- +# 한국투자증권 OpenAPI (실시간 시세, EOD 등) +# 발급: https://apiportal.koreainvestment.com (계좌 + Open API 서비스 신청) +KIS_APP_KEY= +KIS_APP_SECRET= +KIS_ACCOUNT_NO= + +# DART OpenAPI (전자공시 본문) +# 발급: https://opendart.fss.or.kr (회원가입 후 인증키 신청) +DART_API_KEY= + +# HuggingFace (선택. 없어도 공개 모델 다운로드 가능. 토큰 있으면 rate limit 완화) +# 발급: https://huggingface.co/settings/tokens +HUGGINGFACE_TOKEN= + +# --- 모델 디바이스 --- +# auto | cuda | cpu +MODEL_DEVICE=auto + +# --- Backend --- +BACKEND_PORT=8000 +LOG_LEVEL=INFO + +# --- Web --- +WEB_PORT=3000 +NEXT_PUBLIC_API_BASE=http://localhost:8000 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..edf7bb5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,40 @@ +# Env / secrets +.env +.env.local +.env.*.local + +# Python +__pycache__/ +*.py[cod] +*.pyo +.Python +.venv/ +venv/ +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ +*.egg-info/ +dist/ +build/ + +# Models / artifacts (downloaded HF caches, trained LGBM) +backend/artifacts/ +backend/.cache/ +.huggingface/ + +# Node +node_modules/ +.next/ +out/ +*.tsbuildinfo + +# OS +.DS_Store +Thumbs.db + +# Docker volumes mounted locally +postgres_data/ + +# Logs +*.log +logs/ diff --git a/README.md b/README.md index 3aeacc2..770d12d 100644 --- a/README.md +++ b/README.md @@ -1 +1,147 @@ # stock_chart_site + +개인용 주식 차트 + 단기 예측 사이트. 한국 종목 검색 → 현재 차트 표시 → "예상차트 보기" 클릭 시 Chronos+LightGBM 앙상블로 향후 1~5거래일 예측을 차트에 이어 붙임. 사용자가 한 번이라도 예측을 확인한 종목은 자동 저장해서 다음날 실제 가격과 비교 → 오차/방향성 적중률을 누적 → 앙상블 가중치를 자동 보정. + +스펙 원문: `/home/claude/EJClaw/groups/stock_predictor/SPEC.md` (별도 채팅 그룹). + +## 빠른 시작 (Windows + Docker Desktop + RTX 3070 Ti) + +전제: Docker Desktop이 이미 설치되어 있고, GPU 사용하려면 `Settings → Resources → WSL Integration → GPU support`가 켜져 있어야 합니다. + +```cmd +git clone https://git.tkrmagid.kr/tkrmagid/stock_chart_site.git +cd stock_chart_site +build.bat +``` + +`build.bat`이 자동으로 +1. `.env.example` → `.env` 복사 (없는 경우) +2. `nvidia-smi` 로 GPU 감지 → GPU 있으면 GPU 모드, 없으면 CPU 폴백 +3. `docker compose build` + `up -d` + +확인: +- Web: http://localhost:3000 +- Backend health: http://localhost:8000/health +- DB extensions: http://localhost:8000/health/db (`timescaledb`, `vector`, `pg_trgm` 셋 다 켜져 있어야 정상) + +정지: +```cmd +docker compose down +``` + +## 빌드 모드 (수동) + +```bash +# GPU 모드 (RTX 3070 Ti 등 NVIDIA GPU 사용) +docker compose -f docker-compose.yml -f docker-compose.gpu.yml up -d --build + +# CPU 모드 +docker compose up -d --build +``` + +## API 키 발급 (모두 무료) + +키 없어도 pykrx 기반 일봉/뉴스 RSS 만으로 일단 동작합니다. 다음 키를 받으면 데이터 품질이 좋아집니다. + +### 1) 한국투자증권 KIS OpenAPI (실시간 시세 + EOD) + +1. https://apiportal.koreainvestment.com 회원가입 (한국투자증권 계좌 필요) +2. 좌측 "Open API 신청" → 모의/실전 중 하나 신청 +3. 발급 완료 후 마이페이지에서 App Key, App Secret, 계좌번호 확인 +4. `.env` 에 입력: + ``` + KIS_APP_KEY=... + KIS_APP_SECRET=... + KIS_ACCOUNT_NO=... + ``` + +### 2) DART OpenAPI (전자공시 본문) + +1. https://opendart.fss.or.kr 회원가입 +2. 마이페이지 → 인증키 신청 → 즉시 발급 +3. `.env` 에 입력: + ``` + DART_API_KEY=... + ``` + +### 3) HuggingFace (선택, 모델 다운로드 가속) + +토큰 없어도 공개 모델 (`amazon/chronos-bolt-base`, `snunlp/KR-FinBert`) 다운로드가 됩니다. 토큰이 있으면 rate limit이 완화되고 첫 다운로드가 빨라집니다. + +1. https://huggingface.co 회원가입 +2. https://huggingface.co/settings/tokens 에서 Read 토큰 생성 +3. `.env` 에 입력: + ``` + HUGGINGFACE_TOKEN=hf_... + ``` + +## 학습/배치 대상 시드 종목 (10개) + +검색은 KRX 전 종목을 대상으로 동작하지만, 일별 배치/재학습/메트릭 누적은 아래 10개를 우선 대상으로 합니다. 운영하면서 더 의미있는 종목이 보이면 교체합니다. + +| 분류 | 종목 | 코드 | +|---|---|---| +| 대형 인기주 | 삼성전자 | 005930 | +| 대형 인기주 | SK하이닉스 | 000660 | +| 변동성 큰 종목 | 에코프로비엠 | 247540 | +| 변동성 큰 종목 | 한미반도체 | 042700 | +| 최근 인기 테마 | 두산에너빌리티 | 034020 | +| 최근 인기 테마 | 한화에어로스페이스 | 012450 | +| 최근 인기 테마 | HD현대중공업 | 329180 | +| 전통 IT/플랫폼 | NAVER | 035420 | +| 방어주/저변동 | KT&G | 033780 | +| 방어주/저변동 | 한국가스공사 | 036460 | + +## 디렉토리 구조 + +``` +stock_chart_site/ +├── build.bat # Windows: 빌드+기동 +├── docker-compose.yml # db + backend + web +├── docker-compose.gpu.yml # GPU 오버레이 (NVIDIA reservation) +├── .env.example # 환경 변수 템플릿 +├── backend/ +│ ├── Dockerfile # CUDA 12.1 + Python 3.11 +│ ├── pyproject.toml +│ └── app/ +│ ├── main.py # FastAPI entry +│ ├── config.py # env settings +│ ├── db/ +│ │ ├── connection.py +│ │ └── migrations/ +│ │ └── 001_init.sql # DB 스키마 +│ ├── fetch/ # KIS / pykrx / DART / 뉴스 (Phase 1) +│ ├── models/ # Chronos / LightGBM / KR-FinBERT (Phase 2~4) +│ ├── pipelines/ # daily_batch / inference / retrain (Phase 1, 4) +│ └── api/ # FastAPI 라우터 (Phase 5) +└── web/ + ├── Dockerfile + ├── package.json + └── app/ + ├── layout.tsx + └── page.tsx # 검색 + 차트 UI (Phase 6) +``` + +## 진행 계획 + +- Phase 0 — 스캐폴드 (현재): Docker 환경 + DB 스키마 + 빈 FastAPI/Next.js + build.bat +- Phase 1a — pykrx 데이터 파이프: 일봉/외인기관/지수 + DART + 뉴스 RSS + 거시 +- Phase 1b — KIS EOD (키 받으면) +- Phase 2 — KR-FinBERT 감성 점수 + 일별 집계 +- Phase 3 — Chronos zero-shot 예측 적재 +- Phase 4 — LightGBM walk-forward + `prediction_outcomes` 누적 시작 +- Phase 5 — FastAPI 엔드포인트 (검색, 차트, on-demand 예측, 메트릭) +- Phase 6 — Next.js UI (검색 + 현재 차트 + 예상차트 토글) +- Phase 7 (옵션) — 백테스트 페이지 + 주간 자동 재학습 + +## 동작 모델 메모 + +- 예측 트리거: 사용자가 "예상차트 보기" 누른 종목에 대해 즉시 inference. 결과는 `predictions(user_triggered=TRUE)` 로 저장. +- 다음날 00:30 (또는 16:30) 배치: `user_triggered=TRUE` 인 예측 중 `target_date`가 도래한 것들에 대해 실제 가격과 매칭 → `prediction_outcomes` 적재. +- 주간 02:00: 종목/모델별 최근 30일 hit rate 기반으로 앙상블 가중치를 자동 보정. hit rate가 임계 미만이면 LGBM 재학습. + +## 안전/한계 + +- 본인 1인 개인용. 외부 공개/상업 사용 안 함. +- 자동매매 연결 없음. 예측은 참고용. +- 백테스트 정확도와 라이브 정확도는 다르며 단기 방향성 모델의 라이브 상한은 보통 55~60%. diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..4f41ef1 --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,9 @@ +__pycache__ +*.pyc +.venv +.pytest_cache +.mypy_cache +.ruff_cache +tests +artifacts +.cache diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..06cf30c --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,33 @@ +# CUDA 12.1 + Python 3.11. CPU 환경에서는 torch.cuda.is_available()==False 가 되어 자동 폴백. +# Windows + Docker Desktop + WSL2 GPU 패스스루로 RTX 3070 Ti 에서 동작. +FROM nvidia/cuda:12.1.1-runtime-ubuntu22.04 AS base + +ENV DEBIAN_FRONTEND=noninteractive \ + PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + PIP_NO_CACHE_DIR=1 \ + PIP_DISABLE_PIP_VERSION_CHECK=1 \ + TZ=Asia/Seoul + +RUN apt-get update && apt-get install -y --no-install-recommends \ + python3.11 python3.11-venv python3-pip \ + build-essential git curl ca-certificates tzdata \ + libgomp1 \ + && ln -sf /usr/bin/python3.11 /usr/local/bin/python \ + && ln -sf /usr/bin/python3.11 /usr/local/bin/python3 \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +COPY pyproject.toml ./ +# Install PyTorch (CUDA 12.1 wheels) first so the rest of deps don't downgrade it. +RUN pip install --extra-index-url https://download.pytorch.org/whl/cu121 \ + torch==2.3.1 torchvision==0.18.1 +RUN pip install --no-deps -e . || true +RUN pip install -e . + +COPY app ./app + +EXPOSE 8000 + +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] diff --git a/backend/app/__init__.py b/backend/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/api/__init__.py b/backend/app/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/config.py b/backend/app/config.py new file mode 100644 index 0000000..b4db4ca --- /dev/null +++ b/backend/app/config.py @@ -0,0 +1,24 @@ +from __future__ import annotations + +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class Settings(BaseSettings): + model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="ignore") + + database_url: str = "postgresql+psycopg://stock:stockpw@db:5432/stock" + tz: str = "Asia/Seoul" + log_level: str = "INFO" + + # 모델 디바이스 선택. 'auto'는 torch.cuda.is_available() 기반 + model_device: str = "auto" + + # External keys (옵션) + kis_app_key: str | None = None + kis_app_secret: str | None = None + kis_account_no: str | None = None + dart_api_key: str | None = None + huggingface_token: str | None = None + + +settings = Settings() diff --git a/backend/app/db/__init__.py b/backend/app/db/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/db/connection.py b/backend/app/db/connection.py new file mode 100644 index 0000000..d10bcd9 --- /dev/null +++ b/backend/app/db/connection.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +from sqlalchemy import create_engine, text +from sqlalchemy.engine import Engine + +from app.config import settings + + +_engine: Engine | None = None + + +def get_engine() -> Engine: + global _engine + if _engine is None: + _engine = create_engine(settings.database_url, pool_pre_ping=True, future=True) + return _engine + + +def ping() -> dict[str, object]: + """Smoke-test: DB 연결 + 확장 확인.""" + eng = get_engine() + with eng.connect() as conn: + version = conn.execute(text("SELECT version()")).scalar() + exts = conn.execute( + text( + "SELECT extname FROM pg_extension " + "WHERE extname IN ('timescaledb','vector','pg_trgm') ORDER BY extname" + ) + ).scalars().all() + return {"server": version, "extensions": list(exts)} diff --git a/backend/app/db/migrations/001_init.sql b/backend/app/db/migrations/001_init.sql new file mode 100644 index 0000000..ae6bf26 --- /dev/null +++ b/backend/app/db/migrations/001_init.sql @@ -0,0 +1,139 @@ +-- Init schema for stock_chart_site +-- Loaded automatically on first DB container start via /docker-entrypoint-initdb.d + +\set ON_ERROR_STOP on + +CREATE EXTENSION IF NOT EXISTS timescaledb; +CREATE EXTENSION IF NOT EXISTS vector; +CREATE EXTENSION IF NOT EXISTS pg_trgm; + +-- 종목 마스터 (Phase 1 에서 KRX 전체 종목 시드. 검색은 name 또는 code 둘 다 지원) +CREATE TABLE IF NOT EXISTS symbols ( + code TEXT PRIMARY KEY, + name TEXT NOT NULL, + market TEXT NOT NULL, -- 'KOSPI' / 'KOSDAQ' / 'NASDAQ' + sector TEXT, + active BOOLEAN DEFAULT TRUE, + is_seed BOOLEAN DEFAULT FALSE, -- 학습/배치 대상 10종목 여부 + created_at TIMESTAMPTZ DEFAULT NOW() +); +CREATE INDEX IF NOT EXISTS symbols_name_trgm ON symbols USING gin (name gin_trgm_ops); +CREATE INDEX IF NOT EXISTS symbols_active ON symbols(active); + +-- 일별 시세 +CREATE TABLE IF NOT EXISTS ohlcv_daily ( + code TEXT NOT NULL REFERENCES symbols(code), + date DATE NOT NULL, + open NUMERIC, + high NUMERIC, + low NUMERIC, + close NUMERIC, + volume BIGINT, + PRIMARY KEY (code, date) +); +SELECT create_hypertable('ohlcv_daily', 'date', if_not_exists => TRUE); +CREATE INDEX IF NOT EXISTS ohlcv_daily_code_date ON ohlcv_daily(code, date DESC); + +-- 분봉 (M8 인트라데이용, 스키마만 미리 둔다) +CREATE TABLE IF NOT EXISTS ohlcv_1m ( + code TEXT NOT NULL, + ts TIMESTAMPTZ NOT NULL, + open NUMERIC, + high NUMERIC, + low NUMERIC, + close NUMERIC, + volume BIGINT, + PRIMARY KEY (code, ts) +); +SELECT create_hypertable('ohlcv_1m', 'ts', if_not_exists => TRUE); + +-- 거시 / 환율 / 지수 +CREATE TABLE IF NOT EXISTS macro_daily ( + date DATE NOT NULL, + key TEXT NOT NULL, -- 'kospi','kosdaq','usdkrw','us10y',... + value NUMERIC, + PRIMARY KEY (date, key) +); + +-- 외인 / 기관 순매수 (KRW 기준 거래대금) +CREATE TABLE IF NOT EXISTS trading_value_daily ( + code TEXT NOT NULL REFERENCES symbols(code), + date DATE NOT NULL, + foreign_net NUMERIC, + institution_net NUMERIC, + individual_net NUMERIC, + PRIMARY KEY (code, date) +); + +-- 뉴스 / 공시 +CREATE TABLE IF NOT EXISTS news ( + id BIGSERIAL PRIMARY KEY, + code TEXT REFERENCES symbols(code), + source TEXT NOT NULL, -- 'naver_finance' / 'dart' / 'google_rss' + published_at TIMESTAMPTZ NOT NULL, + title TEXT NOT NULL, + url TEXT NOT NULL UNIQUE, + body TEXT, + sentiment_score REAL, -- KR-FinBERT 출력 -1..+1 + sentiment_label TEXT, -- 'positive' / 'neutral' / 'negative' + embedding VECTOR(768), + created_at TIMESTAMPTZ DEFAULT NOW() +); +CREATE INDEX IF NOT EXISTS news_code_pub ON news(code, published_at DESC); +CREATE INDEX IF NOT EXISTS news_pub ON news(published_at DESC); + +-- 모델 예측 이력 +-- user_triggered=TRUE 인 행만 다음날 outcomes 매칭/오차수정 학습에 사용 +CREATE TABLE IF NOT EXISTS predictions ( + id BIGSERIAL PRIMARY KEY, + code TEXT NOT NULL REFERENCES symbols(code), + predicted_at TIMESTAMPTZ NOT NULL, + base_date DATE NOT NULL, -- 예측 기준일(=마지막 관측일) + target_date DATE NOT NULL, + horizon INT NOT NULL, -- 1, 3, 5 + model TEXT NOT NULL, -- 'chronos2' / 'lgbm' / 'ensemble' + direction TEXT, -- 'up' / 'flat' / 'down' + prob_up REAL, + prob_flat REAL, + prob_down REAL, + expected_return REAL, + point_forecast NUMERIC, -- median 가격 + ci_low NUMERIC, -- quantile 10 + ci_high NUMERIC, -- quantile 90 + features_snapshot JSONB, + user_triggered BOOLEAN NOT NULL DEFAULT FALSE, + created_at TIMESTAMPTZ DEFAULT NOW(), + UNIQUE (code, base_date, target_date, horizon, model) +); +CREATE INDEX IF NOT EXISTS predictions_code_target ON predictions(code, target_date DESC); +CREATE INDEX IF NOT EXISTS predictions_user_triggered ON predictions(user_triggered) WHERE user_triggered = TRUE; + +-- 예측 vs 실제 결과 (오차 수정 / 메트릭 / 가중치 튜닝의 입력) +CREATE TABLE IF NOT EXISTS prediction_outcomes ( + prediction_id BIGINT PRIMARY KEY REFERENCES predictions(id) ON DELETE CASCADE, + code TEXT NOT NULL REFERENCES symbols(code), + target_date DATE NOT NULL, + horizon INT NOT NULL, + model TEXT NOT NULL, + predicted_close NUMERIC, + actual_close NUMERIC, + actual_return REAL, + direction_hit BOOLEAN, -- 방향성 적중 여부 + abs_error REAL, + resolved_at TIMESTAMPTZ DEFAULT NOW() +); +CREATE INDEX IF NOT EXISTS po_code_target ON prediction_outcomes(code, target_date DESC); +CREATE INDEX IF NOT EXISTS po_model ON prediction_outcomes(model); + +-- 모델별 롤링 성능 (앙상블 가중치 튜닝에 사용) +CREATE TABLE IF NOT EXISTS model_performance ( + code TEXT NOT NULL REFERENCES symbols(code), + model TEXT NOT NULL, + window_days INT NOT NULL, -- 7, 30 등 + as_of DATE NOT NULL, + hit_rate REAL, + mae REAL, + brier REAL, + sample_count INT, + PRIMARY KEY (code, model, window_days, as_of) +); diff --git a/backend/app/fetch/__init__.py b/backend/app/fetch/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/main.py b/backend/app/main.py new file mode 100644 index 0000000..bc70052 --- /dev/null +++ b/backend/app/main.py @@ -0,0 +1,46 @@ +from __future__ import annotations + +import logging + +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +from app.config import settings +from app.db.connection import ping as db_ping + +logging.basicConfig(level=settings.log_level) +logger = logging.getLogger(__name__) + + +app = FastAPI(title="stock_chart_site", version="0.0.1") + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_methods=["*"], + allow_headers=["*"], +) + + +def _resolved_device() -> str: + if settings.model_device != "auto": + return settings.model_device + try: + import torch # noqa: WPS433 + return "cuda" if torch.cuda.is_available() else "cpu" + except Exception: # noqa: BLE001 + return "cpu" + + +@app.get("/health") +def health() -> dict[str, object]: + return { + "ok": True, + "device": _resolved_device(), + "version": "0.0.1", + } + + +@app.get("/health/db") +def health_db() -> dict[str, object]: + return {"ok": True, **db_ping()} diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/pipelines/__init__.py b/backend/app/pipelines/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/pyproject.toml b/backend/pyproject.toml new file mode 100644 index 0000000..e4c7a35 --- /dev/null +++ b/backend/pyproject.toml @@ -0,0 +1,65 @@ +[project] +name = "stock_chart_site_backend" +version = "0.0.1" +description = "Stock chart + prediction backend (FastAPI + Chronos + LightGBM + KR-FinBERT)" +requires-python = ">=3.11,<3.13" + +dependencies = [ + # web + "fastapi==0.111.0", + "uvicorn[standard]==0.30.0", + "pydantic==2.7.1", + "pydantic-settings==2.3.0", + + # db + "sqlalchemy==2.0.30", + "psycopg[binary]==3.1.19", + "alembic==1.13.1", + + # data + "pandas==2.2.2", + "numpy==1.26.4", + "pykrx==1.0.45", + "yfinance==0.2.40", + "feedparser==6.0.11", + "requests==2.32.3", + "httpx==0.27.0", + "beautifulsoup4==4.12.3", + "lxml==5.2.2", + + # ml + "transformers==4.41.2", + "tokenizers==0.19.1", + "sentencepiece==0.2.0", + "scikit-learn==1.5.0", + "lightgbm==4.3.0", + "ta==0.11.0", + + # scheduler + "apscheduler==3.10.4", + "pytz==2024.1", + + # utils + "python-dotenv==1.0.1", + "loguru==0.7.2", + "tenacity==8.3.0", +] + +[project.optional-dependencies] +dev = [ + "pytest==8.2.1", + "ruff==0.4.7", + "mypy==1.10.0", +] + +[build-system] +requires = ["setuptools>=68"] +build-backend = "setuptools.build_meta" + +[tool.setuptools.packages.find] +where = ["."] +include = ["app*"] + +[tool.ruff] +line-length = 100 +target-version = "py311" diff --git a/build.bat b/build.bat new file mode 100644 index 0000000..6e8250e --- /dev/null +++ b/build.bat @@ -0,0 +1,81 @@ +@echo off +REM stock_chart_site - Windows 빌드/기동 스크립트 +REM 더블클릭하면 .env 준비 -> GPU 감지 -> docker compose up + +setlocal enabledelayedexpansion +cd /d "%~dp0" + +echo === stock_chart_site bootstrap === + +REM 1) .env 준비 +if not exist .env ( + echo .env not found. Copying .env.example -> .env + copy /Y .env.example .env >nul + echo - 키 발급 안내는 README.md 참조. 비워둬도 pykrx 만으로 일단 동작합니다. +) + +REM 2) Docker 설치 / 실행 확인 +where docker >nul 2>&1 +if errorlevel 1 ( + echo [ERROR] docker 명령을 찾을 수 없습니다. Docker Desktop 설치/실행을 확인하세요. + pause + exit /b 1 +) +docker info >nul 2>&1 +if errorlevel 1 ( + echo [ERROR] Docker Desktop이 실행 중이 아닙니다. + pause + exit /b 1 +) + +REM 3) GPU 감지 +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] NVIDIA GPU detected. Using GPU profile. + set COMPOSE_FILES=-f docker-compose.yml -f docker-compose.gpu.yml +) else ( + echo [CPU] NVIDIA GPU not detected. Falling back to CPU mode. + echo Docker Desktop ^> Settings ^> Resources ^> WSL Integration ^> GPU support 를 켜면 GPU 사용 가능합니다. + set COMPOSE_FILES=-f docker-compose.yml +) + +REM 4) Build + up +echo. +echo === docker compose build === +docker compose %COMPOSE_FILES% build +if errorlevel 1 ( + echo [ERROR] build 실패. + pause + exit /b 1 +) + +echo. +echo === docker compose up -d === +docker compose %COMPOSE_FILES% up -d +if errorlevel 1 ( + echo [ERROR] up 실패. + pause + exit /b 1 +) + +echo. +echo === 상태 === +docker compose %COMPOSE_FILES% ps + +echo. +echo 접속: +echo Web http://localhost:3000 +echo Backend http://localhost:8000/health +echo DB ext http://localhost:8000/health/db +echo. +echo 로그 보기: docker compose logs -f backend +echo 정지: docker compose down +echo. +pause +endlocal diff --git a/docker-compose.gpu.yml b/docker-compose.gpu.yml new file mode 100644 index 0000000..21ae651 --- /dev/null +++ b/docker-compose.gpu.yml @@ -0,0 +1,12 @@ +services: + backend: + deploy: + resources: + reservations: + devices: + - driver: nvidia + count: all + capabilities: [gpu] + environment: + MODEL_DEVICE: cuda + NVIDIA_VISIBLE_DEVICES: all diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..60b07af --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,62 @@ +name: stock_chart_site + +services: + db: + image: timescale/timescaledb-ha:pg16 + container_name: scs_db + restart: unless-stopped + environment: + POSTGRES_USER: ${POSTGRES_USER:-stock} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-stockpw} + POSTGRES_DB: ${POSTGRES_DB:-stock} + TZ: ${TZ:-Asia/Seoul} + volumes: + - postgres_data:/home/postgres/pgdata/data + - ./backend/app/db/migrations:/docker-entrypoint-initdb.d:ro + ports: + - "5432:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-stock} -d ${POSTGRES_DB:-stock}"] + interval: 10s + timeout: 5s + retries: 10 + + backend: + build: + context: ./backend + dockerfile: Dockerfile + container_name: scs_backend + restart: unless-stopped + env_file: .env + environment: + DATABASE_URL: postgresql+psycopg://${POSTGRES_USER:-stock}:${POSTGRES_PASSWORD:-stockpw}@db:5432/${POSTGRES_DB:-stock} + depends_on: + db: + condition: service_healthy + ports: + - "${BACKEND_PORT:-8000}:8000" + volumes: + - ./backend:/app + - hf_cache:/root/.cache/huggingface + + web: + build: + context: ./web + dockerfile: Dockerfile + container_name: scs_web + restart: unless-stopped + env_file: .env + environment: + NEXT_PUBLIC_API_BASE: ${NEXT_PUBLIC_API_BASE:-http://localhost:8000} + depends_on: + - backend + ports: + - "${WEB_PORT:-3000}:3000" + volumes: + - ./web:/app + - /app/node_modules + - /app/.next + +volumes: + postgres_data: + hf_cache: diff --git a/web/.dockerignore b/web/.dockerignore new file mode 100644 index 0000000..63a13e7 --- /dev/null +++ b/web/.dockerignore @@ -0,0 +1,5 @@ +node_modules +.next +out +.git +.DS_Store diff --git a/web/Dockerfile b/web/Dockerfile new file mode 100644 index 0000000..37617ef --- /dev/null +++ b/web/Dockerfile @@ -0,0 +1,13 @@ +FROM node:20-alpine + +WORKDIR /app + +ENV NEXT_TELEMETRY_DISABLED=1 + +COPY package.json package-lock.json* ./ +RUN npm install --no-audit --no-fund + +COPY . . + +EXPOSE 3000 +CMD ["npm", "run", "dev"] diff --git a/web/app/globals.css b/web/app/globals.css new file mode 100644 index 0000000..d826bf2 --- /dev/null +++ b/web/app/globals.css @@ -0,0 +1,8 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +html, body { + background: #0b0d12; + color: #e6e8eb; +} diff --git a/web/app/layout.tsx b/web/app/layout.tsx new file mode 100644 index 0000000..2a89d00 --- /dev/null +++ b/web/app/layout.tsx @@ -0,0 +1,15 @@ +import "./globals.css"; +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Stock Chart Site", + description: "개인용 주식 예측 차트", +}; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + +
{children} + + ); +} diff --git a/web/app/page.tsx b/web/app/page.tsx new file mode 100644 index 0000000..ab83ca6 --- /dev/null +++ b/web/app/page.tsx @@ -0,0 +1,14 @@ +export default function HomePage() { + return ( ++ Phase 0 scaffold. 종목 검색 UI는 Phase 6에서 추가됩니다. +
+GET {process.env.NEXT_PUBLIC_API_BASE}/health
+