Files
ai-trade/signal_v1/modules/analysis/market_regime.py
gahusb 7ea1a21487 refactor: web-ai V1 assets → signal_v1/ (graduation prep)
Atomic mv of root V1 assets (main_server.py + modules/ + data/ +
tests/ + entry scripts + docs + logs) into signal_v1/ subdirectory.
load_dotenv() updated to load web-ai/.env explicitly via Path.

Adds web-ai/CLAUDE.md (workspace guide) and web-ai/start.bat
(signal_v1 entry wrapper). Prepares for signal_v2/ Phase 2.

Tests: signal_v1/tests/unit baseline preserved (no regression).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 03:00:11 +09:00

280 lines
12 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
시장 레짐 감지 모듈
- 코스피 지수 수준에 따른 시장 레짐 분류
- 코스피 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})"