fix(hub,gate): cross-margin TP/SL and dedupe hub conditional orders

Gate hedge position triggers use close=false; stop silent ccxt fallback on cross margin. Hub merges agent and Flask TP/SL by trigger price and labels Gate orders correctly.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-04 19:48:04 +08:00
parent ed0805538f
commit 24270944e7
7 changed files with 220 additions and 25 deletions
+73 -4
View File
@@ -293,6 +293,30 @@ def _okx_list(ex: Any, symbol: str | None) -> list[dict]:
return out
def _gate_extract_trigger_rule(info: dict) -> int | None:
if not isinstance(info, dict):
return None
trig = info.get("trigger")
if isinstance(trig, dict) and trig.get("rule") is not None:
try:
return int(trig["rule"])
except (TypeError, ValueError):
pass
try:
return int(info.get("rule"))
except (TypeError, ValueError):
return None
def _gate_tpsl_role_from_rule(rule: int | None, direction: str) -> str | None:
if rule is None:
return None
d = (direction or "long").strip().lower()
if d == "long":
return "sl" if rule == 2 else ("tp" if rule == 1 else None)
return "sl" if rule == 1 else ("tp" if rule == 2 else None)
def _gate_trigger_params(ex: Any) -> dict:
p = {"type": "swap", "trigger": True}
try:
@@ -342,6 +366,10 @@ def _gate_list(ex: Any, symbol: str | None) -> list[dict]:
item["type"] = item.get("type") or "trigger"
n = _normalize_raw_order(item, channel="algo")
if n:
info = o.get("info") if isinstance(o.get("info"), dict) else {}
rule = _gate_extract_trigger_rule(info)
if rule is not None:
n["gate_trigger_rule"] = rule
out.append(n)
except Exception:
pass
@@ -370,11 +398,33 @@ def list_open_orders(ex: Any, exchange_kind: str, symbol: str | None = None) ->
return uniq
def _enrich_gate_conditional_labels(cond: list[dict], side: str) -> None:
"""Gate 仓位类触发单在 ccxt 中常显示为「市价·只减仓」,按 trigger.rule 标为止盈/止损。"""
direction = (side or "long").strip().lower()
for o in cond:
if not isinstance(o, dict):
continue
if (o.get("label") or "").startswith(("止盈", "止损")):
continue
role = _gate_tpsl_role_from_rule(o.get("gate_trigger_rule"), direction)
trig = o.get("trigger_price")
if not role or trig is None:
continue
try:
trig_f = float(trig)
except (TypeError, ValueError):
continue
prefix = "止损" if role == "sl" else "止盈"
o["label"] = f"{prefix} {trig_f:g}"
def attach_orders_to_positions(positions: list[dict], orders: list[dict]) -> None:
for p in positions:
sym = p.get("symbol") or ""
matched = [o for o in orders if symbols_match(sym, o.get("symbol") or "")]
p["conditional_orders"] = [o for o in matched if o.get("category") == "conditional"]
cond = [o for o in matched if o.get("category") == "conditional"]
_enrich_gate_conditional_labels(cond, p.get("side") or "long")
p["conditional_orders"] = cond
p["regular_orders"] = [o for o in matched if o.get("category") != "conditional"]
@@ -596,6 +646,8 @@ def _gate_place_tp_sl_position(
}
if pos_mode in ("hedge", "dual", "double"):
initial["auto_size"] = "close_long" if direction == "long" else "close_short"
# Gate API 1018auto_size=close_long|close_short 时 initial.close 须为 false
initial["close"] = False
sl_s = ex.price_to_precision(symbol, float(stop_loss))
tp_s = ex.price_to_precision(symbol, float(take_profit))
@@ -668,6 +720,11 @@ def _gate_place_tp_sl_legacy(
raise RuntimeError(f"Gate 条件止盈/止损未接受:{last_err}")
def _gate_td_mode_cross() -> bool:
td = (os.getenv("GATE_TD_MODE") or "cross").strip().lower()
return td in ("cross", "cross_margin")
def _gate_place_tp_sl(
ex: Any,
symbol: str,
@@ -677,6 +734,7 @@ def _gate_place_tp_sl(
take_profit: float,
) -> None:
use_pos, exp, pt, pos_mode = _gate_tpsl_env()
pos_err: Exception | None = None
if use_pos:
try:
_gate_place_tp_sl_position(
@@ -684,9 +742,20 @@ def _gate_place_tp_sl(
pos_mode=pos_mode, price_type=pt, expiration=exp,
)
return
except Exception:
pass
_gate_place_tp_sl_legacy(ex, symbol, direction, amount, stop_loss, take_profit)
except Exception as e:
pos_err = e
if _gate_td_mode_cross():
raise RuntimeError(
f"Gate 仓位类止盈/止损未接受(全仓不支持 ccxt 条件单回退):{pos_err}"
) from e
try:
_gate_place_tp_sl_legacy(ex, symbol, direction, amount, stop_loss, take_profit)
except Exception as legacy_err:
if pos_err is not None:
raise RuntimeError(
f"Gate 仓位类止盈/止损未接受:{pos_err};条件单回退亦失败:{legacy_err}"
) from legacy_err
raise
def replace_position_tpsl(