fix: 하드닝 루프 9회차 — 매수 원복·잔고 연속조회·결과 불명 매도·브레이커·경보 보강
- 8회차 HIGH 수정 16건 레드팀 재검증: 12 확인·3 부분·1 미수정 → 전부 보강 - trade_executor/fill_sweep: 늦게 확정된 0체결 BUY → CANCELLED + ActiveTrade close, 부분체결 → 수량 축소(멱등, 재기동 후 TH 재구성) - korea_investment_api: get_balance_all_v2(tr_cont 연속조회, _partial), get_todays_orders_v2(실패 None), 브레이커 청산용 조회 면제(OPEN 중 미집계) - swing/exit_service·mdd_manager: 매도 직전 상한·MDD 축소가 전체 잔고 페이지 사용(§8.5-37), 불명 대기 중 부분익절 skip·쿨다운·pre_hldg 차감, 대기 TTL 15:30+재매수 폐기(대조 1회 선행)(§8.5-38 해소) - swing/scheduler: 청산 TIMED_OUT/SKIPPED/in-flight 시 MDD 축소·진입 보류(예외는 진입만), 3사이클 연속 시 error+텔레그램 - daytrading: 결과 불명 매도 재판정 v3(주문조회 v2 None→보류, 등록 이후 일치 행→accepted, 원장≤0→보류), 재기동 복원 min(주문가능,보유), 잔고 조회 후 일일손실한도 재확인, 단독 실행 Redis 접속 제거 - main: 스트림 무수신 경보 v4(희망 구독 기준), 첫 하트비트 없음 stale(HB_FIRST_BEAT_GRACE_SEC) - 운영: Jenkinsfile health_watch.py 디렉터리 잔존 복구, prod_smoke 사이드카 확인, config 키 12개 선언(동작 무변경) - 진입가 v3(max(AT,PA))는 KIS 이동평균 평단 전제 오류로 레드팀 반증 → 폐기 - CLAUDE.md §3.2 ③-d, §8.5-37/38 갱신, §8.5-39~48 신규(Crisis 손절 소급 §8.5-39 는 사용자 결정 대기) - 유닛 스위트 2781 passed / 0 failed (소켓 차단), 네트워크 시도 0건 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -59,3 +59,68 @@ def test_unknown_registrations_falls_back_to_v2(monkeypatch):
|
||||
monkeypatch.setattr(m, "_kis_stream_last_msg_mono", 100.0)
|
||||
monkeypatch.setattr(m, "_kis_stream_regs_since", None)
|
||||
assert m._kis_stream_health_v3(_app(), now=100.0 + limit + 1, market_open=True, registrations=None)["silent"] is True
|
||||
|
||||
|
||||
# ── R9 E-1: 희망 구독 수 기준 v4 ─────────────────────────────────────────
|
||||
def test_v4_desired_subscribers_zero_regs_silent(monkeypatch):
|
||||
"""구독자는 있는데 등록 0(연결 단절) + 수신 이력 있고 임계 초과 → silent."""
|
||||
limit = m._HB_KIS_STREAM_SILENT_SECONDS
|
||||
monkeypatch.setattr(m, "_kis_stream_last_msg_mono", 100.0)
|
||||
monkeypatch.setattr(m, "_kis_stream_regs_since", 5.0)
|
||||
r = m._kis_stream_health_v4(_app(), now=100.0 + limit + 1, market_open=True, registrations=0, desired=3)
|
||||
assert r["silent"] is True and r["desired"] == 3
|
||||
assert m._kis_stream_regs_since is None
|
||||
|
||||
|
||||
def test_v4_no_desired_subscribers_is_normal(monkeypatch):
|
||||
limit = m._HB_KIS_STREAM_SILENT_SECONDS
|
||||
monkeypatch.setattr(m, "_kis_stream_last_msg_mono", 100.0)
|
||||
monkeypatch.setattr(m, "_kis_stream_regs_since", None)
|
||||
r = m._kis_stream_health_v4(_app(), now=100.0 + limit + 1, market_open=True, registrations=0, desired=0)
|
||||
assert r["silent"] is False
|
||||
|
||||
|
||||
def test_v4_regs_transition_resets_regs_since(monkeypatch):
|
||||
"""regs>0 -> 0 -> >0: 0 구간에서 기준점이 리셋되고 재등록 시각이 새 기준점이 된다."""
|
||||
limit = m._HB_KIS_STREAM_SILENT_SECONDS
|
||||
monkeypatch.setattr(m, "_kis_stream_last_msg_mono", 100.0)
|
||||
monkeypatch.setattr(m, "_kis_stream_regs_since", None)
|
||||
t0 = 100.0 + limit + 10
|
||||
m._kis_stream_health_v4(_app(), now=t0, market_open=True, registrations=5, desired=5)
|
||||
assert m._kis_stream_regs_since == t0
|
||||
m._kis_stream_health_v4(_app(), now=t0 + 1, market_open=True, registrations=0, desired=0)
|
||||
assert m._kis_stream_regs_since is None
|
||||
t1 = t0 + 50
|
||||
m._kis_stream_health_v4(_app(), now=t1, market_open=True, registrations=5, desired=5)
|
||||
assert m._kis_stream_regs_since == t1
|
||||
|
||||
|
||||
def test_v4_sentinel_reads_live_sources(monkeypatch):
|
||||
"""registrations/desired 기본 센티널(-1) 이면 실제 조회 함수를 쓴다."""
|
||||
limit = m._HB_KIS_STREAM_SILENT_SECONDS
|
||||
monkeypatch.setattr(m, "_kis_stream_last_msg_mono", 100.0)
|
||||
monkeypatch.setattr(m, "_kis_stream_regs_since", None)
|
||||
monkeypatch.setattr(m, "_kis_stream_registrations_v2", lambda: 0)
|
||||
monkeypatch.setattr(m, "_kis_stream_desired_subscriptions_v2", lambda: 2)
|
||||
assert m._kis_stream_health_v4(_app(), now=100.0 + limit + 1, market_open=True)["silent"] is True
|
||||
monkeypatch.setattr(m, "_kis_stream_desired_subscriptions_v2", lambda: 0)
|
||||
assert m._kis_stream_health_v4(_app(), now=100.0 + limit + 1, market_open=True)["silent"] is False
|
||||
|
||||
|
||||
def test_v4_unknown_desired_falls_back_to_v3(monkeypatch):
|
||||
limit = m._HB_KIS_STREAM_SILENT_SECONDS
|
||||
monkeypatch.setattr(m, "_kis_stream_last_msg_mono", 100.0)
|
||||
monkeypatch.setattr(m, "_kis_stream_regs_since", None)
|
||||
r = m._kis_stream_health_v4(_app(), now=100.0 + limit + 1, market_open=True, registrations=0, desired=None)
|
||||
assert r["silent"] is False
|
||||
|
||||
|
||||
def test_v4_dead_task_none_stream_and_off_hours(monkeypatch):
|
||||
monkeypatch.setattr(m, "_kis_stream_last_msg_mono", 100.0)
|
||||
assert m._kis_stream_health_v4(_app(done=True), now=101.0, market_open=True, registrations=0, desired=0)["silent"] is True
|
||||
|
||||
class _NoTask:
|
||||
state = type("S", (), {})()
|
||||
assert m._kis_stream_health_v4(_NoTask(), now=101.0, market_open=True, registrations=0, desired=3) is None
|
||||
limit = m._HB_KIS_STREAM_SILENT_SECONDS
|
||||
assert m._kis_stream_health_v4(_app(), now=100.0 + limit + 1, market_open=False, registrations=0, desired=3)["silent"] is False
|
||||
|
||||
Reference in New Issue
Block a user