feat: 프로젝트 기본 구조 구축

This commit is contained in:
2025-10-04 22:11:32 +09:00
parent 01403c7df4
commit ab99abad8a
36 changed files with 32356 additions and 199 deletions

View File

@@ -3,7 +3,10 @@
"allow": [
"Bash(python manage.py:*)",
"Bash(git init:*)",
"Bash(git add:*)"
"Bash(git add:*)",
"Bash(uv add:*)",
"Bash(python test:*)",
"Bash(python:*)"
],
"deny": [],
"ask": []

66
.dockerignore Normal file
View File

@@ -0,0 +1,66 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# Virtual Environment
.venv/
venv/
ENV/
env/
# Django
*.log
db.sqlite3
db.sqlite3-journal
/staticfiles/
/mediafiles/
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
.DS_Store
# Git
.git/
.gitignore
.gitattributes
# Testing
.coverage
.pytest_cache/
htmlcov/
# Documentation
*.md
!README.md
*.pdf
# Scripts
*.sh
!docker-entrypoint.sh
# Other
.env
.env.*
.claude/

17
.env.example Normal file
View File

@@ -0,0 +1,17 @@
# Django 설정
DJANGO_SECRET_KEY=your-secret-key-here
DJANGO_DEBUG=False
DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1
# 환경 설정
DJANGO_ENV=production
# 데이터베이스 (PostgreSQL 사용 시)
# DATABASE_URL=postgresql://user:password@localhost:5432/dbname
# 정적 파일
STATIC_ROOT=/app/staticfiles
STATIC_URL=/static/
# 타임존
TIME_ZONE=Asia/Seoul

428
API_USAGE_GUIDE.md Normal file
View File

@@ -0,0 +1,428 @@
# BAA 전략 API 사용 가이드
## 시작하기
### 1. 서버 실행
```bash
# 개발 서버 실행
python manage.py runserver
# 서버가 http://localhost:8000 에서 실행됩니다
```
### 2. 준비된 스크립트 사용
세 가지 편리한 스크립트가 제공됩니다:
#### A. 전체 예제 실행 (`baa_api_examples.sh`)
모든 주요 기능을 순차적으로 테스트합니다.
```bash
./baa_api_examples.sh
```
**실행 내용:**
1. 사용 가능한 전략 목록 조회
2. BAA-G12 시뮬레이션 모드 실행
3. BAA-G4 실제 데이터 모드 (현재 날짜)
4. BAA-G12 특정 날짜 기준 (2024-01-31)
#### B. 빠른 테스트 (`quick_baa_test.sh`)
단일 포트폴리오를 빠르게 생성하고 요약 정보를 표시합니다.
```bash
# 기본 실행 (BAA-G4, $50,000, 현재 날짜)
./quick_baa_test.sh
# 전략 변형 지정
./quick_baa_test.sh BAA-G12
# 전략 + 초기 자본 지정
./quick_baa_test.sh BAA-G4 100000
# 전략 + 초기 자본 + 특정 날짜
./quick_baa_test.sh BAA-G12 75000 2024-01-31
```
**출력 예시:**
```
====== 포트폴리오 요약 ======
기준일: 2025-10-04
모드: offensive
카나리아 bad 개수: 0
총 투자액: $49947.08
잔여 현금: $52.92
====== 포트폴리오 구성 ======
VEA: 818주 @ $61.06 = $49947.08
====== 카나리아 상태 ======
SPY: 모멘텀=8.55%, bad=false
VWO: 모멘텀=10.15%, bad=false
VEA: 모멘텀=8.63%, bad=false
BND: 모멘텀=1.32%, bad=false
```
#### C. 개별 curl 명령어 참조 (`BAA_CURL_EXAMPLES.md`)
다양한 curl 명령어 예제와 응답 샘플을 포함한 상세 가이드입니다.
---
## API 엔드포인트
### 1. 전략 구현체 목록 조회
**요청:**
```bash
GET /api/strategies/implementations/
```
**응답:**
```json
{
"available_implementations": {
"BoldAssetAllocation": {
"name": "BoldAssetAllocation",
"description": "상대 모멘텀과 절대 모멘텀을 결합한 공격적 전술적 자산배분 전략",
"versions": [...]
}
}
}
```
### 2. 전략 실행
**요청:**
```bash
POST /api/strategies/execute/
Content-Type: application/json
{
"strategy_name": "BoldAssetAllocation",
"version": "1.0.0",
"parameters": {
"initial_capital": 50000,
"variant": "BAA-G4",
"use_real_data": true,
"as_of_date": null
}
}
```
**응답:**
```json
{
"execution_id": 123,
"status": "pending",
"message": "Strategy execution started"
}
```
### 3. 실행 결과 조회
**요청:**
```bash
GET /api/executions/{execution_id}/
```
**응답 (완료 시):**
```json
{
"execution_id": 123,
"strategy": "BoldAssetAllocation",
"version": "1.0.0",
"status": "completed",
"started_at": "2025-10-04T10:30:00Z",
"completed_at": "2025-10-04T10:30:10Z",
"result": {
"mode": "offensive",
"portfolio": [...],
"canary_status": {...}
}
}
```
---
## 주요 파라미터
| 파라미터 | 필수 | 기본값 | 설명 |
|---------|------|--------|------|
| `initial_capital` | ✓ | 100000 | 초기 자본 (달러) |
| `variant` | ✓ | "BAA-G12" | 전략 변형 (BAA-G12, BAA-G4, BAA-G12/T3, BAA-G4/T2, BAA-SPY) |
| `use_real_data` | ✓ | false | 실제 데이터 사용 여부 |
| `as_of_date` | ✗ | null | 기준일 (YYYY-MM-DD 형식, null이면 현재) |
| `offensive_top` | ✗ | 전략별 | 공격 유니버스에서 선택할 자산 수 |
| `defensive_top` | ✗ | 3 | 방어 유니버스에서 선택할 자산 수 |
| `breadth_param` | ✗ | 1 | 카나리아 임계값 |
---
## 전략 변형별 특징
### BAA-G12 (Balanced)
- **추천 대상**: 중위험 선호, 분산 투자
- **자산 수**: 12개 (주식 7 + 대체자산 3 + 채권 2)
- **선택**: Top 6
- **기대 CAGR**: ~14.6%
- **기대 MaxDD**: ~8.7%
```bash
./quick_baa_test.sh BAA-G12 100000
```
### BAA-G4 (Aggressive)
- **추천 대상**: 고위험 선호, 집중 투자
- **자산 수**: 4개 (QQQ, VWO, VEA, BND)
- **선택**: Top 1
- **기대 CAGR**: ~21.0%
- **기대 MaxDD**: ~14.6%
```bash
./quick_baa_test.sh BAA-G4 100000
```
### BAA-G12/T3
- **추천 대상**: BAA-G12와 G4의 중간
- **자산 수**: 12개
- **선택**: Top 3
- **기대 CAGR**: ~16.4%
- **기대 MaxDD**: ~11.4%
```bash
./quick_baa_test.sh BAA-G12/T3 100000
```
### BAA-G4/T2
- **추천 대상**: 적당한 공격성
- **자산 수**: 4개
- **선택**: Top 2
- **기대 CAGR**: ~17.7%
- **기대 MaxDD**: ~12.7%
```bash
./quick_baa_test.sh BAA-G4/T2 100000
```
### BAA-SPY
- **추천 대상**: 단순함 선호, SPY 기반
- **자산 수**: 1개 (SPY)
- **선택**: SPY만 사용
- **기대 CAGR**: ~11.4%
- **기대 MaxDD**: ~16.2%
```bash
./quick_baa_test.sh BAA-SPY 100000
```
---
## 실행 모드
### 시뮬레이션 모드 (`use_real_data: false`)
- **목적**: 백테스트 결과 확인
- **데이터**: 1970-2022년 통계 기반
- **실행 시간**: ~2초
- **결과**: CAGR, Sharpe Ratio, Max DD 등 성과 지표
```bash
curl -X POST http://localhost:8000/api/strategies/execute/ \
-H "Content-Type: application/json" \
-d '{
"strategy_name": "BoldAssetAllocation",
"parameters": {
"variant": "BAA-G12",
"use_real_data": false
}
}'
```
### 실제 데이터 모드 (`use_real_data: true`)
- **목적**: 실제 포트폴리오 제안
- **데이터**: yfinance API를 통한 실시간 가격
- **실행 시간**: ~5-15초 (데이터 다운로드 포함)
- **결과**: 티커별 매수 수량, 현재가, 배분액
```bash
curl -X POST http://localhost:8000/api/strategies/execute/ \
-H "Content-Type: application/json" \
-d '{
"strategy_name": "BoldAssetAllocation",
"parameters": {
"variant": "BAA-G4",
"initial_capital": 50000,
"use_real_data": true
}
}'
```
---
## 포트폴리오 해석
### 공격 모드 (Offensive)
카나리아 자산이 모두 양호할 때 활성화됩니다.
**특징:**
- 주식 및 성장 자산에 투자
- 높은 수익 잠재력
- 선택된 자산에 동일 가중 배분
**예시:**
```json
{
"mode": "offensive",
"canary_bad_count": 0,
"portfolio": [
{"ticker": "VEA", "weight": 100.0, "shares": 818}
]
}
```
### 방어 모드 (Defensive)
카나리아 자산 중 1개 이상이 음수 모멘텀일 때 활성화됩니다.
**특징:**
- 채권 및 안전 자산으로 전환
- 크래시 보호
- Top 3 방어 자산 선택
**예시:**
```json
{
"mode": "defensive",
"canary_bad_count": 1,
"portfolio": [
{"ticker": "DBC", "weight": 33.33, "shares": 1555},
{"ticker": "TLT", "weight": 33.33, "shares": 374},
{"ticker": "LQD", "weight": 33.33, "shares": 327}
]
}
```
---
## 워크플로우 예시
### 월간 리밸런싱 워크플로우
```bash
#!/bin/bash
# monthly_rebalance.sh
# 1. 현재 포트폴리오 생성
EXEC_ID=$(curl -s -X POST http://localhost:8000/api/strategies/execute/ \
-H "Content-Type: application/json" \
-d '{
"strategy_name": "BoldAssetAllocation",
"parameters": {
"initial_capital": 100000,
"variant": "BAA-G12",
"use_real_data": true
}
}' | jq -r '.execution_id')
# 2. 실행 완료 대기
sleep 10
# 3. 결과 조회 및 저장
DATE=$(date +%Y%m%d)
curl -s http://localhost:8000/api/executions/${EXEC_ID}/ \
| jq '.result' > portfolio_${DATE}.json
# 4. 매수 주문 생성 (예시)
jq -r '.portfolio[] | "BUY \(.shares) \(.ticker) @ \(.current_price)"' \
portfolio_${DATE}.json > orders_${DATE}.txt
echo "Portfolio saved to portfolio_${DATE}.json"
echo "Orders saved to orders_${DATE}.txt"
```
---
## 트러블슈팅
### 1. "execution_id": null
**원인**: 전략 이름 또는 파라미터 오류
**해결:**
```bash
# 사용 가능한 전략 확인
curl http://localhost:8000/api/strategies/implementations/
```
### 2. "status": "failed"
**원인**: 파라미터 검증 실패 또는 데이터 다운로드 오류
**확인:**
```bash
curl http://localhost:8000/api/executions/{execution_id}/ | jq '.error_message'
```
### 3. 데이터 다운로드 실패
**원인**: 네트워크 또는 yfinance API 문제
**해결:**
- 인터넷 연결 확인
- 잠시 후 재시도
- 다른 날짜로 시도
### 4. 서버 응답 없음
**확인:**
```bash
# 서버 상태 확인
curl http://localhost:8000/api/strategies/implementations/
# Django 서버 로그 확인
python manage.py runserver # 콘솔 출력 확인
```
---
## 성능 최적화
### 1. 병렬 실행
여러 전략을 동시에 실행할 수 있습니다:
```bash
# 동시 실행
curl -X POST http://localhost:8000/api/strategies/execute/ \
-d '{"strategy_name":"BoldAssetAllocation","parameters":{"variant":"BAA-G12","use_real_data":true}}' &
curl -X POST http://localhost:8000/api/strategies/execute/ \
-d '{"strategy_name":"BoldAssetAllocation","parameters":{"variant":"BAA-G4","use_real_data":true}}' &
wait
```
### 2. 캐싱 (향후 구현 예정)
동일한 날짜의 반복 요청은 캐시된 데이터를 사용할 수 있습니다.
---
## 보안 고려사항
### 프로덕션 환경
- `@csrf_exempt` 제거 및 CSRF 토큰 사용
- API 키 인증 추가
- HTTPS 사용
- Rate limiting 적용
---
## 다음 단계
1. **자동화**: cron을 사용한 월간 자동 리밸런싱
2. **알림**: 이메일/슬랙 알림 설정
3. **대시보드**: 웹 UI 추가
4. **백테스트**: 과거 날짜 범위에 대한 성과 분석
---
## 지원
문제가 발생하면 다음을 확인하세요:
- Django 서버 로그
- `BAA_CURL_EXAMPLES.md` 예제
- `BAA_STRATEGY_README.md` 전략 설명

494
BAA_CURL_EXAMPLES.md Normal file
View File

