feat: 전략 백테스팅 기능 추가

- Introduced a `backtest_strategy` endpoint to enable strategy backtesting with user-specified parameters.
- Implemented a generic backtesting engine allowing rebalancing, equity curve tracking, and performance metric calculations.
- Added `BacktestMixin` for strategies to support backtesting-related operations.
- Extended BAA strategy to support backtesting with ticker data download and portfolio simulation.
- Updated `urls.py` to include the new backtesting endpoint.
- Enhanced logging and error handling throughout the backtesting process.
This commit is contained in:
2026-02-08 13:54:05 +09:00
parent 3be9d8eeba
commit b64a76a8b9
6 changed files with 605 additions and 39 deletions

View File

@@ -1,7 +1,9 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Dict, Any from typing import Dict, Any, List
import json import json
import pandas as pd
class BaseQuantStrategy(ABC): class BaseQuantStrategy(ABC):
""" """
@@ -58,6 +60,29 @@ class BaseQuantStrategy(ABC):
return True return True
class BacktestMixin(ABC):
"""백테스트 가능한 전략을 위한 믹스인 클래스"""
@abstractmethod
def get_backtest_tickers(self, parameters: Dict[str, Any]) -> List[str]:
"""백테스트에 필요한 모든 티커 목록 반환"""
pass
@abstractmethod
def bulk_download_data(self, tickers: List[str], start_date, end_date) -> Dict[str, pd.Series]:
"""전체 기간 데이터 일괄 다운로드"""
pass
@abstractmethod
def calculate_portfolio_for_date(self, parameters: Dict[str, Any], as_of_date, data_cache: Dict[str, pd.Series]) -> Dict[str, Any]:
"""특정 날짜의 포트폴리오 배분 계산 (캐시 데이터 사용)
Returns:
{'mode': str, 'portfolio_weights': dict[str, float]}
"""
pass
class StrategyRegistry: class StrategyRegistry:
"""전략 구현체 레지스트리""" """전략 구현체 레지스트리"""

View File

