Fix LIVE SoT P0/P1: closing state machine, OO exchange fills, BN balances.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-08-08 15:54:45 +08:00
parent 99e58910d3
commit 8d67f3fc6c
10 changed files with 864 additions and 167 deletions
+171
View File
@@ -0,0 +1,171 @@
"""P0 实盘 SoTclosing 状态机、紧急期期、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