@@ -0,0 +1,494 @@
# BAA 전략 API curl 예제
## 서버 실행
```bash
python manage.py runserver
```
기본 URL: `http://localhost:8000/api`
---
## 1. 사용 가능한 전략 구현체 목록 조회
```bash
curl -X GET http://localhost:8000/api/strategies/implementations/ \
-H "Content-Type: application/json" | jq '.'
```
**응답 예시:**
```json
{
"available_implementations": {
"BoldAssetAllocation": {
"name": "BoldAssetAllocation",
"description": "상대 모멘텀과 절대 모멘텀을 결합한 공격적 전술적 자산배분 전략. 카나리아 유니버스 기반 크래시 보호",
"versions": [
{
"version": "1.0.0",
"default_parameters": {
"initial_capital": 100000,
"variant": "BAA-G12",
"offensive_top": 6,
"defensive_top": 3,
"breadth_param": 1,
"transaction_cost": 0.001,
"as_of_date": null,
"use_real_data": false
}
}
]
}
}
}
```
---
## 2. BAA-G12 시뮬레이션 모드 (백테스트 결과)
### 전략 실행 요청
```bash
curl -X POST http://localhost:8000/api/strategies/execute/ \
-H "Content-Type: application/json" \
-d '{
"strategy_name": "BoldAssetAllocation",
"version": "1.0.0",
"parameters": {
"initial_capital": 100000,
"variant": "BAA-G12",
"use_real_data": false
}
}' | jq '.'
```
**응답:**
```json
{
"execution_id": 1,
"status": "pending",
"message": "Strategy execution started"
}
```
### 실행 결과 조회
```bash
curl -X GET http://localhost:8000/api/executions/1/ | jq '.'
```
**응답 예시:**
```json
{
"execution_id": 1,
"strategy": "BoldAssetAllocation",
"version": "1.0.0",
"status": "completed",
"started_at": "2025-10-04T10:30:00Z",
"completed_at": "2025-10-04T10:30:02Z",
"execution_parameters": {
"initial_capital": 100000,
"variant": "BAA-G12",
"use_real_data": false
},
"result": {
"strategy": "BoldAssetAllocation",
"version": "1.0.0",
"variant": "BAA-G12",
"cagr": 14.6,
"max_drawdown": 8.7,
"sharpe_ratio": 1.19,
"upi": 4.81,
"profit_loss": 14600.0,
"final_capital": 114600.0
}
}
```
---
## 3. BAA-G4 실제 데이터 모드 (현재 날짜 기준 포트폴리오)
```bash
curl -X POST http://localhost:8000/api/strategies/execute/ \
-H "Content-Type: application/json" \
-d '{
"strategy_name": "BoldAssetAllocation",
"version": "1.0.0",
"parameters": {
"initial_capital": 50000,
"variant": "BAA-G4",
"use_real_data": true,
"as_of_date": null
}
}' | jq '.'
```
**응답:**
```json
{
"execution_id": 2,
"status": "pending",
"message": "Strategy execution started"
}
```
### 결과 조회 (약 10초 후)
```bash
curl -X GET http://localhost:8000/api/executions/2/ | jq '.'
```
**응답 예시:**
```json
{
"execution_id": 2,
"strategy": "BoldAssetAllocation",
"version": "1.0.0",
"status": "completed",
"result": {
"strategy": "BoldAssetAllocation",
"version": "1.0.0",
"variant": "BAA-G4",
"mode": "offensive",
"as_of_date": "2025-10-04",
"canary_status": {
"SPY": {
"momentum": 0.0854,
"is_bad": false
},
"VWO": {
"momentum": 0.1015,
"is_bad": false
},
"VEA": {
"momentum": 0.0863,
"is_bad": false
},
"BND": {
"momentum": 0.0132,
"is_bad": false
}
},
"canary_bad_count": 0,
"breadth_threshold": 1,
"portfolio": [
{
"ticker": "VEA",
"weight": 100.0,
"target_amount": 50000.0,
"current_price": 61.06,
"shares": 818,
"actual_amount": 49947.08
}
],
"total_allocated": 49947.08,
"cash_remaining": 52.92,
"initial_capital": 50000
}
}
```
---
## 4. 특정 날짜 기준 포트폴리오 (2024-01-31)
```bash
curl -X POST http://localhost:8000/api/strategies/execute/ \
-H "Content-Type: application/json" \
-d '{
"strategy_name": "BoldAssetAllocation",
"version": "1.0.0",
"parameters": {
"initial_capital": 100000,
"variant": "BAA-G12",
"use_real_data": true,
"as_of_date": "2024-01-31"
}
}' | jq '.'
```
**결과 조회:**
```bash
curl -X GET http://localhost:8000/api/executions/3/ | jq '.'
```
**응답 예시 (방어 모드):**
```json
{
"execution_id": 3,
"strategy": "BoldAssetAllocation",
"version": "1.0.0",
"status": "completed",
"result": {
"mode": "defensive",
"as_of_date": "2024-01-31",
"canary_status": {
"SPY": {
"momentum": 0.0815,
"is_bad": false
},
"VWO": {
"momentum": -0.0110,
"is_bad": true
},
"VEA": {
"momentum": 0.0350,
"is_bad": false
},
"BND": {
"momentum": 0.0169,
"is_bad": false
}
},
"canary_bad_count": 1,
"portfolio": [
{
"ticker": "DBC",
"weight": 33.33,
"current_price": 21.43,
"shares": 1555,
"actual_amount": 33317.63
},
{
"ticker": "TLT",
"weight": 33.33,
"current_price": 89.0,
"shares": 374,
"actual_amount": 33287.61
},
{
"ticker": "LQD",
"weight": 33.33,
"current_price": 101.69,
"shares": 327,
"actual_amount": 33253.17
}
],
"total_allocated": 99858.4,
"cash_remaining": 141.6
}
}
```
---
## 5. 모든 전략 변형 예제
### BAA-G12 (Balanced)
```bash
curl -X POST http://localhost:8000/api/strategies/execute/ \
-H "Content-Type: application/json" \
-d '{
"strategy_name": "BoldAssetAllocation",
"parameters": {
"variant": "BAA-G12",
"initial_capital": 100000,
"use_real_data": true
}
}'
```
### BAA-G4 (Aggressive)
```bash
curl -X POST http://localhost:8000/api/strategies/execute/ \
-H "Content-Type: application/json" \
-d '{
"strategy_name": "BoldAssetAllocation",
"parameters": {
"variant": "BAA-G4",
"initial_capital": 100000,
"use_real_data": true
}
}'
```
### BAA-G12/T3
```bash
curl -X POST http://localhost:8000/api/strategies/execute/ \
-H "Content-Type: application/json" \
-d '{
"strategy_name": "BoldAssetAllocation",
"parameters": {
"variant": "BAA-G12/T3",
"initial_capital": 100000,
"use_real_data": true
}
}'
```
### BAA-G4/T2
```bash
curl -X POST http://localhost:8000/api/strategies/execute/ \
-H "Content-Type: application/json" \
-d '{
"strategy_name": "BoldAssetAllocation",
"parameters": {
"variant": "BAA-G4/T2",
"initial_capital": 100000,
"use_real_data": true
}
}'
```
### BAA-SPY
```bash
curl -X POST http://localhost:8000/api/strategies/execute/ \
-H "Content-Type: application/json" \
-d '{
"strategy_name": "BoldAssetAllocation",
"parameters": {
"variant": "BAA-SPY",
"initial_capital": 100000,
"use_real_data": true
}
}'
```
---
## 6. 파라미터 커스터마이징
### 예산 변경
```bash
curl -X POST http://localhost:8000/api/strategies/execute/ \
-H "Content-Type: application/json" \
-d '{
"strategy_name": "BoldAssetAllocation",
"parameters": {
"initial_capital": 250000,
"variant": "BAA-G4",
"use_real_data": true
}
}'
```
### 특정 날짜 + 변형 조합
```bash
curl -X POST http://localhost:8000/api/strategies/execute/ \
-H "Content-Type: application/json" \
-d '{
"strategy_name": "BoldAssetAllocation",
"parameters": {
"initial_capital": 150000,
"variant": "BAA-G12/T3",
"use_real_data": true,
"as_of_date": "2024-06-30"
}
}'
```
---
## 7. 비동기 실행 패턴
### 1단계: 실행 시작
```bash
EXEC_ID=$(curl -s -X POST http://localhost:8000/api/strategies/execute/ \
-H "Content-Type: application/json" \
-d '{
"strategy_name": "BoldAssetAllocation",
"parameters": {
"initial_capital": 50000,
"variant": "BAA-G4",
"use_real_data": true
}
}' | jq -r '.execution_id')
echo "Execution ID: $EXEC_ID"
```
### 2단계: 상태 폴링
```bash
while true; do
STATUS=$(curl -s http://localhost:8000/api/executions/${EXEC_ID}/ | jq -r '.status')
echo "Status: $STATUS"
if [ "$STATUS" = "completed" ] || [ "$STATUS" = "failed" ]; then
break
fi
sleep 2
done
```
### 3단계: 최종 결과 조회
```bash
curl -s http://localhost:8000/api/executions/${EXEC_ID}/ | jq '.result'
```
---
## 8. 에러 처리
### 잘못된 variant
```bash
curl -X POST http://localhost:8000/api/strategies/execute/ \
-H "Content-Type: application/json" \
-d '{
"strategy_name": "BoldAssetAllocation",
"parameters": {
"variant": "INVALID",
"use_real_data": true
}
}'
```
**응답:**
```json
{
"execution_id": 4,
"status": "failed",
"error_message": "Invalid parameters"
}
```
### 누락된 필수 파라미터
```bash
curl -X POST http://localhost:8000/api/strategies/execute/ \
-H "Content-Type: application/json" \
-d '{
"strategy_name": "BoldAssetAllocation",
"parameters": {}
}'
```
---
## 9. 원라인 실행 + 결과 조회
```bash
# 실행하고 10초 후 자동으로 결과 조회
EXEC_ID=$(curl -s -X POST http://localhost:8000/api/strategies/execute/ \
-H "Content-Type: application/json" \
-d '{"strategy_name":"BoldAssetAllocation","parameters":{"initial_capital":50000,"variant":"BAA-G4","use_real_data":true}}' \
| jq -r '.execution_id') && \
echo "Execution ID: $EXEC_ID" && \
sleep 10 && \
curl -s http://localhost:8000/api/executions/${EXEC_ID}/ | jq '.'
```
---
## 참고사항
1. **jq 설치**: JSON 포맷팅을 위해 `jq` 설치 권장
```bash
# macOS
brew install jq
# Ubuntu/Debian
sudo apt-get install jq
```
2. **실행 시간**:
- 시뮬레이션 모드: 약 2초
- 실제 데이터 모드: 약 5-15초 (데이터 다운로드 시간 포함)
3. **날짜 형식**: `YYYY-MM-DD` (예: `2024-01-31`)
4. **초기 자본**: 달러($) 단위
5. **매수 수량**: 정수 주식만 매수, 잔액은 현금 보유

227
BAA_STRATEGY_README.md Normal file
View File

@@ -0,0 +1,227 @@
# Bold Asset Allocation (BAA) 전략 구현
## 개요
Bold Asset Allocation (BAA)는 상대 모멘텀과 절대 모멘텀을 결합한 공격적 전술적 자산배분 전략입니다. Wouter J. Keller의 논문 "Relative and Absolute Momentum in Times of Rising/Low Yields" (2022)를 기반으로 구현되었습니다.
## 주요 특징
### 1. 이중 모멘텀 시스템
- **상대 모멘텀 (SMA12)**: 느린 모멘텀 필터로 공격/방어 유니버스에서 자산 선택
- **절대 모멘텀 (13612W)**: 빠른 모멘텀 필터로 카나리아 유니버스의 크래시 보호
### 2. 카나리아 유니버스 기반 크래시 보호
- 카나리아 유니버스: SPY, VWO, VEA, BND
- Breadth Parameter (B=1): 카나리아 자산 중 **하나라도** 음수 모멘텀이면 방어 모드로 전환
- 결과: 약 60%의 시간을 방어 모드에서 운용
### 3. 다양한 전략 변형
#### BAA-G12 (Balanced)
- **공격 유니버스**: 12개 글로벌 자산 (주식 7 + 대체자산 3 + 채권 2)
- **선택**: Top 6 자산
- **성과** (Dec 1970 - Jun 2022):
- CAGR: 14.6%
- Max DD: 8.7%
- Sharpe Ratio: 1.19
- Defensive Fraction: 57.2%
#### BAA-G4 (Aggressive)
- **공격 유니버스**: 4개 글로벌 자산 (QQQ, VWO, VEA, BND)
- **선택**: Top 1 자산
- **성과** (Dec 1970 - Jun 2022):
- CAGR: 21.0%
- Max DD: 14.6%
- Sharpe Ratio: 1.21
- Defensive Fraction: 57.2%
#### BAA-G12/T3
- BAA-G12와 동일하나 Top 3 선택
#### BAA-G4/T2
- BAA-G4와 동일하나 Top 2 선택
#### BAA-SPY
- 공격 유니버스: SPY만 사용
- 방어/카나리아는 동일
### 4. 방어 유니버스
- **자산**: TIP, DBC, BIL, IEF, TLT, LQD, BND
- **특징**: 인플레이션 보호 채권(TIP) + 원자재(DBC) 포함
- **선택**: Top 3 자산 (SMA12 모멘텀 기준)
- **보호**: BIL보다 모멘텀이 낮은 자산은 BIL로 교체
## 사용 방법
### 1. 시뮬레이션 모드 (백테스트 결과)
```python
from strategies.implementations import BoldAssetAllocation
strategy = BoldAssetAllocation()
result = strategy.execute({
"initial_capital": 100000,
"variant": "BAA-G12", # 또는 "BAA-G4", "BAA-SPY" 등
"use_real_data": False
})
print(result)
```
### 2. 실제 데이터 모드 (포트폴리오 제안)
```python
strategy = BoldAssetAllocation()
# 현재 날짜 기준
result = strategy.execute({
"initial_capital": 50000, # $50,000
"variant": "BAA-G4",
"use_real_data": True,
"as_of_date": None, # 현재 날짜
})
# 결과 예시:
# {
# "mode": "offensive", # 또는 "defensive"
# "canary_status": {
# "SPY": {"momentum": 0.085, "is_bad": false},
# ...
# },
# "portfolio": [
# {
# "ticker": "VEA",
# "weight": 100.0,
# "current_price": 61.06,
# "shares": 818,
# "actual_amount": 49947.08
# }
# ],
# "cash_remaining": 52.92
# }
```
### 3. 특정 날짜 기준 포트폴리오
```python
result = strategy.execute({
"initial_capital": 100000,
"variant": "BAA-G12",
"use_real_data": True,
"as_of_date": "2024-01-31", # YYYY-MM-DD 형식
})
```
## 파라미터 설명
| 파라미터 | 기본값 | 설명 |
|---------|--------|------|
| `initial_capital` | 100000 | 초기 자본 (달러) |
| `variant` | "BAA-G12" | 전략 변형 선택 |
| `offensive_top` | 6 | 공격 유니버스에서 선택할 자산 수 |
| `defensive_top` | 3 | 방어 유니버스에서 선택할 자산 수 |
| `breadth_param` | 1 | 카나리아 자산 중 몇 개가 bad일 때 방어 전환 |
| `transaction_cost` | 0.001 | 거래 비용 (0.1%) |
| `use_real_data` | False | 실제 데이터 사용 여부 |
| `as_of_date` | None | 기준일 (None이면 현재, "YYYY-MM-DD" 형식) |
## 모멘텀 계산 방식
### SMA(12) 상대 모멘텀
```
momentum = (현재가 / SMA(13)) - 1
```
- SMA(13): 최근 13개월 종가의 단순이동평균 (현재 포함)
- 자산 순위 매기기에 사용
### 13612W 절대 모멘텀
```
momentum = (12×RET(1m) + 4×RET(3m) + 2×RET(6m) + 1×RET(12m)) / 19
```
- 1개월 수익률에 가장 높은 가중치 (40%)
- 음수/양수 여부로 크래시 보호 신호 판단
## 월간 리밸런싱 프로세스
1. **카나리아 체크**: 13612W 모멘텀으로 카나리아 유니버스 평가
2. **모드 결정**: B=1이므로 카나리아 자산 중 1개라도 bad면 방어 모드
3. **자산 선택**:
- **공격 모드**: SMA(12) 상위 자산 선택
- **방어 모드**: SMA(12) 상위 자산 선택 + BIL 보호
4. **동일 가중**: 선택된 자산에 동일 비중 배분
5. **매수 수량 계산**: 정수 주식만 매수, 잔액은 현금 보유
## 실행 결과 예시
### 공격 모드 (2025-10-04)
```json
{
"mode": "offensive",
"canary_bad_count": 0,
"portfolio": [
{
"ticker": "VEA",
"weight": 100.0,
"shares": 818,
"actual_amount": 49947.08
}
]
}
```
### 방어 모드 (2024-01-31)
```json
{
"mode": "defensive",
"canary_bad_count": 1,
"canary_status": {
"VWO": {
"momentum": -0.011,
"is_bad": true
}
},
"portfolio": [
{"ticker": "DBC", "weight": 33.33, "shares": 1555},
{"ticker": "TLT", "weight": 33.33, "shares": 374},
{"ticker": "LQD", "weight": 33.33, "shares": 327}
]
}
```
## 성과 지표
### Keller Ratio (K)
```
K = R × (1 - 2D) / (1 - 2D) when D < 25%
```
- R: CAGR
- D: Maximum Drawdown
- 작은 낙폭에는 적게, 큰 낙폭에는 많이 패널티
### 기타 지표
- **UPI** (Ulcer Performance Index): Sharpe Ratio의 변형, 낙폭 기반
- **Sharpe Ratio**: 전통적 위험 조정 수익률
- **Defensive Fraction**: 방어 모드 비율
## 60/40 벤치마크 대비 성과
| 지표 | BAA-G12 | BAA-G4 | 60/40 |
|------|---------|--------|-------|
| CAGR | 14.6% | 21.0% | 9.5% |
| Max DD | 8.7% | 14.6% | 29.5% |
| Sharpe | 1.19 | 1.21 | - |
| Volatility | 8.5% | 13.6% | - |
## 테스트
```bash
python test_baa.py
```
## 참고 문헌
Keller, W. J. (2022). "Relative and Absolute Momentum in Times of Rising/Low Yields: Bold Asset Allocation (BAA)", SSRN 4166845
## 라이선스
이 구현은 교육 및 연구 목적으로만 사용되어야 합니다.

File diff suppressed because it is too large Load Diff

451
CALLBACK_API_GUIDE.md Normal file
View File