@@ -4,17 +4,52 @@
전술적 자산배분, 리스크 패리티 등 다양한 자산에 배분하는 전략들을 포함합니다. 전술적 자산배분, 리스크 패리티 등 다양한 자산에 배분하는 전략들을 포함합니다.
""" """
from typing import Dict, Any from typing import Dict, Any, List
import time import time
import random import random
from datetime import datetime, timedelta from datetime import datetime, timedelta
import yfinance as yf import yfinance as yf
import pandas as pd import pandas as pd
from ..base import BaseQuantStrategy, strategy from ..base import BaseQuantStrategy, BacktestMixin, strategy
# BAA 전략 자산 유니버스 설정
BAA_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,
}
}
@strategy @strategy
class BoldAssetAllocation(BaseQuantStrategy): class BoldAssetAllocation(BaseQuantStrategy, BacktestMixin):
"""Bold Asset Allocation (BAA) 전략 """Bold Asset Allocation (BAA) 전략
상대 모멘텀과 절대 모멘텀을 결합한 공격적 전술적 자산배분 전략. 상대 모멘텀과 절대 모멘텀을 결합한 공격적 전술적 자산배분 전략.
@@ -118,6 +153,149 @@ class BoldAssetAllocation(BaseQuantStrategy):
print(f"Error downloading {ticker}: {e}") print(f"Error downloading {ticker}: {e}")
return pd.Series() return pd.Series()
# === BacktestMixin 구현 ===
def get_backtest_tickers(self, parameters: Dict[str, Any]) -> List[str]:
"""백테스트에 필요한 모든 티커 목록 반환"""
variant = parameters.get('variant', 'BAA-G12')
config = BAA_UNIVERSE_CONFIG.get(variant, BAA_UNIVERSE_CONFIG['BAA-G12'])
tickers = set()
tickers.update(config['offensive'])
tickers.update(config['defensive'])
tickers.update(config['canary'])
tickers.add('BIL') # 안전자산 대체용
return sorted(tickers)
def bulk_download_data(self, tickers: List[str], start_date, end_date) -> Dict[str, pd.Series]:
"""전체 기간 데이터 일괄 다운로드 (모멘텀 lookback 포함)"""
# 모멘텀 계산에 필요한 lookback 기간 추가 (400일)
adjusted_start = start_date - timedelta(days=400)
data_cache = {}
try:
# yfinance 일괄 다운로드
raw_data = yf.download(
tickers, start=adjusted_start, end=end_date,
progress=False, auto_adjust=True
)
if raw_data.empty:
return data_cache
for ticker in tickers:
try:
if len(tickers) == 1:
# 단일 티커: 컬럼 구조가 다름
if 'Close' in raw_data.columns:
series = raw_data['Close'].dropna()
else:
series = raw_data.dropna()
else:
# 다중 티커
if 'Close' in raw_data.columns:
series = raw_data['Close'][ticker].dropna()
else:
series = raw_data[ticker].dropna()
if not series.empty:
data_cache[ticker] = series
except (KeyError, TypeError):
continue
except Exception as e:
print(f"Error bulk downloading data: {e}")
return data_cache
def calculate_portfolio_for_date(self, parameters: Dict[str, Any], as_of_date, data_cache: Dict[str, pd.Series]) -> Dict[str, Any]:
"""특정 날짜의 포트폴리오 배분 계산 (캐시 데이터 사용)"""
variant = parameters.get('variant', 'BAA-G12')
config = BAA_UNIVERSE_CONFIG.get(variant, BAA_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)
def get_prices_from_cache(ticker):
"""캐시에서 as_of_date 이전 데이터만 슬라이싱 (미래 데이터 유출 방지)"""
if ticker not in data_cache:
return pd.Series()
series = data_cache[ticker]
# as_of_date 이전 데이터만 사용
mask = series.index <= pd.Timestamp(as_of_date)
return series[mask]
# 카나리아 유니버스 체크 (13612W 모멘텀 사용)
canary_bad_count = 0
for ticker in config['canary']:
prices = get_prices_from_cache(ticker)
if not prices.empty:
momentum = self._calculate_13612w_momentum(prices)
if momentum < 0:
canary_bad_count += 1
# 방어 모드 여부 결정
is_defensive = canary_bad_count >= breadth_param
portfolio = {}
if is_defensive:
# 방어 유니버스에서 선택
defensive_scores = {}
for ticker in config['defensive']:
prices = get_prices_from_cache(ticker)
if not prices.empty:
momentum = self._calculate_sma12_momentum(prices)
defensive_scores[ticker] = momentum
# BIL 모멘텀
bil_prices = get_prices_from_cache('BIL')
bil_momentum = self._calculate_sma12_momentum(bil_prices) if not bil_prices.empty else 0
# 상위 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]:
if momentum < bil_momentum:
selected_assets.append('BIL')
else:
selected_assets.append(ticker)
if not selected_assets:
selected_assets = ['BIL']
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 = get_prices_from_cache(ticker)
if not prices.empty:
momentum = self._calculate_sma12_momentum(prices)
offensive_scores[ticker] = momentum
sorted_offensive = sorted(offensive_scores.items(), key=lambda x: x[1], reverse=True)
selected_assets = [ticker for ticker, _ in sorted_offensive[:offensive_top]]
if not selected_assets:
selected_assets = ['BIL']
weight_per_asset = 1.0 / len(selected_assets)
for ticker in selected_assets:
portfolio[ticker] = weight_per_asset
return {
'mode': 'defensive' if is_defensive else 'offensive',
'portfolio_weights': portfolio
}
def _calculate_portfolio_real_data(self, parameters: Dict[str, Any]) -> Dict[str, Any]: def _calculate_portfolio_real_data(self, parameters: Dict[str, Any]) -> Dict[str, Any]:
"""실제 데이터를 사용한 포트폴리오 계산""" """실제 데이터를 사용한 포트폴리오 계산"""
variant = parameters.get("variant", "BAA-G12") variant = parameters.get("variant", "BAA-G12")
@@ -130,41 +308,7 @@ class BoldAssetAllocation(BaseQuantStrategy):
elif isinstance(as_of_date, str): elif isinstance(as_of_date, str):
as_of_date = datetime.strptime(as_of_date, "%Y-%m-%d") as_of_date = datetime.strptime(as_of_date, "%Y-%m-%d")
# 자산 유니버스 정의 config = BAA_UNIVERSE_CONFIG.get(variant, BAA_UNIVERSE_CONFIG["BAA-G12"])
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"]) offensive_top = parameters.get("offensive_top", config["offensive_top"])
defensive_top = parameters.get("defensive_top", 3) defensive_top = parameters.get("defensive_top", 3)
breadth_param = parameters.get("breadth_param", 1) breadth_param = parameters.get("breadth_param", 1)

