255 lines
8.1 KiB
Python
255 lines
8.1 KiB
Python
"""
|
|
Strategy Manager
|
|
|
|
전략 등록·실행·관리를 담당하는 오케스트레이터
|
|
"""
|
|
from typing import Dict, List, Optional, Any
|
|
from collections import deque
|
|
import logging
|
|
import asyncio
|
|
from datetime import datetime
|
|
|
|
from app.strategy.base_strategy import BaseStrategy
|
|
from app.strategy.signal import Signal, SignalType
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class StrategyManager:
|
|
"""
|
|
전략 관리자
|
|
|
|
- 전략 등록/삭제
|
|
- 복수 전략 동시 실행
|
|
- 신호 충돌 해결
|
|
- 전략 우선순위 관리
|
|
"""
|
|
|
|
def __init__(self):
|
|
self.strategies: Dict[str, BaseStrategy] = {} # 전략 등록부
|
|
self.priority_order: List[str] = [] # 우선순위
|
|
self.signal_history: deque = deque(maxlen=1000) # 신호 이력 (최대 1000건, 메모리 누수 방지)
|
|
|
|
logger.info("🎯 StrategyManager 초기화")
|
|
|
|
def register_strategy(self, strategy: BaseStrategy, priority: int = 100):
|
|
"""
|
|
전략 등록
|
|
|
|
Args:
|
|
strategy: 등록할 전략 인스턴스
|
|
priority: 우선순위 (낮을수록 먼저 실행)
|
|
"""
|
|
if strategy.name in self.strategies:
|
|
logger.warning(f"⚠️ 전략 이미 등록됨: {strategy.name}")
|
|
return
|
|
|
|
self.strategies[strategy.name] = strategy
|
|
|
|
# 우선순위 삽입
|
|
inserted = False
|
|
for i, name in enumerate(self.priority_order):
|
|
existing_strategy = self.strategies[name]
|
|
existing_priority = getattr(existing_strategy, '_priority', 100)
|
|
if priority < existing_priority:
|
|
self.priority_order.insert(i, strategy.name)
|
|
inserted = True
|
|
break
|
|
|
|
if not inserted:
|
|
self.priority_order.append(strategy.name)
|
|
|
|
strategy._priority = priority
|
|
logger.info(f"✅ 전략 등록: {strategy.name} (priority={priority})")
|
|
|
|
def unregister_strategy(self, strategy_name: str):
|
|
"""전략 등록 해제"""
|
|
if strategy_name in self.strategies:
|
|
del self.strategies[strategy_name]
|
|
self.priority_order.remove(strategy_name)
|
|
logger.info(f"❌ 전략 삭제: {strategy_name}")
|
|
|
|
def get_strategy(self, strategy_name: str) -> Optional[BaseStrategy]:
|
|
"""전략 조회"""
|
|
return self.strategies.get(strategy_name)
|
|
|
|
def list_strategies(self) -> List[str]:
|
|
"""등록된 전략 목록"""
|
|
return list(self.strategies.keys())
|
|
|
|
async def run_strategy(
|
|
self,
|
|
strategy_name: str,
|
|
stock_code: str,
|
|
market_data: Dict[str, Any],
|
|
portfolio_data: Optional[Dict[str, Any]] = None
|
|
) -> Optional[Signal]:
|
|
"""
|
|
특정 전략 단독 실행
|
|
|
|
Args:
|
|
strategy_name: 실행할 전략 이름
|
|
stock_code: 종목코드
|
|
market_data: 시장 데이터
|
|
portfolio_data: 포트폴리오 데이터
|
|
|
|
Returns:
|
|
Signal 또는 None
|
|
"""
|
|
strategy = self.strategies.get(strategy_name)
|
|
if not strategy:
|
|
logger.error(f"❌ 전략 찾을 수 없음: {strategy_name}")
|
|
return None
|
|
|
|
if not strategy.enabled:
|
|
logger.debug(f"⏸️ 전략 비활성화됨: {strategy_name}")
|
|
return None
|
|
|
|
try:
|
|
signal = await strategy.analyze(stock_code, market_data, portfolio_data)
|
|
|
|
if signal:
|
|
self.signal_history.append(signal)
|
|
logger.info(f"📡 신호 생성: {signal}")
|
|
|
|
return signal
|
|
|
|
except Exception as e:
|
|
logger.error(f"❌ [{strategy_name}] 실행 중 오류: {e}", exc_info=True)
|
|
await strategy.on_error(e, {"stock_code": stock_code})
|
|
return None
|
|
|
|
async def run_all_strategies(
|
|
self,
|
|
stock_code: str,
|
|
market_data: Dict[str, Any],
|
|
portfolio_data: Optional[Dict[str, Any]] = None,
|
|
parallel: bool = False
|
|
) -> List[Signal]:
|
|
"""
|
|
모든 전략 동시 실행
|
|
|
|
Args:
|
|
stock_code: 종목코드
|
|
market_data: 시장 데이터
|
|
portfolio_data: 포트폴리오 데이터
|
|
parallel: True = 병렬 실행, False = 순차 실행
|
|
|
|
Returns:
|
|
생성된 Signal 목록
|
|
"""
|
|
signals = []
|
|
|
|
if parallel:
|
|
# 병렬 실행
|
|
tasks = [
|
|
self.run_strategy(name, stock_code, market_data, portfolio_data)
|
|
for name in self.priority_order
|
|
]
|
|
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
signals = [s for s in results if isinstance(s, Signal)]
|
|
else:
|
|
# 순차 실행 (우선순위)
|
|
for strategy_name in self.priority_order:
|
|
signal = await self.run_strategy(
|
|
strategy_name, stock_code, market_data, portfolio_data
|
|
)
|
|
if signal:
|
|
signals.append(signal)
|
|
|
|
return signals
|
|
|
|
def resolve_conflicts(
|
|
self,
|
|
signals: List[Signal],
|
|
method: str = "highest_confidence"
|
|
) -> Optional[Signal]:
|
|
"""
|
|
신호 충돌 해결
|
|
|
|
Args:
|
|
signals: 신호 목록
|
|
method: 해결 방법
|
|
- "highest_confidence": 가장 높은 신뢰도
|
|
- "priority": 전략 우선순위
|
|
- "majority_vote": 다수결
|
|
|
|
Returns:
|
|
최종 선택된 Signal
|
|
"""
|
|
if not signals:
|
|
return None
|
|
|
|
if len(signals) == 1:
|
|
return signals[0]
|
|
|
|
if method == "highest_confidence":
|
|
return max(signals, key=lambda s: s.confidence)
|
|
|
|
elif method == "priority":
|
|
# 우선순위 높은 전략의 신호 선택
|
|
for strategy_name in self.priority_order:
|
|
for signal in signals:
|
|
if signal.strategy_name == strategy_name:
|
|
return signal
|
|
return signals[0]
|
|
|
|
elif method == "majority_vote":
|
|
# 가장 많이 나온 신호 타입 선택
|
|
from collections import Counter
|
|
signal_types = [s.signal_type for s in signals]
|
|
most_common = Counter(signal_types).most_common(1)[0][0]
|
|
|
|
# 해당 타입 중 가장 높은 신뢰도
|
|
matching_signals = [s for s in signals if s.signal_type == most_common]
|
|
return max(matching_signals, key=lambda s: s.confidence)
|
|
|
|
return signals[0]
|
|
|
|
def get_signal_history(
|
|
self,
|
|
stock_code: Optional[str] = None,
|
|
strategy_name: Optional[str] = None,
|
|
limit: int = 100
|
|
) -> List[Signal]:
|
|
"""
|
|
신호 이력 조회
|
|
|
|
Args:
|
|
stock_code: 종목코드 필터
|
|
strategy_name: 전략 이름 필터
|
|
limit: 최대 개수
|
|
|
|
Returns:
|
|
Signal 목록
|
|
"""
|
|
history = list(self.signal_history)
|
|
|
|
if stock_code:
|
|
history = [s for s in history if s.stock_code == stock_code]
|
|
|
|
if strategy_name:
|
|
history = [s for s in history if s.strategy_name == strategy_name]
|
|
|
|
return history[-limit:]
|
|
|
|
def get_stats(self) -> Dict[str, Any]:
|
|
"""전략 통계 정보"""
|
|
return {
|
|
"total_strategies": len(self.strategies),
|
|
"enabled_strategies": sum(1 for s in self.strategies.values() if s.enabled),
|
|
"total_signals": len(self.signal_history),
|
|
"strategies": {
|
|
name: {
|
|
"enabled": strategy.enabled,
|
|
"priority": getattr(strategy, '_priority', 100),
|
|
"signals_count": len([s for s in self.signal_history if s.strategy_name == name])
|
|
}
|
|
for name, strategy in self.strategies.items()
|
|
}
|
|
}
|
|
|
|
|
|
# 싱글톤
|
|
strategy_manager = StrategyManager()
|