Add Binance live trading, anti-stuck open/close recovery, and configurable rate limits.

OKX/Binance LIVE share half_open and option_closed_perp_pending repair paths; private REST throttles default to 1s and are tunable in settings.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-26 21:35:10 +08:00
parent e666230d0b
commit dbc86a1ce6
23 changed files with 2381 additions and 385 deletions
+59
View File
@@ -0,0 +1,59 @@
"""实盘限流 / 退避单测。"""
from __future__ import annotations
import time
from app.live.rate_limit import (
LiveRetryGate,
RateLimitError,
TradeThrottle,
get_throttle,
is_rate_limit_error,
)
def test_is_rate_limit_error() -> None:
assert is_rate_limit_error("HTTP 429 too many")
assert is_rate_limit_error("binance eapi cooldown 12s")
assert is_rate_limit_error(RateLimitError("x", retry_after=5))
assert not is_rate_limit_error("保证金不足")
def test_trade_throttle_cooldown() -> None:
t = TradeThrottle("ut_throttle", min_interval_sec=0.01, cooldown_429_sec=0.3)
t.before_request()
t.mark_http(429)
try:
t.before_request()
assert False, "expected RateLimitError"
except RateLimitError as e:
assert e.retry_after > 0
time.sleep(0.35)
t.before_request() # 冷却结束后可继续
def test_get_throttle_singleton() -> None:
a = get_throttle("ut_shared_x", min_interval_sec=0.01)
b = get_throttle("ut_shared_x")
assert a is b
def test_live_retry_gate_backoff() -> None:
g = LiveRetryGate(base_sec=0.05, max_sec=0.2, rate_limit_min_sec=0.1, trip_after=100)
assert g.allow("k")[0] is True
d1 = g.fail("k")
assert d1 >= 0.05
ok, left = g.allow("k")
assert ok is False
assert left > 0
time.sleep(d1 + 0.02)
assert g.allow("k")[0] is True
g.success("k")
assert g.fails("k") == 0
def test_live_retry_gate_rate_limited_longer() -> None:
g = LiveRetryGate(base_sec=0.01, rate_limit_min_sec=0.2)
d = g.fail("rl", rate_limited=True)
assert d >= 0.2
+69
View File
@@ -0,0 +1,69 @@
"""实盘防卡状态:half_open / option_closed_perp_pending。"""
from __future__ import annotations
from app.sim.ledger import Ledger
from app.sim.matcher import BLOCKING_STATUSES, Matcher
def test_blocking_statuses_include_repair_states() -> None:
assert "half_open" in BLOCKING_STATUSES
assert "option_closed_perp_pending" in BLOCKING_STATUSES
assert "open" in BLOCKING_STATUSES
def test_has_open_position_blocks_half_open(tmp_path, monkeypatch) -> None:
monkeypatch.setenv("MODE", "SIM")
from app.models.db import Database
db = Database(tmp_path / "t.db")
m = Matcher(db)
assert m.has_open_position() is False
with db._lock:
db._conn.execute(
"""UPDATE positions SET
group_id=?, option_inst_id=?, option_side=?, option_qty_eth=?,
option_qty_contracts=?, option_entry_px=?, status=?
WHERE id=1""",
("G-test", "ETH-OPT", "call", 2.0, 200.0, 10.0, "half_open"),
)
db._conn.commit()
assert m.has_open_position() is True
assert m.position_status() == "half_open"
with db._lock:
db._conn.execute(
"UPDATE positions SET status='option_closed_perp_pending' WHERE id=1"
)
db._conn.commit()
assert m.has_open_position() is True
with db._lock:
db._conn.execute(
"""UPDATE positions SET
group_id=NULL, option_inst_id=NULL, status='flat' WHERE id=1"""
)
db._conn.commit()
assert m.has_open_position() is False
db.close()
def test_ledger_allow_negative(tmp_path) -> None:
from app.models.db import Database
db = Database(tmp_path / "l.db")
ledger = Ledger(db)
# 掏空
snap = ledger.snapshot()
ledger.apply_cash(-snap["available"], kind="drain", note="drain")
try:
ledger.apply_cash(-1.0, kind="fail", note="should fail")
assert False, "expected RuntimeError"
except RuntimeError:
pass
# LIVE 镜像允许透支
bal = ledger.apply_cash(-1.0, kind="live", note="ok", allow_negative=True)
assert bal < 0
db.close()
+6 -6
View File
@@ -49,22 +49,22 @@ def test_live_ready_okx_missing_keys(monkeypatch) -> None:
assert "OKX" in reason
def test_live_ready_binance_stub(monkeypatch) -> None:
def test_live_ready_binance_ok(monkeypatch) -> None:
import app.env_store as es
class S:
mode = "LIVE"
is_sim = False
okx_api_key = "k"
okx_api_secret = "s"
okx_api_passphrase = "p"
okx_api_key = ""
okx_api_secret = ""
okx_api_passphrase = ""
binance_api_key = "bk"
binance_api_secret = "bs"
monkeypatch.setattr(es, "get_settings", lambda: S())
ok, reason = live_ready(exchange="binance")
assert ok is False
assert "尚未接入" in reason
assert ok is True
assert reason == "ok"
def test_okx_keys_configured(monkeypatch) -> None: