diff --git a/CLAUDE.md b/CLAUDE.md index 7f06d62..f980a48 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,402 +1,130 @@ -# Auto Stock Trading System - 전략 가이드 (Claude.md) +# Auto Stock Trading System — Claude 컨텍스트 가이드 -> 이 파일은 Claude AI가 자동매매 시스템의 전략과 규칙을 이해하기 위한 컨텍스트 문서입니다. -> 코드 수정, 전략 분석, 백테스트 요청 시 이 파일을 기준으로 판단하세요. +> 이 문서는 Claude AI가 자동매매 시스템의 전략과 규칙을 깊이 이해하고, +> 코드 수정·전략 분석·백테스트 요청 시 올바른 판단을 내리기 위한 컨텍스트 문서다. +> 모든 코드 변경은 이 문서를 기준으로 이루어져야 한다. --- ## 1. 프로젝트 개요 -한국/미국 주식 자동매매 시스템 (풀스택) -- 한국투자증권 API 연동 (모의/실전 전환 가능) -- AI(Claude/Gemini) 기반 시장 분석 및 매매 판단 -- MCP(Model Context Protocol) 서버로 Claude CLI 연동 -- 실시간 WebSocket 시세 + React 대시보드 +이 시스템은 한국·미국 주식을 대상으로 동작하는 풀스택 자동매매 플랫폼이다. 한국투자증권(KIS) REST/WebSocket API와 연동하여 모의·실전 환경을 `.env` 설정 하나로 전환할 수 있다. AI 판단 레이어로는 Claude 3.5 Sonnet과 Gemini 2.0 Flash를 지원하며, 이 역시 `.env`의 `AI_PROVIDER` 값으로 즉시 교체된다. + +백엔드는 Python 3.11 + FastAPI + SQLAlchemy(async) + PostgreSQL + Redis 조합으로 구성되어 있고, 프론트엔드는 React + TypeScript + Vite + Recharts로 만들어진 실시간 대시보드다. Claude CLI와의 연동은 별도의 MCP 서버(stdio 방식)가 담당하며, 전체 인프라는 Docker Compose + Nginx로 묶어서 운영한다. + +개발 환경 포트 배정은 다음과 같다. FastAPI 백엔드는 8000번, MCP 서버는 8001번, Vite 프론트엔드는 5173번, PostgreSQL은 Docker 내부 5432·외부 55432번, Redis는 6379번을 사용한다. --- -## 2. 기술 스택 +## 2. 디렉터리 구조 -| 계층 | 기술 | -|------|------| -| Backend | Python 3.11, FastAPI, SQLAlchemy (async), PostgreSQL, Redis | -| Frontend | React + TypeScript, Vite, Recharts | -| MCP Server | Python, Anthropic SDK, MCP Protocol (stdio) | -| AI | Claude 3.5 Sonnet / Gemini 2.0 Flash (`.env`에서 전환) | -| Infra | Docker Compose, Nginx | +프로젝트 루트 아래에는 `backend/`, `frontend/`, `mcp-server/`, `docker/`, `docs/`, `tests/`, `scripts/` 디렉터리가 존재한다. + +백엔드의 핵심 로직은 `backend/app/` 안에 집약되어 있다. API 엔드포인트는 `api/endpoints/` 아래에 기능별로 분리되어 있으며, 주요 파일로는 시세 조회(`market_data.py`), 포트폴리오 관리 및 ATR 기반 청산(`portfolio.py`), 주문·체결 처리(`trades.py`), 규칙 기반 추천과 자동매매(`recommendations.py`), 매매 규칙 CRUD(`trading_rules.py`), 리스크 관리(`risk_management.py`), ML 앙상블 엔드포인트(`ml_ensemble.py`), 모니터링(`monitoring.py`)이 있다. + +전략 로직은 `services/` 아래에 모여 있다. RSI+볼린저 평균회귀 전략은 `mean_reversion_strategy.py`(v2)가 담당하고, 코스닥 전종목 스캐너는 `kosdaq_scanner.py`, 15분봉 단기 전략은 `short_term_strategy.py`가 각각 구현한다. 그 외에 MDD 관리(`mdd_manager.py`), 연속 손실 추적(`loss_tracker.py`), 피처 엔지니어링(`feature_engineering.py`), ML 앙상블(`ml_ensemble.py`), TensorFlow 예측(`tensorflow_predictor.py`)도 같은 디렉터리에 위치한다. + +MCP 서버는 `mcp-server/` 아래에서 독립적으로 동작한다. `stdio_server.py`가 Claude CLI와의 stdin/stdout 통신을 처리하고, `auto_trade_claude.py`와 `trade_monitor.py`가 실제 매매 판단과 모니터링을 수행한다. --- -## 3. 디렉터리 구조 +## 3. 환경 설정 (.env) -``` -auto-stock/ -├── backend/ -│ ├── app/ -│ │ ├── api/endpoints/ -│ │ │ ├── market_data.py # 시세 조회 -│ │ │ ├── portfolio.py # 포트폴리오 + ATR 기반 청산 -│ │ │ ├── trades.py # 주문/체결 -│ │ │ ├── recommendations.py # 규칙 기반 추천 + 자동매매 -│ │ │ ├── trading_rules.py # 매매 규칙 CRUD -│ │ │ ├── risk_management.py # 리스크 관리 -│ │ │ ├── ml_ensemble.py # ML 앙상블 엔드포인트 -│ │ │ ├── tensorflow_predictions.py -│ │ │ ├── debug.py -│ │ │ └── monitoring.py -│ │ ├── core/ -│ │ │ ├── config.py # 백엔드 설정 (매매 규칙 파라미터) -│ │ │ ├── database.py -│ │ │ ├── json_helpers.py # _dumps() 공용 헬퍼 -│ │ │ └── market_schedule.py # 장 운영시간 -│ │ ├── services/ -│ │ │ ├── korea_investment_api.py -│ │ │ ├── websocket_manager.py -│ │ │ ├── mean_reversion_strategy.py # RSI+볼린저 평균회귀 (v2) -│ │ │ ├── kosdaq_scanner.py # 코스닥 전종목 스캐너 (신규) -│ │ │ ├── short_term_strategy.py # 15분/60분봉 단기 전략 (신규) -│ │ │ ├── mdd_manager.py -│ │ │ ├── loss_tracker.py -│ │ │ ├── feature_engineering.py -│ │ │ ├── ml_ensemble.py -│ │ │ └── tensorflow_predictor.py -│ │ ├── models/models.py -│ │ └── main.py -│ ├── batch_daily_analysis.py -│ ├── trading_rules_config.json -│ └── migrations/ -├── frontend/ -│ └── src/ -│ ├── pages/ -│ ├── components/ -│ ├── hooks/ -│ ├── lib/ -│ └── utils/ -├── mcp-server/ -│ ├── config.py -│ ├── claude_client.py -│ ├── mcp_tools.py -│ ├── stdio_server.py -│ ├── main.py -│ ├── utils.py -│ ├── auto_trade_claude.py -│ └── trade_monitor.py -├── docker/ -│ └── init-db.sql -├── docs/ -├── tests/ -├── scripts/ -├── docker-compose.yml -├── docker-compose.dev.yml -└── .env -``` +`.env` 파일은 절대 커밋하지 않는다. API 키와 비밀 정보가 포함되어 있기 때문이다. 대신 `.env.example`을 참고하여 로컬에서 직접 생성해야 한다. + +주요 환경변수는 다음과 같다. `AI_PROVIDER`는 `gemini` 또는 `claude`로 설정하여 AI 판단 엔진을 전환한다. `TOKEN_SAVE_MODE=true`로 설정하면 Claude API 토큰 소비를 줄이기 위해 뉴스 수를 3개로 제한하고, 차트 데이터를 30일치만 전송하며, JSON을 압축 포맷으로 직렬화한다. `KIS_MODE`는 `virtual`(모의)과 `real`(실전)을 지원한다. + +코스닥 스캐너와 단기 전략에 관련된 설정도 있다. `SCANNER_MARKET`은 `KOSDAQ`, `KOSPI`, `ALL` 중 하나를 선택하고, `SCANNER_INTERVAL`은 스캔 주기를 분 단위로 설정한다. `SHORT_TERM_CAPITAL_RATIO`는 단기 전략에 배분할 자본 비율로, 최대 0.25(25%)를 넘어서는 안 된다. --- -## 4. 환경 설정 (.env) +## 4. 코딩 규칙 -```env -# AI Provider 전환 -AI_PROVIDER=gemini # gemini | claude +코드 수정 시 가장 중요한 원칙은 기존 함수·클래스의 시그니처를 변경하지 않는 것이다. 새로운 전략이나 기능을 추가할 때는 반드시 별도의 파일이나 클래스로 분리하고, 파일명에 `_v2`, `_v3` 같은 버전 접미사를 붙인다. -# 토큰 절약 설정 -TOKEN_SAVE_MODE=true -NEWS_LIMIT=3 -CHART_DAYS=30 -JSON_COMPACT=true -CLAUDE_MAX_TOKENS=1024 -CLAUDE_TEMPERATURE=0.3 +`json.dumps`는 직접 사용하지 않는다. 코드베이스 전체에서 `_dumps()` 헬퍼 함수(`core/json_helpers.py`)를 통해 JSON을 직렬화해야 한다. 이 헬퍼는 `TOKEN_SAVE_MODE`가 활성화된 경우 자동으로 compact 포맷을 적용하여 Claude API 호출 시 토큰 소비를 줄여준다. -# 증권 API 모드 -KIS_MODE=virtual # virtual | real - -# 스캐너 설정 (신규) -SCANNER_MARKET=KOSDAQ # KOSDAQ | KOSPI | ALL -SCANNER_INTERVAL=15 # 스캔 주기 (분) -SHORT_TERM_CAPITAL_RATIO=0.25 # 단기전략 자본 비율 (최대 25%) -``` +AI 프롬프트는 간결하게 작성한다. 필수 필드만 명시하고, JSON 예시는 최소화해야 한다. 변경 단위는 작게 유지하는 것을 원칙으로 하며, 큰 기능 변경보다 작고 검증 가능한 단위로 나누어 커밋한다. --- -## 5. 코딩 규칙 +## 5. 포지션 및 리스크 규칙 -- 기존 함수/클래스 시그니처 유지, 작은 단위 변경 우선 -- 새 전략은 별도 파일/클래스로 추가 (`_v2`, `_v3`) -- AI 프롬프트는 간결하게 (필수 필드만 명시, JSON 예시 최소화) -- `json.dumps`는 `_dumps()` 헬퍼 사용 (토큰 절약 모드 자동 적용) -- `.env` 파일은 커밋하지 않음 (API 키 포함) +### 포지션 크기 + +종목당 최대 투자 비중은 전체 자본의 3~4%로 제한한다. 동시에 보유할 수 있는 종목 수는 기본 최대 20종목이며, 전략에 따라 최대 25종목까지 허용한다. 레버리지·인버스 ETF는 전체 자본의 20% 이내로만 보유해야 한다. + +### 손절 및 청산 + +기본 손절 기준은 시장 유형에 따라 다르다. ETF·대형주 같은 저변동 종목은 -6%에서 손절하고, 테마주·코스닥 고변동 종목은 -10%에서 손절한다. 단, ATR 기반 손절이 우선 적용된다. 손절가는 `진입가 - 2.5 × ATR`로 계산하며, 변동성이 커지면 손절폭이 자동으로 확대된다. + +익절과 트레일링 스탑은 다음과 같이 운용한다. 수익이 2R(2배 ATR) 이상 발생하면 포지션의 절반을 익절하고, 나머지는 `최고가 - 1.5 × ATR`의 트레일링 스탑으로 보호한다. + +### 포트폴리오 MDD 관리 + +MDD가 -10%를 돌파하면 신규 진입을 일시 중단하고, 기존 포지션 크기를 30% 축소한다. 이때 단기 전략(15분봉)은 즉시 중단하고 일봉 전략만 유지한다. MDD가 -20%를 돌파하는 극단적 상황에서는 전체 포지션을 50% 강제 축소하며, 레버리지·인버스 포지션을 우선 청산한다. + +### 가격 필터 (저가주 제한) + +국내 종목은 5,000원 이하의 매수를 금지한다. 단, ETF·리츠·우량 대형 공모주는 화이트리스트로 예외 허용한다. 미국 종목은 4달러 이하의 매수를 금지하며, ETF·리츠는 마찬가지로 예외를 허용할 수 있다. + +### 섹터 상관관계 필터 + +동일 섹터 내 보유 종목은 최대 3개로 제한한다. 기존 보유 종목과의 상관계수가 0.7 이상인 종목은 신규 진입을 차단한다. 상관관계 행렬은 매주 업데이트하여 Redis에 캐싱한다. --- -## 6. 포지션 및 리스크 규칙 +## 6. RSI + 볼린저 평균회귀 전략 v2 -### 6.1 포지션 규칙 -- 1종목당 최대 **3~4%** 자본 사용 -- 동시 보유 종목 수: 기본 최대 **20종목**, 전략에 따라 최대 25종목 -- 레버리지/인버스 ETF: 전체 자본의 **20% 이내** +### 시장 Regime 판단 -### 6.2 손절 및 청산 규칙 -- 기본 손절: - - 저변동 종목/ETF(대형주): **-6%** - - 고변동 종목(테마·코스닥): **-10%** -- ATR 기반 손절 (우선 적용): - - 손절가 = 진입가 - **2.5 × ATR** - - 변동성 크면 손절폭 자동 확대, 줄면 축소 -- 익절/트레일링: - - 2R 이상 수익 시 절반 익절 + 나머지 트레일링 스탑 (**1.5 × ATR**) +이 전략의 출발점은 코스피200 지수의 200일 이동평균선(MA200)을 이용한 시장 상태 분류다. 현재 지수가 MA200 위에 있고 MA200의 기울기가 양수(상승)이면 상승장(BULL)으로 판단한다. 반대로 지수가 MA200 아래에 있고 기울기가 음수면 하락장(BEAR)이다. ADX가 20 미만이거나 MA200 기울기가 0에 가까운 횡보 상태는 SIDEWAYS로 분류하며, 이 구간에서는 평균회귀 신호가 발생해도 완전히 무시한다. 시장 상태 판별은 매일 장 마감 후 업데이트한다. -### 6.3 포트폴리오 MDD 관리 -- MDD **-10%** 돌파 시: - - 신규 진입 일시 중단 - - 기존 포지션 크기 **30% 축소** -- MDD **-20%** 돌파 시: - - 전체 포지션 **50% 강제 축소** - - 레버리지/인버스 포지션 우선 청산 +### 매수 조건 -### 6.4 가격 필터 (저가주 제한) -- 국내: **5,000원 이하** 매수 금지 - - 단, ETF·리츠·우량 대형 공모주는 화이트리스트로 허용 -- 미국: **4달러 이하** 매수 금지 - - ETF·리츠 화이트리스트 예외 가능 +상승장에서는 RSI(14)가 25 이하이고, 종가가 볼린저 밴드 하단의 -3%에서 +1.5% 구간 안에 있으며, 거래량이 20일 평균 이상일 때 매수 신호가 발생한다. 캔들이 양봉이거나 전일 저가 대비 0.5% 이상 반등한 경우를 추가로 확인한다. 분할 매수는 50%-50% 2회로 진행한다. -### 6.5 시간 프레임 운용 -- 기본: **일봉(Daily) + 60분봉(H1)** -- 단기(인트라데이): **15분/60분봉** → 전체 자본의 **최대 20~25%** 이내 - - 슬리피지·수수료 포함 백테스트 필수 +하락장에서는 더 엄격한 조건이 적용된다. RSI(14)가 30 이하이고, 종가가 볼린저 하단 이하이며, 거래량이 20일 평균의 1.5배 이상 급증해야 한다. 여기에 더해 전일 종가보다 높거나 60분봉 전 고가를 돌파하는 되돌림 캔들까지 확인한 후에야 진입한다. 분할 매수는 3회로 나누며, 2일 연속 과매도 상태라면 2회로 축소한다. -### 6.6 섹터 상관관계 필터 -- 동일 섹터 최대 **3종목** 제한 -- 기존 보유 종목과 상관계수 **0.7 이상** 시 진입 제한 -- correlation_matrix: 주간 업데이트 → Redis 캐싱 +복합 타임프레임 선취매도 선택적으로 사용할 수 있다. 일봉 RSI가 40 이하인 관심 종목 중 60분봉 RSI가 30 이하로 떨어지면서 동시에 5일선을 상향 돌파할 때 전체 자본의 10% 이내에서 선취매한다. + +### 청산 조건 + +1차 익절은 RSI(14)가 50 이상으로 회복되거나 종가가 볼린저 중앙선을 상회하거나 수익이 2 × ATR_pct 이상일 때 발동한다. 상승장에서는 40%를 익절하고, 하락장에서는 60%를 익절한다. + +2차 익절(전량 청산)은 `최고가 - 1.5 × ATR` 트레일링 스탑에 걸리거나, RSI가 65 이상으로 과열되거나, 종가가 볼린저 상단의 99% 이상에 도달할 때 이루어진다. + +### 시장 상태별 자본 배분 + +상승장에서는 이 전략에 최대 70%까지 자본을 배분하여 공격적으로 운용한다. 횡보장에서는 진입 자체를 금지한다. 하락장에서는 최대 30%로 보수적으로 운용하며 자본 보호를 최우선으로 삼는다. --- -## 7. RSI + 볼린저 평균회귀 전략 v2 (개선형) +## 7. 공격적 전략 — 월 5% 목표 -### 7.1 시장 Regime 판단 (3분류) +이 시스템의 장기 목표는 1천만 원에서 1억 원으로의 성장이다. 순수 복리(월 5%)로 계산하면 48개월(4년)이 걸리지만, 현실에서는 MDD 발생과 신호 부재를 감안하여 7~8년을 목표로 설정한다. 몬테카를로 1,000회 시뮬레이션 기준 10년 내 1억 달성 확률은 82.5%다. -```python -slope_200 = (ma200_today - ma200_20days_ago) / 20 # 기울기 -adx = talib.ADX(high, low, close, 14)[-1] +단일 종목 대기 방식으로는 연 2~4회 신호밖에 얻지 못한다. 이를 보완하기 위해 코스닥 전종목 스캐너(`kosdaq_scanner.py`)와 15분봉 단기 전략(`short_term_strategy.py`)을 병행하여 신호 빈도를 월 10~30회 수준으로 끌어올린다. -if close > ma200 and slope_200 > 0 and adx > 20: - regime = MarketRegime.BULL # 추세 상승장 -elif close < ma200 and slope_200 < 0 and adx > 20: - regime = MarketRegime.BEAR # 추세 하락장 -else: - regime = MarketRegime.SIDEWAYS # 박스권 (신규 진입 금지) -``` +### 코스닥 전종목 스캐너 -> **판별 기준**: 코스피200 지수 기준 / 매일 장 마감 후 업데이트 -> **SIDEWAYS**: ADX < 20 또는 200MA 횡보(기울기 ≈ 0) → 진입 금지 +`KosdaqScanner` 클래스는 장 마감 후 코스닥 전종목(약 1,600종목)을 스캔한다. 가격 5,000원 이상 + 거래대금 상위 20% 필터를 통과한 종목에 대해 `score = RSI점수×0.5 + BB점수×0.3 + 거래량점수×0.2` 공식으로 복합 점수를 산출하고 상위 20종목을 선별한다. 전체 자본의 25%를 이 전략에 배분하며, 월 5~10회 신호를 기대한다. + +### 15분봉 단기 전략 + +`ShortTermStrategy` 클래스는 일봉 RSI가 40 이하인 관심 종목에 한해 15분봉 신호를 탐색한다. 15분봉 RSI가 30 이하이고, 종가가 볼린저 하단 근처이며, MA5를 상향 돌파하고, 거래량이 평균의 1.3배 이상일 때 진입한다. 청산은 15분봉 RSI가 55 이상으로 회복되거나 수익률이 1.5 × ATR_15m_pct에 도달할 때 이루어진다. 전체 자본의 25%를 배분하며, 수수료 0.3% + 슬리피지 0.2%를 포함한 백테스트가 반드시 선행되어야 한다. MDD -10% 돌파 시 이 전략은 즉시 중단한다. + +### 멀티 전략 자본 배분 + +세 전략을 동시 운용할 때의 배분 기준은 다음과 같다. RSI+볼린저 일봉 전략(v2)에 50%, 코스닥 스캐너에 25%, 15분봉 단기 전략에 25%를 배분하여 합산 월 4~7%의 수익을 목표로 한다. MDD -10% 구간에서는 단기 전략 배분을 즉시 0%로 환원하고 일봉 전략만 유지한다. --- -### 7.2 매수 조건 +## 8. Claude에게 요청 시 주의사항 -#### 공통 전제조건 -- 가격: 국내 5,000원 이상 / 미국 4달러 이상 -- 유동성: 최근 20일 평균 거래대금 상위 20% 이내 +코드를 수정하거나 추가할 때는 반드시 다음 원칙을 지켜야 한다. ---- - -#### 상승장(BULL) 매수 조건 - -```python -rsi_ok = rsi14 <= 25 -pct = (close - bb_lower) / bb_lower * 100 -bb_ok = -3.0 <= pct <= 1.5 -volume_ok = volume > volume_ma20 * 0.8 -candle_ok = close > open or close > prev_low * 1.005 - -bull_signal = rsi_ok and bb_ok and volume_ok and candle_ok -# → 2회 분할 매수 (50% + 50%) -``` - ---- - -#### 하락장(BEAR) 매수 조건 - -```python -rsi_ok = rsi14 <= 30 -bb_ok = close <= bb_lower * 1.01 -volume_surge = volume > volume_ma20 * 1.5 -rebound_ok = close > prev_close or close > h1_prev_high - -bear_signal = rsi_ok and bb_ok and volume_surge and rebound_ok -# → 3회 분할 매수 (단, 2일 연속 과매도면 2회로 축소) -``` - ---- - -#### 복합 타임프레임 선취매 (선택) -- 일봉 RSI ≤ 40 관심 종목 중, 60분봉 RSI ≤ 30 신호 발생 시 선취매 -- 60분봉 필터: 5일 이동평균선 상향 돌파 동시 확인 (Fake-out 방지) -- 비중: 전체 자본의 **10% 이내** - ---- - -### 7.3 청산 조건 - -#### 1차 익절 -```python -first_exit = ( - rsi14 >= 50 - or close >= bb_mid - or profit_pct >= 2 * atr_pct -) -# BULL: 40% 익절 / BEAR: 60% 익절 -``` - -#### 2차 익절 (전량 청산) -```python -trailing_stop = max_close_after_entry - 1.5 * atr - -second_exit = ( - close <= trailing_stop # ATR 트레일링 (최우선) - or rsi14 >= 65 - or close >= bb_upper * 0.99 -) -``` - ---- - -### 7.4 손절 및 추가 진입 제한 - -```python -stop_loss_atr = entry_price - 2.5 * atr -stop_loss_pct = entry_price * 0.92 # -8% -stop_triggered = close <= min(stop_loss_atr, stop_loss_pct) - -# 연속 손실 제한 -if consecutive_losses >= 3: - block_entry_days = 30 -``` - ---- - -### 7.5 시장 상태별 자본 배분 - -| Regime | 전략 운용 비중 | 특징 | -|-----------|----------------|----------------------| -| BULL | **최대 70%** | 공격적, 모멘텀 최대 활용 | -| SIDEWAYS | **진입 금지** | 가짜 신호 차단 | -| BEAR | **최대 30%** | 보수적, 자본 보호 우선 | - ---- - -## 8. 공격적 전략 (월 5% 목표) - -> **목표**: 1천만원 → 1억원 (4년 순수 복리 / 현실 7~8년) -> **핵심**: 단일 종목 대기 → 코스닥 전종목 스캔 + 단기봉 병행으로 신호 빈도 ↑ - -### 8.1 코스닥 전종목 스캐너 (`kosdaq_scanner.py`) - -```python -# 매일 장 마감 후 코스닥 전종목 스캔 → 조건 충족 종목 우선순위 큐에 저장 -class KosdaqScanner: - def scan_all(self) -> list[ScanResult]: - candidates = [] - for ticker in kosdaq_all_tickers: # ~1,600종목 - if not self._price_filter(ticker): # 5,000원 이상 - continue - if not self._liquidity_filter(ticker): # 거래대금 상위 20% - continue - score = self._score(ticker) # RSI + BB + 거래량 복합 점수 - candidates.append(ScanResult(ticker, score)) - return sorted(candidates, key=lambda x: x.score, reverse=True)[:20] - - def _score(self, ticker) -> float: - rsi_score = max(0, (25 - rsi14) / 25) # RSI 낮을수록 ↑ - bb_score = max(0, (0 - pct_vs_lower) / 3) # BB 하단 근접 ↑ - volume_score = min(volume / volume_ma20, 2.0) # 거래량 급증 ↑ - return rsi_score * 0.5 + bb_score * 0.3 + volume_score * 0.2 -``` - -> **기대 효과**: 단일 종목 대기 연 2~4회 → 전종목 스캔으로 월 5~10개 신호 확보 - ---- - -### 8.2 15분봉 단기 전략 (`short_term_strategy.py`) - -```python -class ShortTermStrategy: - MAX_CAPITAL_RATIO = 0.25 # 전체 자본의 최대 25% - TIMEFRAME = "15m" - - def entry_signal(self, ticker) -> bool: - # 일봉 RSI ≤ 40 관심 종목 중에서만 적용 - if daily_rsi > 40: - return False - # 15분봉 조건 - rsi_15m = self._rsi(ticker, "15m", 14) - bb_lower = self._bb_lower(ticker, "15m", 20) - ma5 = self._ma(ticker, "15m", 5) - return ( - rsi_15m <= 30 - and close <= bb_lower * 1.01 - and close > ma5 # 5분봉 MA5 상향 돌파 (Fake-out 방지) - and volume > volume_ma * 1.3 - ) - - def exit_signal(self, position) -> bool: - rsi_15m = self._rsi(position.ticker, "15m", 14) - return rsi_15m >= 55 or profit_pct >= 1.5 * atr_15m_pct -``` - -> **슬리피지 주의**: 15분봉 전략은 반드시 수수료 0.3% + 슬리피지 0.2% 포함 백테스트 필수 - ---- - -### 8.3 멀티 전략 자본 배분 - -| 전략 | 자본 비율 | 기대 월 수익 | 신호 빈도 | -|------|-----------|-------------|-----------| -| RSI+볼린저 일봉 (v2) | 50% | +2~3% | 월 3~5회 | -| 코스닥 스캐너 | 25% | +1~2% | 월 5~10회 | -| 15분봉 단기 전략 | 25% | +1~2% | 월 10~20회 | -| **합계 기대치** | **100%** | **+4~7%** | - | - -> 세 전략 동시 운용 시 월 5% 달성 현실 가능 -> MDD -10% 돌파 시 단기 전략 즉시 중단, 일봉 전략만 유지 - ---- - -### 8.4 수익 목표 시뮬레이션 (복리) - -| 이정표 | 순수 복리 (월 5%) | 현실 예상 (MDD 포함) | -|--------|------------------|---------------------| -| 2,000만 | 15개월 (1.2년) | 20~24개월 | -| 3,000만 | 23개월 (1.9년) | 30~36개월 | -| 5,000만 | 33개월 (2.8년) | 45~54개월 | -| **1억** | **48개월 (4.0년)** | **7~8년** | - -> 10년 내 1억 달성 확률 (몬테카를로 1,000회): **82.5%** - ---- - -## 9. 주요 포트 (개발 환경) - -| 서비스 | 포트 | -|--------|------| -| Backend (FastAPI) | 8000 | -| MCP Server | 8001 | -| Frontend (Vite) | 5173 | -| PostgreSQL | 5432 (Docker: 55432) | -| Redis | 6379 | - ---- - -## 10. Claude에게 요청 시 주의사항 - -1. **기존 코드 시그니처 변경 금지** → 새 기능은 `_v2`, `_v3` 파일로 분리 -2. **백테스트 없이 실전 코드 즉시 적용 금지** → 워크포워드 테스트 필수 -3. **`json.dumps` 직접 사용 금지** → 반드시 `_dumps()` 헬퍼 사용 -4. **리스크 규칙(포지션 크기, 손절, MDD, 가격 필터)** 은 모든 전략에 기본 적용 -5. **SIDEWAYS 구간** 에서는 평균회귀 전략 신호 발생 시 무시 -6. **15분봉 단기 전략** 은 전체 자본의 25% 초과 불가 -7. **MDD -10%** 돌파 시 단기 전략 즉시 중단, 일봉 전략만 유지 -8. **코스닥 스캐너** 는 가격 5,000원 이상 + 거래대금 상위 20% 필터 반드시 적용 +첫째, 기존 코드의 함수·클래스 시그니처는 절대 변경하지 않는다. 새 기능은 항상 `_v2`, `_v3` 접미사가 붙은 별도 파일이나 클래스로 추가한다. 둘째, 백테스트를 거치지 않은 코드를 실전 환경에 즉시 적용해서는 안 된다. 워크포워드 테스트를 포함한 충분한 검증이 선행되어야 한다. 셋째, `json.dumps`를 직접 사용하는 코드는 작성하지 않는다. 반드시 `_dumps()` 헬퍼를 경유해야 한다. 넷째, 포지션 크기·손절가·MDD 규칙·가격 필터 등 리스크 규칙은 어떤 전략에도 예외 없이 기본 적용된다. 다섯째, SIDEWAYS 구간에서 발생한 평균회귀 신호는 코드 레벨에서 무시 처리해야 하며, 사용자에게 진입을 유도하는 응답을 해서는 안 된다. 여섯째, 15분봉 단기 전략의 자본 배분은 전체의 25%를 절대 초과할 수 없다. 일곱째, MDD -10% 돌파 시 단기 전략을 즉시 중단하는 로직이 호출 측에 존재하는지 항상 확인한다. 여덟째, 코스닥 스캐너가 종목을 선별할 때 가격 5,000원 이상 + 거래대금 상위 20% 필터가 반드시 적용되어야 한다. --- diff --git a/backend/app/services/kosdaq_scanner.py b/backend/app/services/kosdaq_scanner.py new file mode 100644 index 0000000..23a7680 --- /dev/null +++ b/backend/app/services/kosdaq_scanner.py @@ -0,0 +1,174 @@ +""" +코스닥 전종목 스캐너 (CLAUDE.md 섹션 8.1) + +매일 장 마감 후 코스닥 전종목(~1,600종목)을 스캔하여 +RSI + 볼린저 밴드 + 거래량 복합 점수로 상위 20종목을 선별한다. +""" +import logging +from dataclasses import dataclass +from typing import List, Optional + +logger = logging.getLogger(__name__) + +# 가격 필터 (CLAUDE.md 섹션 6.4) +MIN_PRICE_KRW = 5_000 + +# 거래대금 상위 20% 임계값 (하위 80% 제외) +LIQUIDITY_PERCENTILE = 0.80 + +# 스캔 결과 상위 N개 +TOP_N = 20 + + +@dataclass +class ScanResult: + ticker: str + score: float + rsi14: float + pct_vs_bb_lower: float # (close - bb_lower) / bb_lower * 100 + volume_ratio: float # current_volume / volume_ma20 + close: float + reason: str = "" + + +@dataclass +class TickerData: + """스캐너에 필요한 종목 데이터 구조체""" + ticker: str + close: float + closes: List[float] # 최근 60일 이상 종가 + volume: int + volumes: List[int] # 최근 20일 거래량 + turnover: float # 당일 거래대금 (원) + turnovers: List[float] # 최근 20일 거래대금 + + +class KosdaqScanner: + """코스닥 전종목 스캐너 + + 매일 장 마감 후 scan_all()을 호출하면 조건을 충족하는 상위 20종목을 + 점수 내림차순으로 반환한다. + + 스코어링 공식 (CLAUDE.md 섹션 8.1): + score = rsi_score × 0.5 + bb_score × 0.3 + volume_score × 0.2 + + - rsi_score = max(0, (25 - rsi14) / 25) # RSI 낮을수록 ↑ + - bb_score = max(0, (0 - pct_vs_lower) / 3) # BB 하단 근접 ↑ + - volume_score = min(volume / volume_ma20, 2.0) # 거래량 급증 ↑ + """ + + def __init__(self, min_price: int = MIN_PRICE_KRW, top_n: int = TOP_N): + self.min_price = min_price + self.top_n = top_n + + # ------------------------------------------------------------------ + # Public API + # ------------------------------------------------------------------ + + def scan_all(self, tickers_data: List[TickerData]) -> List[ScanResult]: + """코스닥 전종목 스캔 → 상위 N개 ScanResult 반환 + + Args: + tickers_data: 외부(KIS API 등)에서 수집한 종목 데이터 리스트 + + Returns: + 점수 내림차순 정렬된 상위 TOP_N개 ScanResult + """ + liquidity_threshold = self._calc_liquidity_threshold(tickers_data) + + candidates: List[ScanResult] = [] + for td in tickers_data: + if not self._price_filter(td.close): + continue + if not self._liquidity_filter(td.turnover, liquidity_threshold): + continue + result = self._score(td) + if result is not None: + candidates.append(result) + + candidates.sort(key=lambda x: x.score, reverse=True) + return candidates[: self.top_n] + + # ------------------------------------------------------------------ + # Filters + # ------------------------------------------------------------------ + + def _price_filter(self, close: float) -> bool: + """CLAUDE.md 섹션 6.4: 국내 5,000원 이하 매수 금지""" + return close >= self.min_price + + def _liquidity_filter(self, turnover: float, threshold: float) -> bool: + """거래대금 상위 20% 이내 필터 (하위 80% 제외)""" + return turnover >= threshold + + # ------------------------------------------------------------------ + # Scoring + # ------------------------------------------------------------------ + + def _score(self, td: TickerData) -> Optional[ScanResult]: + """RSI + 볼린저 밴드 + 거래량 복합 점수 계산 + + Returns: + ScanResult if 최소 조건 충족, else None + """ + from .mean_reversion_strategy import calculate_rsi, calculate_bollinger + + closes = td.closes + if len(closes) < 25: + return None + + rsi14 = calculate_rsi(closes, 14) + bb = calculate_bollinger(closes, 20) + if rsi14 is None or bb is None: + return None + + _, _, bb_lower = bb + if bb_lower == 0: + return None + + volume_ma20 = sum(td.volumes[-20:]) / 20 if len(td.volumes) >= 20 else None + if volume_ma20 is None or volume_ma20 == 0: + return None + + pct_vs_lower = (td.close - bb_lower) / bb_lower * 100 + volume_ratio = td.volume / volume_ma20 + + rsi_score = max(0.0, (25.0 - rsi14) / 25.0) + bb_score = max(0.0, (0.0 - pct_vs_lower) / 3.0) + volume_score = min(volume_ratio, 2.0) + + total_score = rsi_score * 0.5 + bb_score * 0.3 + volume_score * 0.2 + + if total_score <= 0: + return None + + reason_parts = [] + if rsi14 <= 25: + reason_parts.append(f"RSI({rsi14:.1f})≤25") + if pct_vs_lower <= 0: + reason_parts.append(f"BB하단({pct_vs_lower:.1f}%)") + if volume_ratio >= 1.5: + reason_parts.append(f"거래량({volume_ratio:.1f}x)") + + return ScanResult( + ticker=td.ticker, + score=round(total_score, 4), + rsi14=round(rsi14, 2), + pct_vs_bb_lower=round(pct_vs_lower, 2), + volume_ratio=round(volume_ratio, 2), + close=td.close, + reason=" + ".join(reason_parts) if reason_parts else "복합점수", + ) + + # ------------------------------------------------------------------ + # Helpers + # ------------------------------------------------------------------ + + @staticmethod + def _calc_liquidity_threshold(tickers_data: List[TickerData]) -> float: + """거래대금 상위 20% 임계값 계산 (80번째 백분위수)""" + turnovers = sorted([td.turnover for td in tickers_data if td.turnover > 0]) + if not turnovers: + return 0.0 + idx = int(len(turnovers) * LIQUIDITY_PERCENTILE) + return turnovers[min(idx, len(turnovers) - 1)] diff --git a/backend/app/services/short_term_strategy.py b/backend/app/services/short_term_strategy.py new file mode 100644 index 0000000..cc0afeb --- /dev/null +++ b/backend/app/services/short_term_strategy.py @@ -0,0 +1,208 @@ +""" +15분봉 단기 전략 (CLAUDE.md 섹션 8.2) + +전체 자본의 최대 25% 이내에서 운용. +일봉 RSI ≤ 40 관심 종목에 한해 15분봉 신호를 감지한다. + +⚠️ 슬리피지 주의: 수수료 0.3% + 슬리피지 0.2% 포함 백테스트 필수 +""" +import logging +from dataclasses import dataclass +from enum import Enum +from typing import List, Optional + +from .mean_reversion_strategy import calculate_rsi, calculate_bollinger + +logger = logging.getLogger(__name__) + +# 자본 비율 상한 (CLAUDE.md 섹션 8.2 / 8.3) +MAX_CAPITAL_RATIO = 0.25 + +# 비용 가정 — 백테스트 시 왕복으로 적용 +COMMISSION_RATE = 0.003 # 수수료 0.3% +SLIPPAGE_RATE = 0.002 # 슬리피지 0.2% +TOTAL_COST_RATE = COMMISSION_RATE + SLIPPAGE_RATE + + +class TimeFrame(str, Enum): + M15 = "15m" + H1 = "60m" + + +@dataclass +class EntrySignal: + ticker: str + timeframe: TimeFrame + reason: str + rsi: float + close: float + + +@dataclass +class ExitSignal: + action: str # "SELL_ALL" + reason: str + sell_pct: float # 1.0 = 전량 + + +class ShortTermStrategy: + """15분봉 단기 평균회귀 전략 + + 진입 조건 (모두 충족 필요): + 1. 일봉 RSI(14) ≤ 40 — 관심 종목 사전 필터 + 2. 15분봉 RSI(14) ≤ 30 + 3. 15분봉 종가 ≤ BB 하단 × 1.01 + 4. 15분봉 종가 > MA5 — Fake-out 방지 + 5. 15분봉 거래량 > 거래량 MA20 × 1.3 + + 청산 조건 (둘 중 하나 충족): + - 15분봉 RSI(14) ≥ 55 + - 수익률 ≥ 1.5 × ATR_15m_pct + + 자본 제한: + - 이 전략 배분 총액이 전체 자본 × 25% 초과 시 신규 진입 차단 + - MDD -10% 돌파 시 즉시 중단 (호출 측에서 판단) + """ + + TIMEFRAME = TimeFrame.M15 + DAILY_RSI_THRESHOLD = 40.0 # 일봉 사전 필터 + M15_RSI_ENTRY = 30.0 # 15분봉 진입 RSI + M15_RSI_EXIT = 55.0 # 15분봉 청산 RSI + VOLUME_MULTIPLIER = 1.3 # 거래량 배수 + + def __init__(self, max_capital_ratio: float = MAX_CAPITAL_RATIO): + self.max_capital_ratio = max_capital_ratio + + # ------------------------------------------------------------------ + # Public API + # ------------------------------------------------------------------ + + def entry_signal( + self, + ticker: str, + daily_rsi: float, + m15_closes: List[float], + m15_volumes: List[int], + current_volume: int, + ) -> Optional[EntrySignal]: + """15분봉 진입 신호 감지 + + Args: + ticker: 종목코드 + daily_rsi: 일봉 RSI(14) + m15_closes: 15분봉 종가 리스트 (최소 25개) + m15_volumes: 15분봉 거래량 리스트 (최소 20개) + current_volume: 현재 봉 거래량 + + Returns: + EntrySignal if 모든 조건 충족, else None + """ + # 조건 1: 일봉 사전 필터 + if daily_rsi > self.DAILY_RSI_THRESHOLD: + return None + + if len(m15_closes) < 25 or len(m15_volumes) < 20: + return None + + close = m15_closes[-1] + + # 조건 2: 15분봉 RSI + rsi_15m = calculate_rsi(m15_closes, 14) + if rsi_15m is None or rsi_15m > self.M15_RSI_ENTRY: + return None + + # 조건 3: BB 하단 근처 + bb = calculate_bollinger(m15_closes, 20) + if bb is None: + return None + _, _, bb_lower = bb + if close > bb_lower * 1.01: + return None + + # 조건 4: MA5 상향 (Fake-out 방지) + ma5 = sum(m15_closes[-5:]) / 5 + if close <= ma5: + return None + + # 조건 5: 거래량 급증 + vol_ma20 = sum(m15_volumes[-20:]) / 20 + if vol_ma20 == 0 or current_volume <= vol_ma20 * self.VOLUME_MULTIPLIER: + return None + + return EntrySignal( + ticker=ticker, + timeframe=self.TIMEFRAME, + reason=( + f"15분봉 RSI({rsi_15m:.1f})≤30 + BB하단 + MA5돌파 " + f"+ 거래량({current_volume / vol_ma20:.1f}x)" + ), + rsi=round(rsi_15m, 2), + close=close, + ) + + def exit_signal( + self, + ticker: str, + m15_closes: List[float], + entry_price: float, + atr_15m: Optional[float] = None, + ) -> Optional[ExitSignal]: + """15분봉 청산 신호 + + Args: + ticker: 종목코드 + m15_closes: 15분봉 종가 리스트 + entry_price: 진입 가격 + atr_15m: 15분봉 ATR (None 이면 ATR 조건 스킵) + + Returns: + ExitSignal if 청산 조건 충족, else None + """ + if len(m15_closes) < 15: + return None + + close = m15_closes[-1] + rsi_15m = calculate_rsi(m15_closes, 14) + + # 청산 조건 1: RSI ≥ 55 회복 + if rsi_15m is not None and rsi_15m >= self.M15_RSI_EXIT: + return ExitSignal( + action="SELL_ALL", + reason=f"15분봉 RSI({rsi_15m:.1f})≥55 회복", + sell_pct=1.0, + ) + + # 청산 조건 2: 수익률 ≥ 1.5 × ATR_15m_pct + if atr_15m and entry_price > 0: + profit_pct = (close - entry_price) / entry_price + atr_pct = atr_15m / entry_price + if profit_pct >= 1.5 * atr_pct: + return ExitSignal( + action="SELL_ALL", + reason=( + f"익절 수익률({profit_pct * 100:.1f}%) " + f"≥ 1.5×ATR({atr_pct * 100:.1f}%)" + ), + sell_pct=1.0, + ) + + return None + + def is_capital_available( + self, total_assets: float, current_short_term_allocation: float + ) -> bool: + """단기 전략 자본 한도 초과 여부 확인 + + CLAUDE.md 섹션 8.3: 단기 전략에 최대 25% 배분. + MDD -10% 돌파 시에는 호출 측에서 별도로 차단해야 한다. + """ + limit = total_assets * self.max_capital_ratio + return current_short_term_allocation < limit + + @staticmethod + def net_profit_pct(gross_profit_pct: float) -> float: + """수수료 + 슬리피지 차감 후 순수익률 (왕복 기준) + + 백테스트 결과 검증 시 이 함수로 실질 수익률을 계산하라. + """ + return gross_profit_pct - TOTAL_COST_RATE * 2