@@ -0,0 +1,451 @@
# 콜백 API 가이드
## 📋 개요
전략 실행 시 `callback_url` 파라미터를 지정하면, 전략 실행이 완료된 후 해당 URL로 결과를 POST 메서드로 자동 전송합니다.
---
## 🚀 사용 방법
### 1. 기본 사용 예제
```bash
curl -X POST http://localhost:8000/strategies/execute/ \
-H "Content-Type: application/json" \
-d '{
"strategy_name": "BoldAssetAllocation",
"parameters": {
"variant": "BAA-G4",
"initial_capital": 100000
},
"callback_url": "https://your-server.com/webhook/strategy-result"
}'
```
**응답:**
```json
{
"execution_id": 123,
"status": "pending",
"message": "Strategy execution started",
"callback_url": "https://your-server.com/webhook/strategy-result"
}
```
### 2. 콜백 URL 없이 실행 (기존 방식)
```bash
curl -X POST http://localhost:8000/strategies/execute/ \
-H "Content-Type: application/json" \
-d '{
"strategy_name": "MovingAverageCrossover",
"parameters": {
"short_window": 20,
"long_window": 50
}
}'
```
---
## 📨 콜백 페이로드
전략 실행이 완료되면 다음과 같은 JSON 데이터가 `callback_url`로 POST 요청됩니다.
### 성공 시 (status: completed)
```json
{
"execution_id": 123,
"strategy": "BoldAssetAllocation",
"version": "1.0.0",
"status": "completed",
"started_at": "2025-10-04T12:00:00.000Z",
"completed_at": "2025-10-04T12:00:15.000Z",
"execution_parameters": {
"variant": "BAA-G4",
"initial_capital": 100000,
"use_real_data": false
},
"result": {
"strategy": "BoldAssetAllocation",
"variant": "BAA-G4",
"portfolio": {
"offensive_assets": ["VTI", "VEA"],
"weights": {
"VTI": 0.5,
"VEA": 0.5
}
},
"final_value": 125000,
"return": 0.25
}
}
```
### 실패 시 (status: failed)
```json
{
"execution_id": 124,
"strategy": "BoldAssetAllocation",
"version": "1.0.0",
"status": "failed",
"started_at": "2025-10-04T12:05:00.000Z",
"completed_at": "2025-10-04T12:05:02.000Z",
"execution_parameters": {
"variant": "INVALID"
},
"error_message": "Unknown variant: INVALID. Available variants: BAA-G12, BAA-G4, ..."
}
```
---
## 🔍 실행 상태 확인
콜백 전송 정보를 포함한 실행 상태 조회:
```bash
curl http://localhost:8000/executions/123/
```
**응답:**
```json
{
"execution_id": 123,
"strategy": "BoldAssetAllocation",
"version": "1.0.0",
"status": "completed",
"started_at": "2025-10-04T12:00:00.000Z",
"completed_at": "2025-10-04T12:00:15.000Z",
"execution_parameters": {...},
"result": {...},
"callback": {
"url": "https://your-server.com/webhook/strategy-result",
"sent": true,
"sent_at": "2025-10-04T12:00:15.500Z",
"response": {
"status_code": 200,
"response_text": "{\"status\": \"received\"}",
"headers": {
"Content-Type": "application/json"
}
}
}
}
```
---
## 🛠️ 콜백 서버 구현 예제
### Python (Flask)
```python
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/webhook/strategy-result', methods=['POST'])
def strategy_callback():
data = request.json
execution_id = data['execution_id']
status = data['status']
if status == 'completed':
result = data['result']
print(f"전략 실행 완료 (ID: {execution_id})")
print(f"최종 수익률: {result.get('return', 0) * 100:.2f}%")
# 결과를 데이터베이스에 저장하거나 다른 처리
# save_to_database(data)
elif status == 'failed':
error_message = data['error_message']
print(f"전략 실행 실패 (ID: {execution_id}): {error_message}")
# 에러 알림 전송
# send_error_notification(data)
return jsonify({'status': 'received'}), 200
if __name__ == '__main__':
app.run(port=8888)
```
### Node.js (Express)
```javascript
const express = require('express');
const app = express();
app.use(express.json());
app.post('/webhook/strategy-result', (req, res) => {
const { execution_id, status, result, error_message } = req.body;
if (status === 'completed') {
console.log(`전략 실행 완료 (ID: ${execution_id})`);
console.log(`최종 수익률: ${(result.return * 100).toFixed(2)}%`);
// 결과 처리
// saveToDatabase(req.body);
} else if (status === 'failed') {
console.error(`전략 실행 실패 (ID: ${execution_id}): ${error_message}`);
// 에러 처리
// sendErrorNotification(req.body);
}
res.json({ status: 'received' });
});
app.listen(8888, () => {
console.log('콜백 서버 시작: http://localhost:8888');
});
```
### Django View
```python
from django.views.decorators.csrf import csrf_exempt
from django.http import JsonResponse
import json
@csrf_exempt
def strategy_callback(request):
if request.method == 'POST':
data = json.loads(request.body)
execution_id = data['execution_id']
status = data['status']
if status == 'completed':
# 결과 처리
result = data['result']
print(f"전략 실행 완료: {result}")
elif status == 'failed':
# 에러 처리
error = data['error_message']
print(f"전략 실행 실패: {error}")
return JsonResponse({'status': 'received'})
return JsonResponse({'error': 'Method not allowed'}, status=405)
```
---
## 🧪 테스트 방법
### 1. 자동 테스트 스크립트 사용
```bash
python test_callback.py
```
이 스크립트는:
- 로컬에 콜백 수신 서버를 자동으로 시작
- 전략 실행 요청 (콜백 URL 포함)
- 콜백 수신 및 결과 출력
### 2. 수동 테스트
**터미널 1 - 콜백 서버:**
```bash
python -m http.server 8888
```
**터미널 2 - 전략 실행:**
```bash
curl -X POST http://localhost:8000/strategies/execute/ \
-H "Content-Type: application/json" \
-d '{
"strategy_name": "MovingAverageCrossover",
"parameters": {
"short_window": 10,
"long_window": 30
},
"callback_url": "http://localhost:8888/callback"
}'
```
### 3. 외부 테스트 도구
**webhook.site 사용:**
1. https://webhook.site 방문
2. 생성된 고유 URL 복사
3. 해당 URL을 `callback_url`로 사용
```bash
curl -X POST http://localhost:8000/strategies/execute/ \
-H "Content-Type: application/json" \
-d '{
"strategy_name": "BoldAssetAllocation",
"parameters": {"variant": "BAA-G4"},
"callback_url": "https://webhook.site/your-unique-id"
}'
```
4. webhook.site에서 실시간으로 수신된 데이터 확인
---
## ⚙️ 설정 및 동작
### 콜백 타임아웃
콜백 POST 요청은 **10초 타임아웃**이 설정되어 있습니다.
### 재시도 정책
현재 콜백은 **1회만 시도**합니다. 실패 시 재시도하지 않습니다.
실패 시 `callback_response`에 에러 정보가 저장됩니다:
```json
{
"callback": {
"url": "https://unavailable.com/webhook",
"sent": false,
"sent_at": null,
"response": {
"error": "Connection refused"
}
}
}
```
### 보안 고려사항
1. **HTTPS 사용 권장**: 프로덕션 환경에서는 HTTPS 콜백 URL 사용
2. **서명 검증**: 콜백 요청의 진위 확인을 위해 서명 메커니즘 고려
3. **IP 화이트리스트**: 알려진 IP에서만 콜백 수신
4. **Rate Limiting**: 과도한 요청 방지
---
## 📊 활용 사례
### 1. Slack 알림
```python
import requests
@app.route('/webhook/strategy-result', methods=['POST'])
def strategy_callback():
data = request.json
if data['status'] == 'completed':
result = data['result']
message = f"✅ 전략 실행 완료\n" \
f"전략: {data['strategy']}\n" \
f"수익률: {result.get('return', 0) * 100:.2f}%"
else:
message = f"❌ 전략 실행 실패\n" \
f"오류: {data['error_message']}"
# Slack Webhook
requests.post(
'https://hooks.slack.com/services/YOUR/WEBHOOK/URL',
json={'text': message}
)
return jsonify({'status': 'ok'})
```
### 2. 이메일 전송
```python
from django.core.mail import send_mail
@csrf_exempt
def strategy_callback(request):
data = json.loads(request.body)
if data['status'] == 'completed':
subject = f"전략 실행 완료: {data['strategy']}"
message = f"수익률: {data['result']['return'] * 100:.2f}%"
send_mail(
subject,
message,
'noreply@yoursite.com',
['admin@yoursite.com'],
)
return JsonResponse({'status': 'sent'})
```
### 3. 데이터베이스 저장
```python
@csrf_exempt
def strategy_callback(request):
data = json.loads(request.body)
StrategyResult.objects.create(
execution_id=data['execution_id'],
strategy_name=data['strategy'],
status=data['status'],
result=data.get('result'),
error=data.get('error_message'),
completed_at=data.get('completed_at')
)
return JsonResponse({'status': 'saved'})
```
---
## 🐛 문제 해결
### 콜백이 전송되지 않음
**확인사항:**
1. `callback_url`이 올바른 형식인지 확인
2. 콜백 서버가 실행 중인지 확인
3. 방화벽/보안 그룹 설정 확인
4. 실행 상태 API에서 `callback.response` 확인
```bash
curl http://localhost:8000/executions/{ID}/ | python -m json.tool
```
### 타임아웃 발생
콜백 서버의 응답 시간이 10초를 초과하는 경우:
**해결책:**
- 콜백 핸들러에서 빠르게 200 응답 후 백그라운드 처리
- 타임아웃 값 조정 필요 시 `views.py``timeout=10` 수정
### localhost 콜백이 작동하지 않음
Docker 컨테이너 내부에서 실행 중인 경우:
**해결책:**
- `localhost` 대신 호스트 IP 사용
- Docker: `host.docker.internal` 사용 (Mac/Windows)
- ngrok 등으로 로컬 서버 외부 노출
---
## 📚 참고
- **API 전체 가이드**: `API_USAGE_GUIDE.md`
- **BAA 전략 설명**: `BAA_STRATEGY_README.md`
- **배포 가이드**: `DEPLOYMENT_GUIDE.md`
---
## 🔄 업데이트 로그
| 날짜 | 변경 내용 |
|------|-----------|
| 2025-10-04 | 콜백 기능 최초 구현 |

448
DEPLOYMENT_GUIDE.md Normal file
View File

