@@ -0,0 +1,573 @@
|
||||
"""
|
||||
Bitget (direct REST) client for USDT-margined perpetual orders.
|
||||
|
||||
Signing (Bitget):
|
||||
- ACCESS-SIGN = base64(hmac_sha256(secret, timestamp + method + request_path + body))
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import hmac
|
||||
import time
|
||||
from decimal import Decimal, ROUND_DOWN
|
||||
from typing import Any, Dict, Optional, Tuple
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from app.services.live_trading.base import BaseRestClient, LiveOrderResult, LiveTradingError
|
||||
from app.services.live_trading.symbols import to_bitget_um_symbol
|
||||
|
||||
|
||||
class BitgetMixClient(BaseRestClient):
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
api_key: str,
|
||||
secret_key: str,
|
||||
passphrase: str,
|
||||
base_url: str = "https://api.bitget.com",
|
||||
timeout_sec: float = 15.0,
|
||||
):
|
||||
super().__init__(base_url=base_url, timeout_sec=timeout_sec)
|
||||
self.api_key = (api_key or "").strip()
|
||||
self.secret_key = (secret_key or "").strip()
|
||||
self.passphrase = (passphrase or "").strip()
|
||||
if not self.api_key or not self.secret_key or not self.passphrase:
|
||||
raise LiveTradingError("Missing Bitget api_key/secret_key/passphrase")
|
||||
|
||||
# Best-effort cache for public contract metadata used to normalize order sizes.
|
||||
# Key: f"{product_type}:{symbol}" -> (fetched_at_ts, contract_dict)
|
||||
self._contract_cache: Dict[str, Tuple[float, Dict[str, Any]]] = {}
|
||||
self._contract_cache_ttl_sec = 300.0
|
||||
|
||||
# Best-effort cache for leverage settings to avoid spamming set-leverage on every tick.
|
||||
# Key: f"{product_type}:{symbol}:{margin_coin}:{margin_mode}:{hold_side}:{lever}" -> (fetched_at_ts, True)
|
||||
self._lev_cache: Dict[str, Tuple[float, bool]] = {}
|
||||
self._lev_cache_ttl_sec = 60.0
|
||||
|
||||
@staticmethod
|
||||
def _to_dec(x: Any) -> Decimal:
|
||||
try:
|
||||
return Decimal(str(x))
|
||||
except Exception:
|
||||
return Decimal("0")
|
||||
|
||||
@staticmethod
|
||||
def _dec_str(d: Decimal) -> str:
|
||||
try:
|
||||
return format(d, "f")
|
||||
except Exception:
|
||||
return str(d)
|
||||
|
||||
@staticmethod
|
||||
def _floor_to_step(value: Decimal, step: Decimal) -> Decimal:
|
||||
if step is None:
|
||||
return value
|
||||
if value <= 0:
|
||||
return Decimal("0")
|
||||
try:
|
||||
st = Decimal(step)
|
||||
except Exception:
|
||||
st = Decimal("0")
|
||||
if st <= 0:
|
||||
return value
|
||||
try:
|
||||
n = (value / st).to_integral_value(rounding=ROUND_DOWN)
|
||||
return n * st
|
||||
except Exception:
|
||||
return Decimal("0")
|
||||
|
||||
@staticmethod
|
||||
def _normalize_margin_mode(margin_mode: str) -> str:
|
||||
"""
|
||||
Normalize margin mode for Bitget mix orders.
|
||||
|
||||
Bitget expects:
|
||||
- crossed
|
||||
- isolated
|
||||
|
||||
Our system often uses:
|
||||
- cross
|
||||
- isolated
|
||||
"""
|
||||
m = str(margin_mode or "").strip().lower()
|
||||
if not m:
|
||||
return "crossed"
|
||||
if m in ("cross", "crossed"):
|
||||
return "crossed"
|
||||
if m in ("isolated", "iso"):
|
||||
return "isolated"
|
||||
return "crossed"
|
||||
|
||||
def _sign(self, ts_ms: str, method: str, path: str, body: str) -> str:
|
||||
prehash = f"{ts_ms}{method.upper()}{path}{body}"
|
||||
mac = hmac.new(self.secret_key.encode("utf-8"), prehash.encode("utf-8"), hashlib.sha256).digest()
|
||||
return base64.b64encode(mac).decode("utf-8")
|
||||
|
||||
def _headers(self, ts_ms: str, sign: str) -> Dict[str, str]:
|
||||
return {
|
||||
"ACCESS-KEY": self.api_key,
|
||||
"ACCESS-SIGN": sign,
|
||||
"ACCESS-TIMESTAMP": ts_ms,
|
||||
"ACCESS-PASSPHRASE": self.passphrase,
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
def _signed_request(
|
||||
self,
|
||||
method: str,
|
||||
path: str,
|
||||
*,
|
||||
json_body: Optional[Dict[str, Any]] = None,
|
||||
params: Optional[Dict[str, Any]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Bitget signature is computed over (timestamp + method + request_path + body).
|
||||
|
||||
- Use `data=<serialized_json>` to ensure the signed body matches the sent body.
|
||||
- For GET params, include query string into the signed request path.
|
||||
"""
|
||||
ts_ms = str(int(time.time() * 1000))
|
||||
body_str = self._json_dumps(json_body) if json_body is not None else ""
|
||||
|
||||
qs = ""
|
||||
if params:
|
||||
norm = {str(k): "" if v is None else str(v) for k, v in dict(params).items()}
|
||||
qs = urlencode(sorted(norm.items()), doseq=True)
|
||||
signed_path = f"{path}?{qs}" if qs else path
|
||||
|
||||
sign = self._sign(ts_ms, method, signed_path, body_str)
|
||||
code, data, text = self._request(
|
||||
method,
|
||||
path,
|
||||
params=params,
|
||||
data=body_str if body_str else None,
|
||||
headers=self._headers(ts_ms, sign),
|
||||
)
|
||||
if code >= 400:
|
||||
raise LiveTradingError(f"Bitget HTTP {code}: {text[:500]}")
|
||||
if isinstance(data, dict):
|
||||
# Bitget uses code == "00000" for success in many endpoints.
|
||||
c = str(data.get("code") or "")
|
||||
if c and c not in ("00000", "0"):
|
||||
raise LiveTradingError(f"Bitget error: {data}")
|
||||
return data if isinstance(data, dict) else {"raw": data}
|
||||
|
||||
def _public_request(self, method: str, path: str, *, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||
code, data, text = self._request(method, path, params=params, headers=None, json_body=None, data=None)
|
||||
if code >= 400:
|
||||
raise LiveTradingError(f"Bitget HTTP {code}: {text[:500]}")
|
||||
if isinstance(data, dict):
|
||||
c = str(data.get("code") or "")
|
||||
if c and c not in ("00000", "0"):
|
||||
raise LiveTradingError(f"Bitget error: {data}")
|
||||
return data if isinstance(data, dict) else {"raw": data}
|
||||
|
||||
def get_contract(self, *, symbol: str, product_type: str = "USDT-FUTURES") -> Dict[str, Any]:
|
||||
"""
|
||||
Fetch contract metadata (best-effort) from public endpoint.
|
||||
|
||||
Endpoint (Bitget v2 mix): GET /api/v2/mix/market/contracts
|
||||
Params: productType, symbol(optional)
|
||||
"""
|
||||
sym = to_bitget_um_symbol(symbol)
|
||||
pt = str(product_type or "USDT-FUTURES")
|
||||
if not sym:
|
||||
return {}
|
||||
key = f"{pt}:{sym}"
|
||||
now = time.time()
|
||||
cached = self._contract_cache.get(key)
|
||||
if cached:
|
||||
ts, obj = cached
|
||||
if obj and (now - float(ts or 0.0)) <= float(self._contract_cache_ttl_sec or 300.0):
|
||||
return obj
|
||||
|
||||
raw = self._public_request("GET", "/api/v2/mix/market/contracts", params={"productType": pt, "symbol": sym})
|
||||
data = raw.get("data") if isinstance(raw, dict) else None
|
||||
items = data if isinstance(data, list) else ([data] if isinstance(data, dict) else [])
|
||||
first: Dict[str, Any] = items[0] if isinstance(items, list) and items else {}
|
||||
if isinstance(first, dict) and first:
|
||||
self._contract_cache[key] = (now, first)
|
||||
return first if isinstance(first, dict) else {}
|
||||
|
||||
def _normalize_size(self, *, symbol: str, product_type: str, base_size: float) -> Decimal:
|
||||
"""
|
||||
Normalize Bitget mix order size.
|
||||
|
||||
This system computes `amount` as base-asset quantity (e.g. BTC amount).
|
||||
Bitget mix `size` is typically in contracts; convert using contractSize if available,
|
||||
then align to size step / min trade number (best-effort).
|
||||
"""
|
||||
req_base = self._to_dec(base_size)
|
||||
if req_base <= 0:
|
||||
return Decimal("0")
|
||||
|
||||
contract: Dict[str, Any] = {}
|
||||
try:
|
||||
contract = self.get_contract(symbol=symbol, product_type=product_type) or {}
|
||||
except Exception:
|
||||
contract = {}
|
||||
|
||||
# Convert base qty -> contracts if contractSize is provided.
|
||||
ct = self._to_dec(contract.get("contractSize") or contract.get("contractSz") or contract.get("ctVal") or "0")
|
||||
qty = req_base
|
||||
if ct > 0:
|
||||
qty = req_base / ct
|
||||
|
||||
# Determine step size.
|
||||
step = self._to_dec(contract.get("sizeMultiplier") or contract.get("sizeStep") or contract.get("lotSize") or "0")
|
||||
if step <= 0:
|
||||
sp = contract.get("sizePlace")
|
||||
try:
|
||||
places = int(sp) if sp is not None else 0
|
||||
except Exception:
|
||||
places = 0
|
||||
if places >= 0 and places <= 18:
|
||||
step = Decimal("1") / (Decimal("10") ** Decimal(str(places)))
|
||||
|
||||
if step > 0:
|
||||
qty = self._floor_to_step(qty, step)
|
||||
|
||||
# Enforce min trade number if present.
|
||||
mn = self._to_dec(contract.get("minTradeNum") or contract.get("minSize") or contract.get("minQty") or "0")
|
||||
if mn > 0 and qty < mn:
|
||||
return Decimal("0")
|
||||
return qty
|
||||
|
||||
def ping(self) -> bool:
|
||||
code, data, _ = self._request("GET", "/api/v2/public/time")
|
||||
return code == 200 and isinstance(data, dict)
|
||||
|
||||
def get_accounts(self, *, product_type: str = "USDT-FUTURES") -> Dict[str, Any]:
|
||||
"""
|
||||
Private endpoint to validate credentials (best-effort).
|
||||
"""
|
||||
return self._signed_request("GET", "/api/v2/mix/account/accounts", params={"productType": str(product_type or "USDT-FUTURES")})
|
||||
|
||||
def get_positions(self, *, product_type: str = "USDT-FUTURES") -> Dict[str, Any]:
|
||||
"""
|
||||
Get all positions (best-effort).
|
||||
|
||||
Endpoint: GET /api/v2/mix/position/all-position
|
||||
"""
|
||||
return self._signed_request("GET", "/api/v2/mix/position/all-position", params={"productType": str(product_type or "USDT-FUTURES")})
|
||||
|
||||
def set_leverage(
|
||||
self,
|
||||
*,
|
||||
symbol: str,
|
||||
leverage: float,
|
||||
margin_coin: str = "USDT",
|
||||
product_type: str = "USDT-FUTURES",
|
||||
margin_mode: str = "crossed",
|
||||
hold_side: str = "",
|
||||
) -> bool:
|
||||
"""
|
||||
Best-effort set leverage for Bitget mix.
|
||||
|
||||
NOTE: Bitget requires leverage configured via a private endpoint; order placement may otherwise use defaults.
|
||||
Endpoint (v2 mix): POST /api/v2/mix/account/set-leverage (best-effort).
|
||||
"""
|
||||
sym = to_bitget_um_symbol(symbol)
|
||||
pt = str(product_type or "USDT-FUTURES")
|
||||
mc = str(margin_coin or "USDT")
|
||||
mm = self._normalize_margin_mode(margin_mode)
|
||||
hs = str(hold_side or "").strip().lower()
|
||||
try:
|
||||
lv = int(float(leverage or 0))
|
||||
except Exception:
|
||||
lv = 0
|
||||
if not sym or lv <= 0:
|
||||
return False
|
||||
|
||||
cache_key = f"{pt}:{sym}:{mc}:{mm}:{hs}:{lv}"
|
||||
now = time.time()
|
||||
cached = self._lev_cache.get(cache_key)
|
||||
if cached:
|
||||
ts, ok = cached
|
||||
if ok and (now - float(ts or 0.0)) <= float(self._lev_cache_ttl_sec or 60.0):
|
||||
return True
|
||||
|
||||
body: Dict[str, Any] = {
|
||||
"symbol": sym,
|
||||
"productType": pt,
|
||||
"marginCoin": mc,
|
||||
"marginMode": mm,
|
||||
"leverage": str(lv),
|
||||
}
|
||||
# Some Bitget accounts require holdSide for hedge mode; keep best-effort.
|
||||
if hs in ("long", "short"):
|
||||
body["holdSide"] = hs
|
||||
|
||||
try:
|
||||
resp = self._signed_request("POST", "/api/v2/mix/account/set-leverage", json_body=body)
|
||||
ok = isinstance(resp, dict) and str(resp.get("code") or "") in ("00000", "0", "")
|
||||
if ok:
|
||||
self._lev_cache[cache_key] = (now, True)
|
||||
return bool(ok)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def place_market_order(
|
||||
self,
|
||||
*,
|
||||
symbol: str,
|
||||
side: str,
|
||||
size: float,
|
||||
margin_coin: str = "USDT",
|
||||
product_type: str = "USDT-FUTURES",
|
||||
margin_mode: str = "crossed",
|
||||
reduce_only: bool = False,
|
||||
client_order_id: Optional[str] = None,
|
||||
) -> LiveOrderResult:
|
||||
sym = to_bitget_um_symbol(symbol)
|
||||
sd = (side or "").lower()
|
||||
if sd not in ("buy", "sell"):
|
||||
raise LiveTradingError(f"Invalid side: {side}")
|
||||
req = float(size or 0.0)
|
||||
sz_dec = self._normalize_size(symbol=symbol, product_type=product_type, base_size=req)
|
||||
if float(sz_dec or 0) <= 0:
|
||||
raise LiveTradingError(f"Invalid size (below step/min): requested={req}")
|
||||
|
||||
body: Dict[str, Any] = {
|
||||
"symbol": sym,
|
||||
"productType": str(product_type or "USDT-FUTURES"),
|
||||
"marginCoin": str(margin_coin or "USDT"),
|
||||
"marginMode": self._normalize_margin_mode(margin_mode),
|
||||
"side": sd,
|
||||
"orderType": "market",
|
||||
"size": self._dec_str(sz_dec),
|
||||
}
|
||||
if reduce_only:
|
||||
body["reduceOnly"] = "YES"
|
||||
if client_order_id:
|
||||
body["clientOid"] = str(client_order_id)
|
||||
|
||||
raw = self._signed_request("POST", "/api/v2/mix/order/place-order", json_body=body)
|
||||
data = raw.get("data") if isinstance(raw, dict) else None
|
||||
exchange_order_id = ""
|
||||
if isinstance(data, dict):
|
||||
exchange_order_id = str(data.get("orderId") or data.get("clientOid") or "")
|
||||
|
||||
return LiveOrderResult(
|
||||
exchange_id="bitget",
|
||||
exchange_order_id=exchange_order_id,
|
||||
filled=0.0,
|
||||
avg_price=0.0,
|
||||
raw=raw,
|
||||
)
|
||||
|
||||
def place_limit_order(
|
||||
self,
|
||||
*,
|
||||
symbol: str,
|
||||
side: str,
|
||||
size: float,
|
||||
price: float,
|
||||
margin_coin: str = "USDT",
|
||||
product_type: str = "USDT-FUTURES",
|
||||
margin_mode: str = "crossed",
|
||||
reduce_only: bool = False,
|
||||
post_only: bool = False,
|
||||
client_order_id: Optional[str] = None,
|
||||
) -> LiveOrderResult:
|
||||
sym = to_bitget_um_symbol(symbol)
|
||||
sd = (side or "").lower()
|
||||
if sd not in ("buy", "sell"):
|
||||
raise LiveTradingError(f"Invalid side: {side}")
|
||||
req = float(size or 0.0)
|
||||
px = float(price or 0.0)
|
||||
if req <= 0 or px <= 0:
|
||||
raise LiveTradingError("Invalid size/price")
|
||||
sz_dec = self._normalize_size(symbol=symbol, product_type=product_type, base_size=req)
|
||||
if float(sz_dec or 0) <= 0:
|
||||
raise LiveTradingError(f"Invalid size (below step/min): requested={req}")
|
||||
|
||||
body: Dict[str, Any] = {
|
||||
"symbol": sym,
|
||||
"productType": str(product_type or "USDT-FUTURES"),
|
||||
"marginCoin": str(margin_coin or "USDT"),
|
||||
"marginMode": self._normalize_margin_mode(margin_mode),
|
||||
"side": sd,
|
||||
"orderType": "limit",
|
||||
"price": str(px),
|
||||
"size": self._dec_str(sz_dec),
|
||||
}
|
||||
# Force maker behavior when requested (avoid taker fills).
|
||||
if post_only:
|
||||
body["force"] = "post_only"
|
||||
else:
|
||||
body["force"] = "gtc"
|
||||
if reduce_only:
|
||||
body["reduceOnly"] = "YES"
|
||||
if client_order_id:
|
||||
body["clientOid"] = str(client_order_id)
|
||||
raw = self._signed_request("POST", "/api/v2/mix/order/place-order", json_body=body)
|
||||
data = raw.get("data") if isinstance(raw, dict) else None
|
||||
exchange_order_id = str(data.get("orderId") or data.get("clientOid") or "") if isinstance(data, dict) else ""
|
||||
return LiveOrderResult(exchange_id="bitget", exchange_order_id=exchange_order_id, filled=0.0, avg_price=0.0, raw=raw)
|
||||
|
||||
def cancel_order(self, *, symbol: str, product_type: str, margin_coin: str = "USDT", order_id: str = "", client_oid: str = "") -> Dict[str, Any]:
|
||||
body: Dict[str, Any] = {
|
||||
"symbol": to_bitget_um_symbol(symbol),
|
||||
"productType": str(product_type or "USDT-FUTURES"),
|
||||
"marginCoin": str(margin_coin or "USDT"),
|
||||
}
|
||||
if order_id:
|
||||
body["orderId"] = str(order_id)
|
||||
elif client_oid:
|
||||
body["clientOid"] = str(client_oid)
|
||||
else:
|
||||
raise LiveTradingError("Bitget cancel_order requires order_id or client_oid")
|
||||
return self._signed_request("POST", "/api/v2/mix/order/cancel-order", json_body=body)
|
||||
|
||||
def get_order_detail(
|
||||
self,
|
||||
*,
|
||||
symbol: str,
|
||||
product_type: str,
|
||||
order_id: str = "",
|
||||
client_oid: str = "",
|
||||
) -> Dict[str, Any]:
|
||||
params: Dict[str, Any] = {
|
||||
"symbol": to_bitget_um_symbol(symbol),
|
||||
"productType": str(product_type or "USDT-FUTURES"),
|
||||
}
|
||||
if order_id:
|
||||
params["orderId"] = str(order_id)
|
||||
elif client_oid:
|
||||
params["clientOid"] = str(client_oid)
|
||||
else:
|
||||
raise LiveTradingError("Bitget get_order_detail requires order_id or client_oid")
|
||||
return self._signed_request("GET", "/api/v2/mix/order/detail", params=params)
|
||||
|
||||
def get_order_fills(
|
||||
self,
|
||||
*,
|
||||
symbol: str,
|
||||
product_type: str,
|
||||
order_id: str,
|
||||
) -> Dict[str, Any]:
|
||||
params: Dict[str, Any] = {
|
||||
"orderId": str(order_id),
|
||||
"productType": str(product_type or "USDT-FUTURES"),
|
||||
"symbol": to_bitget_um_symbol(symbol),
|
||||
}
|
||||
return self._signed_request("GET", "/api/v2/mix/order/fills", params=params)
|
||||
|
||||
def wait_for_fill(
|
||||
self,
|
||||
*,
|
||||
symbol: str,
|
||||
product_type: str = "USDT-FUTURES",
|
||||
order_id: str,
|
||||
client_oid: str = "",
|
||||
max_wait_sec: float = 3.0,
|
||||
poll_interval_sec: float = 0.5,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Poll order fills/detail to obtain (best-effort) executed size and average price.
|
||||
|
||||
Returns:
|
||||
{
|
||||
"filled": float,
|
||||
"avg_price": float,
|
||||
"fee": float,
|
||||
"fee_ccy": str,
|
||||
"state": str,
|
||||
"detail": {...},
|
||||
"fills": {...}
|
||||
}
|
||||
"""
|
||||
end_ts = time.time() + float(max_wait_sec or 0.0)
|
||||
last_detail: Dict[str, Any] = {}
|
||||
last_fills: Dict[str, Any] = {}
|
||||
state = ""
|
||||
|
||||
# For robust parsing: contractSize helps converting contracts->base if needed.
|
||||
ct = Decimal("0")
|
||||
try:
|
||||
contract = self.get_contract(symbol=symbol, product_type=product_type) or {}
|
||||
ct = self._to_dec(contract.get("contractSize") or contract.get("contractSz") or contract.get("ctVal") or "0")
|
||||
except Exception:
|
||||
ct = Decimal("0")
|
||||
|
||||
while True:
|
||||
# Prefer fills endpoint to calculate accurate weighted average.
|
||||
try:
|
||||
last_fills = self.get_order_fills(symbol=symbol, product_type=product_type, order_id=str(order_id))
|
||||
data = last_fills.get("data") if isinstance(last_fills, dict) else None
|
||||
fill_list = []
|
||||
if isinstance(data, dict):
|
||||
fill_list = data.get("fillList") or []
|
||||
total_base = Decimal("0")
|
||||
total_quote = Decimal("0")
|
||||
total_fee = Decimal("0")
|
||||
fee_ccy = ""
|
||||
if isinstance(fill_list, list):
|
||||
for f in fill_list:
|
||||
try:
|
||||
# Bitget fills may provide either baseVolume or size.
|
||||
# Our system standardizes on base-asset quantity.
|
||||
sz_base = self._to_dec(f.get("baseVolume") or "0")
|
||||
if sz_base <= 0:
|
||||
sz_contracts = self._to_dec(f.get("size") or f.get("fillSize") or "0")
|
||||
if sz_contracts > 0 and ct > 0:
|
||||
sz_base = sz_contracts * ct
|
||||
px = self._to_dec(f.get("fillPrice") or f.get("price") or "0")
|
||||
|
||||
fee_v = f.get("fee")
|
||||
if fee_v is None:
|
||||
fee_v = f.get("fillFee")
|
||||
fee = self._to_dec(fee_v or "0")
|
||||
ccy = str(f.get("feeCoin") or f.get("feeCcy") or f.get("fillFeeCoin") or "").strip()
|
||||
|
||||
if sz_base > 0 and px > 0:
|
||||
total_base += sz_base
|
||||
total_quote += sz_base * px
|
||||
if fee != 0:
|
||||
# Fees may be negative; store absolute cost.
|
||||
total_fee += abs(fee)
|
||||
if (not fee_ccy) and ccy:
|
||||
fee_ccy = ccy
|
||||
except Exception:
|
||||
continue
|
||||
if total_base > 0 and total_quote > 0:
|
||||
return {
|
||||
"filled": float(total_base),
|
||||
"avg_price": float(total_quote / total_base),
|
||||
"fee": float(total_fee),
|
||||
"fee_ccy": str(fee_ccy or ""),
|
||||
"state": state,
|
||||
"detail": last_detail,
|
||||
"fills": last_fills,
|
||||
}
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Fall back to order detail (state + sometimes avg/filled fields).
|
||||
try:
|
||||
last_detail = self.get_order_detail(
|
||||
symbol=symbol,
|
||||
product_type=product_type,
|
||||
order_id=str(order_id or ""),
|
||||
client_oid=str(client_oid or ""),
|
||||
)
|
||||
d = last_detail.get("data") if isinstance(last_detail, dict) else None
|
||||
if isinstance(d, dict):
|
||||
state = str(d.get("state") or d.get("status") or "")
|
||||
avg = float(d.get("priceAvg") or d.get("fillPrice") or 0.0) if (d.get("priceAvg") or d.get("fillPrice")) else 0.0
|
||||
filled = float(d.get("baseVolume") or d.get("filledQty") or 0.0) if (d.get("baseVolume") or d.get("filledQty")) else 0.0
|
||||
if filled > 0 and avg > 0:
|
||||
return {"filled": filled, "avg_price": avg, "fee": 0.0, "fee_ccy": "", "state": state, "detail": last_detail, "fills": last_fills}
|
||||
if state in ("filled", "canceled", "cancelled"):
|
||||
return {"filled": filled, "avg_price": avg, "fee": 0.0, "fee_ccy": "", "state": state, "detail": last_detail, "fills": last_fills}
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if time.time() >= end_ts:
|
||||
return {"filled": 0.0, "avg_price": 0.0, "fee": 0.0, "fee_ccy": "", "state": state, "detail": last_detail, "fills": last_fills}
|
||||
time.sleep(float(poll_interval_sec or 0.5))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user