"""LIVE 交易所 SoT:平仓数量与到期路径。""" from __future__ import annotations from types import SimpleNamespace from app.live.reconcile import perp_close_contracts_okx, perp_close_qty_eth_binance class _FakeOkx: def __init__(self, sz) -> None: self._sz = sz def get_perp_pos_sz(self, _inst, pos_side=None): return self._sz class _FakeBn: def __init__(self, sz) -> None: self._sz = sz def get_perp_pos_sz(self, _inst, position_side=None): return self._sz def test_perp_close_okx_confirmed_flat_never_uses_db() -> None: # 交易所已空:即使 allow_db_fallback=True 也返回 0 assert ( perp_close_contracts_okx( _FakeOkx(0.0), perp_inst="ETH-USDT-SWAP", perp_side="short", perp_qty_eth=8.0, ct_val=0.01, allow_db_fallback=True, ) == 0 ) def test_perp_close_okx_unknown_fail_closed_by_default() -> None: assert ( perp_close_contracts_okx( _FakeOkx(None), perp_inst="ETH-USDT-SWAP", perp_side="long", perp_qty_eth=8.0, ct_val=0.01, ) is None ) def test_perp_close_okx_uses_exchange_size() -> None: assert ( perp_close_contracts_okx( _FakeOkx(123.0), perp_inst="ETH-USDT-SWAP", perp_side="long", perp_qty_eth=1.0, ct_val=0.01, ) == 123 ) def test_perp_close_bn_confirmed_flat_never_uses_db() -> None: assert ( perp_close_qty_eth_binance( _FakeBn(0.0), perp_inst="ETHUSDT", perp_side="short", perp_qty_eth=8.0, allow_db_fallback=True, ) == 0.0 ) def test_matcher_live_residual_no_local_invent(monkeypatch, tmp_path) -> None: monkeypatch.setenv("MODE", "LIVE") from app.models.db import Database from app.sim.matcher import Matcher db = Database(tmp_path / "sot.db") m = Matcher(db) monkeypatch.setattr(m, "_try_exchange_flatten_residual", lambda *a, **k: None) row = { "group_id": "G1", "option_inst_id": "ETH-OPT", "option_side": "call", "option_qty_eth": 2.0, "option_qty_contracts": 200.0, "strike": 2000.0, "initial_premium": 10.0, } assert m._settle_one_residual(row, now_ms=1) is None db.close() def test_okx_expiry_skips_option_order(monkeypatch, tmp_path) -> None: monkeypatch.setenv("MODE", "LIVE") from app.live.executor import OkxLiveExecutor from app.models.db import Database db = Database(tmp_path / "exp.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-exp", "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-exp", "open", "ETH-OPT", "ETH-USDT-SWAP", 1900.0, 1), ) db._conn.commit() placed = {"opt": 0, "perp": 0} 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): if "OPT" in inst_id or "-C" in inst_id or "-P" in inst_id: placed["opt"] += 1 else: placed["perp"] += 1 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: 0.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 assert placed["opt"] == 0 assert placed["perp"] == 1 st = db.fetchone("SELECT status FROM positions WHERE id=1") assert str(st["status"]) == "flat" db.close()