"""미국 매수 경로 loss_tracker 밴 게이트 회귀 (§5.1).""" import pytest from unittest.mock import AsyncMock from app.services import us_market_scheduler as ums class _Order(Exception): pass def _setup(monkeypatch, banned): monkeypatch.setattr(ums, "_get_us_account_info", AsyncMock(return_value={ "exchange_rate": 1400.0, "total_assets_krw": 100_000_000.0, "available_cash_krw": 50_000_000.0, "current_us_allocation_krw": 0.0})) monkeypatch.setattr(ums, "_monitor_us_exits", AsyncMock(return_value=[])) monkeypatch.setattr(ums, "_can_enter_new_position", lambda: True) monkeypatch.setattr(ums.mdd_manager, "can_open_new_position", AsyncMock(return_value=True)) monkeypatch.setattr(ums, "get_us_watchlist", lambda: ["AAPL"]) monkeypatch.setattr(ums, "scan_us_stocks", AsyncMock(return_value=[ {"ticker": "AAPL", "price": 100.0, "rsi": 30.0, "description": "t"}])) monkeypatch.setattr(ums, "calculate_us_position_size", lambda **k: 5_000_000.0) # 공통 종단(주문)은 예외 발생형 — 밴 게이트가 뚫리면 여기서 터진다 async def _boom(*a, **k): raise _Order("order reached") monkeypatch.setattr("app.services.trade_executor._place_order_with_retry", _boom) if isinstance(banned, Exception): monkeypatch.setattr(ums.loss_tracker, "is_banned_safe", AsyncMock(side_effect=banned)) else: monkeypatch.setattr(ums.loss_tracker, "is_banned_safe", AsyncMock(return_value=banned)) @pytest.mark.asyncio async def test_banned_ticker_not_ordered(monkeypatch): _setup(monkeypatch, True) r = await ums.us_trade_cycle() assert r["trades"] == [] @pytest.mark.asyncio async def test_ban_check_failure_is_fail_closed(monkeypatch): _setup(monkeypatch, RuntimeError("db down")) r = await ums.us_trade_cycle() assert r["trades"] == [] @pytest.mark.asyncio async def test_unbanned_reaches_order(monkeypatch): _setup(monkeypatch, False) called = [] async def _rec(*a, **k): called.append(1) raise _Order("x") monkeypatch.setattr("app.services.trade_executor._place_order_with_retry", _rec) await ums.us_trade_cycle() assert called