粉尘低于现货最小可卖量时忽略且不展示、不再重试卖回。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user