"""[R4 2026-10-09] MDD 축소 매도 TradeHistory 가 비용 차감 순손익 + commission + 수익률을 기록한다. 목킹 지점: 주문 종단 safe_place_order / 잔고 get_balance / 기록 종단 enqueue_trade_history(캡처). """ import pytest from app.core.config import settings from app.services import mdd_manager as mm from app.services import trade_executor as te from app.services.korea_investment_api import korea_investment_api class _FakeRedis: def __init__(self): self.store = {} async def set(self, k, v, **kw): self.store[k] = v return True async def get(self, k): return self.store.get(k) def _expected(avg, cur, qty): comm = settings.KR_COMMISSION_RATE tax = settings.KR_SELL_TAX_RATE_KOSPI cost = round(avg * qty * comm + cur * qty * comm + cur * qty * tax, 0) gross = (cur - avg) * qty return gross - cost, cost, (gross - cost) / (avg * qty) * 100 def test_helper_deducts_costs_settings_based(): r = mm._reduction_net_pnl_v2(10000.0, 10100.0, 30) net, cost, rate = _expected(10000.0, 10100.0, 30) assert r["net_profit_loss"] == pytest.approx(net) assert r["commission"] == pytest.approx(cost) assert r["profit_loss_rate"] == pytest.approx(rate) assert r["net_profit_loss"] < (10100.0 - 10000.0) * 30 # gross 보다 작다 @pytest.mark.asyncio async def test_reduction_payload_has_net_pnl(monkeypatch): import app.core.redis as redis_mod monkeypatch.setattr(redis_mod, "get_redis", lambda: _FakeRedis()) async def _nosleep(*a, **k): return None monkeypatch.setattr(mm.asyncio, "sleep", _nosleep) import app.core.market_schedule as _ms monkeypatch.setattr(_ms, "is_market_open", lambda: True) async def _noop(*a, **k): return None monkeypatch.setattr(mm, "_sync_active_trade_after_reduction", _noop) monkeypatch.setattr(mm, "_reflect_reduction", _noop) captured = [] async def _cap(payload): captured.append(payload) import app.services.db_worker as dbw monkeypatch.setattr(dbw, "enqueue_trade_history", _cap) async def _bal(*a, **k): return {"rt_cd": "0", "output1": [ {"pdno": "005930", "prdt_name": "X", "hldg_qty": "100", "prpr": "10100", "pchs_avg_pric": "10000"}]} async def _order(stock_code, order_type, quantity, price=None, **kw): return {"rt_cd": "0", "output": {"ODNO": "O1"}} monkeypatch.setattr(korea_investment_api, "get_balance", _bal) monkeypatch.setattr(te, "safe_place_order", _order) # 알림 fire-and-forget 태스크가 다른 테스트 루프로 새지 않게 차단 monkeypatch.setattr(mm.MDDManager, "_spawn_mdd_alert_v2", staticmethod(lambda *a, **k: None)) m = mm.MDDManager() await m.update(100.0) await m.update(89.0) await m.execute_reduction( {"action": "REDUCE_AND_PAUSE", "reduce_pct": 0.30, "prioritize_leverage": False} ) assert len(captured) == 1 p = captured[0] net, cost, rate = _expected(10000.0, 10100.0, 30) assert p["net_profit_loss"] == pytest.approx(net) assert p["commission"] == pytest.approx(cost) assert p["profit_loss_rate"] == pytest.approx(rate) assert p["strategy_type"] == "mdd_reduction"