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:
@@ -0,0 +1,171 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user