feat: 프로젝트 기본 구조 구축
This commit is contained in:
@@ -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
66
.dockerignore
Normal 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
17
.env.example
Normal 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
428
API_USAGE_GUIDE.md
Normal 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
494
BAA_CURL_EXAMPLES.md
Normal 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
227
BAA_STRATEGY_README.md
Normal 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
|
||||
|
||||
## 라이선스
|
||||
|
||||
이 구현은 교육 및 연구 목적으로만 사용되어야 합니다.
|
||||
27389
Bold Asset Allocation BAA.pdf.pdf
Normal file
27389
Bold Asset Allocation BAA.pdf.pdf
Normal file
File diff suppressed because it is too large
Load Diff
451
CALLBACK_API_GUIDE.md
Normal file
451
CALLBACK_API_GUIDE.md
Normal 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
448
DEPLOYMENT_GUIDE.md
Normal 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
45
Dockerfile
Normal 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
287
README_API.md
Normal 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
301
REFACTORING_SUMMARY.md
Normal 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
412
STRATEGY_MODULES_README.md
Normal 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
150
baa_api_examples.sh
Executable 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
60
callback_example.sh
Executable 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
106
deploy.sh
Executable 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
38
docker-compose.yml
Normal 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
21
docker-entrypoint.sh
Executable 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
98
list_strategies.py
Executable 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()
|
||||
@@ -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
118
quick_baa_test.sh
Executable 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 "=================================="
|
||||
@@ -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}")
|
||||
|
||||
@@ -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',
|
||||
]
|
||||
|
||||
18
strategies/impls/__init__.py
Normal file
18
strategies/impls/__init__.py
Normal 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',
|
||||
]
|
||||
449
strategies/impls/asset_allocation.py
Normal file
449
strategies/impls/asset_allocation.py
Normal 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"
|
||||
}
|
||||
}
|
||||
76
strategies/impls/mean_reversion.py
Normal file
76
strategies/impls/mean_reversion.py
Normal 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)
|
||||
}
|
||||
74
strategies/impls/trend_following.py
Normal file
74
strategies/impls/trend_following.py
Normal 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)
|
||||
}
|
||||
77
strategies/impls/volatility_breakout.py
Normal file
77
strategies/impls/volatility_breakout.py
Normal 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)
|
||||
}
|
||||
0
strategies/management/__init__.py
Normal file
0
strategies/management/__init__.py
Normal file
0
strategies/management/commands/__init__.py
Normal file
0
strategies/management/commands/__init__.py
Normal file
78
strategies/management/commands/init_strategies.py
Normal file
78
strategies/management/commands/init_strategies.py
Normal 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)}개'))
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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}"
|
||||
|
||||
@@ -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
69
test_baa.py
Normal 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
159
test_callback.py
Executable 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()
|
||||
Reference in New Issue
Block a user