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:
@@ -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 1018:auto_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(
|
||||
|
||||
Reference in New Issue
Block a user