feat: 안전형 스윙 개선(서킷브레이커·own_trend) + 매수 현금해결 방어

퀀트 리서치(공정 유니버스+walk-forward)로 검증된 안전 게이트 2종과
매수 경로 현금 해결 방어 코드 추가. 모두 기존 시그니처 유지(§4).

- 지수 서킷브레이커: KODEX200 종가 < MA20이면 신규 스윙 진입 전면 중단
  (하락장 상관붕괴=2024H2형 방어). execute_swing_entries가 기존
  check_market_regime의 drop_from_ma20을 재사용(추가 API 없음). 백테스트
  루프에도 동일 적용. 검증: 최근1년 Sharpe 0.82→1.13, CAGR 7.2→8.6%, MDD 개선.
- own_trend 게이트: 평균회귀·눌림목 매수 시 종목 MA20 >= MA60×0.97일 때만
  진입(하락추세 물타기 방지). 실서비스는 recommendations.is_mean_reversion_candidate
  에서 StockAnalysis.ma20/ma60로, check_buy_signal엔 ma20/ma60 선택 파라미터
  (기본 None=하위호환) 추가. 백테스트(kr_backtest_engine, run_backtest)도 평균회귀 한정.
- 매수 현금 해결 방어(trade_executor._execute_buy): 0-현금 캐시를 신뢰하지 않고
  현금<=0이면 자산과 무관하게 KIS 예수금 재조회, 실현금(>0)일 때만 캐시.
  0-현금 고착으로 수량 0이 되는 잠재 무한루프 방어.

검증: code-verifier 통과(CONFIRMED 버그 없음, None-safe, §3/§4/§5 준수),
pytest 442 통과, 6파일 컨테이너 컴파일·리로드 정상.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBmzf1HDnvFHLraJ8eaaKJ
This commit is contained in:
2026-09-16 13:17:17 +09:00
parent a0973812fb
commit 8a8e3700db
6 changed files with 92 additions and 11 deletions

View File

@@ -545,10 +545,20 @@ def check_buy_signal(
if mp.get("position_size", 0) <= 0:
return None
# [안전형 개선①] own_trend 게이트 (검증 완료: Sharpe 0.27→0.57)
# 종목 자체 MA20 < MA60*0.97 (하락추세)이면 평균회귀(물타기) 진입 차단.
# 모멘텀 돌파(추세추종)에는 미적용 — 전략 2 블록 진입 전에만 반영.
_own_trend_ok = True
if closes and len(closes) >= 60:
_mr_ma20 = sum(closes[-20:]) / 20
_mr_ma60 = sum(closes[-60:]) / 60
if _mr_ma60 > 0 and _mr_ma20 < _mr_ma60 * 0.97:
_own_trend_ok = False
# ═══════════════════════════════════════════════════════════════
# 전략 1: 평균회귀 (RSI 과매도 + BB 하단)
# ═══════════════════════════════════════════════════════════════
if current_volume >= avg_volume_20d:
if current_volume >= avg_volume_20d and _own_trend_ok:
if regime == MarketRegime.BULL:
rsi_thresh = mp["rsi_entry"]
if rsi < rsi_thresh:
@@ -1043,6 +1053,7 @@ def run_simulation(
adaptive_mode = "normal"
mode_params = ADAPTIVE_MODES["normal"]
mode_changes: List[Dict] = [] # 모드 전환 이력
circuit_breaker_days = 0 # [안전형 개선②] 서킷브레이커 발동일 수 (검증용 통계)
total_days = len(all_dates)
last_progress = -1
@@ -1063,6 +1074,15 @@ def run_simulation(
idx_highs_acc.append(idx_bar.high_price)
idx_lows_acc.append(idx_bar.low_price)
# [안전형 개선②] 지수 서킷브레이커 (검증 완료: 2024H2 CAGR -9.9%→+1.0%, MDD -7→-1.9)
# KODEX200 당일 종가 < MA20(20일) → 신규 진입 전면 중단 (기존 보유 청산/관리는 정상).
# 매일 갱신(적응형 모드처럼 5일 지연시키지 않음) — 하락장 상관붕괴에 즉시 반응해야 함.
idx_circuit_breaker = False
if len(idx_closes_acc) >= 20:
_idx_ma20_cb = sum(idx_closes_acc[-20:]) / 20
if idx_closes_acc[-1] < _idx_ma20_cb:
idx_circuit_breaker = True
# 시장 국면 + 적응형 모드 (매 5일마다 업데이트)
regime_update_counter += 1
if regime_update_counter >= 5 and len(idx_closes_acc) >= MA200_PERIOD + MA200_SLOPE_LOOKBACK:
@@ -1084,6 +1104,9 @@ def run_simulation(
if sim_start_date is not None and current_date < sim_start_date:
continue
if idx_circuit_breaker:
circuit_breaker_days += 1
# 현재 평가 자산
position_value = 0.0
for code, pos in positions.items():
@@ -1189,8 +1212,9 @@ def run_simulation(
pos.remaining_pct = remaining_qty / (remaining_qty + sell_qty)
# ---- 매수 시그널 ----
# [안전형 개선②] 서킷브레이커 발동 중이면 신규 진입 전면 스킵 (기존 보유 관리는 위에서 정상 수행)
_max_holdings = mode_params.get("max_holdings", 12)
if len(positions) < _max_holdings:
if len(positions) < _max_holdings and not idx_circuit_breaker:
buy_candidates = []
for code, stock_bars in bars.items():
@@ -1397,6 +1421,11 @@ def run_simulation(
)
metrics = compute_metrics(equity_values, all_trades, initial_capital, _sim_trading_days)
# [안전형 개선②] 서킷브레이커 발동 통계 출력 (검증용)
if _sim_trading_days:
print(f" [서킷브레이커] KODEX200 종가<MA20 발동일수: {circuit_breaker_days}/{_sim_trading_days}일 "
f"({circuit_breaker_days / _sim_trading_days * 100:.1f}%)")
return metrics, all_trades, equity_curve, drawdown_curve, open_positions, mode_changes