Files

172 lines
5.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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