中控增加条件单委托

This commit is contained in:
dekun
2026-05-24 08:09:08 +08:00
parent 4b5fae2946
commit 3b97a59562
9 changed files with 731 additions and 14 deletions
+281 -1
View File
@@ -1,8 +1,10 @@
"""
中控子代理:拉取交易所挂单/条件单并规范化展示;撤销单笔或按合约批量撤销。
中控子代理:拉取交易所挂单/条件单并规范化展示;撤销单笔或按合约批量撤销;挂止盈止损(先撤条件单再挂)
"""
from __future__ import annotations
import os
import time
from typing import Any
@@ -353,3 +355,281 @@ def cancel_orders_for_symbol(
except Exception:
pass
return n
def _binance_cancel_algo_open(ex: Any, symbol: str) -> None:
try:
market = ex.market(symbol)
cid = market.get("id")
if cid and hasattr(ex, "fapiPrivateDeleteAlgoOpenOrders"):
ex.fapiPrivateDeleteAlgoOpenOrders({"symbol": cid})
except Exception:
pass
def _binance_trigger_params() -> dict[str, Any]:
wt = (os.getenv("BINANCE_TRIGGER_WORKING_TYPE") or "CONTRACT_PRICE").strip().upper()
if wt not in ("CONTRACT_PRICE", "MARK_PRICE"):
wt = "CONTRACT_PRICE"
return {"workingType": wt}
def _binance_place_tp_sl(
ex: Any,
symbol: str,
direction: str,
amount: float,
stop_loss: float,
take_profit: float,
*,
position_mode: str = "hedge",
) -> None:
ex.load_markets()
market = ex.market(symbol)
if not market.get("swap"):
raise RuntimeError("仅支持永续合约")
close_side = "sell" if direction == "long" else "buy"
amt = float(ex.amount_to_precision(symbol, float(amount)))
if amt <= 0:
raise RuntimeError("止盈止损:可平数量经精度舍入后为 0")
sl_px = ex.price_to_precision(symbol, float(stop_loss))
tp_px = ex.price_to_precision(symbol, float(take_profit))
common = dict(_binance_trigger_params())
if (position_mode or "hedge").lower() in ("hedge", "dual", "double", "hedged"):
common["positionSide"] = "LONG" if direction == "long" else "SHORT"
last_err: Exception | None = None
for attempt in range(6):
try:
ex.create_order(
symbol, "STOP_MARKET", close_side, amt, None, dict(common, stopPrice=sl_px)
)
time.sleep(0.05)
ex.create_order(
symbol,
"TAKE_PROFIT_MARKET",
close_side,
amt,
None,
dict(common, stopPrice=tp_px),
)
return
except Exception as e:
last_err = e
cancel_orders_for_symbol(ex, "binance", symbol, scope="conditional")
_binance_cancel_algo_open(ex, symbol)
time.sleep(0.2 * (attempt + 1))
raise RuntimeError(f"Binance 未接受止盈/止损:{last_err}")
def _okx_order_params(direction: str, *, reduce_only: bool, pos_mode: str, td_mode: str) -> dict:
params: dict[str, Any] = {"tdMode": td_mode or "cross"}
if (pos_mode or "hedge").lower() in ("hedge", "long_short_mode", "dual"):
params["posSide"] = "long" if direction == "long" else "short"
if reduce_only:
params["reduceOnly"] = True
return params
def _okx_place_tp_sl(
ex: Any,
symbol: str,
direction: str,
amount: float,
stop_loss: float,
take_profit: float,
*,
pos_mode: str = "hedge",
td_mode: str = "cross",
) -> None:
ex.load_markets()
close_side = "sell" if direction == "long" else "buy"
amt = float(ex.amount_to_precision(symbol, float(amount)))
if amt <= 0:
raise RuntimeError("止盈止损:可平数量经精度舍入后为 0")
params = _okx_order_params(direction, reduce_only=True, pos_mode=pos_mode, td_mode=td_mode)
sl_s = ex.price_to_precision(symbol, float(stop_loss))
tp_s = ex.price_to_precision(symbol, float(take_profit))
params["stopLoss"] = {"triggerPrice": sl_s, "type": "market"}
params["takeProfit"] = {"triggerPrice": tp_s, "type": "market"}
ex.create_order(symbol, "market", close_side, amt, None, params)
def _gate_tpsl_env() -> tuple[bool, int, int, str]:
use_pos = (os.getenv("GATE_TPSL_USE_POSITION_ORDER") or "true").lower() in ("1", "true", "yes")
exp = int(os.getenv("GATE_TPSL_TRIGGER_EXPIRATION", str(7 * 86400)))
pt = int(os.getenv("GATE_TPSL_PRICE_TYPE", "0"))
if pt < 0 or pt > 2:
pt = 0
pos_mode = (os.getenv("GATE_POS_MODE") or "hedge").strip().lower()
return use_pos, exp, pt, pos_mode
def _gate_place_tp_sl_position(
ex: Any,
symbol: str,
direction: str,
stop_loss: float,
take_profit: float,
*,
pos_mode: str,
price_type: int,
expiration: int,
) -> None:
ex.load_markets()
market = ex.market(symbol)
if not market.get("swap"):
raise RuntimeError("仅支持永续合约")
settle = market["settleId"]
contract = market["id"]
order_type = "close-long-position" if direction == "long" else "close-short-position"
close_side = "sell" if direction == "long" else "buy"
sl_rule, tp_rule = (2, 1) if close_side == "sell" else (1, 2)
initial: dict[str, Any] = {
"contract": contract,
"size": 0,
"price": "0",
"close": True,
"reduce_only": True,
"tif": "ioc",
"text": "api",
}
if pos_mode in ("hedge", "dual", "double"):
initial["auto_size"] = "close_long" if direction == "long" else "close_short"
sl_s = ex.price_to_precision(symbol, float(stop_loss))
tp_s = ex.price_to_precision(symbol, float(take_profit))
def _payload(trigger_price: str, rule: int) -> dict:
trig: dict[str, Any] = {
"strategy_type": 0,
"price_type": price_type,
"price": trigger_price,
"rule": rule,
}
if expiration > 0:
trig["expiration"] = expiration
return {
"settle": settle,
"initial": dict(initial),
"trigger": trig,
"order_type": order_type,
}
last_err: Exception | None = None
for attempt in range(6):
try:
ex.privateFuturesPostSettlePriceOrders(_payload(sl_s, sl_rule))
try:
ex.privateFuturesPostSettlePriceOrders(_payload(tp_s, tp_rule))
except Exception:
cancel_orders_for_symbol(ex, "gate", symbol, scope="conditional")
raise
return
except Exception as e:
last_err = e
time.sleep(0.2 * (attempt + 1))
raise RuntimeError(f"Gate 仓位类止盈/止损未接受:{last_err}")
def _gate_place_tp_sl_legacy(
ex: Any,
symbol: str,
direction: str,
amount: float,
stop_loss: float,
take_profit: float,
) -> None:
ex.load_markets()
close_side = "sell" if direction == "long" else "buy"
base = {"reduceOnly": True}
last_err: Exception | None = None
for attempt in range(6):
try:
ex.create_order(
symbol,
"market",
close_side,
amount,
None,
dict(base, stopLossPrice=float(stop_loss)),
)
ex.create_order(
symbol,
"market",
close_side,
amount,
None,
dict(base, takeProfitPrice=float(take_profit)),
)
return
except Exception as e:
last_err = e
time.sleep(0.2 * (attempt + 1))
raise RuntimeError(f"Gate 条件止盈/止损未接受:{last_err}")
def _gate_place_tp_sl(
ex: Any,
symbol: str,
direction: str,
amount: float,
stop_loss: float,
take_profit: float,
) -> None:
use_pos, exp, pt, pos_mode = _gate_tpsl_env()
if use_pos:
try:
_gate_place_tp_sl_position(
ex, symbol, direction, stop_loss, take_profit,
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)
def replace_position_tpsl(
ex: Any,
exchange_kind: str,
symbol: str,
direction: str,
amount: float,
stop_loss: float,
take_profit: float,
) -> dict[str, Any]:
"""
先撤销该合约全部条件单,再挂止盈+止损。与四实例策略页逻辑对齐(读各目录 .env 中 GATE_/BINANCE_/OKX_ 参数)。
"""
kind = (exchange_kind or "binance").lower()
direction = (direction or "long").strip().lower()
if direction not in ("long", "short"):
raise ValueError("direction 须为 long 或 short")
sl = float(stop_loss)
tp = float(take_profit)
if sl <= 0 or tp <= 0:
raise ValueError("止损、止盈价格须大于 0")
ex.load_markets()
cancelled = cancel_orders_for_symbol(ex, kind, symbol, scope="conditional")
if kind == "binance":
_binance_cancel_algo_open(ex, symbol)
time.sleep(0.08)
amt = float(amount)
if amt <= 0:
raise ValueError("持仓数量无效")
if kind == "binance":
pm = (os.getenv("BINANCE_POSITION_MODE") or "hedge").strip().lower()
_binance_place_tp_sl(ex, symbol, direction, amt, sl, tp, position_mode=pm)
elif kind == "okx":
pm = (os.getenv("OKX_POS_MODE") or "hedge").strip().lower()
td = (os.getenv("OKX_TD_MODE") or "cross").strip()
_okx_place_tp_sl(ex, symbol, direction, amt, sl, tp, pos_mode=pm, td_mode=td)
else:
_gate_place_tp_sl(ex, symbol, direction, amt, sl, tp)
return {
"symbol": symbol,
"direction": direction,
"amount": amt,
"stop_loss": sl,
"take_profit": tp,
"cancelled_conditional": cancelled,
}