[잔고 관리] - _today_buy_total 인스턴스 변수로 당일 누적 매수 추적 (KIS T+2 미차감 보완) - MAX_BUY_PER_CYCLE, MAX_DAILY_BUY_RATIO 설정 추가 - available_deposit = max_daily_buy - effective_today_buy 계산 [앙상블 & 포지션 사이징] - AdaptiveEnsemble 실제 연동 (하드코딩 가중치 제거) - Kelly Criterion Half-Kelly 포지션 비중 계산 - SignalWeights.normalize() Water-Filling 알고리즘으로 경계 위반 해결 - _accuracy_weighted() 크기 가중 정확도로 통일 - ensemble_weights.json → ensemble_history.json 통합 [LLM 클라이언트] - GeminiLLMClient 추가 (Gemini → Ollama 폴백 체인) - _class_last_call_ts 클래스 변수로 워커 재시작 후에도 스로틀 유지 - Ollama 미실행 조기 감지 및 명확한 오류 메시지 [KIS API] - 모든 requests.get/post에 timeout=Config.HTTP_TIMEOUT 적용 - get_balance()에 today_buy_amt 필드 추가 [장중 전용 운영] - KRXCalendar: exchange_calendars 기반, 2024~2026 공휴일 하드코딩 폴백 - EOD 셧다운: 15:35에 전체 상태 저장 후 서버 자동 종료 - Watchdog: .eod_date 마커로 EOD 후 재시작 차단 - daily_launcher.py: 매일 08:30 실행, 휴장일 감지 후 봇 미시작 - Windows 작업 스케줄러 WebAI_DailyLauncher 등록 [텔레그램 스킬 수정] - PYTHONIOENCODING=utf-8 서브프로세스 환경 설정 (cp949 이모지 오류 해결) - /regime: IPC macro_indices 파싱 구현, --json 모드 input() 블로킹 제거 - /weights: ensemble_history.json 형식 파싱 업데이트 - /model_health: glob 패턴 *_v3.pt 수정 - /postmortem: 거래 없을 때 빈 JSON 출력으로 Telegram 오류 해결 - /macro: price=0 시 prev_close 폴백 표시 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
280 lines
12 KiB
Python
280 lines
12 KiB
Python
"""
|
||
시장 레짐 감지 모듈
|
||
- 코스피 지수 수준에 따른 시장 레짐 분류
|
||
- 코스피 6300 목표 수준에서의 모델 적합성 평가
|
||
- 레짐별 전략 파라미터 자동 조정
|
||
"""
|
||
|
||
from dataclasses import dataclass
|
||
from enum import Enum
|
||
from typing import Optional, Dict
|
||
|
||
|
||
class MarketRegime(Enum):
|
||
BULL_EXTREME = "bull_extreme" # 코스피 5000+ (역사적 극고점, 6300 시나리오)
|
||
BULL_STRONG = "bull_strong" # 코스피 3500~5000 (강한 상승장)
|
||
BULL_NORMAL = "bull_normal" # 코스피 2500~3500 (정상 상승장)
|
||
SIDEWAYS = "sideways" # 코스피 2000~2500 (횡보)
|
||
BEAR_MILD = "bear_mild" # 코스피 1500~2000 (약세)
|
||
BEAR_SEVERE = "bear_severe" # 코스피 1500 미만 (심각한 약세)
|
||
|
||
|
||
@dataclass
|
||
class RegimeAnalysis:
|
||
"""레짐 분석 결과"""
|
||
regime: MarketRegime
|
||
kospi_level: float
|
||
description: str
|
||
recommended_strategy: str
|
||
buy_threshold_adj: float # 매수 임계값 조정치 (+: 더 엄격, -: 완화)
|
||
position_size_adj: float # 포지션 크기 조정 배수 (1.0 = 기본)
|
||
lstm_weight_adj: float # LSTM 앙상블 가중치 조정 (+0.1 = 10% 증가)
|
||
model_recommendation: str # 모델 유지/교체 권고
|
||
risk_level: str # LOW / MEDIUM / HIGH / EXTREME
|
||
|
||
|
||
class MarketRegimeDetector:
|
||
"""
|
||
코스피 지수 수준 기반 시장 레짐 감지기
|
||
|
||
코스피 6300 시나리오:
|
||
- 현재 한국 증시 역대 최고점(2021년 3300) 대비 약 2배 수준
|
||
- BULL_EXTREME 레짐에 해당 → LSTM 단독 의존 지양, Transformer/Mamba 검토 필요
|
||
- 추세 추종 강화 + 고점 리스크 관리 병행
|
||
"""
|
||
|
||
# 레짐별 상세 파라미터
|
||
_REGIME_PARAMS: Dict[MarketRegime, dict] = {
|
||
MarketRegime.BULL_EXTREME: {
|
||
"description": "코스피 극강세장 5000+ (6300 시나리오)",
|
||
"recommended_strategy": (
|
||
"추세 추종 극대화, 트레일링 스탑 확대(ATR×4), "
|
||
"고점 과열 구간으로 포지션 축소 병행"
|
||
),
|
||
"buy_threshold_adj": -0.04, # 강세 모멘텀 → 진입 소폭 완화
|
||
"position_size_adj": 0.75, # 고점 리스크로 포지션 축소
|
||
"lstm_weight_adj": -0.12, # LSTM 비중 축소 (비선형 가격 동작)
|
||
"model_recommendation": (
|
||
"Temporal Fusion Transformer(TFT) 또는 Mamba(SSM) 교체 권고 - "
|
||
"LSTM은 극강세 과열 구간에서 비선형 가격 동작 포착 한계"
|
||
),
|
||
"risk_level": "EXTREME",
|
||
},
|
||
MarketRegime.BULL_STRONG: {
|
||
"description": "코스피 강상승장 3500~5000",
|
||
"recommended_strategy": "추세 추종, 모멘텀 강화, 손절 완화(ATR×2.5)",
|
||
"buy_threshold_adj": -0.03,
|
||
"position_size_adj": 1.1,
|
||
"lstm_weight_adj": 0.05,
|
||
"model_recommendation": "현재 LSTM v3 적합 - 성능 모니터링 유지",
|
||
"risk_level": "MEDIUM",
|
||
},
|
||
MarketRegime.BULL_NORMAL: {
|
||
"description": "코스피 정상 상승장 2500~3500",
|
||
"recommended_strategy": "기본 전략 유지 (기술+LSTM+LLM 균형)",
|
||
"buy_threshold_adj": 0.0,
|
||
"position_size_adj": 1.0,
|
||
"lstm_weight_adj": 0.0,
|
||
"model_recommendation": "현재 LSTM v3 최적 환경",
|
||
"risk_level": "LOW",
|
||
},
|
||
MarketRegime.SIDEWAYS: {
|
||
"description": "코스피 횡보장 2000~2500",
|
||
"recommended_strategy": "박스권 매매, LLM 감성 비중 확대, 빠른 익절",
|
||
"buy_threshold_adj": 0.03,
|
||
"position_size_adj": 0.85,
|
||
"lstm_weight_adj": -0.05,
|
||
"model_recommendation": "현재 LSTM v3 적합 - 감성 분석 가중치 강화",
|
||
"risk_level": "LOW",
|
||
},
|
||
MarketRegime.BEAR_MILD: {
|
||
"description": "코스피 약세장 1500~2000",
|
||
"recommended_strategy": "현금 비중 확대(50%+), 방어주 선별 매수",
|
||
"buy_threshold_adj": 0.08,
|
||
"position_size_adj": 0.5,
|
||
"lstm_weight_adj": 0.0,
|
||
"model_recommendation": "현재 LSTM v3 적합 - 리스크 관리 파라미터 강화",
|
||
"risk_level": "HIGH",
|
||
},
|
||
MarketRegime.BEAR_SEVERE: {
|
||
"description": "코스피 극약세장 1500 미만",
|
||
"recommended_strategy": "전면 현금화, 매수 중단",
|
||
"buy_threshold_adj": 0.20,
|
||
"position_size_adj": 0.2,
|
||
"lstm_weight_adj": 0.0,
|
||
"model_recommendation": "매크로 팩터 기반 방어 모델 전환 필요",
|
||
"risk_level": "EXTREME",
|
||
},
|
||
}
|
||
|
||
@classmethod
|
||
def detect(
|
||
cls,
|
||
kospi_price: float,
|
||
kospi_change_pct: float = 0.0,
|
||
volatility_20d: float = 0.0,
|
||
) -> RegimeAnalysis:
|
||
"""
|
||
코스피 지수 수준 + 변동성으로 시장 레짐 감지
|
||
|
||
Args:
|
||
kospi_price: 현재 코스피 지수 (예: 2600, 6300)
|
||
kospi_change_pct: 전일 대비 등락률 (%)
|
||
volatility_20d: 20일 변동성 (선택, 0이면 무시)
|
||
|
||
Returns:
|
||
RegimeAnalysis: 레짐 분석 결과 및 전략 파라미터
|
||
"""
|
||
# 1. 지수 수준으로 기본 레짐 결정
|
||
if kospi_price >= 5000:
|
||
regime = MarketRegime.BULL_EXTREME
|
||
elif kospi_price >= 3500:
|
||
regime = MarketRegime.BULL_STRONG
|
||
elif kospi_price >= 2500:
|
||
regime = MarketRegime.BULL_NORMAL
|
||
elif kospi_price >= 2000:
|
||
regime = MarketRegime.SIDEWAYS
|
||
elif kospi_price >= 1500:
|
||
regime = MarketRegime.BEAR_MILD
|
||
else:
|
||
regime = MarketRegime.BEAR_SEVERE
|
||
|
||
params = cls._REGIME_PARAMS[regime]
|
||
|
||
# 2. 변동성 기반 포지션 사이징 추가 조정
|
||
position_adj = params["position_size_adj"]
|
||
if volatility_20d > 30:
|
||
position_adj *= 0.6 # 극단적 변동성 → 추가 50% 축소
|
||
elif volatility_20d > 20:
|
||
position_adj *= 0.8 # 높은 변동성 → 20% 축소
|
||
|
||
# 3. 급락 중 레짐 하향 조정 (패닉 감지)
|
||
if kospi_change_pct <= -3.0:
|
||
# 극단적 일일 급락 → 포지션 추가 축소
|
||
position_adj *= 0.5
|
||
print(f"[Regime] PANIC DETECTED (일일 {kospi_change_pct:.1f}%) → 포지션 50% 추가 축소")
|
||
|
||
return RegimeAnalysis(
|
||
regime=regime,
|
||
kospi_level=kospi_price,
|
||
description=params["description"],
|
||
recommended_strategy=params["recommended_strategy"],
|
||
buy_threshold_adj=params["buy_threshold_adj"],
|
||
position_size_adj=round(position_adj, 3),
|
||
lstm_weight_adj=params["lstm_weight_adj"],
|
||
model_recommendation=params["model_recommendation"],
|
||
risk_level=params["risk_level"],
|
||
)
|
||
|
||
@classmethod
|
||
def validate_model_for_regime(
|
||
cls,
|
||
regime: MarketRegime,
|
||
backtest_sharpe: Optional[float] = None,
|
||
backtest_winrate: Optional[float] = None,
|
||
backtest_mdd: Optional[float] = None,
|
||
) -> dict:
|
||
"""
|
||
현재 LSTM v3 모델이 해당 레짐에서 적합한지 검증
|
||
|
||
Returns:
|
||
{
|
||
"is_suitable": bool,
|
||
"confidence_score": float (0~1),
|
||
"recommendation": str,
|
||
"should_replace": bool,
|
||
"alternative_models": list[str],
|
||
"reason": str,
|
||
}
|
||
"""
|
||
result = {
|
||
"is_suitable": True,
|
||
"confidence_score": 0.75,
|
||
"recommendation": "현재 LSTM v3 모델 유지",
|
||
"should_replace": False,
|
||
"alternative_models": [],
|
||
"reason": "정상 상승장 구간 - LSTM v3 최적 환경",
|
||
}
|
||
|
||
# 레짐 기반 기본 평가
|
||
if regime == MarketRegime.BULL_EXTREME:
|
||
result.update({
|
||
"is_suitable": False,
|
||
"confidence_score": 0.38,
|
||
"recommendation": "Transformer 계열 모델 교체 강력 권고",
|
||
"should_replace": True,
|
||
"alternative_models": [
|
||
"Temporal Fusion Transformer (TFT) - 장기 시계열 최강",
|
||
"Mamba (SSM) - 초고속 추론 + 긴 컨텍스트",
|
||
"PatchTST - Transformer 기반 주가 예측 특화",
|
||
"TimesNet - 2D 시계열 변환 + CNN",
|
||
"N-BEATS / N-HiTS - 해석 가능 딥러닝",
|
||
],
|
||
"reason": (
|
||
"코스피 5000+ 극강세장에서 LSTM은 비선형적 가격 급등 패턴을 "
|
||
"충분히 학습하지 못함. Attention 메커니즘만으로는 장기 상승 추세의 "
|
||
"복잡한 의존성 포착에 한계 존재."
|
||
),
|
||
})
|
||
elif regime == MarketRegime.BEAR_SEVERE:
|
||
result.update({
|
||
"is_suitable": False,
|
||
"confidence_score": 0.30,
|
||
"recommendation": "매크로 팩터 + Regime-Switching 모델 교체 권고",
|
||
"should_replace": True,
|
||
"alternative_models": [
|
||
"Regime-Switching LSTM (HMM + LSTM)",
|
||
"매크로 멀티팩터 모델 (환율, 금리, VIX 통합)",
|
||
"GRU + Attention (LSTM 경량 대안)",
|
||
],
|
||
"reason": "극약세장에서는 기술적 지표보다 거시경제 팩터가 지배적",
|
||
})
|
||
elif regime == MarketRegime.BULL_STRONG:
|
||
result.update({
|
||
"confidence_score": 0.72,
|
||
"reason": "강상승장 - LSTM 추세 학습 양호하나 성능 모니터링 필요",
|
||
})
|
||
elif regime == MarketRegime.SIDEWAYS:
|
||
result.update({
|
||
"confidence_score": 0.68,
|
||
"reason": "횡보장 - LSTM 예측력 저하, LLM 감성 보완 필수",
|
||
"recommendation": "현재 LSTM v3 유지 + LLM 감성 가중치 상향",
|
||
})
|
||
|
||
# 백테스트 결과 반영
|
||
if backtest_sharpe is not None:
|
||
if backtest_sharpe < 0:
|
||
result["confidence_score"] *= 0.5
|
||
result["should_replace"] = True
|
||
result["recommendation"] += " ⚠️ Sharpe < 0 → 즉시 교체 검토"
|
||
elif backtest_sharpe < 0.5:
|
||
result["confidence_score"] *= 0.75
|
||
result["recommendation"] += f" (Sharpe={backtest_sharpe:.2f} 미흡)"
|
||
|
||
if backtest_winrate is not None and backtest_winrate < 45:
|
||
result["confidence_score"] *= 0.8
|
||
result["recommendation"] += f" (승률={backtest_winrate:.1f}% 미흡)"
|
||
|
||
if backtest_mdd is not None and backtest_mdd < -25:
|
||
result["confidence_score"] *= 0.7
|
||
result["should_replace"] = True
|
||
result["recommendation"] += f" ⚠️ MDD={backtest_mdd:.1f}% 과다"
|
||
|
||
result["confidence_score"] = round(max(0.0, min(1.0, result["confidence_score"])), 3)
|
||
return result
|
||
|
||
@staticmethod
|
||
def get_regime_label(kospi_price: float) -> str:
|
||
"""간략 레짐 라벨 반환 (로그/UI 표시용)"""
|
||
if kospi_price >= 5000:
|
||
return f"BULL_EXTREME({kospi_price:.0f})"
|
||
elif kospi_price >= 3500:
|
||
return f"BULL_STRONG({kospi_price:.0f})"
|
||
elif kospi_price >= 2500:
|
||
return f"BULL_NORMAL({kospi_price:.0f})"
|
||
elif kospi_price >= 2000:
|
||
return f"SIDEWAYS({kospi_price:.0f})"
|
||
elif kospi_price >= 1500:
|
||
return f"BEAR_MILD({kospi_price:.0f})"
|
||
return f"BEAR_SEVERE({kospi_price:.0f})"
|