@@ -0,0 +1,448 @@
# Docker 배포 가이드
## 📋 목차
1. [사전 요구사항](#사전-요구사항)
2. [빠른 시작](#빠른-시작)
3. [배포 파일 구조](#배포-파일-구조)
4. [배포 방법](#배포-방법)
5. [설정](#설정)
6. [유용한 명령어](#유용한-명령어)
7. [문제 해결](#문제-해결)
---
## 🔧 사전 요구사항
### 필수 소프트웨어
- **Docker**: 20.10 이상
- **Docker Compose**: 2.0 이상
### 설치 확인
```bash
docker --version
docker-compose --version
```
---
## 🚀 빠른 시작
### 1. 자동 배포 스크립트 사용
```bash
./deploy.sh
```
스크립트 실행 후 배포 모드 선택:
- `1` - Production (포트 8000)
- `2` - Development (포트 8001)
- `3` - 모두
### 2. 수동 배포
```bash
# 환경 변수 설정
cp .env.example .env
nano .env # 필요한 설정 수정
# Docker 이미지 빌드 및 실행
docker-compose up -d web
```
### 3. 접속 확인
```bash
# Production
curl http://localhost:8000/strategies/
# Development
curl http://localhost:8001/strategies/
```
---
## 📁 배포 파일 구조
```
executor/
├── Dockerfile # Docker 이미지 정의
├── docker-compose.yml # 컨테이너 오케스트레이션
├── docker-entrypoint.sh # 컨테이너 시작 스크립트
├── .dockerignore # Docker 빌드 시 제외할 파일
├── .env.example # 환경 변수 템플릿
├── deploy.sh # 자동 배포 스크립트
└── DEPLOYMENT_GUIDE.md # 이 파일
```
---
## 📦 배포 방법
### Production 배포
```bash
# 1. 이미지 빌드
docker-compose build web
# 2. 컨테이너 시작
docker-compose up -d web
# 3. 로그 확인
docker-compose logs -f web
```
**접속:** http://localhost:8000
### Development 배포
```bash
# Development 프로파일로 실행
docker-compose --profile dev up -d web-dev
# 로그 확인
docker-compose logs -f web-dev
```
**접속:** http://localhost:8001
### 동시 배포 (Production + Development)
```bash
docker-compose --profile dev up -d
```
---
## ⚙️ 설정
### 환경 변수 (.env)
```bash
# .env.example을 복사하여 시작
cp .env.example .env
```
**주요 설정:**
```bash
# Django 보안
DJANGO_SECRET_KEY=your-secret-key-here
DJANGO_DEBUG=False
DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1,yourdomain.com
# 환경
DJANGO_ENV=production
# 타임존
TIME_ZONE=Asia/Seoul
```
### Docker Compose 설정
**포트 변경:**
```yaml
services:
web:
ports:
- "9000:8000" # 외부:내부
```
**Worker 수 조정:**
```yaml
services:
web:
command: gunicorn --bind 0.0.0.0:8000 --workers 8 executor.wsgi:application
```
**볼륨 추가:**
```yaml
services:
web:
volumes:
- ./data:/app/data
```
---
## 🛠️ 유용한 명령어
### 컨테이너 관리
```bash
# 컨테이너 상태 확인
docker-compose ps
# 컨테이너 중지
docker-compose down
# 컨테이너 재시작
docker-compose restart web
# 컨테이너 완전 제거 (볼륨 포함)
docker-compose down -v
```
### 로그 확인
```bash
# 실시간 로그
docker-compose logs -f
# 특정 서비스 로그
docker-compose logs -f web
# 마지막 100줄
docker-compose logs --tail=100 web
```
### Django 명령어 실행
```bash
# 마이그레이션
docker-compose exec web python manage.py migrate
# 전략 초기화
docker-compose exec web python manage.py init_strategies
# 슈퍼유저 생성
docker-compose exec web python manage.py createsuperuser
# Shell 접속
docker-compose exec web python manage.py shell
# 전략 목록 확인
docker-compose exec web python list_strategies.py
```
### 데이터베이스 백업
```bash
# SQLite 백업
docker-compose exec web sqlite3 /app/db.sqlite3 .dump > backup.sql
# 복원
cat backup.sql | docker-compose exec -T web sqlite3 /app/db.sqlite3
```
### 이미지 관리
```bash
# 이미지 재빌드 (캐시 무시)
docker-compose build --no-cache web
# 이미지 정리
docker image prune -a
# 미사용 리소스 정리
docker system prune -a
```
---
## 🔍 문제 해결
### 1. 포트가 이미 사용 중인 경우
**증상:**
```
Error: bind: address already in use
```
**해결:**
```bash
# 사용 중인 프로세스 확인
lsof -i :8000
# docker-compose.yml에서 포트 변경
ports:
- "8001:8000"
```
### 2. 마이그레이션 오류
**증상:**
```
django.db.utils.OperationalError: no such table
```
**해결:**
```bash
# 컨테이너에서 마이그레이션 재실행
docker-compose exec web python manage.py migrate --run-syncdb
# 또는 DB 초기화
docker-compose down -v
docker-compose up -d
```
### 3. 전략이 초기화되지 않음
**해결:**
```bash
# 수동으로 전략 초기화
docker-compose exec web python manage.py init_strategies
# 컨테이너 재시작
docker-compose restart web
```
### 4. 정적 파일이 로드되지 않음
**해결:**
```bash
# 정적 파일 수집
docker-compose exec web python manage.py collectstatic --noinput
# 권한 확인
docker-compose exec web ls -la /app/staticfiles/
```
### 5. 컨테이너가 즉시 종료됨
**확인:**
```bash
# 로그에서 오류 확인
docker-compose logs web
# 컨테이너 상태 확인
docker-compose ps
```
### 6. Health check 실패
**증상:**
```
Status: unhealthy
```
**해결:**
```bash
# 수동으로 health check 테스트
docker-compose exec web curl -f http://localhost:8000/strategies/
# health check 비활성화 (임시)
# docker-compose.yml에서 healthcheck 섹션 주석 처리
```
---
## 🔐 보안 권장사항
### Production 환경
1. **SECRET_KEY 변경**
```bash
# .env 파일에서 반드시 변경
DJANGO_SECRET_KEY=$(python -c 'from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())')
```
2. **DEBUG 모드 비활성화**
```bash
DJANGO_DEBUG=False
```
3. **ALLOWED_HOSTS 설정**
```bash
DJANGO_ALLOWED_HOSTS=yourdomain.com,www.yourdomain.com
```
4. **방화벽 설정**
```bash
# 필요한 포트만 개방
ufw allow 8000/tcp
```
---
## 📊 모니터링
### 컨테이너 리소스 사용량
```bash
# 실시간 모니터링
docker stats
# 특정 컨테이너
docker stats quantbench-executor
```
### 디스크 사용량
```bash
# Docker 전체 사용량
docker system df
# 상세 정보
docker system df -v
```
---
## 🔄 업데이트
### 코드 업데이트
```bash
# 1. Git에서 최신 코드 가져오기
git pull origin main
# 2. 이미지 재빌드
docker-compose build web
# 3. 컨테이너 재시작
docker-compose up -d web
# 4. 마이그레이션 실행
docker-compose exec web python manage.py migrate
```
### Rolling 업데이트 (무중단 배포)
```bash
# 1. 새 컨테이너 시작
docker-compose up -d --scale web=2 --no-recreate
# 2. 헬스 체크 확인
docker-compose ps
# 3. 이전 컨테이너 종료
docker-compose up -d --scale web=1
```
---
## 📝 추가 정보
### BAA 전략 테스트
```bash
# 컨테이너 내부에서 테스트
docker-compose exec web python test_baa.py
# API 테스트
docker-compose exec web bash baa_api_examples.sh
```
### 전략 목록 확인
```bash
docker-compose exec web python list_strategies.py
```
---
## 🆘 지원
문제가 발생하면:
1. 로그 확인: `docker-compose logs -f`
2. 컨테이너 상태: `docker-compose ps`
3. GitHub Issues에 보고
---
## 📚 참고 문서
- [Django Deployment Checklist](https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/)
- [Docker Best Practices](https://docs.docker.com/develop/dev-best-practices/)
- [Gunicorn Documentation](https://docs.gunicorn.org/)

45
Dockerfile Normal file
View File

@@ -0,0 +1,45 @@
# Python 3.13 기반 이미지
FROM python:3.13-slim
# 환경 변수 설정
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
PIP_NO_CACHE_DIR=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1
# 작업 디렉토리 설정
WORKDIR /app
# 시스템 패키지 업데이트 및 필요한 패키지 설치
RUN apt-get update && \
apt-get install -y --no-install-recommends \
gcc \
&& rm -rf /var/lib/apt/lists/*
# uv 설치
RUN pip install uv
# 의존성 파일 복사
COPY pyproject.toml uv.lock ./
# 의존성 설치
RUN uv pip install --system -r pyproject.toml
# 애플리케이션 코드 복사
COPY . .
# 정적 파일 디렉토리 생성
RUN mkdir -p /app/staticfiles
# 데이터베이스 마이그레이션 및 정적 파일 수집 스크립트
COPY docker-entrypoint.sh /app/
RUN chmod +x /app/docker-entrypoint.sh
# 포트 노출
EXPOSE 8000
# 엔트리포인트 설정
ENTRYPOINT ["/app/docker-entrypoint.sh"]
# 기본 명령어
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "4", "executor.wsgi:application"]

287
README_API.md Normal file
View File

@@ -0,0 +1,287 @@
# BAA 전략 API - 빠른 시작 가이드
## 📁 제공된 파일
### 실행 스크립트
1. **`baa_api_examples.sh`** - 전체 기능 테스트 스크립트
2. **`quick_baa_test.sh`** - 빠른 단일 포트폴리오 생성 스크립트
### 문서
3. **`BAA_CURL_EXAMPLES.md`** - 상세한 curl 명령어 예제 모음
4. **`API_USAGE_GUIDE.md`** - 완전한 API 사용 가이드
5. **`BAA_STRATEGY_README.md`** - BAA 전략 설명 및 이론
---
## 🚀 5분 안에 시작하기
### 1단계: 서버 실행
```bash
python manage.py runserver
```
### 2단계: 포트폴리오 생성
```bash
# 스크립트 실행 권한 부여 (최초 1회)
chmod +x quick_baa_test.sh
# BAA-G4 전략으로 $50,000 포트폴리오 생성
./quick_baa_test.sh
```
**출력 예시:**
```
====== 포트폴리오 요약 ======
기준일: 2025-10-04
모드: offensive
총 투자액: $49947.08
잔여 현금: $52.92
====== 포트폴리오 구성 ======
VEA: 818주 @ $61.06 = $49947.08
```
완료! 🎉
---
## 📋 주요 사용 사례
### 사례 1: 다양한 전략 비교
```bash
./quick_baa_test.sh BAA-G12 # 균형형
./quick_baa_test.sh BAA-G4 # 공격형
./quick_baa_test.sh BAA-SPY # 단순형
```
### 사례 2: 예산별 포트폴리오
```bash
./quick_baa_test.sh BAA-G4 25000 # $25,000
./quick_baa_test.sh BAA-G4 100000 # $100,000
./quick_baa_test.sh BAA-G4 250000 # $250,000
```
### 사례 3: 과거 날짜 기준 분석
```bash
./quick_baa_test.sh BAA-G12 100000 2024-01-31
./quick_baa_test.sh BAA-G12 100000 2024-06-30
./quick_baa_test.sh BAA-G12 100000 2024-09-30
```
---
## 🎯 전략 선택 가이드
| 전략 | 위험도 | 자산 수 | CAGR | Max DD | 추천 대상 |
|------|--------|---------|------|--------|-----------|
| **BAA-G12** | 중 | 12개, Top6 선택 | 14.6% | 8.7% | 균형 잡힌 포트폴리오 |
| **BAA-G4** | 고 | 4개, Top1 선택 | 21.0% | 14.6% | 높은 수익 추구 |
| **BAA-G12/T3** | 중상 | 12개, Top3 선택 | 16.4% | 11.4% | 적당한 분산 |
| **BAA-G4/T2** | 상 | 4개, Top2 선택 | 17.7% | 12.7% | 적당한 집중 |
| **BAA-SPY** | 중하 | 1개 (SPY) | 11.4% | 16.2% | 단순함 선호 |
---
## 🔧 고급 사용법
### curl 명령어로 직접 호출
```bash
# 1. 전략 실행
EXEC_ID=$(curl -s -X POST http://localhost:8000/api/strategies/execute/ \
-H "Content-Type: application/json" \
-d '{
"strategy_name": "BoldAssetAllocation",
"parameters": {
"initial_capital": 50000,
"variant": "BAA-G4",
"use_real_data": true
}
}' | jq -r '.execution_id')
# 2. 결과 조회
sleep 10
curl http://localhost:8000/api/executions/${EXEC_ID}/ | jq '.result'
```
### 모든 예제 한번에 실행
```bash
chmod +x baa_api_examples.sh
./baa_api_examples.sh
```
---
## 📚 더 알아보기
### 전략 이해하기
👉 **`BAA_STRATEGY_README.md`** 읽기
- BAA 전략의 원리와 메커니즘
- 모멘텀 계산 방식
- 카나리아 유니버스의 역할
### API 완전 가이드
👉 **`API_USAGE_GUIDE.md`** 읽기
- 모든 API 엔드포인트 설명
- 워크플로우 예시
- 트러블슈팅 가이드
### curl 명령어 레퍼런스
👉 **`BAA_CURL_EXAMPLES.md`** 읽기
- 다양한 curl 명령어 예제
- 응답 샘플
- 에러 처리 예시
---
## ❓ 자주 묻는 질문
### Q1: 어떤 전략을 선택해야 하나요?
**A:**
- 처음 시작: **BAA-G12** (균형형)
- 높은 수익 추구: **BAA-G4** (공격형)
- 단순함 선호: **BAA-SPY**
### Q2: 얼마나 자주 리밸런싱해야 하나요?
**A:** 월말 (마지막 거래일)에 리밸런싱을 권장합니다.
### Q3: 최소 투자 금액은?
**A:** 제한 없지만, $10,000 이상을 권장합니다 (적절한 분산을 위해).
### Q4: 실행 시간은 얼마나 걸리나요?
**A:**
- 시뮬레이션 모드: ~2초
- 실제 데이터 모드: ~5-15초 (데이터 다운로드 포함)
### Q5: 공격/방어 모드는 무엇인가요?
**A:**
- **공격 모드**: 시장이 좋을 때, 주식 중심 포트폴리오
- **방어 모드**: 시장 위험 감지 시, 채권 중심 포트폴리오
- 카나리아 자산(SPY, VWO, VEA, BND)의 모멘텀으로 자동 전환
---
## 🛠️ 의존성
### 필수
- Python 3.13+
- Django 5.2.7
- yfinance
- pandas, numpy
### 선택 (추천)
- `jq`: JSON 포맷팅을 위해 설치 권장
```bash
# macOS
brew install jq
# Ubuntu/Debian
sudo apt-get install jq
```
---
## 🎬 실행 예제 영상
### 1. 기본 실행
```bash
$ ./quick_baa_test.sh
==================================
BAA 전략 빠른 실행
==================================
변형: BAA-G4
초기 자본: $50000
기준일: null
전략 실행 중...
실행 ID: 1
데이터 다운로드 및 계산 중..........
✓ 실행 완료!
====== 포트폴리오 요약 ======
기준일: 2025-10-04
모드: offensive
총 투자액: $49947.08
잔여 현금: $52.92
====== 포트폴리오 구성 ======
VEA: 818주 @ $61.06 = $49947.08
```
### 2. 전략 비교
```bash
# 세 가지 전략 비교
for variant in BAA-G12 BAA-G4 BAA-SPY; do
echo "=== $variant ==="
./quick_baa_test.sh $variant 100000
sleep 2
done
```
---
## 📊 포트폴리오 예시
### 공격 모드 예시 (2025-10-04)
```json
{
"mode": "offensive",
"portfolio": [
{
"ticker": "VEA",
"weight": 100.0,
"shares": 818,
"current_price": 61.06,
"actual_amount": 49947.08
}
],
"canary_status": {
"SPY": {"momentum": 0.0854, "is_bad": false},
"VWO": {"momentum": 0.1015, "is_bad": false},
"VEA": {"momentum": 0.0863, "is_bad": false},
"BND": {"momentum": 0.0132, "is_bad": false}
}
}
```
### 방어 모드 예시 (2024-01-31)
```json
{
"mode": "defensive",
"portfolio": [
{"ticker": "DBC", "weight": 33.33, "shares": 1555},
{"ticker": "TLT", "weight": 33.33, "shares": 374},
{"ticker": "LQD", "weight": 33.33, "shares": 327}
],
"canary_status": {
"VWO": {"momentum": -0.0110, "is_bad": true}
}
}
```
---
## 🔗 관련 링크
- **전략 논문**: [SSRN 4166845](https://ssrn.com/abstract=4166845)
- **Django 공식 문서**: https://docs.djangoproject.com/
- **yfinance 문서**: https://github.com/ranaroussi/yfinance
---
## 📝 라이선스
교육 및 연구 목적으로만 사용하세요.
---
## 🆘 도움말
문제가 발생하면:
1. 서버가 실행 중인지 확인
2. `BAA_CURL_EXAMPLES.md`의 예제 참조
3. Django 서버 로그 확인
**Happy Trading! 📈**

301
REFACTORING_SUMMARY.md Normal file
View File

@@ -0,0 +1,301 @@
# 전략 모듈 리팩토링 완료 보고서
## 📋 개요
**작업 일자**: 2025-10-04
**작업 내용**: 전략 구현체를 종류별로 모듈 분리
**변경 범위**: `strategies/implementations.py``strategies/impls/` 하위 모듈들
---
## ✅ 완료된 작업
### 1. 새로운 모듈 구조 생성
```
strategies/impls/
├── __init__.py # 모듈 통합
├── trend_following.py # 추세 추종 전략 (74 lines)
├── mean_reversion.py # 평균 회귀 전략 (76 lines)
├── volatility_breakout.py # 변동성 돌파 전략 (77 lines)
└── asset_allocation.py # 자산 배분 전략 (449 lines)
```
### 2. 전략 분류 및 이동
| 전략 | 카테고리 | 이동 경로 |
|------|----------|-----------|
| MovingAverageCrossover | 추세 추종 | → `trend_following.py` |
| RSIMeanReversion | 평균 회귀 | → `mean_reversion.py` |
| BollingerBandBreakout | 변동성 돌파 | → `volatility_breakout.py` |
| BoldAssetAllocation | 자산 배분 | → `asset_allocation.py` |
### 3. 통합 임포트 모듈 업데이트
**`strategies/implementations.py`**
- 기존: 모든 전략 클래스 직접 구현 (600+ lines)
- 변경: 하위 모듈에서 임포트만 수행 (27 lines)
- 호환성: **100% 하위 호환** (기존 코드 수정 불필요)
### 4. 문서화
생성된 문서:
-`STRATEGY_MODULES_README.md` - 모듈 구조 상세 가이드
-`list_strategies.py` - 전략 목록 확인 스크립트
-`REFACTORING_SUMMARY.md` - 본 리팩토링 보고서
---
## 🎯 리팩토링 이점
### 1. 코드 구조화
- **Before**: 단일 파일 600+ lines
- **After**: 카테고리별 파일 74~449 lines
- **개선**: 파일당 평균 169 lines (73% 감소)
### 2. 가독성 향상
```python
# 명확한 카테고리 구분
from strategies.impls.trend_following import MovingAverageCrossover
from strategies.impls.asset_allocation import BoldAssetAllocation
```
### 3. 유지보수성
- 관련 전략끼리 그룹화
- 특정 카테고리만 독립 수정 가능
- 머지 충돌 최소화
### 4. 확장성
- 새 카테고리 쉽게 추가
- 팀 협업 용이
- 독립적 개발 가능
---
## 🔄 하위 호환성
### 기존 코드 (변경 없이 작동)
```python
# 방법 1: implementations에서 임포트 (권장)
from strategies.implementations import BoldAssetAllocation
# 방법 2: 직접 임포트
from strategies import implementations
implementations.BoldAssetAllocation()
# 방법 3: 레지스트리 사용
from strategies.base import StrategyRegistry
strategy = StrategyRegistry.get_strategy("BoldAssetAllocation", "1.0.0")
```
### 새로운 방식 (선택사항)
```python
# 직접 모듈에서 임포트
from strategies.impls.asset_allocation import BoldAssetAllocation
from strategies.impls.trend_following import MovingAverageCrossover
```
---
## 📊 전략 목록
### 추세 추종 (Trend Following)
1. **MovingAverageCrossover** v1.0.0
- 이동평균선 교차 전략
- 파일: `trend_following.py`
### 평균 회귀 (Mean Reversion)
2. **RSIMeanReversion** v1.0.0
- RSI 기반 평균회귀
- 파일: `mean_reversion.py`
### 변동성 돌파 (Volatility Breakout)
3. **BollingerBandBreakout** v2.0.0
- 볼린저 밴드 돌파 전략
- 파일: `volatility_breakout.py`
### 자산 배분 (Asset Allocation)
4. **BoldAssetAllocation** v1.0.0
- BAA 전략 (5가지 변형)
- 파일: `asset_allocation.py`
- 특징: 실제 데이터 기반 포트폴리오 제안
---
## 🧪 테스트 결과
### 테스트 항목
- ✅ 모든 전략 임포트 성공
- ✅ 레지스트리 등록 확인
- ✅ BAA 전략 시뮬레이션 모드 작동
- ✅ BAA 전략 실제 데이터 모드 작동
- ✅ API 엔드포인트 정상 작동
### 테스트 명령어
```bash
# 전략 목록 확인
python list_strategies.py
# BAA 전략 테스트
python test_baa.py
# API 테스트
./quick_baa_test.sh
```
---
## 📁 파일 변경 내역
### 생성된 파일
```
✅ strategies/impls/__init__.py
✅ strategies/impls/trend_following.py
✅ strategies/impls/mean_reversion.py
✅ strategies/impls/volatility_breakout.py
✅ strategies/impls/asset_allocation.py
✅ STRATEGY_MODULES_README.md
✅ list_strategies.py
✅ REFACTORING_SUMMARY.md
```
### 수정된 파일
```
🔄 strategies/implementations.py (600+ lines → 27 lines)
```
### 삭제된 파일
```
❌ (없음 - 기존 파일 보존)
```
---
## 🚀 새 전략 추가 방법
### 1. 카테고리 선택 또는 생성
기존 카테고리:
- `trend_following.py` - 추세 추종
- `mean_reversion.py` - 평균 회귀
- `volatility_breakout.py` - 변동성 돌파
- `asset_allocation.py` - 자산 배분
새 카테고리 (예: 차익거래):
```bash
# 새 파일 생성
touch strategies/impls/arbitrage.py
```
### 2. 전략 클래스 작성
```python
# strategies/impls/arbitrage.py
from typing import Dict, Any
from ..base import BaseQuantStrategy, strategy
@strategy
class StatisticalArbitrage(BaseQuantStrategy):
@property
def name(self) -> str:
return "StatisticalArbitrage"
# ... 나머지 구현
```
### 3. 임포트 경로 추가
```python
# strategies/impls/__init__.py
from .arbitrage import StatisticalArbitrage
__all__ = [
# ... 기존
'StatisticalArbitrage',
]
# strategies/implementations.py
from .impls.arbitrage import StatisticalArbitrage
__all__ = [
# ... 기존
'StatisticalArbitrage',
]
```
---
## 📚 참고 문서
1. **모듈 구조 가이드**: `STRATEGY_MODULES_README.md`
2. **BAA 전략 설명**: `BAA_STRATEGY_README.md`
3. **API 사용법**: `API_USAGE_GUIDE.md`
4. **curl 예제**: `BAA_CURL_EXAMPLES.md`
---
## 🔍 코드 리뷰 체크리스트
- [x] 모든 전략이 올바른 카테고리에 분류됨
- [x] 기존 코드 하위 호환성 유지
- [x] 레지스트리 자동 등록 작동
- [x] 임포트 경로 일관성 유지
- [x] 문서화 완료
- [x] 테스트 통과
- [x] API 정상 작동
---
## 💡 향후 개선 사항
### 단기 (1-2주)
- [ ] 단위 테스트 추가 (각 전략별)
- [ ] CI/CD 파이프라인 설정
- [ ] 타입 힌트 완성도 향상
### 중기 (1-2개월)
- [ ] 전략 성능 비교 대시보드
- [ ] 백테스팅 프레임워크 통합
- [ ] 실시간 포트폴리오 모니터링
### 장기 (3-6개월)
- [ ] 머신러닝 기반 전략 카테고리 추가
- [ ] 멀티 타임프레임 지원
- [ ] 클라우드 배포 자동화
---
## 👥 기여자
- 초기 구현: @jongheonkim
- 리팩토링: Claude (Anthropic)
---
## 📝 변경 이력
| 날짜 | 버전 | 변경 내용 |
|------|------|-----------|
| 2025-10-04 | 1.0.0 | 초기 모듈 분리 완료 |
---
## ✨ 결론
전략 구현체를 종류별로 성공적으로 분리하였습니다.
**핵심 성과**:
- 📦 4개 카테고리, 4개 전략 체계적 분류
- 🔄 100% 하위 호환성 유지
- 📚 완전한 문서화
- ✅ 모든 테스트 통과
**다음 단계**:
`STRATEGY_MODULES_README.md`를 참고하여 새 전략을 추가하거나,
`list_strategies.py`를 실행하여 현재 구조를 확인하세요.
```bash
python list_strategies.py
```

412
STRATEGY_MODULES_README.md Normal file
View File

@@ -0,0 +1,412 @@
# 전략 모듈 구조
## 📁 디렉토리 구조
```
strategies/
├── __init__.py
├── base.py # 기본 클래스 및 레지스트리
├── implementations.py # 통합 임포트 모듈
├── models.py # Django 모델
├── views.py # Django 뷰
├── urls.py # URL 라우팅
└── impls/ # 전략 구현체 모듈
├── __init__.py # 모듈 통합
├── trend_following.py # 추세 추종 전략
├── mean_reversion.py # 평균 회귀 전략
├── volatility_breakout.py # 변동성 돌파 전략
└── asset_allocation.py # 자산 배분 전략
```
---
## 📊 전략 분류
### 1. 추세 추종(Trend Following) 전략
**파일**: `strategies/impls/trend_following.py`
이동평균선, 모멘텀 등 추세를 따라가는 전략들
#### 포함된 전략:
- **MovingAverageCrossover** (v1.0.0)
- 단기/장기 이동평균선 교차 전략
- 골든크로스/데드크로스 기반
- 파라미터: `short_window`, `long_window`
**사용 예시:**
```python
from strategies.impls.trend_following import MovingAverageCrossover
strategy = MovingAverageCrossover()
result = strategy.execute({
"short_window": 20,
"long_window": 50,
"initial_capital": 100000
})
```
---
### 2. 평균 회귀(Mean Reversion) 전략
**파일**: `strategies/impls/mean_reversion.py`
RSI, 볼린저밴드 등 과매수/과매도 구간에서 반대 방향으로 거래
#### 포함된 전략:
- **RSIMeanReversion** (v1.0.0)
- RSI 지표 기반 평균회귀
- 과매수(70)/과매도(30) 구간 역방향 거래
- 파라미터: `rsi_period`, `oversold_threshold`, `overbought_threshold`
**사용 예시:**
```python
from strategies.impls.mean_reversion import RSIMeanReversion
strategy = RSIMeanReversion()
result = strategy.execute({
"rsi_period": 14,
"oversold_threshold": 30,
"overbought_threshold": 70,
"initial_capital": 100000
})
```
---
### 3. 변동성 돌파(Volatility Breakout) 전략
**파일**: `strategies/impls/volatility_breakout.py`
볼린저 밴드, ATR 등 변동성 기반 돌파 전략들
#### 포함된 전략:
- **BollingerBandBreakout** (v2.0.0)
- 볼린저 밴드 상한/하한 돌파 전략
- 변동성 확장 구간 포착
- 파라미터: `period`, `std_dev`, `stop_loss`
**사용 예시:**
```python
from strategies.impls.volatility_breakout import BollingerBandBreakout
strategy = BollingerBandBreakout()
result = strategy.execute({
"period": 20,
"std_dev": 2.0,
"initial_capital": 100000
})
```
---
### 4. 자산 배분(Asset Allocation) 전략
**파일**: `strategies/impls/asset_allocation.py`
전술적 자산배분, 리스크 패리티 등 다양한 자산에 배분하는 전략들
#### 포함된 전략:
- **BoldAssetAllocation** (v1.0.0)
- 상대/절대 모멘텀 결합 전략
- 카나리아 유니버스 기반 크래시 보호
- 5가지 변형: BAA-G12, BAA-G4, BAA-G12/T3, BAA-G4/T2, BAA-SPY
- 실제 데이터 기반 포트폴리오 제안 기능
**사용 예시:**
```python
from strategies.impls.asset_allocation import BoldAssetAllocation
# 시뮬레이션 모드
strategy = BoldAssetAllocation()
result = strategy.execute({
"variant": "BAA-G12",
"initial_capital": 100000,
"use_real_data": False
})
# 실제 데이터 모드
result = strategy.execute({
"variant": "BAA-G4",
"initial_capital": 50000,
"use_real_data": True,
"as_of_date": "2024-01-31"
})
```
---
## 🔧 모듈 사용 방법
### 통합 임포트 (권장)
```python
# 모든 전략을 한 번에 임포트
from strategies import implementations
# 또는 특정 전략만
from strategies.implementations import (
MovingAverageCrossover,
BoldAssetAllocation
)
```
### 개별 모듈 임포트
```python
# 특정 카테고리의 전략만 필요한 경우
from strategies.impls.trend_following import MovingAverageCrossover
from strategies.impls.asset_allocation import BoldAssetAllocation
```
### 레지스트리 사용
```python
from strategies.base import StrategyRegistry
# 등록된 전략 조회
available = StrategyRegistry.list_strategies()
# 전략 인스턴스 생성
strategy = StrategyRegistry.get_strategy(
name="BoldAssetAllocation",
version="1.0.0"
)
```
---
## 새 전략 추가하기
### 1. 적절한 카테고리 선택
전략 유형에 따라 해당 파일 선택:
- 추세 추종 → `trend_following.py`
- 평균 회귀 → `mean_reversion.py`
- 변동성 돌파 → `volatility_breakout.py`
- 자산 배분 → `asset_allocation.py`
새 카테고리가 필요하면 새 파일 생성 (예: `arbitrage.py`)
### 2. 전략 클래스 작성
```python
# strategies/impls/your_category.py
from typing import Dict, Any
from ..base import BaseQuantStrategy, strategy
@strategy
class YourStrategy(BaseQuantStrategy):
"""전략 설명"""
@property
def name(self) -> str:
return "YourStrategy"
@property
def description(self) -> str:
return "전략에 대한 상세 설명"
@property
def version(self) -> str:
return "1.0.0"
@property
def default_parameters(self) -> Dict[str, Any]:
return {
"param1": "value1",
"param2": "value2"
}
def validate_parameters(self, parameters: Dict[str, Any]) -> bool:
# 파라미터 검증 로직
return True
def execute(self, parameters: Dict[str, Any] = None) -> Dict[str, Any]:
# 전략 실행 로직
return {
"strategy": self.name,
"result": "..."
}
```
### 3. 임포트 경로 추가
#### `strategies/impls/__init__.py`에 추가:
```python
from .your_category import YourStrategy
__all__ = [
# ... 기존 전략들
'YourStrategy',
]
```
#### `strategies/implementations.py`에 추가:
```python
from .impls.your_category import YourStrategy
__all__ = [
# ... 기존 전략들
'YourStrategy',
]
```
### 4. 자동 등록 확인
`@strategy` 데코레이터를 사용하면 자동으로 레지스트리에 등록됩니다.
```python
# 확인
python manage.py shell
>>> from strategies.base import StrategyRegistry
>>> StrategyRegistry.list_strategies()
# YourStrategy가 포함되어 있는지 확인
```
---
## 📝 모범 사례
### 1. 파일 조직
- 한 파일에 너무 많은 전략을 넣지 마세요 (최대 5개 권장)
- 유사한 특성의 전략끼리 그룹화
### 2. 명명 규칙
- 클래스명: PascalCase (예: `MovingAverageCrossover`)
- 파일명: snake_case (예: `trend_following.py`)
- 전략 이름(name): 클래스명과 동일
### 3. 문서화
- 각 파일 상단에 독스트링으로 카테고리 설명
- 각 전략 클래스에 독스트링으로 전략 설명
- 파라미터 의미를 주석으로 명시
### 4. 의존성 관리
- 공통 의존성: 파일 상단에 import
- 선택적 의존성: 함수/메서드 내부에서 import
```python
# 좋은 예
import time
import random
from typing import Dict, Any
from ..base import BaseQuantStrategy, strategy
@strategy
class MyStrategy(BaseQuantStrategy):
def execute(self, parameters):
# 여기서만 필요한 라이브러리
import some_optional_lib
...
```
---
## 🔄 마이그레이션 가이드
### 기존 `implementations.py`에서 분리된 코드 사용하기
#### Before (기존):
```python
from strategies.implementations import BoldAssetAllocation
```
#### After (현재):
```python
# 방법 1: 기존과 동일하게 사용 (권장)
from strategies.implementations import BoldAssetAllocation
# 방법 2: 직접 모듈에서 임포트
from strategies.impls.asset_allocation import BoldAssetAllocation
```
**👍 호환성**: 기존 코드 수정 불필요!
---
## 🧪 테스트
### 개별 모듈 테스트
```python
# test_trend_following.py
from strategies.impls.trend_following import MovingAverageCrossover
def test_moving_average():
strategy = MovingAverageCrossover()
result = strategy.execute()
assert result["strategy"] == "MovingAverageCrossover"
```
### 통합 테스트
```python
# test_all_strategies.py
from strategies.implementations import (
MovingAverageCrossover,
RSIMeanReversion,
BollingerBandBreakout,
BoldAssetAllocation
)
strategies = [
MovingAverageCrossover(),
RSIMeanReversion(),
BollingerBandBreakout(),
BoldAssetAllocation()
]
for strategy in strategies:
result = strategy.execute()
assert "strategy" in result
```
---
## 🎯 장점
### 1. 코드 구조화
- 관련 전략끼리 그룹화
- 파일 크기 감소 (가독성 향상)
- 명확한 책임 분리
### 2. 유지보수성
- 특정 카테고리만 수정 가능
- 머지 충돌 감소
- 버그 추적 용이
### 3. 확장성
- 새 카테고리 쉽게 추가
- 독립적인 개발 가능
- 팀 협업 용이
### 4. 성능
- 필요한 모듈만 로드
- 메모리 사용 최적화
- 빠른 개발 서버 재시작
---
## 📚 참고
- **기본 클래스**: `strategies/base.py`
- **레지스트리**: `strategies/base.py - StrategyRegistry`
- **API 가이드**: `API_USAGE_GUIDE.md`
- **BAA 전략**: `BAA_STRATEGY_README.md`
---
## ❓ FAQ
### Q: 기존 코드가 작동하지 않나요?
**A:** 아니요! `implementations.py`가 자동으로 모든 모듈을 임포트하므로 기존 코드는 그대로 작동합니다.
### Q: 새 카테고리를 추가하려면?
**A:**
1. `strategies/impls/new_category.py` 생성
2. `strategies/impls/__init__.py`에 임포트 추가
3. `strategies/implementations.py`에 임포트 추가
### Q: 전략을 다른 카테고리로 옮기려면?
**A:**
1. 전략 클래스를 새 파일로 복사
2.`__init__.py` 파일의 임포트 경로 수정
3. 기존 파일에서 전략 삭제
### Q: 한 전략이 여러 카테고리에 속한다면?
**A:** 가장 주요한 특성에 맞는 카테고리에 배치하세요. 또는 `hybrid.py` 같은 새 카테고리를 만드세요.

150
baa_api_examples.sh Executable file
View File

@@ -0,0 +1,150 @@
#!/bin/bash
# BAA 전략 API 호출 예제 스크립트
# 사용법: ./baa_api_examples.sh
# 서버 URL (필요시 수정)
BASE_URL="http://localhost:8000/api"
echo "=================================="
echo "BAA 전략 API 호출 예제"
echo "=================================="
echo ""
# 색상 코드
GREEN='\033[0;32m'
BLUE='\033[0;34m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# ===================================
# 1. 사용 가능한 전략 구현체 목록 조회
# ===================================
echo -e "${BLUE}[1] 사용 가능한 전략 구현체 목록 조회${NC}"
echo "curl -X GET ${BASE_URL}/strategies/implementations/"
echo ""
curl -X GET "${BASE_URL}/strategies/implementations/" \
-H "Content-Type: application/json" | jq '.'
echo ""
echo ""
# ===================================
# 2. BAA-G12 시뮬레이션 모드 실행
# ===================================
echo -e "${BLUE}[2] BAA-G12 시뮬레이션 모드 실행${NC}"
SIMULATION_PAYLOAD='{
"strategy_name": "BoldAssetAllocation",
"version": "1.0.0",
"parameters": {
"initial_capital": 100000,
"variant": "BAA-G12",
"use_real_data": false
}
}'
echo "curl -X POST ${BASE_URL}/strategies/execute/"
echo "Payload:"
echo "$SIMULATION_PAYLOAD" | jq '.'
echo ""
EXEC_ID=$(curl -s -X POST "${BASE_URL}/strategies/execute/" \
-H "Content-Type: application/json" \
-d "$SIMULATION_PAYLOAD" | jq -r '.execution_id')
echo -e "${GREEN}실행 ID: $EXEC_ID${NC}"
echo ""
# 실행 완료 대기
echo "실행 완료 대기 중..."
sleep 3
# 결과 조회
echo -e "${BLUE}시뮬레이션 결과 조회${NC}"
curl -s -X GET "${BASE_URL}/executions/${EXEC_ID}/" | jq '.'
echo ""
echo ""
# ===================================
# 3. BAA-G4 실제 데이터 모드 (현재 날짜)
# ===================================
echo -e "${BLUE}[3] BAA-G4 실제 데이터 모드 - 현재 날짜 기준 포트폴리오${NC}"
REAL_DATA_PAYLOAD='{
"strategy_name": "BoldAssetAllocation",
"version": "1.0.0",
"parameters": {
"initial_capital": 50000,
"variant": "BAA-G4",
"use_real_data": true,
"as_of_date": null
}
}'
echo "curl -X POST ${BASE_URL}/strategies/execute/"
echo "Payload:"
echo "$REAL_DATA_PAYLOAD" | jq '.'
echo ""
EXEC_ID2=$(curl -s -X POST "${BASE_URL}/strategies/execute/" \
-H "Content-Type: application/json" \
-d "$REAL_DATA_PAYLOAD" | jq -r '.execution_id')
echo -e "${GREEN}실행 ID: $EXEC_ID2${NC}"
echo ""
# 실행 완료 대기 (실제 데이터는 더 오래 걸림)
echo "데이터 다운로드 및 계산 중..."
sleep 10
# 결과 조회
echo -e "${BLUE}실제 포트폴리오 결과 조회${NC}"
curl -s -X GET "${BASE_URL}/executions/${EXEC_ID2}/" | jq '.'
echo ""
echo ""
# ===================================
# 4. BAA-G12 특정 날짜 기준
# ===================================
echo -e "${BLUE}[4] BAA-G12 특정 날짜 기준 (2024-01-31)${NC}"
DATE_PAYLOAD='{
"strategy_name": "BoldAssetAllocation",
"version": "1.0.0",
"parameters": {
"initial_capital": 100000,
"variant": "BAA-G12",
"use_real_data": true,
"as_of_date": "2024-01-31"
}
}'
echo "curl -X POST ${BASE_URL}/strategies/execute/"
echo "Payload:"
echo "$DATE_PAYLOAD" | jq '.'
echo ""
EXEC_ID3=$(curl -s -X POST "${BASE_URL}/strategies/execute/" \
-H "Content-Type: application/json" \
-d "$DATE_PAYLOAD" | jq -r '.execution_id')
echo -e "${GREEN}실행 ID: $EXEC_ID3${NC}"
echo ""
# 실행 완료 대기
echo "데이터 다운로드 및 계산 중..."
sleep 10
# 결과 조회
echo -e "${BLUE}특정 날짜 포트폴리오 결과 조회${NC}"
curl -s -X GET "${BASE_URL}/executions/${EXEC_ID3}/" | jq '.'
echo ""
echo ""
# ===================================
# 5. 실행 상태 조회만 하기
# ===================================
echo -e "${YELLOW}[5] 특정 실행 ID 상태 조회 예제${NC}"
echo "curl -X GET ${BASE_URL}/executions/{execution_id}/"
echo ""
echo "예: curl -X GET ${BASE_URL}/executions/${EXEC_ID}/"
echo ""
echo -e "${GREEN}모든 테스트 완료!${NC}"

60
callback_example.sh Executable file
View File

@@ -0,0 +1,60 @@
#!/bin/bash
# 콜백 기능 사용 예제 스크립트
echo "=========================================="
echo "콜백 기능 사용 예제"
echo "=========================================="
echo ""
# 1. 콜백 수신 서버 시작 (별도 터미널에서)
echo "1. 먼저 콜백 수신 서버를 시작하세요 (별도 터미널에서):"
echo ""
echo " python -m http.server 8888"
echo ""
echo " 또는 더 상세한 로깅을 위해:"
echo ""
echo " python test_callback.py"
echo ""
echo "----------------------------------------"
echo ""
# 2. 콜백 URL을 포함한 전략 실행
echo "2. 콜백 URL을 포함하여 전략 실행:"
echo ""
CALLBACK_URL="http://localhost:8888/callback"
curl -X POST http://localhost:8000/strategies/execute/ \
-H "Content-Type: application/json" \
-d '{
"strategy_name": "BoldAssetAllocation",
"parameters": {
"variant": "BAA-G4",
"initial_capital": 100000,
"use_real_data": false
},
"callback_url": "'$CALLBACK_URL'"
}' | python -m json.tool
echo ""
echo "----------------------------------------"
echo ""
# 3. 실행 상태 확인 예제
echo "3. 실행 상태 확인 (EXECUTION_ID를 위 응답에서 가져오세요):"
echo ""
echo " curl http://localhost:8000/executions/{EXECUTION_ID}/ | python -m json.tool"
echo ""
echo "----------------------------------------"
echo ""
# 4. 다양한 콜백 URL 예제
echo "4. 다양한 콜백 URL 예제:"
echo ""
echo " 로컬 서버: http://localhost:8888/callback"
echo " webhook.site: https://webhook.site/your-unique-id"
echo " requestbin: https://requestbin.com/your-bin"
echo " ngrok: https://your-subdomain.ngrok.io/callback"
echo ""
echo "=========================================="

106
deploy.sh Executable file
View File

@@ -0,0 +1,106 @@
#!/bin/bash
set -e
echo "=========================================="
echo "QuantBench Executor Deployment Script"
echo "=========================================="
echo ""
# 컬러 출력
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# 함수 정의
print_success() {
echo -e "${GREEN}$1${NC}"
}
print_error() {
echo -e "${RED}$1${NC}"
}
print_warning() {
echo -e "${YELLOW}$1${NC}"
}
# Docker 설치 확인
if ! command -v docker &> /dev/null; then
print_error "Docker가 설치되어 있지 않습니다."
echo "Docker를 먼저 설치해주세요: https://docs.docker.com/get-docker/"
exit 1
fi
if ! command -v docker-compose &> /dev/null; then
print_error "Docker Compose가 설치되어 있지 않습니다."
echo "Docker Compose를 먼저 설치해주세요: https://docs.docker.com/compose/install/"
exit 1
fi
print_success "Docker 환경 확인 완료"
# .env 파일 확인
if [ ! -f .env ]; then
print_warning ".env 파일이 없습니다. .env.example을 복사합니다."
cp .env.example .env
print_warning ".env 파일을 수정하여 적절한 설정값을 입력해주세요."
fi
# 배포 모드 선택
echo ""
echo "배포 모드를 선택하세요:"
echo "1) Production (포트 8000)"
echo "2) Development (포트 8001)"
echo "3) 모두"
read -p "선택 (1/2/3): " MODE
case $MODE in
1)
print_success "Production 모드로 배포합니다..."
docker-compose up -d web
;;
2)
print_success "Development 모드로 배포합니다..."
docker-compose --profile dev up -d web-dev
;;
3)
print_success "Production & Development 모드로 배포합니다..."
docker-compose --profile dev up -d
;;
*)
print_error "잘못된 선택입니다."
exit 1
;;
esac
echo ""
print_success "배포가 완료되었습니다!"
echo ""
# 컨테이너 상태 확인
echo "=========================================="
echo "컨테이너 상태"
echo "=========================================="
docker-compose ps
echo ""
echo "=========================================="
echo "접속 정보"
echo "=========================================="
if [ "$MODE" = "1" ] || [ "$MODE" = "3" ]; then
echo "Production: http://localhost:8000/strategies/"
fi
if [ "$MODE" = "2" ] || [ "$MODE" = "3" ]; then
echo "Development: http://localhost:8001/strategies/"
fi
echo ""
echo "=========================================="
echo "유용한 명령어"
echo "=========================================="
echo "로그 확인: docker-compose logs -f"
echo "컨테이너 중지: docker-compose down"
echo "컨테이너 재시작: docker-compose restart"
echo "전략 초기화: docker-compose exec web python manage.py init_strategies"
echo "=========================================="

38
docker-compose.yml Normal file
View File

@@ -0,0 +1,38 @@
version: '3.8'
services:
web:
build: .
container_name: quantbench-executor
ports:
- "8000:8000"
volumes:
- ./db.sqlite3:/app/db.sqlite3
- ./staticfiles:/app/staticfiles
environment:
- DJANGO_ENV=production
- PYTHONUNBUFFERED=1
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/strategies/"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
# Development 모드 (선택사항)
web-dev:
build: .
container_name: quantbench-executor-dev
ports:
- "8001:8000"
volumes:
- .:/app
- /app/.venv
environment:
- DJANGO_ENV=development
- PYTHONUNBUFFERED=1
command: python manage.py runserver 0.0.0.0:8000
restart: unless-stopped
profiles:
- dev

21
docker-entrypoint.sh Executable file
View File

@@ -0,0 +1,21 @@
#!/bin/bash
set -e
echo "Starting Django application..."
# 데이터베이스 마이그레이션
echo "Running database migrations..."
python manage.py migrate --noinput
# 전략 초기화
echo "Initializing strategies..."
python manage.py init_strategies
# 정적 파일 수집 (production 환경에서만)
if [ "$DJANGO_ENV" = "production" ]; then
echo "Collecting static files..."
python manage.py collectstatic --noinput
fi
# 전달된 명령어 실행
exec "$@"

98
list_strategies.py Executable file
View File

@@ -0,0 +1,98 @@
#!/usr/bin/env python
"""전략 목록 및 구조 확인 스크립트"""
import os
from strategies.base import StrategyRegistry
from strategies import implementations
def main():
print("=" * 80)
print("전략 모듈 구조")
print("=" * 80)
print()
# 모듈 구조 출력
print("📁 strategies/impls/")
print("├── __init__.py")
print("├── trend_following.py # 추세 추종 전략")
print("├── mean_reversion.py # 평균 회귀 전략")
print("├── volatility_breakout.py # 변동성 돌파 전략")
print("└── asset_allocation.py # 자산 배분 전략")
print()
print()
# 레지스트리에 등록된 전략 목록
print("=" * 80)
print("등록된 전략 목록")
print("=" * 80)
print()
available_strategies = StrategyRegistry.list_strategies()
# 카테고리별로 그룹화
categories = {
"추세 추종 (Trend Following)": ["MovingAverageCrossover"],
"평균 회귀 (Mean Reversion)": ["RSIMeanReversion"],
"변동성 돌파 (Volatility Breakout)": ["BollingerBandBreakout"],
"자산 배분 (Asset Allocation)": ["BoldAssetAllocation"],
}
for category, strategy_names in categories.items():
print(f"📊 {category}")
print("-" * 80)
for strategy_name in strategy_names:
if strategy_name in available_strategies:
strategy_info = available_strategies[strategy_name]
print(f"\n{strategy_name}")
print(f" 설명: {strategy_info['description']}")
for version_info in strategy_info['versions']:
print(f" 버전: {version_info['version']}")
print(f" 파라미터:")
for param, value in version_info['default_parameters'].items():
if isinstance(value, (int, float)):
print(f" - {param}: {value}")
elif isinstance(value, bool):
print(f" - {param}: {value}")
elif isinstance(value, str) and value:
print(f" - {param}: '{value}'")
elif value is None:
print(f" - {param}: null")
print()
# 통계
print("=" * 80)
print("통계")
print("=" * 80)
total = len(available_strategies)
print(f"총 전략 수: {total}")
print(f"카테고리 수: {len(categories)}")
print()
# 파일 크기 확인
print("=" * 80)
print("모듈 파일 크기")
print("=" * 80)
impls_dir = os.path.join(os.path.dirname(__file__), "strategies", "impls")
files = {
"trend_following.py": "추세 추종",
"mean_reversion.py": "평균 회귀",
"volatility_breakout.py": "변동성 돌파",
"asset_allocation.py": "자산 배분"
}
for filename, description in files.items():
filepath = os.path.join(impls_dir, filename)
if os.path.exists(filepath):
size = os.path.getsize(filepath)
lines = sum(1 for _ in open(filepath, 'r', encoding='utf-8'))
print(f"{filename:25s} ({description:12s}): {size:6,d} bytes, {lines:4d} lines")
print()
if __name__ == "__main__":
main()

View File

@@ -5,4 +5,7 @@ description = "Add your description here"
requires-python = ">=3.13"
dependencies = [
"django>=5.2.7",
"yfinance>=0.2.66",
"gunicorn>=21.2.0",
"requests>=2.31.0",
]

118
quick_baa_test.sh Executable file
View File

@@ -0,0 +1,118 @@
#!/bin/bash
# BAA 전략 빠른 테스트 스크립트
# 사용법: ./quick_baa_test.sh [variant] [capital] [date]
BASE_URL="http://localhost:8000/api"
# 기본값 설정
VARIANT="${1:-BAA-G4}"
CAPITAL="${2:-50000}"
DATE="${3:-null}"
echo "=================================="
echo "BAA 전략 빠른 실행"
echo "=================================="
echo "변형: $VARIANT"
echo "초기 자본: \$$CAPITAL"
echo "기준일: $DATE"
echo ""
# 날짜 처리
if [ "$DATE" = "null" ]; then
DATE_JSON="null"
else
DATE_JSON="\"$DATE\""
fi
# 실제 데이터 모드로 실행
echo "전략 실행 중..."
RESPONSE=$(curl -s -X POST "${BASE_URL}/strategies/execute/" \
-H "Content-Type: application/json" \
-d "{
\"strategy_name\": \"BoldAssetAllocation\",
\"version\": \"1.0.0\",
\"parameters\": {
\"initial_capital\": $CAPITAL,
\"variant\": \"$VARIANT\",
\"use_real_data\": true,
\"as_of_date\": $DATE_JSON
}
}")
EXEC_ID=$(echo "$RESPONSE" | jq -r '.execution_id')
if [ "$EXEC_ID" = "null" ] || [ -z "$EXEC_ID" ]; then
echo "오류: 실행 실패"
echo "$RESPONSE" | jq '.'
exit 1
fi
echo "실행 ID: $EXEC_ID"
echo ""
# 진행 상황 표시
echo -n "데이터 다운로드 및 계산 중"
for i in {1..10}; do
sleep 1
echo -n "."
done
echo ""
echo ""
# 결과 조회
echo "결과 조회 중..."
RESULT=$(curl -s -X GET "${BASE_URL}/executions/${EXEC_ID}/")
STATUS=$(echo "$RESULT" | jq -r '.status')
if [ "$STATUS" = "completed" ]; then
echo "✓ 실행 완료!"
echo ""
# 주요 정보 추출
MODE=$(echo "$RESULT" | jq -r '.result.mode')
AS_OF_DATE=$(echo "$RESULT" | jq -r '.result.as_of_date')
TOTAL_ALLOCATED=$(echo "$RESULT" | jq -r '.result.total_allocated')
CASH_REMAINING=$(echo "$RESULT" | jq -r '.result.cash_remaining')
CANARY_BAD=$(echo "$RESULT" | jq -r '.result.canary_bad_count')
echo "====== 포트폴리오 요약 ======"
echo "기준일: $AS_OF_DATE"
echo "모드: $MODE"
echo "카나리아 bad 개수: $CANARY_BAD"
echo "총 투자액: \$$TOTAL_ALLOCATED"
echo "잔여 현금: \$$CASH_REMAINING"
echo ""
echo "====== 포트폴리오 구성 ======"
echo "$RESULT" | jq -r '.result.portfolio[] | "\(.ticker): \(.shares)주 @ $\(.current_price) = $\(.actual_amount)"'
echo ""
echo "====== 카나리아 상태 ======"
echo "$RESULT" | jq -r '.result.canary_status | to_entries[] | "\(.key): 모멘텀=\(.value.momentum | .*100 | round/100)%, bad=\(.value.is_bad)"'
echo ""
# 전체 JSON 저장
FILENAME="baa_result_${EXEC_ID}_$(date +%Y%m%d_%H%M%S).json"
echo "$RESULT" | jq '.' > "$FILENAME"
echo "전체 결과가 $FILENAME 에 저장되었습니다."
elif [ "$STATUS" = "failed" ]; then
echo "✗ 실행 실패"
ERROR=$(echo "$RESULT" | jq -r '.error_message')
echo "오류: $ERROR"
else
echo "상태: $STATUS"
echo "전체 응답:"
echo "$RESULT" | jq '.'
fi
echo ""
echo "=================================="
echo "사용 예시:"
echo " ./quick_baa_test.sh # BAA-G4, \$50,000, 현재 날짜"
echo " ./quick_baa_test.sh BAA-G12 # BAA-G12, \$50,000, 현재 날짜"
echo " ./quick_baa_test.sh BAA-G4 100000 # BAA-G4, \$100,000, 현재 날짜"
echo " ./quick_baa_test.sh BAA-G12 75000 2024-01-31 # BAA-G12, \$75,000, 2024-01-31"
echo "=================================="

View File

@@ -4,3 +4,56 @@ from django.apps import AppConfig
class StrategiesConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'strategies'
def ready(self):
"""앱 초기화 시 전략 데이터를 DB에 로드"""
# 마이그레이션이나 다른 명령어 실행 중에는 초기화하지 않음
import sys
if 'runserver' in sys.argv or 'test' in sys.argv:
self._initialize_strategies()
def _initialize_strategies(self):
"""전략 레지스트리를 DB에 초기화"""
try:
from .base import StrategyRegistry
from .models import QuantStrategy, StrategyVersion
available_strategies = StrategyRegistry.list_strategies()
for strategy_name, strategy_info in available_strategies.items():
# QuantStrategy 생성 또는 업데이트
strategy_obj, created = QuantStrategy.objects.get_or_create(
name=strategy_name,
defaults={
'description': strategy_info['description'],
'is_active': True
}
)
# 설명이 변경된 경우 업데이트
if not created and strategy_obj.description != strategy_info['description']:
strategy_obj.description = strategy_info['description']
strategy_obj.save()
# 각 버전 생성
for version_info in strategy_info['versions']:
version_obj, version_created = StrategyVersion.objects.get_or_create(
strategy=strategy_obj,
version=version_info['version'],
defaults={
'implementation_key': f"{strategy_name}:{version_info['version']}",
'parameters': version_info['default_parameters'],
'is_current': True
}
)
# 파라미터가 변경된 경우 업데이트
if not version_created and version_obj.parameters != version_info['default_parameters']:
version_obj.parameters = version_info['default_parameters']
version_obj.save()
except Exception as e:
# DB가 아직 준비되지 않았거나 마이그레이션 전인 경우 무시
import logging
logger = logging.getLogger(__name__)
logger.debug(f"전략 초기화 건너뜀: {e}")

View File

@@ -1,202 +1,27 @@
from typing import Dict, Any
import time
import random
import math
from .base import BaseQuantStrategy, strategy
"""
전략 구현체 통합 모듈
모든 전략 구현체를 임포트하여 레지스트리에 등록합니다.
실제 구현은 strategies/impls/ 하위 모듈에 분리되어 있습니다.
"""
@strategy
class MovingAverageCrossover(BaseQuantStrategy):
"""이동평균선 교차 전략"""
# 각 카테고리별 전략 임포트
from .impls.trend_following import MovingAverageCrossover
from .impls.mean_reversion import RSIMeanReversion
from .impls.volatility_breakout import BollingerBandBreakout
from .impls.asset_allocation import BoldAssetAllocation
@property
def name(self) -> str:
return "MovingAverageCrossover"
# __all__에 등록하여 외부에서 임포트 가능하도록 설정
__all__ = [
# Trend Following
'MovingAverageCrossover',
@property
def description(self) -> str:
return "단기 이동평균선이 장기 이동평균선을 상향 돌파할 때 매수, 하향 돌파할 때 매도하는 전략"
# Mean Reversion
'RSIMeanReversion',
@property
def version(self) -> str:
return "1.0.0"
# Volatility Breakout
'BollingerBandBreakout',
@property
def default_parameters(self) -> Dict[str, Any]:
return {
"short_window": 20,
"long_window": 50,
"initial_capital": 100000,
"position_size": 0.1
}
def validate_parameters(self, parameters: Dict[str, Any]) -> bool:
required_params = ["short_window", "long_window", "initial_capital"]
for param in required_params:
if param not in parameters:
return False
if parameters["short_window"] >= parameters["long_window"]:
return False
return True
def execute(self, parameters: Dict[str, Any] = None) -> Dict[str, Any]:
if parameters is None:
parameters = self.default_parameters
if not self.validate_parameters(parameters):
raise ValueError("Invalid parameters")
# 시뮬레이션 실행
time.sleep(1) # 실행 시간 시뮬레이션
# 모의 결과 생성
profit_rate = random.uniform(-0.15, 0.25)
trades_count = random.randint(15, 60)
win_rate = random.uniform(0.45, 0.75)
return {
"strategy": self.name,
"version": self.version,
"profit_loss": round(parameters["initial_capital"] * profit_rate, 2),
"profit_rate": round(profit_rate * 100, 2),
"trades_executed": trades_count,
"win_rate": round(win_rate, 3),
"execution_time": "1.2s",
"parameters_used": parameters,
"final_capital": round(parameters["initial_capital"] * (1 + profit_rate), 2)
}
@strategy
class RSIMeanReversion(BaseQuantStrategy):
"""RSI 평균회귀 전략"""
@property
def name(self) -> str:
return "RSIMeanReversion"
@property
def description(self) -> str:
return "RSI 지표를 이용한 평균회귀 전략. RSI가 과매수/과매도 구간에서 반대 방향으로 거래"
@property
def version(self) -> str:
return "1.0.0"
@property
def default_parameters(self) -> Dict[str, Any]:
return {
"rsi_period": 14,
"oversold_threshold": 30,
"overbought_threshold": 70,
"initial_capital": 100000,
"position_size": 0.05
}
def validate_parameters(self, parameters: Dict[str, Any]) -> bool:
required_params = ["rsi_period", "oversold_threshold", "overbought_threshold", "initial_capital"]
for param in required_params:
if param not in parameters:
return False
if not (0 < parameters["oversold_threshold"] < parameters["overbought_threshold"] < 100):
return False
return True
def execute(self, parameters: Dict[str, Any] = None) -> Dict[str, Any]:
if parameters is None:
parameters = self.default_parameters
if not self.validate_parameters(parameters):
raise ValueError("Invalid parameters")
# 시뮬레이션 실행
time.sleep(1.5) # 실행 시간 시뮬레이션
# 모의 결과 생성
profit_rate = random.uniform(-0.10, 0.18)
trades_count = random.randint(25, 80)
win_rate = random.uniform(0.40, 0.65)
return {
"strategy": self.name,
"version": self.version,
"profit_loss": round(parameters["initial_capital"] * profit_rate, 2),
"profit_rate": round(profit_rate * 100, 2),
"trades_executed": trades_count,
"win_rate": round(win_rate, 3),
"execution_time": "1.5s",
"parameters_used": parameters,
"final_capital": round(parameters["initial_capital"] * (1 + profit_rate), 2),
"max_drawdown": round(random.uniform(0.05, 0.20), 3)
}
@strategy
class BollingerBandBreakout(BaseQuantStrategy):
"""볼린저 밴드 돌파 전략"""
@property
def name(self) -> str:
return "BollingerBandBreakout"
@property
def description(self) -> str:
return "볼린저 밴드 상한선 돌파시 매수, 하한선 돌파시 매도하는 돌파 전략"
@property
def version(self) -> str:
return "2.0.0"
@property
def default_parameters(self) -> Dict[str, Any]:
return {
"period": 20,
"std_dev": 2.0,
"initial_capital": 100000,
"position_size": 0.08,
"stop_loss": 0.05
}
def validate_parameters(self, parameters: Dict[str, Any]) -> bool:
required_params = ["period", "std_dev", "initial_capital"]
for param in required_params:
if param not in parameters:
return False
if parameters["std_dev"] <= 0 or parameters["period"] <= 0:
return False
return True
def execute(self, parameters: Dict[str, Any] = None) -> Dict[str, Any]:
if parameters is None:
parameters = self.default_parameters
if not self.validate_parameters(parameters):
raise ValueError("Invalid parameters")
# 시뮬레이션 실행
time.sleep(2) # 실행 시간 시뮬레이션
# 모의 결과 생성
profit_rate = random.uniform(-0.20, 0.30)
trades_count = random.randint(10, 40)
win_rate = random.uniform(0.35, 0.70)
return {
"strategy": self.name,
"version": self.version,
"profit_loss": round(parameters["initial_capital"] * profit_rate, 2),
"profit_rate": round(profit_rate * 100, 2),
"trades_executed": trades_count,
"win_rate": round(win_rate, 3),
"execution_time": "2.0s",
"parameters_used": parameters,
"final_capital": round(parameters["initial_capital"] * (1 + profit_rate), 2),
"sharpe_ratio": round(random.uniform(0.5, 2.5), 2),
"volatility": round(random.uniform(0.15, 0.35), 3)
}
# Asset Allocation
'BoldAssetAllocation',
]

View File

@@ -0,0 +1,18 @@
"""
전략 구현체 패키지
각 전략 유형별로 모듈을 분리하여 관리합니다.
"""
# 각 모듈에서 전략을 import
from .trend_following import MovingAverageCrossover
from .mean_reversion import RSIMeanReversion
from .volatility_breakout import BollingerBandBreakout
from .asset_allocation import BoldAssetAllocation
__all__ = [
'MovingAverageCrossover',
'RSIMeanReversion',
'BollingerBandBreakout',
'BoldAssetAllocation',
]

View File

@@ -0,0 +1,449 @@
"""
자산 배분(Asset Allocation) 전략 구현체
전술적 자산배분, 리스크 패리티 등 다양한 자산에 배분하는 전략들을 포함합니다.
"""
from typing import Dict, Any
import time
import random
from datetime import datetime, timedelta
import yfinance as yf
import pandas as pd
from ..base import BaseQuantStrategy, strategy
@strategy
class BoldAssetAllocation(BaseQuantStrategy):
"""Bold Asset Allocation (BAA) 전략
상대 모멘텀과 절대 모멘텀을 결합한 공격적 전술적 자산배분 전략.
느린 상대 모멘텀(SMA12)과 빠른 절대 모멘텀(13612W)을 조합하여
카나리아 유니버스 기반의 크래시 보호 메커니즘을 구현.
"""
@property
def name(self) -> str:
return "BoldAssetAllocation"
@property
def description(self) -> str:
return "상대 모멘텀과 절대 모멘텀을 결합한 공격적 전술적 자산배분 전략. 카나리아 유니버스 기반 크래시 보호"
@property
def version(self) -> str:
return "1.0.0"
@property
def default_parameters(self) -> Dict[str, Any]:
return {
"initial_capital": 100000,
"variant": "BAA-G12", # BAA-G12 (balanced), BAA-G4 (aggressive), BAA-SPY
"offensive_top": 6, # TO: 공격 유니버스에서 선택할 자산 수 (BAA-G12: 6, BAA-G4: 1)
"defensive_top": 3, # TD: 방어 유니버스에서 선택할 자산 수
"breadth_param": 1, # B: 카나리아 자산 중 몇 개가 bad일 때 방어로 전환
"transaction_cost": 0.001, # 0.1% 거래비용
"as_of_date": None, # 기준일 (None이면 현재 날짜)
"use_real_data": False, # 실제 데이터 사용 여부
}
def validate_parameters(self, parameters: Dict[str, Any]) -> bool:
required_params = ["initial_capital", "variant"]
for param in required_params:
if param not in parameters:
return False
if parameters["initial_capital"] <= 0:
return False
valid_variants = ["BAA-G12", "BAA-G4", "BAA-SPY", "BAA-G12/T3", "BAA-G4/T2"]
if parameters.get("variant") not in valid_variants:
return False
return True
def _calculate_sma12_momentum(self, prices: pd.Series) -> float:
"""SMA(12) 모멘텀 계산: pt / SMA(13) - 1"""
if len(prices) < 13:
return 0.0
# numpy scalar 또는 pandas Series를 float로 변환
mean_val = prices.tail(13).mean()
sma13 = float(mean_val.item()) if hasattr(mean_val, 'item') else float(mean_val)
current_val = prices.iloc[-1]
current_price = float(current_val.item()) if hasattr(current_val, 'item') else float(current_val)
return (current_price / sma13) - 1
def _calculate_13612w_momentum(self, prices: pd.Series) -> float:
"""13612W 모멘텀 계산: 가중평균 (1m:12, 3m:4, 6m:2, 12m:1)"""
if len(prices) < 252: # 최소 12개월 데이터 필요
return 0.0
current_val = prices.iloc[-1]
current_price = float(current_val.item()) if hasattr(current_val, 'item') else float(current_val)
# 각 기간별 수익률 계산
def get_price(idx):
val = prices.iloc[idx]
return float(val.item()) if hasattr(val, 'item') else float(val)
ret_1m = (current_price / get_price(-21) - 1) if len(prices) > 21 else 0.0
ret_3m = (current_price / get_price(-63) - 1) if len(prices) > 63 else 0.0
ret_6m = (current_price / get_price(-126) - 1) if len(prices) > 126 else 0.0
ret_12m = (current_price / get_price(-252) - 1) if len(prices) > 252 else 0.0
# 가중평균
weighted_momentum = (12 * ret_1m + 4 * ret_3m + 2 * ret_6m + 1 * ret_12m) / 19
return float(weighted_momentum)
def _get_ticker_data(self, ticker: str, end_date: datetime) -> pd.Series:
"""특정 티커의 가격 데이터 가져오기"""
start_date = end_date - timedelta(days=400) # 13개월 + 여유
try:
data = yf.download(ticker, start=start_date, end=end_date, progress=False, auto_adjust=True)
if data.empty:
return pd.Series()
# 데이터 구조에 따라 처리
if 'Close' in data.columns:
return data['Close']
elif isinstance(data, pd.Series):
return data
else:
return pd.Series()
except Exception as e:
print(f"Error downloading {ticker}: {e}")
return pd.Series()
def _calculate_portfolio_real_data(self, parameters: Dict[str, Any]) -> Dict[str, Any]:
"""실제 데이터를 사용한 포트폴리오 계산"""
variant = parameters.get("variant", "BAA-G12")
as_of_date = parameters.get("as_of_date")
initial_capital = parameters["initial_capital"]
# 기준일 설정
if as_of_date is None:
as_of_date = datetime.now()
elif isinstance(as_of_date, str):
as_of_date = datetime.strptime(as_of_date, "%Y-%m-%d")
# 자산 유니버스 정의
universe_config = {
"BAA-G12": {
"offensive": ["SPY", "QQQ", "IWM", "VGK", "EWJ", "VWO", "VNQ", "DBC", "GLD", "TLT", "HYG", "LQD"],
"defensive": ["TIP", "DBC", "BIL", "IEF", "TLT", "LQD", "BND"],
"canary": ["SPY", "VWO", "VEA", "BND"],
"offensive_top": 6,
},
"BAA-G4": {
"offensive": ["QQQ", "VWO", "VEA", "BND"],
"defensive": ["TIP", "DBC", "BIL", "IEF", "TLT", "LQD", "BND"],
"canary": ["SPY", "VWO", "VEA", "BND"],
"offensive_top": 1,
},
"BAA-G12/T3": {
"offensive": ["SPY", "QQQ", "IWM", "VGK", "EWJ", "VWO", "VNQ", "DBC", "GLD", "TLT", "HYG", "LQD"],
"defensive": ["TIP", "DBC", "BIL", "IEF", "TLT", "LQD", "BND"],
"canary": ["SPY", "VWO", "VEA", "BND"],
"offensive_top": 3,
},
"BAA-G4/T2": {
"offensive": ["QQQ", "VWO", "VEA", "BND"],
"defensive": ["TIP", "DBC", "BIL", "IEF", "TLT", "LQD", "BND"],
"canary": ["SPY", "VWO", "VEA", "BND"],
"offensive_top": 2,
},
"BAA-SPY": {
"offensive": ["SPY"],
"defensive": ["TIP", "DBC", "BIL", "IEF", "TLT", "LQD", "BND"],
"canary": ["SPY", "VWO", "VEA", "BND"],
"offensive_top": 1,
}
}
config = universe_config.get(variant, universe_config["BAA-G12"])
offensive_top = parameters.get("offensive_top", config["offensive_top"])
defensive_top = parameters.get("defensive_top", 3)
breadth_param = parameters.get("breadth_param", 1)
# 카나리아 유니버스 체크 (13612W 모멘텀 사용)
canary_bad_count = 0
canary_status = {}
for ticker in config["canary"]:
prices = self._get_ticker_data(ticker, as_of_date)
if not prices.empty:
momentum = self._calculate_13612w_momentum(prices)
is_bad = bool(momentum < 0)
canary_status[ticker] = {
"momentum": float(momentum),
"is_bad": is_bad
}
if is_bad:
canary_bad_count += 1
# 방어 모드 여부 결정
is_defensive = canary_bad_count >= breadth_param
# 포트폴리오 구성
portfolio = {}
current_prices = {}
if is_defensive:
# 방어 유니버스에서 선택
defensive_scores = {}
for ticker in config["defensive"]:
prices = self._get_ticker_data(ticker, as_of_date)
if not prices.empty:
momentum = self._calculate_sma12_momentum(prices)
defensive_scores[ticker] = momentum
price_val = prices.iloc[-1]
current_prices[ticker] = float(price_val.item()) if hasattr(price_val, 'item') else float(price_val)
# 상위 defensive_top개 선택
sorted_defensive = sorted(defensive_scores.items(), key=lambda x: x[1], reverse=True)
selected_assets = []
for ticker, momentum in sorted_defensive[:defensive_top]:
# BIL보다 나쁜 자산은 BIL로 교체
bil_prices = self._get_ticker_data("BIL", as_of_date)
bil_momentum = self._calculate_sma12_momentum(bil_prices) if not bil_prices.empty else 0
if momentum < bil_momentum:
selected_assets.append("BIL")
if "BIL" not in current_prices and not bil_prices.empty:
price_val = bil_prices.iloc[-1]
current_prices["BIL"] = float(price_val.item()) if hasattr(price_val, 'item') else float(price_val)
else:
selected_assets.append(ticker)
# 선택된 자산이 없는 경우 BIL로 대체
if not selected_assets:
selected_assets = ["BIL"]
bil_prices = self._get_ticker_data("BIL", as_of_date)
if not bil_prices.empty:
price_val = bil_prices.iloc[-1]
current_prices["BIL"] = float(price_val.item()) if hasattr(price_val, 'item') else float(price_val)
# 동일 가중
weight_per_asset = 1.0 / len(selected_assets)
for ticker in selected_assets:
if ticker in portfolio:
portfolio[ticker] += weight_per_asset
else:
portfolio[ticker] = weight_per_asset
else:
# 공격 유니버스에서 선택 (SMA12 상대 모멘텀)
offensive_scores = {}
for ticker in config["offensive"]:
prices = self._get_ticker_data(ticker, as_of_date)
if not prices.empty:
momentum = self._calculate_sma12_momentum(prices)
offensive_scores[ticker] = momentum
price_val = prices.iloc[-1]
current_prices[ticker] = float(price_val.item()) if hasattr(price_val, 'item') else float(price_val)
# 상위 offensive_top개 선택
sorted_offensive = sorted(offensive_scores.items(), key=lambda x: x[1], reverse=True)
selected_assets = [ticker for ticker, _ in sorted_offensive[:offensive_top]]
# 선택된 자산이 없는 경우 BIL로 대체
if not selected_assets:
selected_assets = ["BIL"]
bil_prices = self._get_ticker_data("BIL", as_of_date)
if not bil_prices.empty:
price_val = bil_prices.iloc[-1]
current_prices["BIL"] = float(price_val.item()) if hasattr(price_val, 'item') else float(price_val)
# 동일 가중
weight_per_asset = 1.0 / len(selected_assets)
for ticker in selected_assets:
portfolio[ticker] = weight_per_asset
# 실제 매수 수량 계산
allocation_details = []
total_allocated = 0
for ticker, weight in portfolio.items():
if ticker in current_prices:
allocation_amount = float(initial_capital * weight)
price = float(current_prices[ticker])
shares = int(allocation_amount / price) # 정수 주식만 매수
actual_amount = float(shares * price)
allocation_details.append({
"ticker": ticker,
"weight": round(weight * 100, 2),
"target_amount": round(allocation_amount, 2),
"current_price": round(price, 2),
"shares": shares,
"actual_amount": round(actual_amount, 2)
})
total_allocated += actual_amount
cash_remaining = initial_capital - total_allocated
return {
"as_of_date": as_of_date.strftime("%Y-%m-%d"),
"mode": "defensive" if is_defensive else "offensive",
"canary_status": canary_status,
"canary_bad_count": canary_bad_count,
"breadth_threshold": breadth_param,
"portfolio": allocation_details,
"total_allocated": round(total_allocated, 2),
"cash_remaining": round(cash_remaining, 2),
"initial_capital": initial_capital,
"portfolio_summary": {
ticker: weight for ticker, weight in portfolio.items()
}
}
def execute(self, parameters: Dict[str, Any] = None) -> Dict[str, Any]:
if parameters is None:
parameters = self.default_parameters
if not self.validate_parameters(parameters):
raise ValueError("Invalid parameters")
# 실제 데이터를 사용하는 경우
if parameters.get("use_real_data", False):
real_portfolio = self._calculate_portfolio_real_data(parameters)
return {
"strategy": self.name,
"version": self.version,
"variant": parameters.get("variant", "BAA-G12"),
"mode": "real_data_portfolio",
"execution_time": "variable",
"parameters_used": parameters,
**real_portfolio
}
variant = parameters.get("variant", "BAA-G12")
# 시뮬레이션 실행
time.sleep(1.8) # 실행 시간 시뮬레이션
# 전략 변형에 따른 결과 시뮬레이션 (Full Sample 기준)
variant_stats = {
"BAA-G12": {
"cagr": 0.146,
"max_dd": 0.087,
"volatility": 0.085,
"sharpe": 1.19,
"upi": 4.81,
"defensive_fraction": 0.572,
"turnover": 4.72,
"offensive_assets": ["SPY", "QQQ", "IWM", "VGK", "EWJ", "VWO", "VNQ", "DBC", "GLD", "TLT", "HYG", "LQD"],
"defensive_assets": ["TIP", "DBC", "BIL", "IEF", "TLT", "LQD", "BND"],
"canary_assets": ["SPY", "VWO", "VEA", "BND"]
},
"BAA-G4": {
"cagr": 0.210,
"max_dd": 0.146,
"volatility": 0.136,
"sharpe": 1.21,
"upi": 5.20,
"defensive_fraction": 0.572,
"turnover": 5.23,
"offensive_assets": ["QQQ", "VWO", "VEA", "BND"],
"defensive_assets": ["TIP", "DBC", "BIL", "IEF", "TLT", "LQD", "BND"],
"canary_assets": ["SPY", "VWO", "VEA", "BND"]
},
"BAA-G12/T3": {
"cagr": 0.164,
"max_dd": 0.114,
"volatility": 0.104,
"sharpe": 1.13,
"upi": 4.23,
"defensive_fraction": 0.572,
"turnover": 5.13,
"offensive_assets": ["SPY", "QQQ", "IWM", "VGK", "EWJ", "VWO", "VNQ", "DBC", "GLD", "TLT", "HYG", "LQD"],
"defensive_assets": ["TIP", "DBC", "BIL", "IEF", "TLT", "LQD", "BND"],
"canary_assets": ["SPY", "VWO", "VEA", "BND"]
},
"BAA-G4/T2": {
"cagr": 0.177,
"max_dd": 0.127,
"volatility": 0.106,
"sharpe": 1.25,
"upi": 5.08,
"defensive_fraction": 0.572,
"turnover": 5.02,
"offensive_assets": ["QQQ", "VWO", "VEA", "BND"],
"defensive_assets": ["TIP", "DBC", "BIL", "IEF", "TLT", "LQD", "BND"],
"canary_assets": ["SPY", "VWO", "VEA", "BND"]
},
"BAA-SPY": {
"cagr": 0.114,
"max_dd": 0.162,
"volatility": 0.095,
"sharpe": 0.72,
"upi": 1.88,
"defensive_fraction": 0.572,
"turnover": 4.73,
"offensive_assets": ["SPY"],
"defensive_assets": ["TIP", "DBC", "BIL", "IEF", "TLT", "LQD", "BND"],
"canary_assets": ["SPY", "VWO", "VEA", "BND"]
}
}
stats = variant_stats.get(variant, variant_stats["BAA-G12"])
# 약간의 랜덤성 추가 (±5%)
cagr = stats["cagr"] * random.uniform(0.95, 1.05)
max_dd = stats["max_dd"] * random.uniform(0.95, 1.05)
volatility = stats["volatility"] * random.uniform(0.95, 1.05)
# 최종 자본 계산 (CAGR 기반 - 간단히 1년 성과로 시뮬레이션)
final_capital = parameters["initial_capital"] * (1 + cagr)
profit_loss = final_capital - parameters["initial_capital"]
profit_rate = (profit_loss / parameters["initial_capital"]) * 100
# 거래 횟수 (Turnover 기반)
trades_count = int(stats["turnover"] * 12 * random.uniform(0.9, 1.1)) # 연간 turnover
# Keller Ratio 계산: K = R(1-2D/(1-2D)) when D<25%
if max_dd < 0.25:
keller_ratio = cagr * (1 - 2 * max_dd) / (1 - 2 * max_dd)
else:
keller_ratio = 0.0
return {
"strategy": self.name,
"version": self.version,
"variant": variant,
"profit_loss": round(profit_loss, 2),
"profit_rate": round(profit_rate, 2),
"final_capital": round(final_capital, 2),
"cagr": round(cagr * 100, 2),
"max_drawdown": round(max_dd * 100, 2),
"volatility": round(volatility * 100, 2),
"sharpe_ratio": round(stats["sharpe"], 2),
"upi": round(stats["upi"], 2),
"keller_ratio": round(keller_ratio * 100, 2),
"defensive_fraction": round(stats["defensive_fraction"] * 100, 2),
"trades_executed": trades_count,
"annual_turnover": round(stats["turnover"] * 100, 2),
"execution_time": "1.8s",
"parameters_used": parameters,
"portfolio_details": {
"offensive_universe": stats["offensive_assets"],
"defensive_universe": stats["defensive_assets"],
"canary_universe": stats["canary_assets"],
"offensive_selection": parameters.get("offensive_top", 6),
"defensive_selection": parameters.get("defensive_top", 3),
"relative_momentum": "SMA(12)",
"absolute_momentum": "13612W",
"breadth_parameter": parameters.get("breadth_param", 1)
},
"methodology": {
"relative_momentum_filter": "SMA(12) - slow momentum for offensive/defensive selection",
"absolute_momentum_filter": "13612W - fast momentum for canary universe",
"crash_protection": "Switch to defensive when any canary asset shows negative momentum (B=1)",
"rebalancing": "Monthly, last trading day"
}
}

View File

@@ -0,0 +1,76 @@
"""
평균 회귀(Mean Reversion) 전략 구현체
RSI, 볼린저밴드 등 과매수/과매도 구간에서 반대 방향으로 거래하는 전략들을 포함합니다.
"""
from typing import Dict, Any
import time
import random
from ..base import BaseQuantStrategy, strategy
@strategy
class RSIMeanReversion(BaseQuantStrategy):
"""RSI 평균회귀 전략"""
@property
def name(self) -> str:
return "RSIMeanReversion"
@property
def description(self) -> str:
return "RSI 지표를 이용한 평균회귀 전략. RSI가 과매수/과매도 구간에서 반대 방향으로 거래"
@property
def version(self) -> str:
return "1.0.0"
@property
def default_parameters(self) -> Dict[str, Any]:
return {
"rsi_period": 14,
"oversold_threshold": 30,
"overbought_threshold": 70,
"initial_capital": 100000,
"position_size": 0.05
}
def validate_parameters(self, parameters: Dict[str, Any]) -> bool:
required_params = ["rsi_period", "oversold_threshold", "overbought_threshold", "initial_capital"]
for param in required_params:
if param not in parameters:
return False
if not (0 < parameters["oversold_threshold"] < parameters["overbought_threshold"] < 100):
return False
return True
def execute(self, parameters: Dict[str, Any] = None) -> Dict[str, Any]:
if parameters is None:
parameters = self.default_parameters
if not self.validate_parameters(parameters):
raise ValueError("Invalid parameters")
# 시뮬레이션 실행
time.sleep(1.5) # 실행 시간 시뮬레이션
# 모의 결과 생성
profit_rate = random.uniform(-0.10, 0.18)
trades_count = random.randint(25, 80)
win_rate = random.uniform(0.40, 0.65)
return {
"strategy": self.name,
"version": self.version,
"profit_loss": round(parameters["initial_capital"] * profit_rate, 2),
"profit_rate": round(profit_rate * 100, 2),
"trades_executed": trades_count,
"win_rate": round(win_rate, 3),
"execution_time": "1.5s",
"parameters_used": parameters,
"final_capital": round(parameters["initial_capital"] * (1 + profit_rate), 2),
"max_drawdown": round(random.uniform(0.05, 0.20), 3)
}

View File

@@ -0,0 +1,74 @@
"""
추세 추종(Trend Following) 전략 구현체
이동평균선, 모멘텀 등 추세를 따라가는 전략들을 포함합니다.
"""
from typing import Dict, Any
import time
import random
from ..base import BaseQuantStrategy, strategy
@strategy
class MovingAverageCrossover(BaseQuantStrategy):
"""이동평균선 교차 전략"""
@property
def name(self) -> str:
return "MovingAverageCrossover"
@property
def description(self) -> str:
return "단기 이동평균선이 장기 이동평균선을 상향 돌파할 때 매수, 하향 돌파할 때 매도하는 전략"
@property
def version(self) -> str:
return "1.0.0"
@property
def default_parameters(self) -> Dict[str, Any]:
return {
"short_window": 20,
"long_window": 50,
"initial_capital": 100000,
"position_size": 0.1
}
def validate_parameters(self, parameters: Dict[str, Any]) -> bool:
required_params = ["short_window", "long_window", "initial_capital"]
for param in required_params:
if param not in parameters:
return False
if parameters["short_window"] >= parameters["long_window"]:
return False
return True
def execute(self, parameters: Dict[str, Any] = None) -> Dict[str, Any]:
if parameters is None:
parameters = self.default_parameters
if not self.validate_parameters(parameters):
raise ValueError("Invalid parameters")
# 시뮬레이션 실행
time.sleep(1) # 실행 시간 시뮬레이션
# 모의 결과 생성
profit_rate = random.uniform(-0.15, 0.25)
trades_count = random.randint(15, 60)
win_rate = random.uniform(0.45, 0.75)
return {
"strategy": self.name,
"version": self.version,
"profit_loss": round(parameters["initial_capital"] * profit_rate, 2),
"profit_rate": round(profit_rate * 100, 2),
"trades_executed": trades_count,
"win_rate": round(win_rate, 3),
"execution_time": "1.2s",
"parameters_used": parameters,
"final_capital": round(parameters["initial_capital"] * (1 + profit_rate), 2)
}

View File

@@ -0,0 +1,77 @@
"""
변동성 돌파(Volatility Breakout) 전략 구현체
볼린저 밴드, ATR 등 변동성 기반 돌파 전략들을 포함합니다.
"""
from typing import Dict, Any
import time
import random
from ..base import BaseQuantStrategy, strategy
@strategy
class BollingerBandBreakout(BaseQuantStrategy):
"""볼린저 밴드 돌파 전략"""
@property
def name(self) -> str:
return "BollingerBandBreakout"
@property
def description(self) -> str:
return "볼린저 밴드 상한선 돌파시 매수, 하한선 돌파시 매도하는 돌파 전략"
@property
def version(self) -> str:
return "2.0.0"
@property
def default_parameters(self) -> Dict[str, Any]:
return {
"period": 20,
"std_dev": 2.0,
"initial_capital": 100000,
"position_size": 0.08,
"stop_loss": 0.05
}
def validate_parameters(self, parameters: Dict[str, Any]) -> bool:
required_params = ["period", "std_dev", "initial_capital"]
for param in required_params:
if param not in parameters:
return False
if parameters["std_dev"] <= 0 or parameters["period"] <= 0:
return False
return True
def execute(self, parameters: Dict[str, Any] = None) -> Dict[str, Any]:
if parameters is None:
parameters = self.default_parameters
if not self.validate_parameters(parameters):
raise ValueError("Invalid parameters")
# 시뮬레이션 실행
time.sleep(2) # 실행 시간 시뮬레이션
# 모의 결과 생성
profit_rate = random.uniform(-0.20, 0.30)
trades_count = random.randint(10, 40)
win_rate = random.uniform(0.35, 0.70)
return {
"strategy": self.name,
"version": self.version,
"profit_loss": round(parameters["initial_capital"] * profit_rate, 2),
"profit_rate": round(profit_rate * 100, 2),
"trades_executed": trades_count,
"win_rate": round(win_rate, 3),
"execution_time": "2.0s",
"parameters_used": parameters,
"final_capital": round(parameters["initial_capital"] * (1 + profit_rate), 2),
"sharpe_ratio": round(random.uniform(0.5, 2.5), 2),
"volatility": round(random.uniform(0.15, 0.35), 3)
}

View File

View File

@@ -0,0 +1,78 @@
"""
전략 레지스트리를 DB에 초기화하는 management command
"""
from django.core.management.base import BaseCommand
from strategies.base import StrategyRegistry
from strategies.models import QuantStrategy, StrategyVersion
class Command(BaseCommand):
help = '레지스트리에 등록된 모든 전략을 데이터베이스에 초기화합니다'
def handle(self, *args, **options):
self.stdout.write(self.style.MIGRATE_HEADING('전략 초기화 시작...'))
# 레지스트리에서 모든 전략 가져오기
available_strategies = StrategyRegistry.list_strategies()
created_strategies = 0
updated_strategies = 0
created_versions = 0
for strategy_name, strategy_info in available_strategies.items():
# QuantStrategy 생성 또는 업데이트
strategy_obj, created = QuantStrategy.objects.get_or_create(
name=strategy_name,
defaults={
'description': strategy_info['description'],
'is_active': True
}
)
if created:
created_strategies += 1
self.stdout.write(
self.style.SUCCESS(f' ✓ 전략 생성: {strategy_name}')
)
else:
# 설명이 변경된 경우 업데이트
if strategy_obj.description != strategy_info['description']:
strategy_obj.description = strategy_info['description']
strategy_obj.save()
updated_strategies += 1
self.stdout.write(
self.style.WARNING(f' ⟳ 전략 업데이트: {strategy_name}')
)
# 각 버전 생성
for version_info in strategy_info['versions']:
version_obj, version_created = StrategyVersion.objects.get_or_create(
strategy=strategy_obj,
version=version_info['version'],
defaults={
'implementation_key': f"{strategy_name}:{version_info['version']}",
'parameters': version_info['default_parameters'],
'is_current': True # 각 전략의 버전을 현재 버전으로 설정
}
)
if version_created:
created_versions += 1
self.stdout.write(
self.style.SUCCESS(f' → 버전 생성: v{version_info["version"]}')
)
else:
# 파라미터가 변경된 경우 업데이트
if version_obj.parameters != version_info['default_parameters']:
version_obj.parameters = version_info['default_parameters']
version_obj.save()
self.stdout.write(
self.style.WARNING(f' → 버전 업데이트: v{version_info["version"]}')
)
self.stdout.write('')
self.stdout.write(self.style.MIGRATE_HEADING('초기화 완료!'))
self.stdout.write(self.style.SUCCESS(f' 생성된 전략: {created_strategies}'))
self.stdout.write(self.style.SUCCESS(f' 업데이트된 전략: {updated_strategies}'))
self.stdout.write(self.style.SUCCESS(f' 생성된 버전: {created_versions}'))
self.stdout.write(self.style.SUCCESS(f' 총 전략 수: {len(available_strategies)}'))

View File

@@ -0,0 +1,33 @@
# Generated by Django 5.2.7 on 2025-10-04 13:02
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('strategies', '0002_remove_strategyversion_code_and_more'),
]
operations = [
migrations.AddField(
model_name='strategyexecution',
name='callback_response',
field=models.JSONField(blank=True, help_text='콜백 응답 데이터', null=True),
),
migrations.AddField(
model_name='strategyexecution',
name='callback_sent',
field=models.BooleanField(default=False, help_text='콜백 전송 완료 여부'),
),
migrations.AddField(
model_name='strategyexecution',
name='callback_sent_at',
field=models.DateTimeField(blank=True, help_text='콜백 전송 일시', null=True),
),
migrations.AddField(
model_name='strategyexecution',
name='callback_url',
field=models.URLField(blank=True, help_text='전략 실행 완료 후 결과를 전송할 콜백 URL', max_length=500, null=True),
),
]

View File

@@ -73,6 +73,10 @@ class StrategyExecution(models.Model):
completed_at = models.DateTimeField(null=True, blank=True)
result = models.JSONField(null=True, blank=True)
error_message = models.TextField(blank=True)
callback_url = models.URLField(max_length=500, blank=True, null=True, help_text="전략 실행 완료 후 결과를 전송할 콜백 URL")
callback_sent = models.BooleanField(default=False, help_text="콜백 전송 완료 여부")
callback_sent_at = models.DateTimeField(null=True, blank=True, help_text="콜백 전송 일시")
callback_response = models.JSONField(null=True, blank=True, help_text="콜백 응답 데이터")
def __str__(self):
return f"Execution of {self.strategy_version} - {self.status}"

View File

@@ -6,11 +6,15 @@ from django.utils import timezone
import json
import threading
import time
import requests
import logging
from .models import QuantStrategy, StrategyVersion, StrategyExecution
from .base import StrategyRegistry
from . import implementations # 구현체들을 로드하여 레지스트리에 등록
logger = logging.getLogger(__name__)
@require_http_methods(["GET"])
def list_strategies(request):
@@ -55,6 +59,7 @@ def execute_strategy(request):
strategy_name = data.get('strategy_name')
version = data.get('version')
execution_parameters = data.get('parameters', {})
callback_url = data.get('callback_url')
if not strategy_name:
return JsonResponse({
@@ -79,7 +84,8 @@ def execute_strategy(request):
execution = StrategyExecution.objects.create(
strategy_version=strategy_version,
execution_parameters=execution_parameters,
status='pending'
status='pending',
callback_url=callback_url
)
def run_strategy():
@@ -108,13 +114,19 @@ def execute_strategy(request):
execution.completed_at = timezone.now()
execution.save()
finally:
# 콜백 URL이 있으면 결과 전송
if execution.callback_url:
send_callback(execution)
thread = threading.Thread(target=run_strategy)
thread.start()
return JsonResponse({
'execution_id': execution.id,
'status': 'pending',
'message': 'Strategy execution started'
'message': 'Strategy execution started',
'callback_url': callback_url
})
except json.JSONDecodeError:
@@ -127,6 +139,65 @@ def execute_strategy(request):
}, status=500)
def send_callback(execution):
"""전략 실행 완료 후 콜백 URL로 결과 전송"""
if not execution.callback_url:
return
try:
# 콜백 데이터 구성
callback_data = {
'execution_id': execution.id,
'strategy': execution.strategy_version.strategy.name,
'version': execution.strategy_version.version,
'status': execution.status,
'started_at': execution.started_at.isoformat(),
'completed_at': execution.completed_at.isoformat() if execution.completed_at else None,
'execution_parameters': execution.execution_parameters
}
if execution.status == 'completed' and execution.result:
callback_data['result'] = execution.result
if execution.status == 'failed' and execution.error_message:
callback_data['error_message'] = execution.error_message
# POST 요청 전송 (타임아웃 10초)
response = requests.post(
execution.callback_url,
json=callback_data,
headers={'Content-Type': 'application/json'},
timeout=10
)
# 콜백 전송 결과 저장
execution.callback_sent = True
execution.callback_sent_at = timezone.now()
execution.callback_response = {
'status_code': response.status_code,
'response_text': response.text[:500], # 처음 500자만 저장
'headers': dict(response.headers)
}
execution.save()
logger.info(f"Callback sent successfully to {execution.callback_url} for execution {execution.id}")
except requests.exceptions.Timeout:
logger.error(f"Callback timeout for execution {execution.id} to {execution.callback_url}")
execution.callback_response = {'error': 'timeout'}
execution.save()
except requests.exceptions.RequestException as e:
logger.error(f"Callback failed for execution {execution.id} to {execution.callback_url}: {str(e)}")
execution.callback_response = {'error': str(e)}
execution.save()
except Exception as e:
logger.error(f"Unexpected error sending callback for execution {execution.id}: {str(e)}")
execution.callback_response = {'error': f'unexpected_error: {str(e)}'}
execution.save()
@require_http_methods(["GET"])
def execution_status(request, execution_id):
execution = get_object_or_404(StrategyExecution, id=execution_id)
@@ -149,6 +220,15 @@ def execution_status(request, execution_id):
if execution.status == 'failed' and execution.error_message:
response_data['error_message'] = execution.error_message
# 콜백 정보 추가
if execution.callback_url:
response_data['callback'] = {
'url': execution.callback_url,
'sent': execution.callback_sent,
'sent_at': execution.callback_sent_at.isoformat() if execution.callback_sent_at else None,
'response': execution.callback_response
}
return JsonResponse(response_data)

69
test_baa.py Normal file
View File

@@ -0,0 +1,69 @@
#!/usr/bin/env python
"""BAA 전략 테스트 스크립트"""
from strategies.implementations import BoldAssetAllocation
import json
def test_simulation_mode():
"""시뮬레이션 모드 테스트"""
print("=" * 80)
print("시뮬레이션 모드 테스트 (BAA-G12)")
print("=" * 80)
strategy = BoldAssetAllocation()
result = strategy.execute({
"initial_capital": 100000,
"variant": "BAA-G12",
"use_real_data": False
})
print(json.dumps(result, indent=2, ensure_ascii=False))
print()
def test_real_data_mode():
"""실제 데이터 모드 테스트"""
print("=" * 80)
print("실제 데이터 모드 테스트 (BAA-G4)")
print("=" * 80)
strategy = BoldAssetAllocation()
result = strategy.execute({
"initial_capital": 50000, # $50,000
"variant": "BAA-G4",
"use_real_data": True,
"as_of_date": None, # 현재 날짜
})
print(json.dumps(result, indent=2, ensure_ascii=False))
print()
def test_specific_date():
"""특정 날짜 기준 테스트"""
print("=" * 80)
print("특정 날짜 기준 테스트 (2024-01-31)")
print("=" * 80)
strategy = BoldAssetAllocation()
result = strategy.execute({
"initial_capital": 100000,
"variant": "BAA-G12",
"use_real_data": True,
"as_of_date": "2024-01-31",
})
print(json.dumps(result, indent=2, ensure_ascii=False))
print()
if __name__ == "__main__":
# 시뮬레이션 모드 테스트
test_simulation_mode()
# 실제 데이터 모드 테스트
print("\n실제 데이터 다운로드 중... (시간이 걸릴 수 있습니다)\n")
test_real_data_mode()
# 특정 날짜 테스트
test_specific_date()

159
test_callback.py Executable file
View File

@@ -0,0 +1,159 @@
#!/usr/bin/env python
"""콜백 기능 테스트 스크립트"""
import os
import django
import json
import time
from http.server import HTTPServer, BaseHTTPRequestHandler
import threading
# Django 설정
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'executor.settings')
django.setup()
from strategies.models import StrategyExecution
import requests
class CallbackHandler(BaseHTTPRequestHandler):
"""콜백 수신용 간단한 HTTP 서버"""
received_data = None
def do_POST(self):
content_length = int(self.headers['Content-Length'])
post_data = self.rfile.read(content_length)
# 수신한 데이터 저장
CallbackHandler.received_data = json.loads(post_data.decode('utf-8'))
print("\n" + "="*80)
print("콜백 수신!")
print("="*80)
print(json.dumps(CallbackHandler.received_data, indent=2, ensure_ascii=False))
print("="*80 + "\n")
# 200 OK 응답
self.send_response(200)
self.send_header('Content-Type', 'application/json')
self.end_headers()
self.wfile.write(json.dumps({'status': 'received'}).encode('utf-8'))
def log_message(self, format, *args):
# 기본 로그 메시지 비활성화
pass
def start_callback_server(port=8888):
"""콜백 수신 서버 시작"""
server = HTTPServer(('localhost', port), CallbackHandler)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
print(f"✓ 콜백 서버 시작: http://localhost:{port}")
return server
def test_callback():
"""콜백 기능 테스트"""
print("\n" + "="*80)
print("콜백 기능 테스트")
print("="*80 + "\n")
# 1. 콜백 서버 시작
callback_port = 8888
callback_url = f"http://localhost:{callback_port}/callback"
server = start_callback_server(callback_port)
time.sleep(1)
# 2. 전략 실행 요청 (콜백 URL 포함)
print("전략 실행 요청 중...")
request_data = {
"strategy_name": "MovingAverageCrossover",
"parameters": {
"short_window": 10,
"long_window": 30,
"initial_capital": 50000
},
"callback_url": callback_url
}
response = requests.post(
'http://localhost:8000/strategies/execute/',
json=request_data
)
if response.status_code != 200:
print(f"✗ 실행 요청 실패: {response.text}")
server.shutdown()
return
result = response.json()
execution_id = result['execution_id']
print(f"✓ 실행 시작됨 (ID: {execution_id})")
print(f" 콜백 URL: {result.get('callback_url', 'N/A')}")
# 3. 전략 실행 완료 대기
print("\n전략 실행 완료 대기 중...")
max_wait = 30
waited = 0
while waited < max_wait:
time.sleep(2)
waited += 2
# 상태 확인
status_response = requests.get(f'http://localhost:8000/executions/{execution_id}/')
status_data = status_response.json()
print(f" 상태: {status_data['status']} ({waited}초 경과)")
if status_data['status'] in ['completed', 'failed']:
break
# 4. 콜백 수신 대기
print("\n콜백 수신 대기 중...")
time.sleep(3)
# 5. 결과 확인
status_response = requests.get(f'http://localhost:8000/executions/{execution_id}/')
final_status = status_response.json()
print("\n" + "="*80)
print("최종 결과")
print("="*80)
print(f"실행 상태: {final_status['status']}")
if 'callback' in final_status:
callback_info = final_status['callback']
print(f"\n콜백 정보:")
print(f" URL: {callback_info['url']}")
print(f" 전송 완료: {callback_info['sent']}")
print(f" 전송 시각: {callback_info['sent_at']}")
print(f" 응답 코드: {callback_info['response'].get('status_code', 'N/A')}")
if CallbackHandler.received_data:
print(f"\n✓ 콜백 데이터 수신 성공!")
print(f" 실행 ID: {CallbackHandler.received_data.get('execution_id')}")
print(f" 전략: {CallbackHandler.received_data.get('strategy')}")
print(f" 상태: {CallbackHandler.received_data.get('status')}")
else:
print(f"\n✗ 콜백 데이터 수신 실패")
print("="*80 + "\n")
# 6. 서버 종료
server.shutdown()
if __name__ == '__main__':
try:
test_callback()
except KeyboardInterrupt:
print("\n\n테스트 중단됨")
except Exception as e:
print(f"\n\n✗ 오류 발생: {e}")
import traceback
traceback.print_exc()