feat: 프로젝트 기본 구조 구축
This commit is contained in:
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 | 콜백 기능 최초 구현 |
|
||||
Reference in New Issue
Block a user