中控增加条件单委托
This commit is contained in:
@@ -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,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user