View File

View File

@@ -0,0 +1,292 @@
"""
백테스트 엔진 모듈
전략에 무관한 범용 백테스팅 로직을 제공합니다.
BacktestMixin을 구현한 전략이면 어떤 전략이든 백테스트할 수 있습니다.
"""
import math
import logging
from datetime import datetime
import numpy as np
import pandas as pd
from ..base import BacktestMixin
logger = logging.getLogger(__name__)
def run_backtest(strategy_instance, parameters):
"""범용 백테스트 실행
Args:
strategy_instance: BacktestMixin을 구현한 전략 인스턴스
parameters: 백테스트 파라미터 (backtest_start_date, backtest_end_date, initial_capital 등)
Returns:
백테스트 결과 딕셔너리
"""
if not isinstance(strategy_instance, BacktestMixin):
raise ValueError(f'{strategy_instance.__class__.__name__} does not implement BacktestMixin')
# 파라미터 추출
start_date_str = parameters['backtest_start_date']
end_date_str = parameters['backtest_end_date']
initial_capital = parameters.get('initial_capital', 100000)
transaction_cost_rate = parameters.get('transaction_cost', 0.001)
start_date = datetime.strptime(start_date_str, '%Y-%m-%d')
end_date = datetime.strptime(end_date_str, '%Y-%m-%d')
# 1. 티커 수집
tickers = strategy_instance.get_backtest_tickers(parameters)
# 2. 전체 기간 데이터 일괄 다운로드
data_cache = strategy_instance.bulk_download_data(tickers, start_date, end_date)
if not data_cache:
raise ValueError('Failed to download market data')
# 3. 리밸런싱 날짜 생성 (월말 영업일)
rebalance_dates = pd.date_range(
start=start_date, end=end_date, freq='BMS'
)
# BMS = Business Month Start -> 첫 영업일이지만, 실제로는 월말에 리밸런싱하므로 BMonthEnd 사용
rebalance_dates = pd.date_range(
start=start_date, end=end_date, freq='BME'
)
if len(rebalance_dates) == 0:
raise ValueError('No rebalancing dates in the given period')
# 4. 모든 티커의 공통 거래일 인덱스 구축
all_dates = set()
for series in data_cache.values():
all_dates.update(series.index)
all_dates = sorted(all_dates)
all_trading_dates = pd.DatetimeIndex(all_dates)
# start_date 이후의 거래일만
all_trading_dates = all_trading_dates[all_trading_dates >= pd.Timestamp(start_date)]
all_trading_dates = all_trading_dates[all_trading_dates <= pd.Timestamp(end_date)]
if len(all_trading_dates) == 0:
raise ValueError('No trading dates in the given period')
# 5. 백테스트 루프
portfolio_value = float(initial_capital)
current_weights = {} # ticker -> weight
prev_weights = {}
daily_equity_curve = []
monthly_returns_list = []
allocation_history = []
# 리밸런싱 날짜를 set으로 변환하여 빠른 조회
rebal_date_set = set()
for rd in rebalance_dates:
# 가장 가까운 거래일 찾기 (rd 이하)
candidates = all_trading_dates[all_trading_dates <= rd]
if len(candidates) > 0:
rebal_date_set.add(candidates[-1])
rebal_dates_sorted = sorted(rebal_date_set)
month_start_value = portfolio_value
last_rebal_idx = -1
for i, date in enumerate(all_trading_dates):
# 일별 포트폴리오 가치 업데이트 (리밸런싱 사이)
if current_weights and i > 0:
prev_date = all_trading_dates[i - 1]
daily_return = 0.0
for ticker, weight in current_weights.items():
if ticker in data_cache:
series = data_cache[ticker]
if date in series.index and prev_date in series.index:
prev_price = float(series[prev_date])
curr_price = float(series[date])
if prev_price > 0:
ticker_return = (curr_price / prev_price) - 1
daily_return += weight * ticker_return
portfolio_value *= (1 + daily_return)
# 리밸런싱 체크
if date in rebal_date_set:
result = strategy_instance.calculate_portfolio_for_date(
parameters, date, data_cache
)
new_weights = result['portfolio_weights']
mode = result['mode']
# 턴오버 계산
turnover = _calculate_turnover(current_weights, new_weights)
cost = portfolio_value * turnover * transaction_cost_rate
portfolio_value -= cost
prev_weights = current_weights.copy()
current_weights = new_weights.copy()
allocation_history.append({
'date': date.strftime('%Y-%m-%d'),
'mode': mode,
'weights': {k: round(v, 4) for k, v in new_weights.items()},
'turnover': round(turnover, 4),
'cost': round(cost, 2)
})
# 일별 equity curve 기록
daily_equity_curve.append({
'date': date.strftime('%Y-%m-%d'),
'value': round(portfolio_value, 2)
})
# 월말 수익률 기록
if date in rebal_date_set:
monthly_return = (portfolio_value / month_start_value) - 1 if month_start_value > 0 else 0
monthly_returns_list.append({
'date': date.strftime('%Y-%m-%d'),
'return': round(monthly_return, 6)
})
month_start_value = portfolio_value
# 6. 성과 지표 계산
daily_values = [d['value'] for d in daily_equity_curve]
monthly_rets = [m['return'] for m in monthly_returns_list]
metrics = calculate_performance_metrics(daily_values, monthly_rets, initial_capital)
# drawdown 시리즈 계산
drawdown_series = _calculate_drawdown_series(daily_equity_curve)
return {
'strategy': strategy_instance.name,
'variant': parameters.get('variant', ''),
'backtest_start_date': start_date_str,
'backtest_end_date': end_date_str,
'initial_capital': initial_capital,
'final_capital': round(portfolio_value, 2),
'metrics': metrics,
'daily_equity_curve': daily_equity_curve,
'monthly_returns': monthly_returns_list,
'allocation_history': allocation_history,
'drawdown_series': drawdown_series,
}
def calculate_performance_metrics(daily_values, monthly_returns, initial_capital):
"""백테스트 성과 지표 계산
Args:
daily_values: 일별 포트폴리오 가치 리스트
monthly_returns: 월별 수익률 리스트
initial_capital: 초기 자본
Returns:
성과 지표 딕셔너리
"""
if not daily_values or len(daily_values) < 2:
return {}
final_value = daily_values[-1]
total_return = (final_value / initial_capital - 1) * 100
# 일별 수익률
daily_returns = []
for i in range(1, len(daily_values)):
if daily_values[i - 1] > 0:
daily_returns.append(daily_values[i] / daily_values[i - 1] - 1)
if not daily_returns:
return {}
daily_returns_arr = np.array(daily_returns)
trading_days = len(daily_returns)
years = trading_days / 252
# CAGR
if years > 0 and initial_capital > 0:
cagr = (final_value / initial_capital) ** (1 / years) - 1
else:
cagr = 0
# 연율화 변동성
annual_volatility = float(np.std(daily_returns_arr) * np.sqrt(252))
# 최대 낙폭 (MDD)
peak = daily_values[0]
max_dd = 0
for val in daily_values:
if val > peak:
peak = val
dd = (peak - val) / peak
if dd > max_dd:
max_dd = dd
# Sharpe ratio (무위험 수익률 0 가정)
if annual_volatility > 0:
sharpe_ratio = cagr / annual_volatility
else:
sharpe_ratio = 0
# Sortino ratio
downside_returns = daily_returns_arr[daily_returns_arr < 0]
if len(downside_returns) > 0:
downside_vol = float(np.std(downside_returns) * np.sqrt(252))
sortino_ratio = cagr / downside_vol if downside_vol > 0 else 0
else:
sortino_ratio = 0
# Calmar ratio
calmar_ratio = cagr / max_dd if max_dd > 0 else 0
# 월별 통계
if monthly_returns:
win_months = sum(1 for r in monthly_returns if r > 0)
win_rate = (win_months / len(monthly_returns)) * 100
best_month = max(monthly_returns) * 100
worst_month = min(monthly_returns) * 100
else:
win_rate = 0
best_month = 0
worst_month = 0
return {
'cagr': round(cagr * 100, 2),
'total_return': round(total_return, 2),
'annual_volatility': round(annual_volatility * 100, 2),
'max_drawdown': round(max_dd * 100, 2),
'sharpe_ratio': round(sharpe_ratio, 2),
'sortino_ratio': round(sortino_ratio, 2),
'calmar_ratio': round(calmar_ratio, 2),
'win_rate': round(win_rate, 2),
'best_month': round(best_month, 2),
'worst_month': round(worst_month, 2),
}
def _calculate_turnover(old_weights, new_weights):
"""포트폴리오 턴오버 계산 (0~2 범위)"""
all_tickers = set(list(old_weights.keys()) + list(new_weights.keys()))
turnover = 0.0
for ticker in all_tickers:
old_w = old_weights.get(ticker, 0)
new_w = new_weights.get(ticker, 0)
turnover += abs(new_w - old_w)
return turnover
def _calculate_drawdown_series(daily_equity_curve):
"""drawdown 시리즈 계산"""
drawdowns = []
peak = 0
for entry in daily_equity_curve:
val = entry['value']
if val > peak:
peak = val
dd = (val - peak) / peak if peak > 0 else 0
drawdowns.append({
'date': entry['date'],
'drawdown': round(dd * 100, 2)
})
return drawdowns

