Files
2026-04-22 00:56:29 +08:00

729 lines
30 KiB
Python

from __future__ import annotations
import hashlib
import hmac
import re
from typing import Any
from urllib.parse import urlencode
from ..config import read_live_trading_config, read_network_settings
from ..http_client import HttpRequestError, cached_get_json, request_json
from ..utils import clamp, now_iso, num
from .base import ExchangeGateway
from .catalog import exchange_config
class BinanceGateway(ExchangeGateway):
exchange_id = "binance"
display_name = "Binance"
market_label = "USDT futures"
default_backdrop_symbol = "BTCUSDT"
public_base_url = exchange_config("binance")["defaultBaseUrl"]
symbol_pattern = re.compile(r"^[A-Z0-9]{2,}USDT$")
transfer_income_types = {
"TRANSFER",
"INTERNAL_TRANSFER",
"CROSS_COLLATERAL_TRANSFER",
"COIN_SWAP_DEPOSIT",
"COIN_SWAP_WITHDRAW",
"STRATEGY_UMFUTURES_TRANSFER",
}
def __init__(self) -> None:
self._server_time_offset_ms = 0
@staticmethod
def _exchange_margin_type(value: Any) -> str:
normalized = str(value or "cross").strip().lower()
if normalized in {"isolated", "isolate"}:
return "ISOLATED"
return "CROSSED"
@staticmethod
def _exchange_position_side(side: str) -> str:
return "SHORT" if str(side or "").strip().lower() == "short" else "LONG"
def _current_position_mode(self, config: dict[str, Any]) -> str:
payload = self._signed_request_json(config, "GET", "/fapi/v1/positionSide/dual")
dual_side = payload.get("dualSidePosition") if isinstance(payload, dict) else False
if str(dual_side).strip().lower() == "true" or dual_side is True:
return "hedge"
return "oneway"
def _resolved_position_mode(self, config: dict[str, Any]) -> str:
cached = str(config.get("_resolvedPositionMode") or "").strip().lower()
if cached in {"hedge", "oneway"}:
return cached
fallback = "hedge" if str(config.get("positionMode") or "").strip().lower() == "hedge" else "oneway"
config["_resolvedPositionMode"] = fallback
return fallback
def validate_symbol(self, symbol: str) -> bool:
normalized = self.normalize_symbol(symbol)
return bool(self.symbol_pattern.fullmatch(normalized))
def base_asset_from_symbol(self, symbol: str) -> str:
normalized = self.normalize_symbol(symbol)
return normalized[:-4] if normalized.endswith("USDT") else normalized
def _cache_policy_for_kline_interval(self, interval: str) -> tuple[int, int]:
interval = str(interval or "").lower()
if interval == "1m":
return 20, 60 * 60
if interval == "5m":
return 30, 2 * 60 * 60
if interval == "15m":
return 60, 3 * 60 * 60
if interval == "1h":
return 5 * 60, 12 * 60 * 60
if interval == "4h":
return 15 * 60, 48 * 60 * 60
return 60, 6 * 60 * 60
def _query(self, base_url: str, endpoint: str, params: dict[str, Any] | None = None) -> str:
params = params or {}
filtered = {key: value for key, value in params.items() if value not in (None, "", [])}
encoded = urlencode(filtered)
if not encoded:
return f"{base_url.rstrip('/')}{endpoint}"
return f"{base_url.rstrip('/')}{endpoint}?{encoded}"
def _public_get_json(
self,
endpoint: str,
params: dict[str, Any] | None = None,
*,
namespace: str,
ttl_seconds: int,
max_stale_seconds: int,
) -> Any:
network_settings = read_network_settings()
url = self._query(self.public_base_url, endpoint, params)
return cached_get_json(
url,
namespace=f"binance_{namespace}",
ttl_seconds=ttl_seconds,
max_stale_seconds=max_stale_seconds,
timeout_seconds=45,
network_settings=network_settings,
)
def _parse_klines(self, rows: list[list[Any]] | None) -> list[dict[str, Any]]:
parsed: list[dict[str, Any]] = []
for row in rows or []:
close_value = num(row[4]) if len(row) > 4 else None
if close_value is None:
continue
parsed.append(
{
"openTime": row[0],
"open": num(row[1]),
"high": num(row[2]),
"low": num(row[3]),
"close": close_value,
"volume": num(row[5]),
"closeTime": row[6],
"quoteVolume": num(row[7]),
}
)
return parsed
def fetch_all_tickers_24h(self) -> list[dict[str, Any]]:
payload = self._public_get_json(
"/fapi/v1/ticker/24hr",
namespace="ticker24h_all",
ttl_seconds=60,
max_stale_seconds=45 * 60,
)
return payload if isinstance(payload, list) else []
def fetch_all_premium_index(self) -> list[dict[str, Any]]:
payload = self._public_get_json(
"/fapi/v1/premiumIndex",
namespace="premium_all",
ttl_seconds=60,
max_stale_seconds=45 * 60,
)
return payload if isinstance(payload, list) else []
def fetch_ticker_24h(self, symbol: str) -> dict[str, Any]:
payload = self._public_get_json(
"/fapi/v1/ticker/24hr",
params={"symbol": self.normalize_symbol(symbol)},
namespace="ticker24h_symbol",
ttl_seconds=60,
max_stale_seconds=45 * 60,
)
return {
"symbol": self.normalize_symbol(symbol),
"lastPrice": num(payload.get("lastPrice")),
"priceChangePct": num(payload.get("priceChangePercent")),
"quoteVolume": num(payload.get("quoteVolume")),
"highPrice": num(payload.get("highPrice")),
"lowPrice": num(payload.get("lowPrice")),
"count": num(payload.get("count")),
}
def fetch_premium(self, symbol: str) -> dict[str, Any]:
payload = self._public_get_json(
"/fapi/v1/premiumIndex",
params={"symbol": self.normalize_symbol(symbol)},
namespace="premium_symbol",
ttl_seconds=60,
max_stale_seconds=45 * 60,
)
return {
"symbol": self.normalize_symbol(symbol),
"markPrice": num(payload.get("markPrice")),
"indexPrice": num(payload.get("indexPrice")),
"fundingPct": (num(payload.get("lastFundingRate")) or 0) * 100,
"nextFundingTime": payload.get("nextFundingTime"),
}
def fetch_klines(self, symbol: str, interval: str, limit: int) -> list[dict[str, Any]]:
ttl_seconds, max_stale_seconds = self._cache_policy_for_kline_interval(interval)
rows = self._public_get_json(
"/fapi/v1/klines",
params={"symbol": self.normalize_symbol(symbol), "interval": interval, "limit": limit},
namespace=f"klines_{interval}",
ttl_seconds=ttl_seconds,
max_stale_seconds=max_stale_seconds,
)
return self._parse_klines(rows if isinstance(rows, list) else [])
def resolved_base_url(self, config: dict[str, Any]) -> str:
return str(config.get("baseUrl") or self.public_base_url)
def live_execution_status(
self,
live_config: dict[str, Any] | None = None,
trading_settings: dict[str, Any] | None = None,
) -> dict[str, Any]:
live_config = live_config or read_live_trading_config()
issues = []
if not live_config.get("apiKey"):
issues.append("Live trading API key is missing.")
if not live_config.get("apiSecret"):
issues.append("Live trading API secret is missing.")
resolved_position_mode = "hedge" if str(live_config.get("positionMode") or "").strip().lower() == "hedge" else "oneway"
if not issues:
try:
resolved_position_mode = self._current_position_mode(live_config)
live_config["_resolvedPositionMode"] = resolved_position_mode
except Exception:
live_config["_resolvedPositionMode"] = resolved_position_mode
can_sync = not issues
can_execute = can_sync and live_config.get("enabled") and not live_config.get("dryRun")
return {
"configEnabled": live_config.get("enabled") is True,
"dryRun": live_config.get("dryRun") is True,
"armed": can_execute,
"canSync": can_sync,
"canExecute": can_execute,
"issues": issues,
"baseUrl": self.resolved_base_url(live_config),
"exchange": self.exchange_id,
"positionMode": resolved_position_mode,
}
def _signed_params(self, config: dict[str, Any], params: dict[str, Any] | None = None) -> str:
payload = dict(params or {})
payload["timestamp"] = str(int(__import__("time").time() * 1000) + int(self._server_time_offset_ms or 0))
payload["recvWindow"] = str(int(config.get("recvWindow") or 5000))
query_string = urlencode({key: value for key, value in payload.items() if value not in (None, "")})
signature = hmac.new(
str(config["apiSecret"]).encode("utf-8"),
query_string.encode("utf-8"),
hashlib.sha256,
).hexdigest()
return f"{query_string}&signature={signature}"
def _sync_server_time_offset(self, config: dict[str, Any]) -> None:
network_settings = read_network_settings()
base_url = self.resolved_base_url(config).rstrip("/")
payload = request_json(
"GET",
f"{base_url}/fapi/v1/time",
timeout_seconds=10,
network_settings=network_settings,
)
server_time = num(payload.get("serverTime")) if isinstance(payload, dict) else None
local_time = int(__import__("time").time() * 1000)
if server_time is not None:
self._server_time_offset_ms = int(server_time) - local_time
def _signed_request_json(
self,
config: dict[str, Any],
method: str,
endpoint: str,
params: dict[str, Any] | None = None,
) -> Any:
network_settings = read_network_settings()
base_url = self.resolved_base_url(config).rstrip("/")
headers_base = {"X-MBX-APIKEY": config["apiKey"]}
def send_once() -> Any:
query = self._signed_params(config, params)
url = f"{base_url}{endpoint}"
if method.upper() in {"GET", "DELETE"}:
return request_json(
method,
f"{url}?{query}",
headers=headers_base,
timeout_seconds=45,
network_settings=network_settings,
)
return request_json(
method,
url,
headers={
**headers_base,
"content-type": "application/x-www-form-urlencoded",
},
payload=query,
timeout_seconds=45,
network_settings=network_settings,
)
try:
return send_once()
except HttpRequestError as error:
if "-1021" in str(error):
self._sync_server_time_offset(config)
return send_once()
raise HttpRequestError(f"{error} [endpoint {method.upper()} {endpoint}]") from error
def _exchange_info(self, config: dict[str, Any]) -> dict[str, Any]:
network_settings = read_network_settings()
url = f"{self.resolved_base_url(config).rstrip('/')}/fapi/v1/exchangeInfo"
payload = cached_get_json(
url,
namespace="binance_live_exchange_info",
ttl_seconds=6 * 60 * 60,
max_stale_seconds=7 * 24 * 60 * 60,
timeout_seconds=45,
network_settings=network_settings,
)
return payload if isinstance(payload, dict) else {}
def _symbol_info(self, config: dict[str, Any], symbol: str) -> dict[str, Any]:
normalized = self.normalize_symbol(symbol)
info = self._exchange_info(config)
for item in info.get("symbols", []):
if self.normalize_symbol(item.get("symbol")) == normalized:
return item
raise ValueError(f"{self.display_name} symbol not found: {normalized}")
def _step_precision(self, step_size: str | float | int | None) -> int:
text = str(step_size or "1")
if "." not in text:
return 0
return len(text.rstrip("0").split(".")[1])
def _round_down_to_step(self, value: float, step_size: float, precision: int) -> float:
units = int(value / step_size)
return round(units * step_size, precision)
def _round_to_step(self, value: float, step_size: float, precision: int) -> float:
units = round(value / step_size)
return round(units * step_size, precision)
def normalize_quantity(
self,
config: dict[str, Any],
symbol: str,
*,
reference_price: float | None = None,
quantity: float | None = None,
notional_usd: float | None = None,
) -> float:
info = self._symbol_info(config, symbol)
filters = info.get("filters") or []
filter_row = next((item for item in filters if item.get("filterType") in {"MARKET_LOT_SIZE", "LOT_SIZE"}), None)
if not filter_row:
raise ValueError(f"Could not determine lot size for {self.normalize_symbol(symbol)}")
step_size = num(filter_row.get("stepSize")) or 1
min_qty = num(filter_row.get("minQty")) or 0
precision = self._step_precision(filter_row.get("stepSize"))
raw_quantity = quantity if quantity is not None else ((notional_usd or 0) / (reference_price or 0) if reference_price else 0)
normalized = self._round_down_to_step(max(0.0, float(raw_quantity or 0)), step_size, precision)
if normalized < min_qty:
raise ValueError(f"Quantity for {self.normalize_symbol(symbol)} is below exchange minimum.")
return normalized
def normalize_price(self, config: dict[str, Any], symbol: str, price: float) -> float:
info = self._symbol_info(config, symbol)
filters = info.get("filters") or []
filter_row = next((item for item in filters if item.get("filterType") == "PRICE_FILTER"), None)
if not filter_row:
return float(price)
tick_size = num(filter_row.get("tickSize")) or 0
if tick_size <= 0:
return float(price)
precision = self._step_precision(filter_row.get("tickSize"))
return self._round_to_step(float(price), tick_size, precision)
def _safe_signed_call(
self,
config: dict[str, Any],
method: str,
endpoint: str,
params: dict[str, Any] | None = None,
) -> Any:
try:
return self._signed_request_json(config, method, endpoint, params)
except Exception as error:
message = str(error)
if (
"No need to change margin type" in message
or "-4046" in message
or "-4067" in message
or "-4068" in message
):
return {"ignored": True}
raise
def _income_summary(self, config: dict[str, Any]) -> dict[str, Any]:
limit = 1000
now_ms = int(__import__("time").time() * 1000)
ninety_days_ms = 90 * 24 * 60 * 60 * 1000
rows = self._signed_request_json(
config,
"GET",
"/fapi/v1/income",
{
"startTime": now_ms - ninety_days_ms,
"endTime": now_ms,
"limit": limit,
},
)
entries = rows if isinstance(rows, list) else []
net_cashflow = 0.0
realized_pnl = 0.0
funding_fee = 0.0
commission = 0.0
other_income = 0.0
for row in entries:
if not isinstance(row, dict):
continue
income_type = str(row.get("incomeType") or "").strip().upper()
income_value = num(row.get("income")) or 0
if income_type in self.transfer_income_types:
net_cashflow += income_value
elif income_type == "REALIZED_PNL":
realized_pnl += income_value
elif income_type == "FUNDING_FEE":
funding_fee += income_value
elif income_type == "COMMISSION":
commission += income_value
else:
other_income += income_value
note = None
if len(entries) >= limit:
note = "Binance income history hit the 1000-row sync cap; very active accounts may need a deeper backfill."
return {
"netCashflowUsd": net_cashflow,
"incomeRealizedPnlUsd": realized_pnl,
"fundingFeeUsd": funding_fee,
"commissionUsd": commission,
"otherIncomeUsd": other_income,
"accountingUpdatedAt": now_iso(),
"accountingNote": note,
}
@staticmethod
def _session_start_ms(session_started_at: str | None) -> int | None:
if not session_started_at:
return None
try:
value = __import__("datetime").datetime.fromisoformat(str(session_started_at).replace("Z", "+00:00"))
except Exception:
return None
return int(value.timestamp() * 1000)
def _exchange_closed_trades(self, config: dict[str, Any], session_started_at: str | None) -> list[dict[str, Any]]:
session_start_ms = self._session_start_ms(session_started_at)
if session_start_ms is None:
return []
rows = self._signed_request_json(
config,
"GET",
"/fapi/v1/income",
{
"incomeType": "REALIZED_PNL",
"startTime": session_start_ms,
"endTime": int(__import__("time").time() * 1000),
"limit": 1000,
},
)
entries = rows if isinstance(rows, list) else []
closed_trades: list[dict[str, Any]] = []
for row in entries:
if not isinstance(row, dict):
continue
realized_pnl = num(row.get("income"))
if realized_pnl is None or abs(realized_pnl) <= 1e-12:
continue
symbol = self.normalize_symbol(row.get("symbol"))
closed_at_ms = num(row.get("time"))
closed_at = (
__import__("datetime").datetime.utcfromtimestamp(closed_at_ms / 1000).replace(microsecond=0).isoformat() + "Z"
if closed_at_ms is not None
else now_iso()
)
closed_trades.append(
{
"id": f"binance-realized-{row.get('tranId') or row.get('tradeId') or symbol}-{int(closed_at_ms or 0)}",
"symbol": symbol,
"baseAsset": self.base_asset_from_symbol(symbol),
"realizedPnl": realized_pnl,
"asset": str(row.get("asset") or "USDT").strip().upper() or "USDT",
"closedAt": closed_at,
"info": str(row.get("info") or "").strip(),
"source": "binance_realized_pnl",
}
)
closed_trades.sort(key=lambda item: str(item.get("closedAt") or ""))
return closed_trades
def apply_symbol_settings(self, config: dict[str, Any], symbol: str) -> None:
leverage = int(clamp(config.get("defaultLeverage"), 1, 125))
normalized = self.normalize_symbol(symbol)
self._safe_signed_call(config, "POST", "/fapi/v1/leverage", {"symbol": normalized, "leverage": leverage})
self._safe_signed_call(
config,
"POST",
"/fapi/v1/marginType",
{"symbol": normalized, "marginType": self._exchange_margin_type(config.get("marginType"))},
)
def fetch_account_snapshot(self, config: dict[str, Any], session_started_at: str | None = None) -> dict[str, Any]:
account = self._signed_request_json(config, "GET", "/fapi/v2/account")
positions = self._signed_request_json(config, "GET", "/fapi/v2/positionRisk")
open_orders_payload = self._signed_request_json(config, "GET", "/fapi/v1/openOrders")
open_algo_orders_payload = self._signed_request_json(config, "GET", "/fapi/v1/openAlgoOrders")
try:
config["_resolvedPositionMode"] = self._current_position_mode(config)
except Exception:
config["_resolvedPositionMode"] = self._resolved_position_mode(config)
accounting_summary = {
"netCashflowUsd": None,
"incomeRealizedPnlUsd": None,
"fundingFeeUsd": None,
"commissionUsd": None,
"otherIncomeUsd": None,
"accountingUpdatedAt": None,
"accountingNote": None,
}
try:
accounting_summary.update(self._income_summary(config))
except Exception as error:
accounting_summary["accountingNote"] = f"Binance income sync failed: {error}"
exchange_closed_trades: list[dict[str, Any]] = []
try:
exchange_closed_trades = self._exchange_closed_trades(config, session_started_at)
except Exception as error:
note = f"Binance closed-trade sync failed: {error}"
if accounting_summary.get("accountingNote"):
accounting_summary["accountingNote"] = f"{accounting_summary['accountingNote']} | {note}"
else:
accounting_summary["accountingNote"] = note
open_positions = []
for row in positions or []:
raw_amount = num(row.get("positionAmt"))
if raw_amount is None or abs(raw_amount) <= 1e-9:
continue
symbol = self.normalize_symbol(row.get("symbol"))
raw_position_side = str(row.get("positionSide") or "").strip().upper()
if raw_position_side == "LONG":
side = "long"
elif raw_position_side == "SHORT":
side = "short"
else:
side = "long" if raw_amount > 0 else "short"
quantity = abs(raw_amount)
entry_price = num(row.get("entryPrice")) or 0
mark_price = num(row.get("markPrice")) or entry_price
notional_usd = abs(num(row.get("notional")) or (mark_price * quantity))
open_positions.append(
{
"id": f"live-{symbol}-{side}",
"symbol": symbol,
"baseAsset": self.base_asset_from_symbol(symbol),
"side": side,
"quantity": quantity,
"initialQuantity": quantity,
"entryPrice": entry_price,
"notionalUsd": notional_usd,
"initialNotionalUsd": notional_usd,
"stopLoss": None,
"takeProfit": None,
"lastMarkPrice": mark_price,
"lastMarkTime": now_iso(),
"leverage": num(row.get("leverage")) or 1,
"status": "open",
"openedAt": None,
"updatedAt": now_iso(),
"source": self.exchange_id,
"entryReason": "synced_from_exchange",
"decisionId": None,
"exchangePositionSide": raw_position_side or ("BOTH" if self._resolved_position_mode(config) == "oneway" else self._exchange_position_side(side)),
}
)
open_orders = []
for row in open_orders_payload or []:
if not isinstance(row, dict):
continue
symbol = self.normalize_symbol(row.get("symbol"))
open_orders.append(
{
"id": f"std-{row.get('orderId') or row.get('clientOrderId') or symbol}",
"symbol": symbol,
"baseAsset": self.base_asset_from_symbol(symbol),
"side": str(row.get("side") or "").upper(),
"positionSide": str(row.get("positionSide") or "").upper(),
"type": str(row.get("type") or row.get("origType") or "").upper(),
"status": str(row.get("status") or "").upper(),
"price": num(row.get("price")),
"triggerPrice": num(row.get("stopPrice")),
"quantity": num(row.get("origQty")) or num(row.get("quantity")),
"reduceOnly": str(row.get("reduceOnly") or "").lower() == "true" or row.get("reduceOnly") is True,
"closePosition": str(row.get("closePosition") or "").lower() == "true" or row.get("closePosition") is True,
"workingType": str(row.get("workingType") or "").upper(),
"source": "binance_standard_order",
"updatedAt": now_iso(),
}
)
for row in open_algo_orders_payload or []:
if not isinstance(row, dict):
continue
symbol = self.normalize_symbol(row.get("symbol"))
open_orders.append(
{
"id": f"algo-{row.get('algoId') or row.get('clientAlgoId') or symbol}",
"symbol": symbol,
"baseAsset": self.base_asset_from_symbol(symbol),
"side": str(row.get("side") or "").upper(),
"positionSide": str(row.get("positionSide") or "").upper(),
"type": str(row.get("type") or row.get("algoType") or "").upper(),
"status": str(row.get("status") or "OPEN").upper(),
"price": num(row.get("price")),
"triggerPrice": num(row.get("triggerPrice")) or num(row.get("stopPrice")),
"quantity": num(row.get("quantity")) or num(row.get("origQty")),
"reduceOnly": str(row.get("reduceOnly") or "").lower() == "true" or row.get("reduceOnly") is True,
"closePosition": str(row.get("closePosition") or "").lower() == "true" or row.get("closePosition") is True,
"workingType": str(row.get("workingType") or "").upper(),
"source": "binance_algo_order",
"updatedAt": now_iso(),
}
)
return {
"walletBalanceUsd": num(account.get("totalWalletBalance")) or num(account.get("walletBalance")) or 0,
"equityUsd": num(account.get("totalMarginBalance")) or num(account.get("totalWalletBalance")) or 0,
"availableBalanceUsd": num(account.get("availableBalance")) or 0,
"unrealizedPnlUsd": num(account.get("totalUnrealizedProfit")) or 0,
"positionMode": self._resolved_position_mode(config),
**accounting_summary,
"exchangeClosedTrades": exchange_closed_trades,
"openPositions": open_positions,
"openOrders": open_orders,
"raw": {
"account": account,
"positions": positions,
"openOrders": open_orders_payload,
"openAlgoOrders": open_algo_orders_payload,
},
}
def cancel_all_open_orders(self, config: dict[str, Any], symbol: str) -> Any:
normalized = self.normalize_symbol(symbol)
standard_result = self._signed_request_json(config, "DELETE", "/fapi/v1/allOpenOrders", {"symbol": normalized})
algo_result = self._signed_request_json(config, "DELETE", "/fapi/v1/algoOpenOrders", {"symbol": normalized})
return {
"standard": standard_result,
"algo": algo_result,
}
def place_market_order(
self,
config: dict[str, Any],
*,
symbol: str,
side: str,
quantity: float,
reduce_only: bool = False,
) -> dict[str, Any]:
params = {
"symbol": self.normalize_symbol(symbol),
"side": str(side or "").upper(),
"type": "MARKET",
"quantity": quantity,
}
if self._resolved_position_mode(config) == "hedge":
if params["side"] == "BUY":
params["positionSide"] = "SHORT" if reduce_only else "LONG"
else:
params["positionSide"] = "LONG" if reduce_only else "SHORT"
elif reduce_only:
params["reduceOnly"] = "true"
return self._signed_request_json(config, "POST", "/fapi/v1/order", params)
def place_protection_orders(
self,
config: dict[str, Any],
*,
symbol: str,
position_side: str,
stop_loss: float | None,
take_profit: float | None,
) -> list[dict[str, Any]]:
created: list[dict[str, Any]] = []
if stop_loss is None and take_profit is None:
return created
normalized = self.normalize_symbol(symbol)
close_side = "SELL" if position_side == "long" else "BUY"
hedge_mode = self._resolved_position_mode(config) == "hedge"
exchange_position_side = self._exchange_position_side(position_side)
if stop_loss is not None:
stop_order = {
"algoType": "CONDITIONAL",
"symbol": normalized,
"side": close_side,
"type": "STOP_MARKET",
"triggerPrice": self.normalize_price(config, normalized, stop_loss),
"closePosition": "true",
"workingType": "MARK_PRICE",
}
if hedge_mode:
stop_order["positionSide"] = exchange_position_side
created.append(
self._signed_request_json(
config,
"POST",
"/fapi/v1/algoOrder",
stop_order,
)
)
if take_profit is not None:
take_profit_order = {
"algoType": "CONDITIONAL",
"symbol": normalized,
"side": close_side,
"type": "TAKE_PROFIT_MARKET",
"triggerPrice": self.normalize_price(config, normalized, take_profit),
"closePosition": "true",
"workingType": "MARK_PRICE",
}
if hedge_mode:
take_profit_order["positionSide"] = exchange_position_side
created.append(
self._signed_request_json(
config,
"POST",
"/fapi/v1/algoOrder",
take_profit_order,
)
)
return created