Files
crypto_monitor/manual_trading_hub/exchange_orders.py
T
dekun 24270944e7 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>
2026-06-04 19:48:04 +08:00

806 lines
26 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
中控子代理:拉取交易所挂单/条件单并规范化展示;撤销单笔或按合约批量撤销;挂止盈止损(先撤条件单再挂)。
"""
from __future__ import annotations
import os
import time
from typing import Any
from okx_orders_lib import fetch_okx_all_open_orders
def _coerce_float(*values) -> float | None:
for v in values:
if v is None or v == "":
continue
try:
return float(v)
except (TypeError, ValueError):
continue
return None
def _symbol_base_coin(symbol: str) -> str:
"""ZEC/USDT:USDT、ZEC-USDT-SWAP 等统一为标的币 ZEC。"""
s = (symbol or "").strip().upper()
if not s:
return ""
if "-SWAP" in s:
s = s.replace("-SWAP", "")
if "-" in s:
return s.split("-", 1)[0]
if "/" in s:
return s.split("/", 1)[0]
if ":" in s:
return s.split(":", 1)[0]
return s
def symbols_match(position_symbol: str, order_symbol: str) -> bool:
a = (position_symbol or "").strip().upper()
b = (order_symbol or "").strip().upper()
if not a or not b:
return False
if a == b:
return True
ba, bb = _symbol_base_coin(a), _symbol_base_coin(b)
if ba and bb and ba == bb:
return True
for suf in (":USDT", "/USDT:USDT", "/USDT"):
a2 = a.replace(suf, "")
b2 = b.replace(suf, "")
if f"{a2}/USDT" == b or f"{a2}/USDT:USDT" == b:
return True
if f"{b2}/USDT" == a or f"{b2}/USDT:USDT" == a:
return True
if a2 == b2:
return True
return False
def _order_type_str(order: dict) -> str:
info = order.get("info") or {}
if isinstance(info, dict):
for key in ("orderType", "type", "origType", "algoType", "ordType"):
val = info.get(key)
if val:
return str(val).upper()
return str(order.get("type") or "").upper()
def _is_conditional_type(typ: str) -> bool:
t = (typ or "").upper()
if not t:
return False
keys = ("STOP", "TAKE_PROFIT", "TRAIL", "TRIGGER", "CONDITIONAL", "OCO")
return any(k in t for k in keys)
def _order_label(typ: str, side: str, reduce_only: bool | None) -> str:
t = (typ or "").upper()
side_l = (side or "").lower()
parts = []
if "TAKE_PROFIT" in t:
parts.append("止盈")
elif "STOP" in t:
parts.append("止损")
elif "LIMIT" in t:
parts.append("限价")
elif "MARKET" in t:
parts.append("市价")
else:
parts.append(typ or "委托")
if side_l == "buy":
parts.append("买入")
elif side_l == "sell":
parts.append("卖出")
if reduce_only:
parts.append("·只减仓")
return " ".join(parts)
def _normalize_raw_order(order: dict, *, channel: str) -> dict[str, Any] | None:
if not isinstance(order, dict):
return None
info = order.get("info") or {}
if not isinstance(info, dict):
info = {}
oid = order.get("id") or info.get("algoId") or info.get("orderId") or info.get("ordId")
if oid is None:
return None
sym = str(order.get("symbol") or info.get("symbol") or info.get("instId") or "")
typ = _order_type_str(order)
side = str(order.get("side") or info.get("side") or "").lower()
reduce_only = order.get("reduceOnly")
if reduce_only is None:
reduce_only = info.get("reduceOnly")
try:
reduce_only = bool(reduce_only) if reduce_only is not None else None
except (TypeError, ValueError):
reduce_only = None
sl_trig = _coerce_float(info.get("slTriggerPx"), order.get("stopLossPrice"))
tp_trig = _coerce_float(info.get("tpTriggerPx"), order.get("takeProfitPrice"))
trig = _coerce_float(
order.get("stopPrice"),
order.get("triggerPrice"),
info.get("triggerPrice"),
info.get("stopPrice"),
info.get("triggerPx"),
sl_trig,
tp_trig,
)
price = _coerce_float(order.get("price"), info.get("price"), info.get("ordPx"))
amt = _coerce_float(order.get("amount"), order.get("remaining"), info.get("quantity"), info.get("origQty"), info.get("sz"))
category = "conditional" if _is_conditional_type(typ) or channel == "algo" else "limit"
label = _order_label(typ, side, reduce_only)
if sl_trig is not None and tp_trig is not None:
label = f"止盈止损 SL={sl_trig:g} TP={tp_trig:g}"
elif sl_trig is not None:
label = f"止损 {sl_trig:g}"
elif tp_trig is not None:
label = f"止盈 {tp_trig:g}"
return {
"id": str(oid),
"symbol": sym,
"channel": channel,
"category": category,
"label": label,
"type": typ,
"side": side,
"amount": amt,
"trigger_price": trig,
"price": price,
"reduce_only": reduce_only,
"status": str(order.get("status") or info.get("status") or "open"),
}
def _okx_normalize_orders(raw: dict, channel: str) -> list[dict[str, Any]]:
"""OKX 算法单常一笔同时含 SL+TP,拆成两条供中控「交易所止盈止损」展示。"""
n = _normalize_raw_order(dict(raw), channel=channel)
if not n:
return []
info = raw.get("info") or {}
if not isinstance(info, dict):
info = {}
sl_trig = _coerce_float(info.get("slTriggerPx"), raw.get("stopLossPrice"))
tp_trig = _coerce_float(info.get("tpTriggerPx"), raw.get("takeProfitPrice"))
if sl_trig is None or tp_trig is None or sl_trig == tp_trig:
return [n]
base_id = n["id"]
rows: list[dict[str, Any]] = []
for role, px, lbl in (
("sl", sl_trig, f"止损 {sl_trig:g}"),
("tp", tp_trig, f"止盈 {tp_trig:g}"),
):
row = dict(n)
row["id"] = f"{base_id}:{role}"
row["algo_id"] = base_id
row["label"] = lbl
row["trigger_price"] = px
row["category"] = "conditional"
row["channel"] = channel
rows.append(row)
return rows
def _okx_algo_order_id(order_id: str) -> str:
oid = str(order_id or "")
if ":" in oid:
return oid.split(":", 1)[0]
return oid
def _binance_list(ex: Any, symbol: str | None) -> list[dict]:
ex.load_markets()
out: list[dict] = []
symbols: list[str] = []
if symbol:
try:
symbols = [ex.market(symbol)["symbol"]]
except Exception:
symbols = [symbol]
else:
symbols = []
try:
for p in ex.fetch_positions() or []:
sym = p.get("symbol")
if sym:
symbols.append(sym)
except Exception:
pass
if symbol and not symbols:
symbols = [symbol]
def collect(ex_sym: str) -> None:
market = ex.market(ex_sym)
contract_id = market.get("id")
try:
for o in ex.fetch_open_orders(ex_sym) or []:
item = dict(o)
item["_channel"] = "regular"
n = _normalize_raw_order(item, channel="regular")
if n:
out.append(n)
except Exception:
pass
try:
if contract_id and hasattr(ex, "fapiPrivateGetOpenAlgoOrders"):
raw = ex.fapiPrivateGetOpenAlgoOrders({"symbol": contract_id})
items = raw if isinstance(raw, list) else (raw.get("orders") or raw.get("data") or [])
for info in items or []:
if not isinstance(info, dict):
continue
wrapped = {
"id": info.get("algoId") or info.get("orderId"),
"symbol": ex_sym,
"info": info,
"type": info.get("orderType") or info.get("type"),
"side": (info.get("side") or "").lower(),
"amount": info.get("quantity") or info.get("origQty"),
"stopPrice": info.get("triggerPrice") or info.get("stopPrice"),
"reduceOnly": info.get("reduceOnly"),
}
n = _normalize_raw_order(wrapped, channel="algo")
if n:
out.append(n)
except Exception:
pass
if symbols:
seen = set()
for s in symbols:
if s in seen:
continue
seen.add(s)
collect(s)
return out
def _okx_list(ex: Any, symbol: str | None) -> list[dict]:
ex.load_markets()
out: list[dict] = []
symbols: list[str] = []
if symbol:
try:
symbols = [ex.market(symbol)["symbol"]]
except Exception:
symbols = [symbol]
else:
try:
for p in ex.fetch_positions() or []:
sym = p.get("symbol")
if sym:
symbols.append(sym)
except Exception:
pass
if symbol and not symbols:
symbols = [symbol]
seen: set[tuple[str, str]] = set()
for sym in symbols:
try:
for o in fetch_okx_all_open_orders(ex, sym):
ch = "algo" if _is_conditional_type(_order_type_str(o)) else "regular"
for n in _okx_normalize_orders(dict(o), channel=ch):
key = (n["id"], n.get("channel") or ch)
if key in seen:
continue
seen.add(key)
out.append(n)
except Exception:
pass
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:
ex.load_unified_status()
if ex.options.get("unifiedAccount"):
p["unifiedAccount"] = True
except Exception:
pass
return p
def _gate_list(ex: Any, symbol: str | None) -> list[dict]:
ex.load_markets()
out: list[dict] = []
symbols: list[str] = []
if symbol:
try:
symbols = [ex.market(symbol)["symbol"]]
except Exception:
symbols = [symbol]
else:
try:
for p in ex.fetch_positions() or []:
sym = p.get("symbol")
if sym:
symbols.append(sym)
except Exception:
pass
if symbol and not symbols:
symbols = [symbol]
trig_params = _gate_trigger_params(ex)
seen = set()
for sym in symbols:
if sym in seen:
continue
seen.add(sym)
try:
for o in ex.fetch_open_orders(sym) or []:
n = _normalize_raw_order(dict(o), channel="regular")
if n:
out.append(n)
except Exception:
pass
try:
for o in ex.fetch_open_orders(sym, params=trig_params) or []:
item = dict(o)
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
return out
def list_open_orders(ex: Any, exchange_kind: str, symbol: str | None = None) -> list[dict]:
kind = (exchange_kind or "binance").lower()
if kind == "binance":
orders = _binance_list(ex, symbol)
elif kind == "okx":
orders = _okx_list(ex, symbol)
else:
orders = _gate_list(ex, symbol)
if symbol:
orders = [o for o in orders if symbols_match(symbol, o.get("symbol") or "")]
# 去重 id+channel
seen: set[tuple[str, str]] = set()
uniq: list[dict] = []
for o in orders:
key = (o["id"], o["channel"])
if key in seen:
continue
seen.add(key)
uniq.append(o)
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 "")]
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"]
def cancel_order(
ex: Any,
exchange_kind: str,
symbol: str,
order_id: str,
channel: str = "regular",
) -> None:
kind = (exchange_kind or "binance").lower()
ex.load_markets()
market = ex.market(symbol)
unified = market["symbol"]
ch = (channel or "regular").lower()
if kind == "binance" and ch == "algo":
contract_id = market.get("id")
if contract_id and hasattr(ex, "fapiPrivateDeleteAlgoOrder"):
ex.fapiPrivateDeleteAlgoOrder({"symbol": contract_id, "algoId": str(order_id)})
return
params = None
if kind == "gate" and ch == "algo":
params = _gate_trigger_params(ex)
elif kind == "okx" and ch == "algo":
params = {"stop": True}
oid = _okx_algo_order_id(order_id) if kind == "okx" else str(order_id)
ex.cancel_order(oid, unified, params)
def cancel_orders_for_symbol(
ex: Any,
exchange_kind: str,
symbol: str,
*,
scope: str = "all",
) -> int:
"""scope: all | conditional | limit"""
orders = list_open_orders(ex, exchange_kind, symbol)
if scope == "conditional":
orders = [o for o in orders if o.get("category") == "conditional"]
elif scope == "limit":
orders = [o for o in orders if o.get("category") != "conditional"]
n = 0
for o in orders:
try:
cancel_order(ex, exchange_kind, symbol, o["id"], o.get("channel") or "regular")
n += 1
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,
for_algo_tpsl: bool = False,
) -> dict:
params: dict[str, Any] = {"tdMode": td_mode or "cross"}
if (pos_mode or "hedge").lower() in ("hedge", "long_short_mode", "dual"):
ps = "long" if direction == "long" else "short"
params["posSide"] = ps
params["positionSide"] = ps
# OKX 条件/OCO 算法单勿带 reduceOnly,否则可能被当市价减仓立即成交
if reduce_only and not for_algo_tpsl:
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:
"""OKX 永续:一笔 OCO 算法单挂止盈+止损(勿 reduceOnly + 分两笔 market)。"""
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")
base = _okx_order_params(
direction,
reduce_only=False,
pos_mode=pos_mode,
td_mode=td_mode,
for_algo_tpsl=True,
)
sl_px = ex.price_to_precision(symbol, float(stop_loss))
tp_px = ex.price_to_precision(symbol, float(take_profit))
order_params = {
**base,
"stopLossPrice": float(sl_px),
"takeProfitPrice": float(tp_px),
"tpOrdPx": "-1",
"slOrdPx": "-1",
}
last_err: Exception | None = None
for attempt in range(6):
try:
ex.create_order(symbol, "oco", close_side, amt, None, order_params)
return
except Exception as e:
last_err = e
cancel_orders_for_symbol(ex, "okx", symbol, scope="conditional")
time.sleep(0.2 * (attempt + 1))
raise RuntimeError(f"OKX 未接受止盈/止损条件单:{last_err}")
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"
# 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))
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_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,
direction: str,
amount: float,
stop_loss: float,
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(
ex, symbol, direction, stop_loss, take_profit,
pos_mode=pos_mode, price_type=pt, expiration=exp,
)
return
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(
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,
}