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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

View File

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

View File

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

View File

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

View File

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