"""P0 实盘 SoT:closing 状态机、紧急期期、recover Put、到期无 intrinsic。""" from __future__ import annotations from types import SimpleNamespace from app.sim.matcher import BLOCKING_STATUSES def test_closing_in_blocking_statuses() -> None: assert "closing" in BLOCKING_STATUSES def test_has_open_position_blocks_closing(tmp_path, monkeypatch) -> None: monkeypatch.setenv("MODE", "SIM") from app.models.db import Database from app.sim.matcher import Matcher db = Database(tmp_path / "c.db") m = Matcher(db) with db._lock: db._conn.execute( """UPDATE positions SET group_id=?, option_inst_id=?, option2_inst_id=?, status='closing', hedge_mode='option_option' WHERE id=1""", ("G1", "C", "P"), ) db._conn.commit() assert m.has_open_position() is True db.close() def test_recover_opening_refuses_orphan_put(monkeypatch) -> None: monkeypatch.setenv("MODE", "LIVE") from app.config import get_settings get_settings.cache_clear() import app.live.reconcile as rec from app.live.reconcile import recover_stuck_opening class _Ex: def current_position(self): return { "status": "opening", "group_id": "G-oo", "option_inst_id": "ETH-CALL", "perp_side": "oo_put:ETH-PUT", "option_qty_eth": 1, "option_qty_contracts": 100, } @property def db(self): return SimpleNamespace() def _client(self): return object() def fake_opt(_c, inst): if "PUT" in inst: return 5.0 return 0.0 monkeypatch.setattr(rec, "_executor_client_and_exchange", lambda _e: (object(), "okx")) monkeypatch.setattr(rec, "exchange_option_abs_size", fake_opt) monkeypatch.setattr(rec, "exchange_perp_abs_size", lambda *_a, **_k: 0.0) monkeypatch.setattr( rec, "resolve_perp_inst_id", lambda *_a, **_k: "ETH-USDT-SWAP" ) r = recover_stuck_opening(_Ex()) assert r is not None assert r.ok is False assert "Put" in (r.detail or "") get_settings.cache_clear() def test_expiry_fill_zero_not_intrinsic(monkeypatch, tmp_path) -> None: monkeypatch.setenv("MODE", "LIVE") from app.config import get_settings get_settings.cache_clear() from app.live.executor import OkxLiveExecutor from app.models.db import Database db = Database(tmp_path / "e.db") ex = OkxLiveExecutor(db) monkeypatch.setattr(ex, "_guard_live", lambda: None) with db._lock: db._conn.execute( """UPDATE positions SET group_id=?, perp_side=?, perp_qty_eth=?, perp_entry_px=?, option_inst_id=?, option_side=?, option_qty_eth=?, option_qty_contracts=?, option_entry_px=?, status='open' WHERE id=1""", ("G-e", "short", 4.0, 2000.0, "ETH-OPT", "call", 1.0, 100.0, 20.0), ) db._conn.execute( """INSERT INTO groups(group_id, status, option_inst_id, perp_inst_id, strike, open_at_ms) VALUES (?,?,?,?,?,?)""", ("G-e", "open", "ETH-OPT", "ETH-USDT-SWAP", 1900.0, 1), ) db._conn.commit() class _C: def get_ct_val(self, *_a, **_k): return 0.01 def get_perp_pos_sz(self, *_a, **_k): return 400.0 def place_market(self, *, inst_id, side, sz, **_k): return SimpleNamespace(avg_px=2010.0, fee=0.1, sz=float(sz)) monkeypatch.setattr(ex, "_client", lambda: _C()) monkeypatch.setattr( "app.live.executor.exchange_option_abs_size", lambda *_a, **_k: 2.0 ) monkeypatch.setattr(ex, "_group_strike", lambda *_a, **_k: 1900.0) monkeypatch.setattr(ex, "_close_spot_px", lambda *_a, **_k: 1950.0) monkeypatch.setattr( "app.live.executor.get_session", lambda: SimpleNamespace(snapshot=lambda: {}), ) monkeypatch.setattr( "app.live.executor.resolve_perp_inst_id", lambda *_a, **_k: "ETH-USDT-SWAP", ) monkeypatch.setattr( "app.live.live_pnl.reconcile_closed_group_pnl", lambda **_k: 0.0, ) r = ex.close_group(reason="expiry", bypass_liquidity=True) assert r.ok, r.detail row = db.fetchone( "SELECT fill_px, notional FROM fills WHERE group_id=? AND leg='option' AND action='close'", ("G-e",), ) assert row is not None assert float(row["fill_px"]) == 0.0 assert float(row["notional"] or 0) == 0.0 db.close() get_settings.cache_clear() def test_binance_fetch_balances_maps_usdt(monkeypatch) -> None: from app.live.binance_trade import BinanceTradeClient c = BinanceTradeClient.__new__(BinanceTradeClient) def _signed(client, method, path, params=None): if "fapi" in str(getattr(client, "base_url", "")) or path.startswith("/fapi"): return [{"asset": "USDT", "availableBalance": "100.5"}] if "marginAccount" in path: return {"asset": [{"asset": "USDT", "available": "80"}]} return [] c._signed = _signed # type: ignore c._fapi = SimpleNamespace(base_url="https://fapi") c._eapi = SimpleNamespace(base_url="https://eapi") # simpler: patch by path def signed2(_client, method, path, params=None): if path == "/fapi/v2/balance": return [{"asset": "USDT", "availableBalance": "100.5"}] if path == "/eapi/v1/marginAccount": return {"asset": [{"asset": "USDT", "available": "80"}]} return [] c._signed = signed2 # type: ignore bal = BinanceTradeClient.fetch_balances(c) assert bal["trading_usdt"] == 100.5 assert bal["trading_usdc"] == 100.5 # mapped from USDT when no USDC