feat(phase-0): scaffold backend + web + docker + DB schema
- docker-compose.yml: timescaledb-ha (timescaledb 2.27 + vectorscale + pgvector + pgai) + backend (FastAPI, CUDA 12.1) + web (Next.js 14) - docker-compose.gpu.yml: GPU profile overlay for RTX 3070 Ti - build.bat: Windows bootstrap, auto-detects nvidia-smi and selects GPU/CPU compose - backend: Dockerfile, pyproject.toml, FastAPI skeleton with /health and /health/db - DB migration 001_init.sql: symbols (with trigram search), ohlcv_daily/1m (hypertables), macro_daily, trading_value_daily, news (vector embedding), predictions (with user_triggered flag for on-demand UX), prediction_outcomes, model_performance - web: Next.js 14 + Tailwind + lightweight-charts placeholder - README: KIS/DART/HuggingFace token issuance guides + 10 seed tickers + run instructions
This commit is contained in:
39
.env.example
Normal file
39
.env.example
Normal file
@@ -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
|
||||||
40
.gitignore
vendored
Normal file
40
.gitignore
vendored
Normal file
@@ -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/
|
||||||
146
README.md
146
README.md
@@ -1 +1,147 @@
|
|||||||
# stock_chart_site
|
# 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%.
|
||||||
|
|||||||
9
backend/.dockerignore
Normal file
9
backend/.dockerignore
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
__pycache__
|
||||||
|
*.pyc
|
||||||
|
.venv
|
||||||
|
.pytest_cache
|
||||||
|
.mypy_cache
|
||||||
|
.ruff_cache
|
||||||
|
tests
|
||||||
|
artifacts
|
||||||
|
.cache
|
||||||
33
backend/Dockerfile
Normal file
33
backend/Dockerfile
Normal file
@@ -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"]
|
||||||
0
backend/app/__init__.py
Normal file
0
backend/app/__init__.py
Normal file
0
backend/app/api/__init__.py
Normal file
0
backend/app/api/__init__.py
Normal file
24
backend/app/config.py
Normal file
24
backend/app/config.py
Normal file
@@ -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()
|
||||||
0
backend/app/db/__init__.py
Normal file
0
backend/app/db/__init__.py
Normal file
30
backend/app/db/connection.py
Normal file
30
backend/app/db/connection.py
Normal file
@@ -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)}
|
||||||
139
backend/app/db/migrations/001_init.sql
Normal file
139
backend/app/db/migrations/001_init.sql
Normal file
@@ -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)
|
||||||
|
);
|
||||||
0
backend/app/fetch/__init__.py
Normal file
0
backend/app/fetch/__init__.py
Normal file
46
backend/app/main.py
Normal file
46
backend/app/main.py
Normal file
@@ -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()}
|
||||||
0
backend/app/models/__init__.py
Normal file
0
backend/app/models/__init__.py
Normal file
0
backend/app/pipelines/__init__.py
Normal file
0
backend/app/pipelines/__init__.py
Normal file
65
backend/pyproject.toml
Normal file
65
backend/pyproject.toml
Normal file
@@ -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"
|
||||||
81
build.bat
Normal file
81
build.bat
Normal file
@@ -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
|
||||||
12
docker-compose.gpu.yml
Normal file
12
docker-compose.gpu.yml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
services:
|
||||||
|
backend:
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
reservations:
|
||||||
|
devices:
|
||||||
|
- driver: nvidia
|
||||||
|
count: all
|
||||||
|
capabilities: [gpu]
|
||||||
|
environment:
|
||||||
|
MODEL_DEVICE: cuda
|
||||||
|
NVIDIA_VISIBLE_DEVICES: all
|
||||||
62
docker-compose.yml
Normal file
62
docker-compose.yml
Normal file
@@ -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:
|
||||||
5
web/.dockerignore
Normal file
5
web/.dockerignore
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
node_modules
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
.git
|
||||||
|
.DS_Store
|
||||||
13
web/Dockerfile
Normal file
13
web/Dockerfile
Normal file
@@ -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"]
|
||||||
8
web/app/globals.css
Normal file
8
web/app/globals.css
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
background: #0b0d12;
|
||||||
|
color: #e6e8eb;
|
||||||
|
}
|
||||||
15
web/app/layout.tsx
Normal file
15
web/app/layout.tsx
Normal file
@@ -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 (
|
||||||
|
<html lang="ko">
|
||||||
|
<body className="min-h-screen">{children}</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
14
web/app/page.tsx
Normal file
14
web/app/page.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
export default function HomePage() {
|
||||||
|
return (
|
||||||
|
<main className="mx-auto max-w-3xl px-6 py-16">
|
||||||
|
<h1 className="text-3xl font-bold tracking-tight">Stock Chart Site</h1>
|
||||||
|
<p className="mt-3 text-sm text-zinc-400">
|
||||||
|
Phase 0 scaffold. 종목 검색 UI는 Phase 6에서 추가됩니다.
|
||||||
|
</p>
|
||||||
|
<div className="mt-8 rounded-md border border-zinc-800 bg-zinc-900/50 p-4 text-sm">
|
||||||
|
<div className="font-medium">Backend health</div>
|
||||||
|
<code className="mt-2 block text-zinc-400">GET {process.env.NEXT_PUBLIC_API_BASE}/health</code>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
5
web/next-env.d.ts
vendored
Normal file
5
web/next-env.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
/// <reference types="next" />
|
||||||
|
/// <reference types="next/image-types/global" />
|
||||||
|
|
||||||
|
// NOTE: This file should not be edited
|
||||||
|
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||||
7
web/next.config.js
Normal file
7
web/next.config.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
|
reactStrictMode: true,
|
||||||
|
output: "standalone",
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = nextConfig;
|
||||||
26
web/package.json
Normal file
26
web/package.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"name": "stock-chart-site-web",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev -p 3000 -H 0.0.0.0",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start -p 3000 -H 0.0.0.0",
|
||||||
|
"lint": "next lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"next": "14.2.3",
|
||||||
|
"react": "18.3.1",
|
||||||
|
"react-dom": "18.3.1",
|
||||||
|
"lightweight-charts": "4.1.7"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "20.12.12",
|
||||||
|
"@types/react": "18.3.3",
|
||||||
|
"@types/react-dom": "18.3.0",
|
||||||
|
"typescript": "5.4.5",
|
||||||
|
"tailwindcss": "3.4.4",
|
||||||
|
"postcss": "8.4.38",
|
||||||
|
"autoprefixer": "10.4.19"
|
||||||
|
}
|
||||||
|
}
|
||||||
3
web/postcss.config.js
Normal file
3
web/postcss.config.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: { tailwindcss: {}, autoprefixer: {} },
|
||||||
|
};
|
||||||
9
web/tailwind.config.js
Normal file
9
web/tailwind.config.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
content: [
|
||||||
|
"./app/**/*.{ts,tsx}",
|
||||||
|
"./components/**/*.{ts,tsx}",
|
||||||
|
],
|
||||||
|
theme: { extend: {} },
|
||||||
|
plugins: [],
|
||||||
|
};
|
||||||
21
web/tsconfig.json
Normal file
21
web/tsconfig.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": false,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"incremental": true,
|
||||||
|
"plugins": [{ "name": "next" }],
|
||||||
|
"paths": { "@/*": ["./*"] }
|
||||||
|
},
|
||||||
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user