docs: add project config docs, analysis report, and e2e signal cancel test
Add CLAUDE.md and AGENTS.md for AI-assisted development guidance, analysis report with screenshots, and Playwright-based e2e test for signal cancellation flow. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
f818bd3290
commit
2ad2f56d31
67
AGENTS.md
Normal file
67
AGENTS.md
Normal file
@ -0,0 +1,67 @@
|
||||
# AGENTS.md - galaxis-po 개발 에이전트 가이드
|
||||
|
||||
## 프로젝트 개요
|
||||
퀀트 & 퇴직연금 포트폴리오 관리 앱.
|
||||
김종봉 전략 기반 백테스팅, 신호 생성, 포트폴리오 관리 기능 제공.
|
||||
|
||||
## 기술 스택
|
||||
- **Backend**: FastAPI, Python 3.12, SQLAlchemy, PostgreSQL, uv
|
||||
- **Frontend**: Next.js 15, React 19, TypeScript, Tailwind CSS
|
||||
- **Infra**: Docker, Docker Compose
|
||||
|
||||
## 디렉토리 구조
|
||||
```
|
||||
galaxis-po/
|
||||
├── backend/
|
||||
│ ├── app/ # FastAPI 앱 (main.py, api/, core/, models/)
|
||||
│ ├── jobs/ # 스케줄러, 데이터 수집 잡
|
||||
│ ├── alembic/ # DB 마이그레이션
|
||||
│ └── requirements.txt / pyproject.toml
|
||||
├── frontend/ # Next.js 앱
|
||||
├── docs/plans/ # 설계 문서 (구현 전 반드시 확인)
|
||||
└── quant.md # 김종봉 전략 상세 가이드
|
||||
```
|
||||
|
||||
## 개발 원칙
|
||||
|
||||
### 코드 작성 시
|
||||
1. `docs/plans/` 의 관련 설계 문서를 먼저 확인할 것
|
||||
2. `quant.md` 에 전략 로직이 정의되어 있음 — 임의 변경 금지
|
||||
3. 기존 코드 스타일 유지 (Python: snake_case, TS: camelCase)
|
||||
4. 모든 API 엔드포인트는 `backend/app/api/` 하위에 router로 추가
|
||||
5. DB 스키마 변경 시 alembic migration 파일 함께 생성
|
||||
|
||||
### 금지 사항
|
||||
- `.env` 파일 수정 금지 (`.env.example` 참고만 가능)
|
||||
- `docker-compose.prod.yml` 임의 수정 금지
|
||||
- 테스트 없는 비즈니스 로직 추가 금지
|
||||
|
||||
### 작업 완료 조건
|
||||
- [ ] 기능 구현
|
||||
- [ ] 관련 테스트 추가 또는 기존 테스트 통과 확인
|
||||
- [ ] 타입 에러 없음 (Python: mypy / TS: tsc --noEmit)
|
||||
- [ ] 작업 내용 요약 보고
|
||||
|
||||
## 자주 쓰는 명령
|
||||
```bash
|
||||
# 백엔드 개발 서버
|
||||
cd backend && uv run uvicorn app.main:app --reload
|
||||
|
||||
# 프론트엔드 개발 서버
|
||||
cd frontend && npm run dev
|
||||
|
||||
# DB 마이그레이션
|
||||
cd backend && uv run alembic upgrade head
|
||||
|
||||
# 테스트 실행
|
||||
cd backend && uv run pytest
|
||||
```
|
||||
|
||||
## 보고 형식
|
||||
작업 완료 시:
|
||||
```
|
||||
완료: [작업명]
|
||||
변경 파일: [파일 목록]
|
||||
주요 내용: [한 줄 요약]
|
||||
주의사항: [있을 경우만]
|
||||
```
|
||||
86
CLAUDE.md
Normal file
86
CLAUDE.md
Normal file
@ -0,0 +1,86 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
Galaxis-Po is a quant portfolio management application for DC pension (퇴직연금) investing. It implements the Kim Jong-bong (김종봉) strategy for backtesting, signal generation, and portfolio management. The strategy logic is defined in `quant.md` — do not modify it without explicit instruction.
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Backend:** FastAPI, Python 3.12, SQLAlchemy, PostgreSQL, uv (package manager)
|
||||
- **Frontend:** Next.js 15 (App Router), React 19, TypeScript, Tailwind CSS v4, shadcn/ui (Radix primitives)
|
||||
- **Infrastructure:** Docker Compose, PostgreSQL 18
|
||||
|
||||
## Common Commands
|
||||
|
||||
```bash
|
||||
# Backend dev server (from repo root)
|
||||
cd backend && uv run uvicorn app.main:app --reload
|
||||
|
||||
# Frontend dev server
|
||||
cd frontend && npm run dev
|
||||
|
||||
# Run all backend tests
|
||||
cd backend && uv run pytest
|
||||
|
||||
# Run a single test file
|
||||
cd backend && uv run pytest tests/unit/test_kjb_signal.py -v
|
||||
|
||||
# Run e2e tests
|
||||
cd backend && uv run pytest tests/e2e/ -v
|
||||
|
||||
# DB migration
|
||||
cd backend && uv run alembic upgrade head
|
||||
|
||||
# Create new migration
|
||||
cd backend && uv run alembic revision --autogenerate -m "description"
|
||||
|
||||
# Frontend lint
|
||||
cd frontend && npm run lint
|
||||
|
||||
# Frontend type check
|
||||
cd frontend && npx tsc --noEmit
|
||||
|
||||
# Start all services via Docker
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### Backend (`backend/`)
|
||||
|
||||
- `app/main.py` — FastAPI app with lifespan manager (seeds admin user, starts APScheduler)
|
||||
- `app/api/` — Route handlers (routers): auth, admin, portfolio, strategy, market, backtest, snapshot, data_explorer, signal
|
||||
- `app/models/` — SQLAlchemy ORM models: user, stock, portfolio, signal, backtest
|
||||
- `app/schemas/` — Pydantic request/response schemas
|
||||
- `app/services/` — Business logic layer:
|
||||
- `collectors/` — Market data collectors (pykrx for Korean stock data, DART API for financials)
|
||||
- `strategy/` — Kim Jong-bong strategy implementation (signal generation, factor calculation)
|
||||
- `backtest/` — Backtesting engine
|
||||
- `rebalance.py` — Portfolio rebalancing logic
|
||||
- `price_service.py`, `factor_calculator.py`, `returns_calculator.py` — Quant utilities
|
||||
- `app/core/` — Config (pydantic-settings from `.env`), database (SQLAlchemy), security (JWT/bcrypt)
|
||||
- `jobs/` — APScheduler background jobs: data collection, signal generation, portfolio snapshots
|
||||
- `alembic/` — Database migrations
|
||||
- `tests/` — `unit/` and `e2e/` test directories
|
||||
|
||||
### Frontend (`frontend/`)
|
||||
|
||||
- Next.js App Router at `src/app/` with pages: portfolio, strategy, signals, backtest, admin, login
|
||||
- `src/components/` — UI components organized by domain (portfolio, strategy, charts, layout, ui)
|
||||
- `src/lib/api.ts` — Backend API client
|
||||
- Uses lightweight-charts for financial charts, recharts for other visualizations
|
||||
|
||||
## Development Rules
|
||||
|
||||
1. Check `docs/plans/` for relevant design documents before implementing features
|
||||
2. All API endpoints go under `backend/app/api/` as routers
|
||||
3. DB schema changes require an alembic migration
|
||||
4. Do not modify `.env` or `docker-compose.prod.yml`
|
||||
5. Python: snake_case; TypeScript: camelCase
|
||||
6. External APIs: KIS (Korea Investment & Securities) for market data, DART for financial statements, pykrx for Korean exchange data
|
||||
|
||||
## Environment
|
||||
|
||||
Backend config is loaded via pydantic-settings from environment variables / `.env` file. Key variables: `DATABASE_URL`, `JWT_SECRET`, `KIS_APP_KEY`, `KIS_APP_SECRET`, `KIS_ACCOUNT_NO`, `DART_API_KEY`. See `.env.example` for reference.
|
||||
378
docs/plans/2026-03-18-analysis-report.md
Normal file
378
docs/plans/2026-03-18-analysis-report.md
Normal file
@ -0,0 +1,378 @@
|
||||
# Galaxis-Po 코드 품질 및 완성도 심층 분석 보고서
|
||||
|
||||
**분석일:** 2026-03-18
|
||||
**분석 범위:** Backend (FastAPI), Frontend (Next.js), DB 모델, 설계 문서
|
||||
|
||||
---
|
||||
|
||||
## 목차
|
||||
|
||||
1. [미구현/불완전 기능 분석](#1-미구현불완전-기능-분석)
|
||||
2. [코드 품질 이슈](#2-코드-품질-이슈)
|
||||
3. [보안/안정성 이슈](#3-보안안정성-이슈)
|
||||
4. [성능 이슈](#4-성능-이슈)
|
||||
5. [Walk-forward 분석 구현 가능성 평가](#5-walk-forward-분석-구현-가능성-평가)
|
||||
6. [종합 평가 및 권고사항](#6-종합-평가-및-권고사항)
|
||||
|
||||
---
|
||||
|
||||
## 1. 미구현/불완전 기능 분석
|
||||
|
||||
### 1.1 Backend API 엔드포인트 전수 현황
|
||||
|
||||
총 **9개 라우터**, **44개 엔드포인트** + 헬스체크 1개
|
||||
|
||||
| 라우터 | 경로 접두사 | 엔드포인트 수 | 인증 필요 |
|
||||
|--------|------------|:------------:|:---------:|
|
||||
| auth | `/api/auth` | 4 | 부분적 |
|
||||
| admin | `/api/admin` | 8 | 전체 |
|
||||
| portfolio | `/api/portfolios` | 16 | 전체 |
|
||||
| strategy | `/api/strategy` | 4 | 전체 |
|
||||
| market | `/api/market` | 3 | 전체 |
|
||||
| backtest | `/api/backtest` | 7 | 전체 |
|
||||
| snapshot | `/api/portfolios/{id}` | 5 | 전체 |
|
||||
| data_explorer | `/api/data` | 6 | 전체 |
|
||||
| signal | `/api/signal` | 4 | 전체 |
|
||||
|
||||
### 1.2 Frontend 페이지 현황
|
||||
|
||||
총 **15개 페이지**, **32개 API 호출**
|
||||
|
||||
| 페이지 | 경로 | 주요 기능 |
|
||||
|--------|------|----------|
|
||||
| 로그인 | `/login` | SHA-256 해시 비밀번호 로그인 |
|
||||
| 대시보드 | `/` | 총자산, 수익률, 자산배분 차트 |
|
||||
| 포트폴리오 목록 | `/portfolio` | 포트폴리오 카드 그리드 |
|
||||
| 포트폴리오 생성 | `/portfolio/new` | 이름, 유형 선택 |
|
||||
| 포트폴리오 상세 | `/portfolio/[id]` | 보유종목, 거래내역, 분석 탭 |
|
||||
| 리밸런싱 | `/portfolio/[id]/rebalance` | 전략 선택, 수동 가격 입력, 적용 |
|
||||
| 포트폴리오 이력 | `/portfolio/[id]/history` | 스냅샷, 수익률 추이 |
|
||||
| 전략 목록 | `/strategy` | 4개 전략 카드 |
|
||||
| KJB 전략 | `/strategy/kjb` | 김종봉 전략 실행 |
|
||||
| 멀티팩터 전략 | `/strategy/multi-factor` | 가중치 설정, 실행 |
|
||||
| 퀄리티 전략 | `/strategy/quality` | F-Score 기반 |
|
||||
| 가치모멘텀 전략 | `/strategy/value-momentum` | 가치+모멘텀 가중치 |
|
||||
| 전략 비교 | `/strategy/compare` | 3개 전략 병렬 비교 |
|
||||
| 백테스트 | `/backtest` | 생성, 결과 조회 |
|
||||
| 백테스트 상세 | `/backtest/[id]` | 수익곡선, 드로다운, 거래내역 |
|
||||
| 관리자 데이터 | `/admin/data` | 6개 수집기 실행/상태 |
|
||||
| 데이터 탐색기 | `/admin/data/explorer` | 종목/ETF/섹터/밸류에이션 조회 |
|
||||
|
||||
### 1.3 API-UI 매핑 갭 분석
|
||||
|
||||
#### API 있으나 UI 없는 항목
|
||||
|
||||
| API 엔드포인트 | 상태 | 설명 |
|
||||
|---------------|------|------|
|
||||
| `PUT /api/portfolios/{id}` | UI 없음 | 포트폴리오 이름/유형 수정 기능 |
|
||||
| `DELETE /api/portfolios/{id}` | UI 없음 | 포트폴리오 삭제 기능 |
|
||||
| `PUT /api/portfolios/{id}/targets` | UI 없음 | 목표 비중 설정 (독립 UI 없음, 리밸런싱에서 간접 사용) |
|
||||
| `PUT /api/portfolios/{id}/holdings` | UI 없음 | 보유종목 직접 설정 |
|
||||
| `POST /api/portfolios/{id}/transactions` | UI 없음 | 수동 거래 추가 (신호 실행 외) |
|
||||
| `GET /api/portfolios/{id}/rebalance` | UI 없음 | 자동 리밸런싱 계산 (수동 계산만 UI에 있음) |
|
||||
| `POST /api/portfolios/{id}/rebalance/simulate` | UI 없음 | 추가 투자 시뮬레이션 |
|
||||
| `GET /api/market/stocks/{ticker}` | UI 없음 | 개별 종목 상세 정보 |
|
||||
| `GET /api/market/stocks/{ticker}/prices` | UI 없음 | 개별 종목 가격 차트 (data_explorer와 중복) |
|
||||
| `GET /api/market/search` | UI 없음 | 종목 검색 (독립 UI 없음) |
|
||||
| `DELETE /api/backtest/{id}` | UI 없음 | 백테스트 삭제 |
|
||||
| `POST /api/auth/register` | 비활성 | 코드에서 403 반환으로 비활성화됨 |
|
||||
| `POST /api/admin/collect/backfill` | UI 없음 | 과거 데이터 백필 기능 |
|
||||
|
||||
#### UI 있으나 실제 데이터가 아닌 항목
|
||||
|
||||
| UI 요소 | 위치 | 설명 |
|
||||
|---------|------|------|
|
||||
| 포트폴리오 가치 차트 | `/portfolio/[id]` | 90일 시뮬레이션 사인파 데이터 사용 (실제 스냅샷 기반 아님) |
|
||||
|
||||
### 1.4 설계 문서 vs 구현 대조
|
||||
|
||||
| 설계 문서 | 구현 상태 | 미구현 항목 |
|
||||
|----------|:---------:|-----------|
|
||||
| Phase 1: Foundation | 완료 | - |
|
||||
| Phase 2: Data Collection | 완료 | Financial collector 설계는 있으나 UI에서 직접 트리거 불가 |
|
||||
| Phase 3: Portfolio Management | 대부분 완료 | 포트폴리오 수정/삭제 UI 없음, 수동 거래 입력 UI 없음 |
|
||||
| Phase 4: Quant Strategy | 완료 | - |
|
||||
| Phase 5: Backtest Engine | 완료 | 백테스트 삭제/비교 UI 없음 |
|
||||
| Phase 6: Finishing | 부분 완료 | 종합 테스트 미흡, 프로덕션 배포 문서화 미완 |
|
||||
| KJB 전략 설계 | 완료 | DailyBacktestEngine, TradingPortfolio 모두 구현 |
|
||||
| 리밸런싱 설계 | 완료 | 추가 투자 시뮬레이션 UI 없음 |
|
||||
| DC 시나리오 갭 분석 | 문서만 존재 | 6개 시나리오 검증 미구현 |
|
||||
| 프로덕션 배포 설계 | 문서만 존재 | Nginx, SSL, 모니터링 등 미구현 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 코드 품질 이슈
|
||||
|
||||
### 2.1 TODO/FIXME/HACK 검색 결과
|
||||
|
||||
**결과: 0건** - Backend/Frontend 모두 TODO, FIXME, HACK, XXX, NotImplementedError 없음.
|
||||
|
||||
### 2.2 에러 핸들링 이슈
|
||||
|
||||
#### Frontend - console.error만 있고 사용자 피드백 없는 경우
|
||||
|
||||
| 파일 | 위치 | 설명 |
|
||||
|------|------|------|
|
||||
| `frontend/src/app/signals/page.tsx` | L161, L176, L185 | `fetchTodaySignals`, `fetchHistorySignals`, `fetchPortfolios` 에러 시 console만 |
|
||||
| `frontend/src/app/signals/page.tsx` | L246-254 | 포지션 사이징 API 에러 무시 |
|
||||
| `frontend/src/app/admin/data/explorer/page.tsx` | L121-135 | ETF 가격 조회 에러 시 빈 배열로 대체 |
|
||||
| `frontend/src/app/backtest/page.tsx` | 중첩 fetch | 내부 상세 조회 실패 시 UI 피드백 없음 |
|
||||
|
||||
#### Frontend - Error Boundary 부재
|
||||
|
||||
- 전체 프론트엔드에 React Error Boundary가 없음
|
||||
- 컴포넌트 렌더링 에러 시 전체 페이지 크래시 가능
|
||||
|
||||
### 2.3 타입 안전성 이슈
|
||||
|
||||
| 위치 | 설명 |
|
||||
|------|------|
|
||||
| `frontend/src/app/portfolio/[id]/rebalance/page.tsx` L313 | `as { data: Portfolio[] }` 강제 캐스팅 |
|
||||
| `frontend/src/app/admin/data/explorer/page.tsx` 다수 | 데이터 아이템 `as` 타입 단언 다수 사용 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 보안/안정성 이슈
|
||||
|
||||
### 3.1 심각도별 분류
|
||||
|
||||
#### CRITICAL - 하드코딩된 시크릿
|
||||
|
||||
| 파일 | 라인 | 내용 |
|
||||
|------|------|------|
|
||||
| `backend/app/core/config.py` | L14 | `database_url` 기본값에 비밀번호 포함: `postgresql://galaxy:devpassword@localhost:5432/galaxy_po` |
|
||||
| `backend/app/core/config.py` | L17 | `jwt_secret` 기본값: `"dev-jwt-secret-change-in-production"` |
|
||||
|
||||
> pydantic-settings로 환경변수 오버라이드 가능하나, 소스코드에 기본값이 남아 있음
|
||||
|
||||
#### HIGH - JWT 토큰 관리
|
||||
|
||||
| 이슈 | 설명 |
|
||||
|------|------|
|
||||
| 토큰 무효화 불가 | `POST /api/auth/logout`이 서버 측 토큰 무효화 없이 클라이언트에 의존 |
|
||||
| localStorage 저장 | `frontend/src/lib/api.ts` L18,25,32 - XSS 공격 시 토큰 탈취 가능 |
|
||||
|
||||
#### MEDIUM - 인증 패턴
|
||||
|
||||
| 파일 | 설명 |
|
||||
|------|------|
|
||||
| `backend/app/api/backtest.py` L96,143,176,217,259 | 백테스트 조회 시 `user_id` 필터 없이 전체 조회 후 소유권 확인 - 비효율적이며 정보 노출 가능 |
|
||||
|
||||
### 3.2 SQL 인젝션
|
||||
|
||||
**위험도: 없음** - 모든 DB 쿼리가 SQLAlchemy ORM 사용. raw SQL 쿼리 없음.
|
||||
|
||||
### 3.3 민감 정보 로깅
|
||||
|
||||
**위험도: 없음** - 비밀번호, 토큰, API 키 로깅 없음 확인.
|
||||
|
||||
### 3.4 인증 없이 접근 가능한 엔드포인트
|
||||
|
||||
| 엔드포인트 | 인증 | 비고 |
|
||||
|-----------|:----:|------|
|
||||
| `GET /health` | 불필요 | 정상 (헬스체크) |
|
||||
| `POST /api/auth/login` | 불필요 | 정상 (로그인) |
|
||||
| `POST /api/auth/register` | 불필요 | 비활성화 (403 반환) |
|
||||
| `POST /api/auth/logout` | 불필요 | 클라이언트 측 처리 |
|
||||
|
||||
> 그 외 모든 엔드포인트는 `CurrentUser` 의존성으로 인증 필수
|
||||
|
||||
---
|
||||
|
||||
## 4. 성능 이슈
|
||||
|
||||
### 4.1 N+1 쿼리 패턴
|
||||
|
||||
| 파일 | 위치 | 심각도 | 설명 |
|
||||
|------|------|:------:|------|
|
||||
| `backend/app/api/backtest.py` | L71-82 | HIGH | `list_backtests`에서 모든 백테스트 순회하며 `bt.result` lazy 로딩 → N+1 |
|
||||
| `frontend/src/app/portfolio/page.tsx` | L55-70 | MEDIUM | 포트폴리오 목록에서 각 포트폴리오별 detail API 개별 호출 |
|
||||
|
||||
### 4.2 인덱스 누락 (DB)
|
||||
|
||||
현재 인덱스: `users(email)`, `users(id)`, `job_logs(id)`, `signals(date, ticker)`, 각 테이블 PK만 존재
|
||||
|
||||
#### Tier 1 - 백테스트 성능 (긴급)
|
||||
|
||||
```sql
|
||||
-- 가격 데이터 (백테스트에서 가장 빈번하게 조회)
|
||||
CREATE INDEX idx_price_date ON prices(date);
|
||||
|
||||
-- 종목 유니버스 필터링
|
||||
CREATE INDEX idx_stock_market ON stocks(market);
|
||||
CREATE INDEX idx_stock_market_cap ON stocks(market_cap DESC);
|
||||
|
||||
-- 밸류에이션 스크리닝
|
||||
CREATE INDEX idx_valuation_base_date ON valuations(base_date);
|
||||
```
|
||||
|
||||
#### Tier 2 - 포트폴리오/신호 (중요)
|
||||
|
||||
```sql
|
||||
-- 신호 조회
|
||||
CREATE INDEX idx_signal_status_date ON signals(status, date);
|
||||
|
||||
-- 거래 내역
|
||||
CREATE INDEX idx_transaction_portfolio_executed ON transactions(portfolio_id, executed_at);
|
||||
|
||||
-- 백테스트 목록
|
||||
CREATE INDEX idx_backtest_user_created ON backtests(user_id, created_at);
|
||||
CREATE INDEX idx_backtest_status ON backtests(status);
|
||||
```
|
||||
|
||||
#### Tier 3 - 최적화
|
||||
|
||||
```sql
|
||||
-- 재무 데이터
|
||||
CREATE INDEX idx_financial_base_date ON financials(base_date);
|
||||
|
||||
-- ETF 필터링
|
||||
CREATE INDEX idx_etf_asset_class ON etfs(asset_class);
|
||||
CREATE INDEX idx_etf_price_date ON etf_prices(date);
|
||||
```
|
||||
|
||||
### 4.3 대용량 데이터 처리 이슈
|
||||
|
||||
| 파일 | 위치 | 설명 |
|
||||
|------|------|------|
|
||||
| `backend/app/services/backtest/engine.py` | L352 | `Stock.query.all()` - 전체 종목 메모리 로드 |
|
||||
| `backend/app/services/backtest/daily_engine.py` | L147-187 | 다수의 `.all()` 호출로 대용량 데이터 메모리 로드 |
|
||||
| `backend/app/services/price_service.py` | L92,114,180 | 전체 가격 데이터 제한 없이 로드 |
|
||||
| `backend/app/api/data_explorer.py` | L138,180 | 종목 가격 이력 페이지네이션 없이 전체 반환 |
|
||||
| `backend/app/api/snapshot.py` | L49,230 | 스냅샷 전체 조회 제한 없음 |
|
||||
| `backend/app/api/portfolio.py` | L39 | 포트폴리오 전체 조회 제한 없음 |
|
||||
|
||||
### 4.4 비동기 작업 관리
|
||||
|
||||
| 이슈 | 설명 |
|
||||
|------|------|
|
||||
| 데몬 스레드 사용 | 데이터 수집기가 daemon thread로 실행 → 앱 종료 시 작업 유실 가능 |
|
||||
| 작업 큐 미사용 | Celery/RQ 등 없이 in-process thread 실행 → 재시작 시 상태 복구 불가 |
|
||||
|
||||
---
|
||||
|
||||
## 5. Walk-forward 분석 구현 가능성 평가
|
||||
|
||||
### 5.1 현재 백테스트 엔진 구조
|
||||
|
||||
```
|
||||
BacktestWorker.submit_backtest()
|
||||
├── strategy_type == "kjb"
|
||||
│ └── DailyBacktestEngine (신호 기반 일일 매매)
|
||||
│ ├── TradingPortfolio (개별 포지션 관리)
|
||||
│ └── KJBSignalGenerator (매수/매도 조건 판단)
|
||||
└── strategy_type != "kjb"
|
||||
└── BacktestEngine (정기 리밸런싱)
|
||||
├── VirtualPortfolio (균등 가중 포트폴리오)
|
||||
└── MetricsCalculator (수익률, MDD, 샤프 등)
|
||||
```
|
||||
|
||||
**핵심 클래스:**
|
||||
- `BacktestEngine` (engine.py) - 주기적 리밸런싱 시뮬레이션
|
||||
- `DailyBacktestEngine` (daily_engine.py) - 일일 신호 기반 매매
|
||||
- `VirtualPortfolio` - 단순 포트폴리오 (리밸런싱용)
|
||||
- `TradingPortfolio` - 포지션 기반 포트폴리오 (손절/익절 관리)
|
||||
- `MetricsCalculator` - 독립적 성과 지표 계산
|
||||
|
||||
### 5.2 Walk-forward 구현 가능성: **높음 (HIGHLY FEASIBLE)**
|
||||
|
||||
#### 유리한 구조적 요소
|
||||
|
||||
| 요소 | 설명 |
|
||||
|------|------|
|
||||
| 데이터 분할 용이 | `_get_trading_days()`가 날짜 범위 필터링 지원 → 학습/검증 윈도우 분할 가능 |
|
||||
| 전략 재사용성 | 전략 인스턴스화가 분리됨 → 동일 전략을 다른 기간에 실행 가능 |
|
||||
| 메트릭스 독립성 | `MetricsCalculator`가 값 배열만 받음 → 백테스트 인스턴스와 무관 |
|
||||
| 글로벌 상태 없음 | 각 `BacktestEngine` 인스턴스가 독립적 → 여러 기간 동시 실행 가능 |
|
||||
| 결과 분리 저장 | metrics, equity curve, holdings, transactions 별도 테이블 → 학습/검증 비교 가능 |
|
||||
|
||||
#### 구현 방안
|
||||
|
||||
```
|
||||
1. WalkForwardEngine 클래스 추가
|
||||
- train_window, test_window, step_size 파라미터
|
||||
- 롤링 윈도우 생성기
|
||||
|
||||
2. 각 윈도우에서:
|
||||
- 학습 기간: 기존 BacktestEngine으로 전략 실행 → 최적 파라미터 도출
|
||||
- 검증 기간: 학습된 파라미터로 별도 BacktestEngine 실행
|
||||
|
||||
3. 전체 검증 기간 결과 합산 → 최종 성과 지표 계산
|
||||
```
|
||||
|
||||
#### 예상 작업량
|
||||
|
||||
| 작업 | 예상 규모 | 설명 |
|
||||
|------|:---------:|------|
|
||||
| `WalkForwardEngine` 클래스 | 중 | 윈도우 분할 + 순차 실행 로직 |
|
||||
| 파라미터 최적화 모듈 | 중~대 | 전략별 파라미터 그리드 서치 또는 최적화 |
|
||||
| DB 모델 추가 | 소 | `WalkForwardResult` 테이블 (윈도우별 결과 저장) |
|
||||
| API 엔드포인트 | 소 | 생성/조회/결과 반환 |
|
||||
| Frontend UI | 중 | 설정 폼 + 윈도우별 결과 시각화 |
|
||||
| **총합** | **중~대 규모** | 약 5-8개 파일 신규/수정 |
|
||||
|
||||
#### 주의사항
|
||||
- 백테스트 실행 시간이 윈도우 수만큼 배수로 증가 → 백그라운드 실행 필수
|
||||
- 파라미터 최적화 시 과적합(overfitting) 방지 로직 필요
|
||||
- 현재 daemon thread 방식으로는 장시간 실행에 부적합 → Celery 도입 검토
|
||||
|
||||
---
|
||||
|
||||
## 6. 종합 평가 및 권고사항
|
||||
|
||||
### 6.1 종합 스코어카드
|
||||
|
||||
| 항목 | 점수 | 평가 |
|
||||
|------|:----:|------|
|
||||
| 기능 완성도 | 8/10 | 핵심 기능 구현 완료, CRUD UI 일부 누락 |
|
||||
| 코드 품질 | 8/10 | TODO/FIXME 없음, 일관된 코드 스타일 |
|
||||
| 보안 | 6/10 | SQL 인젝션 안전, 하드코딩 시크릿/토큰 관리 취약 |
|
||||
| 성능 | 5/10 | 인덱스 부재, N+1 쿼리, 무제한 데이터 로드 |
|
||||
| 테스트 커버리지 | 미측정 | unit/e2e 구조 존재, 커버리지 분석 필요 |
|
||||
| 프로덕션 준비도 | 4/10 | 작업 큐 없음, 모니터링/로깅 미흡 |
|
||||
|
||||
### 6.2 우선순위별 권고사항
|
||||
|
||||
#### 즉시 조치 (P0)
|
||||
|
||||
1. **하드코딩 시크릿 제거** - `config.py`의 database_url, jwt_secret 기본값을 환경변수 필수로 변경
|
||||
2. **DB 인덱스 추가** - Tier 1 인덱스 마이그레이션 생성 (백테스트 성능 직결)
|
||||
3. **N+1 쿼리 수정** - `backtest.py` list_backtests에 `joinedload(Backtest.result)` 추가
|
||||
|
||||
#### 단기 개선 (P1)
|
||||
|
||||
4. **포트폴리오 CRUD UI 완성** - 수정/삭제/수동 거래 입력 화면 추가
|
||||
5. **에러 핸들링 통일** - Frontend Error Boundary 추가, console.error를 사용자 피드백으로 전환
|
||||
6. **페이지네이션 적용** - 가격 이력, 스냅샷, 거래 내역 등 무제한 로드 수정
|
||||
7. **백테스트 삭제 UI** - 불필요한 백테스트 정리 기능
|
||||
|
||||
#### 중기 개선 (P2)
|
||||
|
||||
8. **JWT httpOnly 쿠키 전환** - localStorage에서 secure cookie로 변경
|
||||
9. **작업 큐 도입** - Celery/RQ로 데이터 수집 및 백테스트 실행 안정화
|
||||
10. **포트폴리오 가치 차트 실데이터화** - 시뮬레이션 사인파 → 스냅샷 기반 실제 데이터
|
||||
11. **React Error Boundary** 추가
|
||||
|
||||
#### 장기 개선 (P3)
|
||||
|
||||
12. **Walk-forward 분석 구현** - 위 평가 참고
|
||||
13. **백테스트 비교 기능** - 전략 간 성과 비교 UI
|
||||
14. **DC 시나리오 검증** - 6개 시나리오 자동 검증 파이프라인
|
||||
15. **프로덕션 인프라** - Nginx, SSL, 모니터링, 로깅 체계
|
||||
|
||||
---
|
||||
|
||||
### 6.3 파일별 이슈 요약
|
||||
|
||||
| 파일 | 이슈 유형 | 심각도 |
|
||||
|------|----------|:------:|
|
||||
| `backend/app/core/config.py` | 하드코딩 시크릿 | CRITICAL |
|
||||
| `backend/app/api/backtest.py` | N+1 쿼리, 비효율적 인증 패턴 | HIGH |
|
||||
| `backend/app/services/backtest/engine.py` | 전체 종목 메모리 로드 | MEDIUM |
|
||||
| `backend/app/services/backtest/daily_engine.py` | 다수 unbounded `.all()` | MEDIUM |
|
||||
| `backend/app/services/price_service.py` | 무제한 가격 데이터 로드 | MEDIUM |
|
||||
| `backend/app/api/data_explorer.py` | 가격 이력 페이지네이션 없음 | MEDIUM |
|
||||
| `frontend/src/lib/api.ts` | localStorage 토큰 저장 | MEDIUM |
|
||||
| `frontend/src/app/signals/page.tsx` | 에러 핸들링 누락 (console만) | LOW |
|
||||
| `frontend/src/app/portfolio/[id]/page.tsx` | 시뮬레이션 차트 데이터 | LOW |
|
||||
| DB migrations | 성능 인덱스 대부분 누락 | HIGH |
|
||||
BIN
docs/screenshots/debug-signals.png
Normal file
BIN
docs/screenshots/debug-signals.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 53 KiB |
BIN
docs/screenshots/portfolio-detail.png
Normal file
BIN
docs/screenshots/portfolio-detail.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
BIN
docs/screenshots/signals-history.png
Normal file
BIN
docs/screenshots/signals-history.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 53 KiB |
BIN
docs/screenshots/signals-page.png
Normal file
BIN
docs/screenshots/signals-page.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 53 KiB |
BIN
docs/screenshots/strategy-compare.png
Normal file
BIN
docs/screenshots/strategy-compare.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
152
frontend/e2e/signal-cancel.spec.ts
Normal file
152
frontend/e2e/signal-cancel.spec.ts
Normal file
@ -0,0 +1,152 @@
|
||||
import { test, expect, Page } from "@playwright/test";
|
||||
|
||||
const TEST_USER = { username: "testuser", password: "testpass123" };
|
||||
|
||||
async function login(page: Page) {
|
||||
await page.goto("/login");
|
||||
await page.locator("#username").fill(TEST_USER.username);
|
||||
await page.locator("#password").fill(TEST_USER.password);
|
||||
await page.locator('button[type="submit"]').click();
|
||||
await page.waitForURL("**/", { timeout: 10000 });
|
||||
}
|
||||
|
||||
test.describe("Signal Cancel & Related Pages", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await login(page);
|
||||
});
|
||||
|
||||
test("should access signals page and see signal table", async ({ page }) => {
|
||||
await page.goto("/signals");
|
||||
|
||||
// Wait for page title to appear
|
||||
await expect(page.getByText("KJB 매매 신호")).toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
// Signal table should be visible
|
||||
await expect(page.locator("table").first()).toBeVisible();
|
||||
|
||||
// Summary cards should be visible
|
||||
await expect(page.getByText("매수 신호")).toBeVisible();
|
||||
await expect(page.getByText("매도 신호", { exact: true })).toBeVisible();
|
||||
|
||||
await page.screenshot({
|
||||
path: "../docs/screenshots/signals-page.png",
|
||||
fullPage: true,
|
||||
});
|
||||
});
|
||||
|
||||
test("should show cancel button for EXECUTED signals", async ({ page }) => {
|
||||
await page.goto("/signals");
|
||||
|
||||
await expect(page.getByText("KJB 매매 신호")).toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
// Switch to history view for executed signals
|
||||
const historyButton = page.getByText("신호 이력");
|
||||
if (await historyButton.isVisible()) {
|
||||
await historyButton.click();
|
||||
await page.waitForTimeout(2000);
|
||||
}
|
||||
|
||||
// Check if any executed signal rows exist
|
||||
const executedBadges = page.locator('text="실행됨"');
|
||||
const count = await executedBadges.count();
|
||||
|
||||
if (count > 0) {
|
||||
// There should be a cancel button near the executed signal
|
||||
const cancelButtons = page.locator('button:has-text("취소")');
|
||||
const cancelCount = await cancelButtons.count();
|
||||
expect(cancelCount).toBeGreaterThan(0);
|
||||
|
||||
await page.screenshot({
|
||||
path: "../docs/screenshots/signals-executed-with-cancel.png",
|
||||
fullPage: true,
|
||||
});
|
||||
} else {
|
||||
// No executed signals - verify the page structure is correct
|
||||
console.log(
|
||||
"No EXECUTED signals found - cancel button test skipped (no data)"
|
||||
);
|
||||
await expect(page.locator("table").first()).toBeVisible();
|
||||
|
||||
await page.screenshot({
|
||||
path: "../docs/screenshots/signals-history.png",
|
||||
fullPage: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test("should show realized/unrealized PnL cards on portfolio detail page", async ({
|
||||
page,
|
||||
}) => {
|
||||
// First check if any portfolio exists
|
||||
await page.goto("/portfolio");
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Try to find a portfolio link
|
||||
const portfolioLinks = page.locator('a[href^="/portfolio/"]');
|
||||
const linkCount = await portfolioLinks.count();
|
||||
|
||||
if (linkCount > 0) {
|
||||
await portfolioLinks.first().click();
|
||||
await page.waitForTimeout(3000);
|
||||
} else {
|
||||
await page.goto("/portfolio/1");
|
||||
await page.waitForTimeout(3000);
|
||||
}
|
||||
|
||||
// Check for realized/unrealized PnL cards
|
||||
const realizedPnlLabel = page.getByText("실현 수익");
|
||||
const unrealizedPnlLabel = page.getByText("미실현 수익");
|
||||
|
||||
const hasRealizedCard = await realizedPnlLabel
|
||||
.isVisible()
|
||||
.catch(() => false);
|
||||
const hasUnrealizedCard = await unrealizedPnlLabel
|
||||
.isVisible()
|
||||
.catch(() => false);
|
||||
|
||||
if (hasRealizedCard && hasUnrealizedCard) {
|
||||
await expect(realizedPnlLabel).toBeVisible();
|
||||
await expect(unrealizedPnlLabel).toBeVisible();
|
||||
await expect(page.getByText("매도 확정 손익")).toBeVisible();
|
||||
await expect(page.getByText("보유 중 평가 손익")).toBeVisible();
|
||||
} else {
|
||||
console.log(
|
||||
"Portfolio detail page may not have data - PnL cards not visible"
|
||||
);
|
||||
}
|
||||
|
||||
await page.screenshot({
|
||||
path: "../docs/screenshots/portfolio-detail.png",
|
||||
fullPage: true,
|
||||
});
|
||||
});
|
||||
|
||||
test("should render strategy compare page", async ({ page }) => {
|
||||
await page.goto("/strategy/compare");
|
||||
|
||||
// Wait for page title
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "전략 비교" })
|
||||
).toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
// Check description text
|
||||
await expect(
|
||||
page.getByText("멀티팩터, 퀄리티, 밸류모멘텀")
|
||||
).toBeVisible();
|
||||
|
||||
// Check the compare execution button
|
||||
const runButton = page.getByText("전략 비교 실행");
|
||||
await expect(runButton).toBeVisible();
|
||||
|
||||
await page.screenshot({
|
||||
path: "../docs/screenshots/strategy-compare.png",
|
||||
fullPage: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
64
frontend/package-lock.json
generated
64
frontend/package-lock.json
generated
@ -28,6 +28,7 @@
|
||||
"tailwind-merge": "^3.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.58.2",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^22",
|
||||
"@types/react": "^19",
|
||||
@ -1279,6 +1280,22 @@
|
||||
"node": ">=12.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.58.2",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz",
|
||||
"integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright": "1.58.2"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/number": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
|
||||
@ -4817,6 +4834,21 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
@ -6574,6 +6606,38 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.58.2",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz",
|
||||
"integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.58.2"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.58.2",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz",
|
||||
"integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/possible-typed-array-names": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
|
||||
|
||||
@ -29,6 +29,7 @@
|
||||
"tailwind-merge": "^3.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.58.2",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^22",
|
||||
"@types/react": "^19",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user