feat: MTF bot kill switch 및 비용 모델 추가

- src/config.py: COST_MODEL, COST_SCENARIOS 비용 모델 상수 추가
- src/mtf_bot.py: bps 기반 kill switch (Fast Kill + Slow Kill) 구현
- tests/test_mtf_bot.py: kill switch 테스트 케이스 추가

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
21in7
2026-05-04 09:02:51 +09:00
parent 29e307d7b2
commit 98476cc972
3 changed files with 235 additions and 0 deletions

View File

@@ -421,3 +421,107 @@ class TestExecutionManager:
# TP/SL = 2.3/1.5 = 1.533...
expected_rr = round(2.3 / 1.5, 2)
assert result["risk_reward"] == expected_rr
# ═══════════════════════════════════════════════════════════════════
# Test 5: ExecutionManager 킬스위치
# ═══════════════════════════════════════════════════════════════════
class TestExecutionManagerKillSwitch:
"""ExecutionManager 킬스위치 (Fast Kill / Slow Kill) 테스트."""
def _make_em(self) -> ExecutionManager:
"""JSONL 복원 없이 깨끗한 ExecutionManager 생성."""
em = ExecutionManager.__new__(ExecutionManager)
em.symbol = "TESTUSDT"
em.current_position = None
em._entry_price = None
em._entry_ts = None
em._sl_price = None
em._tp_price = None
em._atr_at_entry = None
em._killed = False
em._trade_history = []
return em
def test_fast_kill_triggers_after_8_consecutive_losses(self):
"""8연속 손실 시 Fast Kill 발동."""
em = self._make_em()
for _ in range(8):
em._append_trade_history(-50.0)
assert em._check_kill_switch() is True
assert em._killed is True
def test_fast_kill_not_triggered_with_7_losses(self):
"""7연속 손실은 Fast Kill 미발동."""
em = self._make_em()
for _ in range(7):
em._append_trade_history(-50.0)
assert em._check_kill_switch() is False
assert em._killed is False
def test_fast_kill_broken_by_single_win(self):
"""연속 손실 중 1회 수익이 있으면 Fast Kill 미발동."""
em = self._make_em()
for _ in range(4):
em._append_trade_history(-50.0)
em._append_trade_history(10.0) # 중간에 수익
for _ in range(3):
em._append_trade_history(-50.0)
assert em._check_kill_switch() is False
def test_slow_kill_triggers_when_pf_below_threshold(self):
"""최근 15거래 PF < 0.75 시 Slow Kill 발동."""
em = self._make_em()
# 12패 (-100 bps each) + 3승 (+50 bps each)
# gross_profit=150, gross_loss=1200, PF=0.125
for _ in range(12):
em._append_trade_history(-100.0)
for _ in range(3):
em._append_trade_history(50.0)
assert em._check_kill_switch() is True
assert em._killed is True
def test_slow_kill_not_triggered_when_pf_above_threshold(self):
"""PF > 0.75면 Slow Kill 미발동."""
em = self._make_em()
# 7패 (-100 each) + 8승 (+100 each)
# gross_profit=800, gross_loss=700, PF=1.14
for _ in range(7):
em._append_trade_history(-100.0)
for _ in range(8):
em._append_trade_history(100.0)
assert em._check_kill_switch() is False
def test_killed_state_blocks_new_entry(self):
"""킬스위치 발동 후 신규 진입이 차단된다."""
em = self._make_em()
em._killed = True
result = em.execute("EXECUTE_LONG", 2.0, 0.01)
assert result is None
def test_killed_state_allows_existing_sl_tp(self):
"""킬스위치 발동 후에도 기존 포지션의 청산은 정상 동작."""
em = self._make_em()
# 먼저 포지션 진입
em.execute("EXECUTE_SHORT", 2.0, 0.01)
# 킬스위치 발동
em._killed = True
# 청산은 정상 동작해야 함
em.close_position("SL 히트", exit_price=2.015, pnl_bps=-75.0)
assert em.current_position is None
def test_kill_switch_integrated_with_close(self):
"""close_position 호출 시 자동으로 킬스위치 판정이 실행된다."""
em = self._make_em()
# 7번 손실 기록
for _ in range(7):
em._append_trade_history(-50.0)
# 포지션 진입 후 8번째 손실로 청산
em.execute("EXECUTE_SHORT", 2.0, 0.01)
em.close_position("SL 히트", exit_price=2.015, pnl_bps=-75.0)
# 8연패 → Fast Kill 발동
assert em._killed is True
# 다음 진입 차단
assert em.execute("EXECUTE_LONG", 1.95, 0.01) is None