View File

@@ -4,6 +4,7 @@ from . import views
urlpatterns = [ urlpatterns = [
path('strategies/', views.list_strategies, name='list_strategies'), path('strategies/', views.list_strategies, name='list_strategies'),
path('strategies/execute/', views.execute_strategy, name='execute_strategy'), path('strategies/execute/', views.execute_strategy, name='execute_strategy'),
path('strategies/backtest/', views.backtest_strategy, name='backtest_strategy'),
path('strategies/implementations/', views.list_available_implementations, name='list_available_implementations'), path('strategies/implementations/', views.list_available_implementations, name='list_available_implementations'),
path('executions/<int:execution_id>/', views.execution_status, name='execution_status'), path('executions/<int:execution_id>/', views.execution_status, name='execution_status'),
] ]

View File

@@ -11,6 +11,7 @@ import logging
from .models import QuantStrategy, StrategyVersion, StrategyExecution from .models import QuantStrategy, StrategyVersion, StrategyExecution
from .base import StrategyRegistry from .base import StrategyRegistry
from .services.backtest import run_backtest
from . import implementations # 구현체들을 로드하여 레지스트리에 등록 from . import implementations # 구현체들을 로드하여 레지스트리에 등록
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -239,3 +240,106 @@ def list_available_implementations(request):
return JsonResponse({ return JsonResponse({
'available_implementations': available_strategies 'available_implementations': available_strategies
}) })
@csrf_exempt
@require_http_methods(["POST"])
def backtest_strategy(request):
"""전략 백테스트 실행"""
try:
data = json.loads(request.body)
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({
'error': 'strategy_name is required'
}, status=400)
# 백테스트 필수 파라미터 검증
if 'backtest_start_date' not in execution_parameters:
return JsonResponse({
'error': 'parameters.backtest_start_date is required'
}, status=400)
if 'backtest_end_date' not in execution_parameters:
return JsonResponse({
'error': 'parameters.backtest_end_date is required'
}, status=400)
strategy = get_object_or_404(QuantStrategy, name=strategy_name, is_active=True)
if version:
strategy_version = get_object_or_404(
StrategyVersion,
strategy=strategy,
version=version
)
else:
strategy_version = strategy.versions.filter(is_current=True).first()
if not strategy_version:
return JsonResponse({
'error': 'No current version found for this strategy'
}, status=404)
# execution_mode를 backtest로 표시
backtest_params = execution_parameters.copy()
backtest_params['execution_mode'] = 'backtest'
execution = StrategyExecution.objects.create(
strategy_version=strategy_version,
execution_parameters=backtest_params,
status='pending',
callback_url=callback_url
)
def run_backtest_task():
try:
execution.status = 'running'
execution.save()
strategy_impl = strategy_version.get_strategy_implementation()
# 기본 파라미터와 실행 파라미터를 병합
merged_parameters = strategy_impl.default_parameters.copy()
merged_parameters.update(execution_parameters)
# 백테스트 실행
result = run_backtest(strategy_impl, merged_parameters)
execution.status = 'completed'
execution.result = result
execution.completed_at = timezone.now()
execution.save()
except Exception as e:
logger.exception(f'Backtest failed for execution {execution.id}')
execution.status = 'failed'
execution.error_message = str(e)
execution.completed_at = timezone.now()
execution.save()
finally:
if execution.callback_url:
send_callback(execution)
thread = threading.Thread(target=run_backtest_task)
thread.start()
return JsonResponse({
'execution_id': execution.id,
'status': 'pending',
'message': 'Backtest execution started',
'callback_url': callback_url
})
except json.JSONDecodeError:
return JsonResponse({
'error': 'Invalid JSON in request body'
}, status=400)
except Exception as e:
return JsonResponse({
'error': str(e)
}, status=500)