推送按合约规则取整,减半腿精确一半

100 USDT 的仓位在 Bitget 上不是最小下单量的问题(minTradeUSDT=5、
minTradeNum 极小,10U 都能下),是**步长取整**的问题:

  SOL  步长 0.1 币 ≈ 10.7U → 50U 腿只能取 0.4 币 = 42.7U,偏 14.6%
  LINK 步长 1 币 ≈ 11.7U   → 4 币 = 46.8U,偏 6.4%
  BTC  步长 0.0001 ≈ 8.0U  → 0.0006 = 47.8U,偏 4.4%

减半腿变成全仓的 43% 而非 50%,剩下 57% 暴露在 8ATR 目标上,而回测的收益
结构假设 50/50。小额下这不影响机制验证,但会让 P&L 读不出回测那个结构。

解法是入场量取到**步长的偶数倍**,这样一半天然落在步长上。实测十个币减半腿
全部精确 50%,名义额落在 93.6~106.7 USDT——小额实盘无所谓。

同时把价位对齐到 tick(priceEndStep × 10^-pricePlace),否则限价单会被拒。
规则拉一次缓存;拉不到就退化为不取整并打日志,不阻断推送。

费率一事已核实无需改动:预算假设的挂牌 taker 0.040% / maker 0.016% 正是
Bitget VIP2 的官方档(返 50% 后 2.0 / 0.8bp)。合约接口返的 6bp/2bp 是 VIP0
基础档,不适用。8bp 门控阈值不变。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jack
2026-08-28 15:42:17 +08:00
co-authored by Cursor
parent 97457b0518
commit c57adc8cb0
2 changed files with 175 additions and 6 deletions
+96
View File
@@ -0,0 +1,96 @@
"""100 USDT 的仓位在各币上能不能下出来——下单精度与最小量。
手工小额实盘的第一个坑不在策略,在交易规则。100 USDT 的仓位要拆成两半
3 ATR 减半 50 USDT、8 ATR 目标 50 USDT),任一半低于最小下单量就下不出去,
或者被精度取整到与计划偏差很大的数量。
取整偏差会直接扭曲收益结构:若 50 USDT 被取整到 40,减半那一腿实际只出了
40%,剩余 60% 暴露在 8 ATR 目标上。回测的收益结构假设是 50/50。
python research/live/probe_rules.py --notional 100
"""
from __future__ import annotations
import argparse
import asyncio
import os
import sys
from decimal import Decimal
from pathlib import Path
HERE = Path(__file__).resolve()
sys.path.insert(0, str(HERE.parents[1]))
sys.path.insert(0, str(HERE.parent))
SYMS = os.environ.get(
"SYMS", "BTC,ETH,SOL,BNB,XRP,DOGE,ADA,AVAX,LINK,LTC").split(",")
async def run(notional: float) -> None:
from hummingbot.connector.derivative.bitget_perpetual.bitget_perpetual_derivative import ( # noqa: E501
BitgetPerpetualDerivative,
)
pairs = [f"{s}-USDT" for s in SYMS]
conn = BitgetPerpetualDerivative(
bitget_perpetual_api_key="", bitget_perpetual_secret_key="",
bitget_perpetual_passphrase="", trading_pairs=pairs,
trading_required=False)
await conn.start_network()
for _ in range(60):
await asyncio.sleep(1)
if conn.trading_rules and all(p in conn.trading_rules for p in pairs):
break
print(f"仓位 {notional:.0f} USDT · 减半腿 {notional / 2:.0f} USDT\n")
print(f" {'':<6}{'现价':>11}{'最小量':>12}{'量步长':>12}"
f"{'最小名义':>10} 减半腿可行性")
bad = []
for s in SYMS:
p = f"{s}-USDT"
r = conn.trading_rules.get(p)
if r is None:
print(f" {s:<6}{'取不到规则':>11}")
continue
ob = conn.get_order_book(p)
px = float((ob.get_price(True) + ob.get_price(False)) / 2) if ob \
else float("nan")
min_amt = float(r.min_order_size)
step = float(r.min_base_amount_increment)
min_not = float(r.min_notional_size or 0)
half_base = (notional / 2) / px
# 按步长向下取整——交易所就是这么处理的,向上取会下不出去
q = Decimal(str(half_base)) // Decimal(str(step)) * Decimal(str(step))
got = float(q)
if got < min_amt or (min_not and got * px < min_not):
verdict = f"⛔ 下不出(需 ≥ {max(min_amt, min_not / px):.6f}"
bad.append(s)
else:
dev = abs(got * px - notional / 2) / (notional / 2) * 1e4
verdict = f"{got:.6f} 币,偏差 {dev:.0f}bp"
if dev > 100:
verdict += " ⚠ 取整偏差大"
bad.append(s)
print(f" {s:<6}{px:>11,.4f}{min_amt:>12.6f}{step:>12.6f}"
f"{min_not:>10.1f} {verdict}")
print()
if bad:
print(f"{notional:.0f} USDT 下这些币的减半腿有问题:{','.join(bad)}")
print(f" 要么提高仓位,要么这些币不做减半、单腿到 8 ATR 出场——但后者")
print(f" 改了回测的收益结构,不能直接套用原预算。")
else:
print(f" {notional:.0f} USDT 在全部 {len(SYMS)} 个币上都能拆成两半下出。")
await conn.stop_network()
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--notional", type=float, default=100.0)
a = ap.parse_args()
asyncio.run(run(a.notional))
if __name__ == "__main__":
main()