下单体去掉 tradeSide:账户是单向持仓,带它会被 40774 全拒
实盘首批 4 个信号全部下单失败,8 次尝试(4 信号 × 2 腿)都是 [40774] The order type for unilateral position must also be the unilateral position type。投递链路其余部分完全正常:延后 0.0~0.2s、ssh 在线 255 分钟 零重连、去重 0、闸全过。纯粹是下单体语法。 官方文档:单向持仓下要忽略 tradeSide(side 取 buy/sell 即可),平仓用 side 取反 + reduceOnly=YES;而 reduceOnly 也只在单向模式下有效。双向持仓 才需要 tradeSide=open/close。我们发的是双向语法打到单向账户,是整体拒单 而不是部分降级。 三处下单体(entry_with_stop / tp_limit / close_market)都去掉 tradeSide。 reduceOnly 保留——它在单向模式下正是防止反手开出反向仓的那个参数。 另加 setup_position_mode():把模式钉成单向并读回核对,与既有的"每次启动 都设一遍逐仓和杠杆、不假设交易所侧状态"一致。调用点放在 reconcile 之后, 因为有持仓或挂单时交易所不允许切换。读回不是单向时推 Telegram 告警。 空跑不可能验到这个:dry=True 在发请求之前就返回假成功。这类"下单体字段 组合"的问题只有真单会暴露,和之前 oid 里 - 字符那条同一类。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+32
-5
@@ -155,6 +155,25 @@ class Bitget:
|
|||||||
"marginCoin": MARGIN_COIN,
|
"marginCoin": MARGIN_COIN,
|
||||||
"marginMode": mode})
|
"marginMode": mode})
|
||||||
|
|
||||||
|
async def set_position_mode(self, mode: str = "one_way_mode") -> dict:
|
||||||
|
"""单向 / 双向持仓。**按 productType 生效,不是按 symbol。**
|
||||||
|
|
||||||
|
必须显式设,因为它决定下单体的语法,两者不匹配会被整体拒单:
|
||||||
|
|
||||||
|
单向:side=buy/sell,**不带** tradeSide;平仓用 reduceOnly=YES
|
||||||
|
双向:side + tradeSide=open/close;reduceOnly 在这个模式下无效
|
||||||
|
|
||||||
|
实盘上曾因为带着 tradeSide 打到单向账户,连续 8 次下单全被 40774 拒掉
|
||||||
|
(4 个信号 × 2 条腿),而链路其余部分完全正常。
|
||||||
|
|
||||||
|
我们永不同时持有两个方向,所以单向是对的模式;且 reduceOnly 只在单向
|
||||||
|
下可用,而出场腿依赖它防止反手开出反向仓。
|
||||||
|
|
||||||
|
交易所侧有持仓或挂单时切换会失败——所以调用点放在 reconcile 之后。
|
||||||
|
"""
|
||||||
|
return await self._req("POST", "/api/v2/mix/account/set-position-mode",
|
||||||
|
body={"productType": PRODUCT, "posMode": mode})
|
||||||
|
|
||||||
async def entry_with_stop(self, symbol: str, side: str, size: str,
|
async def entry_with_stop(self, symbol: str, side: str, size: str,
|
||||||
stop_px: str, client_oid: str) -> dict:
|
stop_px: str, client_oid: str) -> dict:
|
||||||
"""市价入场,**同时**把止损挂到服务端。
|
"""市价入场,**同时**把止损挂到服务端。
|
||||||
@@ -162,10 +181,12 @@ class Bitget:
|
|||||||
`presetStopLossPrice` 触发后按市价执行,这正是成本模型要的(止损是
|
`presetStopLossPrice` 触发后按市价执行,这正是成本模型要的(止损是
|
||||||
taker)。`clientOid` 给交易所级幂等——重发同一个 oid 会被拒,比本地
|
taker)。`clientOid` 给交易所级幂等——重发同一个 oid 会被拒,比本地
|
||||||
去重可靠,因为「已发出但没收到回复」这种情况本地判不了。
|
去重可靠,因为「已发出但没收到回复」这种情况本地判不了。
|
||||||
|
|
||||||
|
**不带 `tradeSide`**:账户是单向持仓,带了会被 40774 整体拒单。
|
||||||
"""
|
"""
|
||||||
body = {"symbol": symbol, "productType": PRODUCT,
|
body = {"symbol": symbol, "productType": PRODUCT,
|
||||||
"marginMode": "isolated", "marginCoin": MARGIN_COIN,
|
"marginMode": "isolated", "marginCoin": MARGIN_COIN,
|
||||||
"size": size, "side": side, "tradeSide": "open",
|
"size": size, "side": side,
|
||||||
"orderType": "market", "clientOid": client_oid,
|
"orderType": "market", "clientOid": client_oid,
|
||||||
"presetStopLossPrice": stop_px}
|
"presetStopLossPrice": stop_px}
|
||||||
if self.dry:
|
if self.dry:
|
||||||
@@ -180,11 +201,14 @@ class Bitget:
|
|||||||
|
|
||||||
`side` 传的是**平仓方向**(多头止盈是 sell)。`post_only` 保证是 maker:
|
`side` 传的是**平仓方向**(多头止盈是 sell)。`post_only` 保证是 maker:
|
||||||
成本模型里止盈那 60% 按 maker 费率计且不吃滑点,用 taker 会破坏预算。
|
成本模型里止盈那 60% 按 maker 费率计且不吃滑点,用 taker 会破坏预算。
|
||||||
`reduceOnly` 防止在单向模式下反手开出一个反向仓。
|
|
||||||
|
单向持仓下平仓的写法是 `side` 取反 + `reduceOnly=YES`,**不带**
|
||||||
|
`tradeSide`。`reduceOnly` 也正好只在单向模式下有效,它防止反手开出
|
||||||
|
一个反向仓。
|
||||||
"""
|
"""
|
||||||
body = {"symbol": symbol, "productType": PRODUCT,
|
body = {"symbol": symbol, "productType": PRODUCT,
|
||||||
"marginMode": "isolated", "marginCoin": MARGIN_COIN,
|
"marginMode": "isolated", "marginCoin": MARGIN_COIN,
|
||||||
"size": size, "side": side, "tradeSide": "close",
|
"size": size, "side": side,
|
||||||
"orderType": "limit", "price": px, "force": "post_only",
|
"orderType": "limit", "price": px, "force": "post_only",
|
||||||
"reduceOnly": "YES", "clientOid": client_oid}
|
"reduceOnly": "YES", "clientOid": client_oid}
|
||||||
if self.dry:
|
if self.dry:
|
||||||
@@ -195,11 +219,14 @@ class Bitget:
|
|||||||
|
|
||||||
async def close_market(self, symbol: str, hold_side: str,
|
async def close_market(self, symbol: str, hold_side: str,
|
||||||
size: str, client_oid: str) -> dict:
|
size: str, client_oid: str) -> dict:
|
||||||
"""市价平(超时腿与对账用)。"""
|
"""市价平(超时腿与对账用)。
|
||||||
|
|
||||||
|
同 tp_limit:单向持仓下是 `side` 取反 + `reduceOnly`,不带 `tradeSide`。
|
||||||
|
"""
|
||||||
side = "sell" if hold_side == "long" else "buy"
|
side = "sell" if hold_side == "long" else "buy"
|
||||||
body = {"symbol": symbol, "productType": PRODUCT,
|
body = {"symbol": symbol, "productType": PRODUCT,
|
||||||
"marginMode": "isolated", "marginCoin": MARGIN_COIN,
|
"marginMode": "isolated", "marginCoin": MARGIN_COIN,
|
||||||
"size": size, "side": side, "tradeSide": "close",
|
"size": size, "side": side,
|
||||||
"orderType": "market", "reduceOnly": "YES",
|
"orderType": "market", "reduceOnly": "YES",
|
||||||
"clientOid": client_oid}
|
"clientOid": client_oid}
|
||||||
if self.dry:
|
if self.dry:
|
||||||
|
|||||||
@@ -335,6 +335,37 @@ class Exec:
|
|||||||
"BITGET_PASSPHRASE(末项无 API_)。先跑 --dry-run。")
|
"BITGET_PASSPHRASE(末项无 API_)。先跑 --dry-run。")
|
||||||
await self.setup_symbols()
|
await self.setup_symbols()
|
||||||
await self.reconcile()
|
await self.reconcile()
|
||||||
|
# 放在 reconcile 之后:有持仓或挂单时交易所不允许切换持仓模式,而
|
||||||
|
# reconcile 刚把仓位平干净
|
||||||
|
await self.setup_position_mode()
|
||||||
|
|
||||||
|
async def setup_position_mode(self) -> None:
|
||||||
|
"""把持仓模式钉成单向,并读回核对。
|
||||||
|
|
||||||
|
为什么必须显式设而不是假设:持仓模式决定下单体的语法,两者不匹配是
|
||||||
|
**整体拒单**,不是部分降级。实盘上就因为带着 `tradeSide`(双向语法)
|
||||||
|
打到单向账户,连续 8 次下单全被 40774 拒掉——4 个信号 × 2 条腿,而
|
||||||
|
投递链路那时完全正常(延后 0.0~0.2s、ssh 零重连)。
|
||||||
|
|
||||||
|
单向是我们要的:永不同时持有两个方向,且出场腿依赖 `reduceOnly`,
|
||||||
|
而它只在单向模式下有效。
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
d = await self.api.set_position_mode("one_way_mode") or {}
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
print(f" ⛔ 设持仓模式失败 {type(e).__name__}: {e}", flush=True)
|
||||||
|
print(" 若账户实际是双向持仓,下单会被 40774 全部拒掉。"
|
||||||
|
"先在 App 里平掉所有仓位与挂单,再切成单向", flush=True)
|
||||||
|
await tg.error("设持仓模式失败,下单可能被 40774 全拒", str(e))
|
||||||
|
return
|
||||||
|
got = str(d.get("posMode") or "")
|
||||||
|
if got and got != "one_way_mode":
|
||||||
|
print(f" ⛔ 持仓模式仍是 {got},下单体是单向语法,会被 40774 拒",
|
||||||
|
flush=True)
|
||||||
|
await tg.error(f"持仓模式是 {got},不是单向", "下单会被 40774 拒掉")
|
||||||
|
else:
|
||||||
|
print(f" 已设持仓模式 单向{'(已读回核对)' if got else ''}",
|
||||||
|
flush=True)
|
||||||
|
|
||||||
async def setup_symbols(self) -> None:
|
async def setup_symbols(self) -> None:
|
||||||
"""逐仓 + 杠杆。每次启动都设一遍,不假设交易所侧的状态。
|
"""逐仓 + 杠杆。每次启动都设一遍,不假设交易所侧的状态。
|
||||||
|
|||||||
Reference in New Issue
Block a user