粉尘低于现货最小可卖量时忽略且不展示、不再重试卖回。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -2317,15 +2317,33 @@
|
|||||||
return n.toFixed(8).replace(/\.?0+$/, "");
|
return n.toFixed(8).replace(/\.?0+$/, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function spotSellMinSz(ss, uly) {
|
||||||
|
const u = String(uly || "ETH").toUpperCase();
|
||||||
|
const fromApi = u === "BTC" ? Number(ss && ss.min_sz_btc) : Number(ss && ss.min_sz_eth);
|
||||||
|
if (Number.isFinite(fromApi) && fromApi > 0) return fromApi;
|
||||||
|
return u === "BTC" ? 0.00001 : 0.0001;
|
||||||
|
}
|
||||||
|
|
||||||
function shouldShowSpotSellBanner(d, positions) {
|
function shouldShowSpotSellBanner(d, positions) {
|
||||||
// 币本位下始终露出「重试卖回」(含有仓):便于清交易户残留,文案随选中/残留标的变化
|
// 仅当交易户有「可卖」(>= 现货 minSz)残留时展示;粉尘不展示、不提示重试
|
||||||
|
const ss = (d && d.spot_sell) || {};
|
||||||
|
const eth = Number(ss.trading_eth || 0);
|
||||||
|
const btc = Number(ss.trading_btc || 0);
|
||||||
|
const ethOk = eth >= spotSellMinSz(ss, "ETH");
|
||||||
|
const btcOk = btc >= spotSellMinSz(ss, "BTC");
|
||||||
|
if (ethOk || btcOk) return true;
|
||||||
const hasCoinPos = (positions || []).some(function (p) { return isCoinPos(p); });
|
const hasCoinPos = (positions || []).some(function (p) { return isCoinPos(p); });
|
||||||
const coinMode =
|
const coinMode =
|
||||||
isCoinMarginMode() ||
|
isCoinMarginMode() ||
|
||||||
hasCoinPos ||
|
hasCoinPos ||
|
||||||
(d && d.options_margin_mode === "coin") ||
|
(d && d.options_margin_mode === "coin");
|
||||||
!!(d && d.spot_sell);
|
// 待卖回桥且仍有可卖数量时才露按钮(粉尘桥后端会关掉)
|
||||||
return !!coinMode;
|
if (coinMode && ss.bridge_status === "pending_sell_spot") {
|
||||||
|
const bu = String(ss.bridge_underlying || "").toUpperCase();
|
||||||
|
const amt = bu === "BTC" ? btc : eth;
|
||||||
|
return amt >= spotSellMinSz(ss, bu || "ETH");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function spotSellUnderlying(ss) {
|
function spotSellUnderlying(ss) {
|
||||||
@@ -2334,14 +2352,16 @@
|
|||||||
const bridgeUly = String(ss.bridge_underlying || "").trim().toUpperCase();
|
const bridgeUly = String(ss.bridge_underlying || "").trim().toUpperCase();
|
||||||
const eth = Number(ss.trading_eth || 0);
|
const eth = Number(ss.trading_eth || 0);
|
||||||
const btc = Number(ss.trading_btc || 0);
|
const btc = Number(ss.trading_btc || 0);
|
||||||
// 待卖回桥优先;否则优先当前选中标的上的残留,再比大小
|
const ethMin = spotSellMinSz(ss, "ETH");
|
||||||
|
const btcMin = spotSellMinSz(ss, "BTC");
|
||||||
|
// 待卖回桥优先;否则优先当前选中标的上的可卖残留
|
||||||
if (bridgeUly === "BTC" || bridgeUly === "ETH") {
|
if (bridgeUly === "BTC" || bridgeUly === "ETH") {
|
||||||
if (ss.bridge_status === "pending_sell_spot") return bridgeUly;
|
if (ss.bridge_status === "pending_sell_spot") return bridgeUly;
|
||||||
}
|
}
|
||||||
if (selected === "BTC" && btc > 1e-12) return "BTC";
|
if (selected === "BTC" && btc >= btcMin) return "BTC";
|
||||||
if (selected === "ETH" && eth > 1e-12) return "ETH";
|
if (selected === "ETH" && eth >= ethMin) return "ETH";
|
||||||
if (btc > eth && btc > 1e-12) return "BTC";
|
if (btc >= btcMin && btc >= eth) return "BTC";
|
||||||
if (eth > 1e-12) return "ETH";
|
if (eth >= ethMin) return "ETH";
|
||||||
if (bridgeUly === "BTC" || bridgeUly === "ETH") return bridgeUly;
|
if (bridgeUly === "BTC" || bridgeUly === "ETH") return bridgeUly;
|
||||||
return (ss.default_underly || selected || "ETH").toUpperCase();
|
return (ss.default_underly || selected || "ETH").toUpperCase();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -161,10 +161,18 @@ def trading_account_label(
|
|||||||
parts.append(f"{float(usdt):.2f} USDT")
|
parts.append(f"{float(usdt):.2f} USDT")
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
pass
|
pass
|
||||||
eth_txt = _fmt_coin_amount(eth, min_amt=1e-6)
|
# 低于现货最小可卖的粉尘不展示(与卖回逻辑一致)
|
||||||
|
try:
|
||||||
|
from lib.options.options_spot_bridge_lib import spot_min_sell_sz
|
||||||
|
|
||||||
|
eth_min = spot_min_sell_sz(None, "ETH")
|
||||||
|
btc_min = spot_min_sell_sz(None, "BTC")
|
||||||
|
except Exception:
|
||||||
|
eth_min, btc_min = 0.0001, 0.00001
|
||||||
|
eth_txt = _fmt_coin_amount(eth, min_amt=eth_min)
|
||||||
if eth_txt is not None:
|
if eth_txt is not None:
|
||||||
parts.append(f"{eth_txt} ETH")
|
parts.append(f"{eth_txt} ETH")
|
||||||
btc_txt = _fmt_coin_amount(btc, min_amt=1e-7)
|
btc_txt = _fmt_coin_amount(btc, min_amt=btc_min)
|
||||||
if btc_txt is not None:
|
if btc_txt is not None:
|
||||||
parts.append(f"{btc_txt} BTC")
|
parts.append(f"{btc_txt} BTC")
|
||||||
return "\n".join(parts) if parts else "—"
|
return "\n".join(parts) if parts else "—"
|
||||||
|
|||||||
@@ -1158,7 +1158,9 @@ function formatTradingAccountLabel(usdt, eth, btc, marginMode) {
|
|||||||
const pushCoin = (v, ccy) => {
|
const pushCoin = (v, ccy) => {
|
||||||
if (v === null || v === undefined || v === "") return;
|
if (v === null || v === undefined || v === "") return;
|
||||||
const n = Number(v);
|
const n = Number(v);
|
||||||
if (Number.isNaN(n) || !(n >= (ccy === "BTC" ? 1e-7 : 1e-6))) return;
|
// 低于 OKX 现货常见 minSz 的粉尘不展示
|
||||||
|
const minSz = ccy === "BTC" ? 0.00001 : 0.0001;
|
||||||
|
if (Number.isNaN(n) || !(n >= minSz)) return;
|
||||||
const txt = String(n.toFixed(6)).replace(/\.?0+$/, "");
|
const txt = String(n.toFixed(6)).replace(/\.?0+$/, "");
|
||||||
parts.push(`${txt || "0"} ${ccy}`);
|
parts.push(`${txt || "0"} ${ccy}`);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1644,7 +1644,9 @@ function formatTradingAccountLabel(usdt, eth, btc, marginMode) {
|
|||||||
const pushCoin = (v, ccy) => {
|
const pushCoin = (v, ccy) => {
|
||||||
if (v == null || v === "") return;
|
if (v == null || v === "") return;
|
||||||
const n = Number(v);
|
const n = Number(v);
|
||||||
if (Number.isNaN(n) || !(n >= (ccy === "BTC" ? 1e-7 : 1e-6))) return;
|
// 低于 OKX 现货常见 minSz 的粉尘不展示
|
||||||
|
const minSz = ccy === "BTC" ? 0.00001 : 0.0001;
|
||||||
|
if (Number.isNaN(n) || !(n >= minSz)) return;
|
||||||
const txt = String(n.toFixed(6)).replace(/\.?0+$/, "");
|
const txt = String(n.toFixed(6)).replace(/\.?0+$/, "");
|
||||||
parts.push(`${txt || "0"} ${ccy}`);
|
parts.push(`${txt || "0"} ${ccy}`);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -507,7 +507,8 @@ def sweep_spot_after_options_flat(
|
|||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
avail = _spot_bridge.fetch_trading_coin_available(exchange, uly)
|
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
|
continue
|
||||||
except Exception:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -1546,11 +1546,21 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
|||||||
open_bridges = list_open_bridges(conn_b)
|
open_bridges = list_open_bridges(conn_b)
|
||||||
bal = cfg["fetch_options_balances"](ex, scope="main")
|
bal = cfg["fetch_options_balances"](ex, scope="main")
|
||||||
bridge = open_bridges[0] if open_bridges else None
|
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 = {
|
spot_sell_hint = {
|
||||||
"bridge_status": str(bridge.get("status") or "") if bridge else None,
|
"bridge_status": str(bridge.get("status") or "") if bridge else None,
|
||||||
"bridge_underlying": str(bridge.get("underlying") or "").upper() 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(),
|
"default_underly": (cfg.get("default_underly") or "ETH").strip().upper(),
|
||||||
}
|
}
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
@@ -13,6 +13,61 @@ BRIDGE_HOLDING = "holding"
|
|||||||
BRIDGE_PENDING_SELL = "pending_sell_spot"
|
BRIDGE_PENDING_SELL = "pending_sell_spot"
|
||||||
BRIDGE_CLOSED = "closed"
|
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:
|
def ensure_bridge_table(conn: sqlite3.Connection) -> None:
|
||||||
conn.execute(
|
conn.execute(
|
||||||
@@ -229,8 +284,13 @@ def spot_market_sell_coin_to_usdt(
|
|||||||
amt = float(avail)
|
amt = float(avail)
|
||||||
if amt <= 0:
|
if amt <= 0:
|
||||||
return {"ok": False, "msg": f"{ccy} 数量须大于 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)
|
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)
|
inst_id = spot_quote_inst_id(ccy)
|
||||||
try:
|
try:
|
||||||
# 现货卖出数量精度:截到 8 位
|
# 现货卖出数量精度:截到 8 位
|
||||||
@@ -349,6 +409,43 @@ def sell_residual_after_option_flat(
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
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 可安全另开写钱包
|
# 此处无业务连接:模拟盘 convert 可安全另开写钱包
|
||||||
sell = spot_market_sell_coin_to_usdt(ex, underlying=underlying)
|
sell = spot_market_sell_coin_to_usdt(ex, underlying=underlying)
|
||||||
if bid is None:
|
if bid is None:
|
||||||
@@ -361,16 +458,35 @@ def sell_residual_after_option_flat(
|
|||||||
|
|
||||||
def _apply_bridge(c: sqlite3.Connection) -> dict[str, Any]:
|
def _apply_bridge(c: sqlite3.Connection) -> dict[str, Any]:
|
||||||
if not sell.get("ok"):
|
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(
|
update_bridge(
|
||||||
c,
|
c,
|
||||||
bid,
|
bid,
|
||||||
status=BRIDGE_PENDING_SELL,
|
status=BRIDGE_PENDING_SELL,
|
||||||
inst_id=inst_id,
|
inst_id=inst_id,
|
||||||
message=str(sell.get("msg") or "卖回 USDT 失败"),
|
message=msg,
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
"ok": False,
|
"ok": False,
|
||||||
"msg": sell.get("msg") or "卖回 USDT 失败",
|
"msg": msg,
|
||||||
"bridge_id": bid,
|
"bridge_id": bid,
|
||||||
"bridge_status": BRIDGE_PENDING_SELL,
|
"bridge_status": BRIDGE_PENDING_SELL,
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-1
@@ -472,8 +472,13 @@ def _patch_spot_bridge_lib() -> None:
|
|||||||
avail = fetch_trading_coin_available(ex, coin)
|
avail = fetch_trading_coin_available(ex, coin)
|
||||||
if avail is None or float(avail) <= 0:
|
if avail is None or float(avail) <= 0:
|
||||||
return {"ok": False, "msg": f"交易账户无可用 {coin}"}
|
return {"ok": False, "msg": f"交易账户无可用 {coin}"}
|
||||||
|
from lib.options.options_spot_bridge_lib import is_sellable_spot_coin, spot_min_sell_sz
|
||||||
|
|
||||||
|
if not is_sellable_spot_coin(ex, coin, avail):
|
||||||
|
min_sz = spot_min_sell_sz(ex, coin)
|
||||||
|
return {"ok": False, "msg": f"{coin} 可卖数量过小(低于现货最小 {min_sz:g})"}
|
||||||
sell_sz = max(0.0, float(avail) * 0.999)
|
sell_sz = max(0.0, float(avail) * 0.999)
|
||||||
if sell_sz <= 0:
|
if sell_sz <= 0 or not is_sellable_spot_coin(ex, coin, sell_sz):
|
||||||
return {"ok": False, "msg": f"{coin} 可卖数量过小"}
|
return {"ok": False, "msg": f"{coin} 可卖数量过小"}
|
||||||
pub = _sim_public_exchange(ex)
|
pub = _sim_public_exchange(ex)
|
||||||
if pub is None:
|
if pub is None:
|
||||||
|
|||||||
Reference in New Issue
Block a user