粉尘低于现货最小可卖量时忽略且不展示、不再重试卖回。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-10-07 11:16:13 +08:00
parent 80ef5ceccb
commit 030b237981
8 changed files with 183 additions and 19 deletions
+2 -1
View File
@@ -507,7 +507,8 @@ def sweep_spot_after_options_flat(
continue
try:
avail = _spot_bridge.fetch_trading_coin_available(exchange, uly)
if avail is None or float(avail) <= 1e-8:
# 低于现货 minSz 的粉尘不扫、不重试
if avail is None or not _spot_bridge.is_sellable_spot_coin(exchange, uly, avail):
continue
except Exception:
continue
+12 -2
View File
@@ -1546,11 +1546,21 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
open_bridges = list_open_bridges(conn_b)
bal = cfg["fetch_options_balances"](ex, scope="main")
bridge = open_bridges[0] if open_bridges else None
from lib.options.options_spot_bridge_lib import (
display_sellable_coin,
spot_min_sell_sz,
)
raw_eth = bal.get("trading_eth_avail") or bal.get("trading_eth")
raw_btc = bal.get("trading_btc_avail") or bal.get("trading_btc")
spot_sell_hint = {
"bridge_status": str(bridge.get("status") or "") if bridge else None,
"bridge_underlying": str(bridge.get("underlying") or "").upper() if bridge else None,
"trading_eth": bal.get("trading_eth_avail") or bal.get("trading_eth"),
"trading_btc": bal.get("trading_btc_avail") or bal.get("trading_btc"),
# 粉尘归零:不够现货最小可卖则前端不展示、不提示重试
"trading_eth": display_sellable_coin(ex, "ETH", raw_eth),
"trading_btc": display_sellable_coin(ex, "BTC", raw_btc),
"min_sz_eth": spot_min_sell_sz(ex, "ETH"),
"min_sz_btc": spot_min_sell_sz(ex, "BTC"),
"default_underly": (cfg.get("default_underly") or "ETH").strip().upper(),
}
finally:
+118 -2
View File
@@ -13,6 +13,61 @@ BRIDGE_HOLDING = "holding"
BRIDGE_PENDING_SELL = "pending_sell_spot"
BRIDGE_CLOSED = "closed"
# 拉取 instruments 失败时的兜底(与 OKX 现货常见 minSz 对齐)
_SPOT_MIN_SZ_FALLBACK: dict[str, float] = {"ETH": 0.0001, "BTC": 0.00001}
_SPOT_MIN_SZ_CACHE: dict[str, tuple[float, float]] = {} # ccy -> (min_sz, mono_ts)
_SPOT_MIN_SZ_TTL_SEC = 3600.0
def spot_min_sell_sz(ex: Any | None, underlying: str) -> float:
"""ETH/BTC-USDT 现货最小下单量(base ccy);粉尘低于此值不卖、不展示."""
ccy = (underlying or "ETH").strip().upper() or "ETH"
fb = float(_SPOT_MIN_SZ_FALLBACK.get(ccy) or _SPOT_MIN_SZ_FALLBACK["ETH"])
now = time.monotonic()
cached = _SPOT_MIN_SZ_CACHE.get(ccy)
if cached and (now - cached[1]) < _SPOT_MIN_SZ_TTL_SEC:
return float(cached[0])
min_sz = fb
if ex is not None:
try:
inst_id = spot_quote_inst_id(ccy)
rows = ex.public_get_public_instruments({"instType": "SPOT", "instId": inst_id}) or {}
data = rows.get("data") if isinstance(rows, dict) else None
if not data and hasattr(ex, "publicGetPublicInstruments"):
rows = ex.publicGetPublicInstruments({"instType": "SPOT", "instId": inst_id}) or {}
data = rows.get("data") if isinstance(rows, dict) else None
row = (data or [None])[0] if data else None
if isinstance(row, dict):
raw = _safe_float(row.get("minSz"))
if raw is not None and raw > 0:
min_sz = float(raw)
except Exception:
pass
_SPOT_MIN_SZ_CACHE[ccy] = (min_sz, now)
return min_sz
def is_sellable_spot_coin(ex: Any | None, underlying: str, amount: Any) -> bool:
"""数量是否达到现货最小可卖(达到才重试/展示)."""
try:
amt = float(amount or 0)
except (TypeError, ValueError):
return False
if amt <= 0:
return False
return amt + 1e-15 >= spot_min_sell_sz(ex, underlying)
def display_sellable_coin(ex: Any | None, underlying: str, amount: Any) -> float:
"""展示用:不够 minSz 的粉尘归零."""
try:
amt = float(amount or 0)
except (TypeError, ValueError):
return 0.0
if not is_sellable_spot_coin(ex, underlying, amt):
return 0.0
return amt
def ensure_bridge_table(conn: sqlite3.Connection) -> None:
conn.execute(
@@ -229,8 +284,13 @@ def spot_market_sell_coin_to_usdt(
amt = float(avail)
if amt <= 0:
return {"ok": False, "msg": f"{ccy} 数量须大于 0"}
min_sz = spot_min_sell_sz(ex, ccy)
if amt + 1e-15 < min_sz:
return {"ok": False, "msg": f"{ccy} 可卖数量过小(低于现货最小 {min_sz:g})"}
# 留一点粉尘避免精度拒单
sell_sz = max(0.0, amt * 0.999)
if sell_sz + 1e-15 < min_sz:
return {"ok": False, "msg": f"{ccy} 可卖数量过小(低于现货最小 {min_sz:g})"}
inst_id = spot_quote_inst_id(ccy)
try:
# 现货卖出数量精度:截到 8 位
@@ -349,6 +409,43 @@ def sell_residual_after_option_flat(
except Exception:
pass
# 粉尘:不够现货最小下单量则忽略,不再重试;有桥则直接关闭
try:
avail_chk = fetch_trading_coin_available(ex, underlying)
except Exception:
avail_chk = None
if avail_chk is not None and not is_sellable_spot_coin(ex, underlying, avail_chk):
dust_msg = "粉尘低于现货最小可卖,已忽略"
if bid is None:
return {"ok": True, "msg": dust_msg, "skipped": True, "dust": True}
def _close_dust(c: sqlite3.Connection) -> dict[str, Any]:
update_bridge(
c,
bid,
status=BRIDGE_CLOSED,
inst_id=inst_id,
message=dust_msg,
close=True,
)
return {
"ok": True,
"msg": dust_msg,
"skipped": True,
"dust": True,
"bridge_id": bid,
"bridge_status": BRIDGE_CLOSED,
}
if get_db is not None:
conn_d = get_db()
try:
return _close_dust(conn_d)
finally:
try:
conn_d.close()
except Exception:
pass
return _close_dust(conn_or_get_db)
# 此处无业务连接:模拟盘 convert 可安全另开写钱包
sell = spot_market_sell_coin_to_usdt(ex, underlying=underlying)
if bid is None:
@@ -361,16 +458,35 @@ def sell_residual_after_option_flat(
def _apply_bridge(c: sqlite3.Connection) -> dict[str, Any]:
if not sell.get("ok"):
msg = str(sell.get("msg") or "卖回 USDT 失败")
# 交易所拒单「过小」也按粉尘关闭桥,避免一直 pending 重试
if "过小" in msg or "minimum" in msg.lower() or "min sz" in msg.lower():
update_bridge(
c,
bid,
status=BRIDGE_CLOSED,
inst_id=inst_id,
message="粉尘低于现货最小可卖,已忽略",
close=True,
)
return {
"ok": True,
"msg": "粉尘低于现货最小可卖,已忽略",
"skipped": True,
"dust": True,
"bridge_id": bid,
"bridge_status": BRIDGE_CLOSED,
}
update_bridge(
c,
bid,
status=BRIDGE_PENDING_SELL,
inst_id=inst_id,
message=str(sell.get("msg") or "卖回 USDT 失败"),
message=msg,
)
return {
"ok": False,
"msg": sell.get("msg") or "卖回 USDT 失败",
"msg": msg,
"bridge_id": bid,
"bridge_status": BRIDGE_PENDING_SELL,
}