"""R5: MDD 축소는 DT 인메모리가 0 이어도 전략명 없는 close/upsert 를 내지 않는다.""" import pytest from app.services import mdd_manager as m from app.services.db_worker import DT_STRATEGY_NAMES @pytest.fixture def cap(monkeypatch): out = {"up": [], "wr": []} async def _up(p): out["up"].append(p) async def _wr(p): out["wr"].append(p) async def _boom(*a, **k): raise AssertionError("전략명 없는 close 금지") monkeypatch.setattr("app.services.db_worker.enqueue_active_trade_upsert", _up) monkeypatch.setattr("app.services.db_worker.enqueue_db_write", _wr) monkeypatch.setattr("app.services.db_worker.enqueue_close_active_trade", _boom) monkeypatch.setattr("app.services.db_worker.enqueue_close_active_trade_v2", _boom) monkeypatch.setattr(m, "_dt_position_qty", lambda _c: 0) return out @pytest.mark.asyncio async def test_full_close_without_dt_uses_non_dt_filter(cap): await m._sync_active_trade_after_reduction("005930", 10, 0) assert len(cap["wr"]) == 1 d = cap["wr"][0]["data"] assert cap["wr"][0]["type"] == "close_active_trade" assert tuple(d["strategy_name_not"]) == tuple(DT_STRATEGY_NAMES) @pytest.mark.asyncio async def test_partial_without_dt_uses_non_dt_filter(cap): await m._sync_active_trade_after_reduction("005930", 10, 7) assert len(cap["up"]) == 1 p = cap["up"][0] assert p["quantity"] == 7 assert tuple(p["strategy_name_not"]) == tuple(DT_STRATEGY_NAMES) def test_dt_qty_failure_is_logged_not_silent(monkeypatch, caplog): import app.services.daytrading.position_manager as pm class _Boom: @property def positions(self): raise RuntimeError("boom") monkeypatch.setattr(pm, "day_trading_state", _Boom()) with caplog.at_level("WARNING"): assert m._dt_position_qty("005930") == 0 assert any("DT 인메모리 수량 조회 실패" in r.message for r in caplog.records) @pytest.mark.asyncio @pytest.mark.parametrize("remaining", [0, 3, 7]) async def test_dt_present_never_emits_strategyless_payload(cap, monkeypatch, remaining): """DT 병존 시에도 모든 close/upsert payload 는 전략 필터를 가진다.""" monkeypatch.setattr(m, "_dt_position_qty", lambda _c: 4) await m._sync_active_trade_after_reduction("005930", 10, remaining) for p in cap["up"]: assert p.get("strategy_name") or p.get("strategy_name_not") for w in cap["wr"]: d = w["data"] assert d.get("strategy_name") or d.get("strategy_names") or d.get("strategy_name_not")