@@ -0,0 +1,508 @@
|
||||
"""
|
||||
KuCoin (direct REST) client (spot).
|
||||
|
||||
Signing (v2):
|
||||
- KC-API-SIGN = base64(hmac_sha256(secret, timestamp + method + requestPathWithQuery + body))
|
||||
- KC-API-PASSPHRASE = base64(hmac_sha256(secret, passphrase))
|
||||
- KC-API-KEY-VERSION: 2
|
||||
"""
|
||||
|
||||
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_kucoin_symbol
|
||||
|
||||
|
||||
class KucoinSpotClient(BaseRestClient):
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
api_key: str,
|
||||
secret_key: str,
|
||||
passphrase: str,
|
||||
base_url: str = "https://api.kucoin.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 KuCoin api_key/secret_key/passphrase")
|
||||
|
||||
def _b64_hmac_sha256(self, key: str, msg: str) -> str:
|
||||
mac = hmac.new(key.encode("utf-8"), msg.encode("utf-8"), hashlib.sha256).digest()
|
||||
return base64.b64encode(mac).decode("utf-8")
|
||||
|
||||
def _headers(self, ts_ms: str, sign: str) -> Dict[str, str]:
|
||||
# passphrase must be signed (v2)
|
||||
p = self._b64_hmac_sha256(self.secret_key, self.passphrase)
|
||||
return {
|
||||
"KC-API-KEY": self.api_key,
|
||||
"KC-API-SIGN": sign,
|
||||
"KC-API-TIMESTAMP": ts_ms,
|
||||
"KC-API-PASSPHRASE": p,
|
||||
"KC-API-KEY-VERSION": "2",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
def _signed_request(self, method: str, path: str, *, params: Optional[Dict[str, Any]] = None, json_body: Optional[Dict[str, Any]] = None) -> Any:
|
||||
m = str(method or "GET").upper()
|
||||
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
|
||||
prehash = f"{ts_ms}{m}{signed_path}{body_str}"
|
||||
sign = self._b64_hmac_sha256(self.secret_key, prehash)
|
||||
code, data, text = self._request(m, path, params=params, data=body_str if body_str else None, headers=self._headers(ts_ms, sign))
|
||||
if code >= 400:
|
||||
raise LiveTradingError(f"KuCoin HTTP {code}: {text[:500]}")
|
||||
return data
|
||||
|
||||
def _public_request(self, method: str, path: str, *, params: Optional[Dict[str, Any]] = None) -> Any:
|
||||
code, data, text = self._request(method, path, params=params, headers=None, json_body=None, data=None)
|
||||
if code >= 400:
|
||||
raise LiveTradingError(f"KuCoin HTTP {code}: {text[:500]}")
|
||||
return data
|
||||
|
||||
def ping(self) -> bool:
|
||||
try:
|
||||
d = self._public_request("GET", "/api/v1/timestamp")
|
||||
return isinstance(d, dict) and str(d.get("code") or "") in ("200000", "0", "")
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def get_accounts(self) -> Any:
|
||||
return self._signed_request("GET", "/api/v1/accounts")
|
||||
|
||||
def place_limit_order(self, *, symbol: str, side: str, size: float, price: float, client_order_id: Optional[str] = None) -> LiveOrderResult:
|
||||
sd = (side or "").strip().lower()
|
||||
if sd not in ("buy", "sell"):
|
||||
raise LiveTradingError(f"Invalid side: {side}")
|
||||
qty = float(size or 0.0)
|
||||
px = float(price or 0.0)
|
||||
if qty <= 0 or px <= 0:
|
||||
raise LiveTradingError("Invalid size/price")
|
||||
body: Dict[str, Any] = {
|
||||
"clientOid": str(client_order_id or str(int(time.time() * 1000))),
|
||||
"side": sd,
|
||||
"symbol": to_kucoin_symbol(symbol),
|
||||
"type": "limit",
|
||||
"price": str(px),
|
||||
"size": str(qty),
|
||||
"timeInForce": "GTC",
|
||||
}
|
||||
raw = self._signed_request("POST", "/api/v1/orders", json_body=body)
|
||||
oid = ""
|
||||
if isinstance(raw, dict):
|
||||
d = raw.get("data")
|
||||
if isinstance(d, dict):
|
||||
oid = str(d.get("orderId") or "")
|
||||
elif isinstance(d, str):
|
||||
oid = str(d)
|
||||
return LiveOrderResult(exchange_id="kucoin", exchange_order_id=oid, filled=0.0, avg_price=0.0, raw=raw if isinstance(raw, dict) else {"raw": raw})
|
||||
|
||||
def place_market_order(
|
||||
self,
|
||||
*,
|
||||
symbol: str,
|
||||
side: str,
|
||||
size: float,
|
||||
client_order_id: Optional[str] = None,
|
||||
quote_size: bool = False,
|
||||
) -> LiveOrderResult:
|
||||
"""
|
||||
KuCoin market order:
|
||||
- sell: use size (base quantity)
|
||||
- buy: typically use funds (quote quantity). Set quote_size=True to treat `size` as funds.
|
||||
"""
|
||||
sd = (side or "").strip().lower()
|
||||
if sd not in ("buy", "sell"):
|
||||
raise LiveTradingError(f"Invalid side: {side}")
|
||||
qty = float(size or 0.0)
|
||||
if qty <= 0:
|
||||
raise LiveTradingError("Invalid size")
|
||||
body: Dict[str, Any] = {
|
||||
"clientOid": str(client_order_id or str(int(time.time() * 1000))),
|
||||
"side": sd,
|
||||
"symbol": to_kucoin_symbol(symbol),
|
||||
"type": "market",
|
||||
}
|
||||
if sd == "buy" and quote_size:
|
||||
body["funds"] = str(qty)
|
||||
else:
|
||||
body["size"] = str(qty)
|
||||
raw = self._signed_request("POST", "/api/v1/orders", json_body=body)
|
||||
oid = ""
|
||||
if isinstance(raw, dict):
|
||||
d = raw.get("data")
|
||||
if isinstance(d, dict):
|
||||
oid = str(d.get("orderId") or "")
|
||||
elif isinstance(d, str):
|
||||
oid = str(d)
|
||||
return LiveOrderResult(exchange_id="kucoin", exchange_order_id=oid, filled=0.0, avg_price=0.0, raw=raw if isinstance(raw, dict) else {"raw": raw})
|
||||
|
||||
def cancel_order(self, *, order_id: str = "", client_order_id: str = "") -> Any:
|
||||
if order_id:
|
||||
return self._signed_request("DELETE", f"/api/v1/orders/{str(order_id)}")
|
||||
if client_order_id:
|
||||
return self._signed_request("DELETE", f"/api/v1/order/client-order/{str(client_order_id)}")
|
||||
raise LiveTradingError("KuCoin cancel_order requires order_id or client_order_id")
|
||||
|
||||
def get_order(self, *, order_id: str = "", client_order_id: str = "") -> Any:
|
||||
if order_id:
|
||||
return self._signed_request("GET", f"/api/v1/orders/{str(order_id)}")
|
||||
if client_order_id:
|
||||
return self._signed_request("GET", f"/api/v1/order/client-order/{str(client_order_id)}")
|
||||
raise LiveTradingError("KuCoin get_order requires order_id or client_order_id")
|
||||
|
||||
def get_fills(self, *, order_id: str) -> Any:
|
||||
return self._signed_request("GET", "/api/v1/fills", params={"orderId": str(order_id)})
|
||||
|
||||
def wait_for_fill(self, *, order_id: str, max_wait_sec: float = 10.0, poll_interval_sec: float = 0.5) -> Dict[str, Any]:
|
||||
end_ts = time.time() + float(max_wait_sec or 0.0)
|
||||
last: Dict[str, Any] = {}
|
||||
while True:
|
||||
try:
|
||||
resp = self.get_order(order_id=str(order_id))
|
||||
last = resp if isinstance(resp, dict) else {"raw": resp}
|
||||
except Exception:
|
||||
last = last or {}
|
||||
data = last.get("data") if isinstance(last, dict) else None
|
||||
od = data if isinstance(data, dict) else {}
|
||||
status = str(od.get("isActive") if od else "")
|
||||
filled = 0.0
|
||||
avg_price = 0.0
|
||||
fee = 0.0
|
||||
fee_ccy = ""
|
||||
try:
|
||||
filled = float(od.get("dealSize") or 0.0)
|
||||
except Exception:
|
||||
filled = 0.0
|
||||
try:
|
||||
funds = float(od.get("dealFunds") or 0.0)
|
||||
if filled > 0 and funds > 0:
|
||||
avg_price = funds / filled
|
||||
except Exception:
|
||||
avg_price = 0.0
|
||||
try:
|
||||
fee = abs(float(od.get("fee") or 0.0))
|
||||
except Exception:
|
||||
fee = 0.0
|
||||
fee_ccy = str(od.get("feeCurrency") or "").strip()
|
||||
if filled > 0 and avg_price > 0:
|
||||
return {"filled": filled, "avg_price": avg_price, "fee": fee, "fee_ccy": fee_ccy, "status": status, "order": last}
|
||||
# If order is inactive, consider it terminal
|
||||
try:
|
||||
is_active = bool(od.get("isActive"))
|
||||
except Exception:
|
||||
is_active = False
|
||||
if not is_active:
|
||||
return {"filled": filled, "avg_price": avg_price, "fee": fee, "fee_ccy": fee_ccy, "status": status, "order": last}
|
||||
if time.time() >= end_ts:
|
||||
return {"filled": filled, "avg_price": avg_price, "fee": fee, "fee_ccy": fee_ccy, "status": status, "order": last}
|
||||
time.sleep(float(poll_interval_sec or 0.5))
|
||||
|
||||
|
||||
class KucoinFuturesClient(BaseRestClient):
|
||||
"""
|
||||
KuCoin Futures (USDT perpetual) direct REST client.
|
||||
|
||||
Notes:
|
||||
- Base URL typically: https://api-futures.kucoin.com
|
||||
- Auth headers/signing are the same KC-API-* style as spot (v2 passphrase signing),
|
||||
but endpoints and symbol formats differ.
|
||||
- Futures order size is typically in contracts; we convert from "base qty" best-effort.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
api_key: str,
|
||||
secret_key: str,
|
||||
passphrase: str,
|
||||
base_url: str = "https://api-futures.kucoin.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 KuCoin Futures api_key/secret_key/passphrase")
|
||||
|
||||
# Best-effort contract cache: symbol -> (ts, contract_dict)
|
||||
self._contract_cache: Dict[str, Tuple[float, Dict[str, Any]]] = {}
|
||||
self._contract_cache_ttl_sec = 300.0
|
||||
|
||||
def _b64_hmac_sha256(self, key: str, msg: str) -> str:
|
||||
mac = hmac.new(key.encode("utf-8"), msg.encode("utf-8"), hashlib.sha256).digest()
|
||||
return base64.b64encode(mac).decode("utf-8")
|
||||
|
||||
def _headers(self, ts_ms: str, sign: str) -> Dict[str, str]:
|
||||
p = self._b64_hmac_sha256(self.secret_key, self.passphrase)
|
||||
return {
|
||||
"KC-API-KEY": self.api_key,
|
||||
"KC-API-SIGN": sign,
|
||||
"KC-API-TIMESTAMP": ts_ms,
|
||||
"KC-API-PASSPHRASE": p,
|
||||
"KC-API-KEY-VERSION": "2",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
def _signed_request(self, method: str, path: str, *, params: Optional[Dict[str, Any]] = None, json_body: Optional[Dict[str, Any]] = None) -> Any:
|
||||
m = str(method or "GET").upper()
|
||||
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
|
||||
prehash = f"{ts_ms}{m}{signed_path}{body_str}"
|
||||
sign = self._b64_hmac_sha256(self.secret_key, prehash)
|
||||
code, data, text = self._request(m, path, params=params, data=body_str if body_str else None, headers=self._headers(ts_ms, sign))
|
||||
if code >= 400:
|
||||
raise LiveTradingError(f"KuCoinFutures HTTP {code}: {text[:500]}")
|
||||
return data
|
||||
|
||||
def _public_request(self, method: str, path: str, *, params: Optional[Dict[str, Any]] = None) -> Any:
|
||||
code, data, text = self._request(method, path, params=params, headers=None, json_body=None, data=None)
|
||||
if code >= 400:
|
||||
raise LiveTradingError(f"KuCoinFutures HTTP {code}: {text[:500]}")
|
||||
return data
|
||||
|
||||
def ping(self) -> bool:
|
||||
try:
|
||||
d = self._public_request("GET", "/api/v1/timestamp")
|
||||
return isinstance(d, dict) and str(d.get("code") or "") in ("200000", "0", "")
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def get_contract(self, *, symbol: str) -> Dict[str, Any]:
|
||||
from app.services.live_trading.symbols import to_kucoin_futures_symbol
|
||||
|
||||
sym = to_kucoin_futures_symbol(symbol)
|
||||
if not sym:
|
||||
return {}
|
||||
now = time.time()
|
||||
cached = self._contract_cache.get(sym)
|
||||
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
|
||||
# KuCoin futures active contracts list
|
||||
raw = self._public_request("GET", "/api/v1/contracts/active")
|
||||
data = (raw.get("data") if isinstance(raw, dict) else None) or []
|
||||
found: Dict[str, Any] = {}
|
||||
if isinstance(data, list):
|
||||
for it in data:
|
||||
if not isinstance(it, dict):
|
||||
continue
|
||||
if str(it.get("symbol") or "").upper() == sym.upper():
|
||||
found = it
|
||||
break
|
||||
if found:
|
||||
self._contract_cache[sym] = (now, found)
|
||||
return found
|
||||
|
||||
def _base_to_contracts(self, *, symbol: str, base_size: float) -> int:
|
||||
"""
|
||||
Convert base-asset qty -> contracts best-effort using multiplier.
|
||||
"""
|
||||
from app.services.live_trading.symbols import to_kucoin_futures_symbol
|
||||
|
||||
req = Decimal(str(base_size or 0.0))
|
||||
if req <= 0:
|
||||
return 0
|
||||
sym = to_kucoin_futures_symbol(symbol)
|
||||
meta: Dict[str, Any] = {}
|
||||
try:
|
||||
meta = self.get_contract(symbol=sym) or {}
|
||||
except Exception:
|
||||
meta = {}
|
||||
# multiplier is base per contract for many KuCoin perps (best-effort)
|
||||
mult = Decimal(str(meta.get("multiplier") or meta.get("lotSize") or "0"))
|
||||
if mult <= 0:
|
||||
mult = Decimal("1")
|
||||
ct = (req / mult).to_integral_value(rounding=ROUND_DOWN)
|
||||
try:
|
||||
return int(ct)
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
def get_accounts(self) -> Any:
|
||||
# Futures account overview
|
||||
return self._signed_request("GET", "/api/v1/account-overview", params={"currency": "USDT"})
|
||||
|
||||
def get_positions(self) -> Any:
|
||||
return self._signed_request("GET", "/api/v1/positions")
|
||||
|
||||
def set_leverage(self, *, symbol: str, leverage: float) -> bool:
|
||||
from app.services.live_trading.symbols import to_kucoin_futures_symbol
|
||||
|
||||
sym = to_kucoin_futures_symbol(symbol)
|
||||
try:
|
||||
lv = int(float(leverage or 1.0))
|
||||
except Exception:
|
||||
lv = 1
|
||||
if lv < 1:
|
||||
lv = 1
|
||||
body = {"symbol": sym, "leverage": str(lv)}
|
||||
try:
|
||||
_ = self._signed_request("POST", "/api/v1/position/leverage", json_body=body)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def place_market_order(
|
||||
self,
|
||||
*,
|
||||
symbol: str,
|
||||
side: str,
|
||||
size: float,
|
||||
reduce_only: bool = False,
|
||||
client_order_id: Optional[str] = None,
|
||||
) -> LiveOrderResult:
|
||||
from app.services.live_trading.symbols import to_kucoin_futures_symbol
|
||||
|
||||
sd = (side or "").strip().lower()
|
||||
if sd not in ("buy", "sell"):
|
||||
raise LiveTradingError(f"Invalid side: {side}")
|
||||
sym = to_kucoin_futures_symbol(symbol)
|
||||
qty_ct = self._base_to_contracts(symbol=sym, base_size=float(size or 0.0))
|
||||
if qty_ct <= 0:
|
||||
raise LiveTradingError("Invalid size (converted contracts <= 0)")
|
||||
body: Dict[str, Any] = {
|
||||
"clientOid": str(client_order_id or str(int(time.time() * 1000))),
|
||||
"side": sd,
|
||||
"symbol": sym,
|
||||
"type": "market",
|
||||
"size": qty_ct,
|
||||
}
|
||||
if reduce_only:
|
||||
body["reduceOnly"] = True
|
||||
raw = self._signed_request("POST", "/api/v1/orders", json_body=body)
|
||||
oid = ""
|
||||
if isinstance(raw, dict):
|
||||
d = raw.get("data")
|
||||
if isinstance(d, dict):
|
||||
oid = str(d.get("orderId") or "")
|
||||
elif isinstance(d, str):
|
||||
oid = str(d)
|
||||
return LiveOrderResult(exchange_id="kucoin", exchange_order_id=oid, filled=0.0, avg_price=0.0, raw=raw if isinstance(raw, dict) else {"raw": raw})
|
||||
|
||||
def place_limit_order(
|
||||
self,
|
||||
*,
|
||||
symbol: str,
|
||||
side: str,
|
||||
size: float,
|
||||
price: float,
|
||||
reduce_only: bool = False,
|
||||
post_only: bool = False,
|
||||
client_order_id: Optional[str] = None,
|
||||
) -> LiveOrderResult:
|
||||
from app.services.live_trading.symbols import to_kucoin_futures_symbol
|
||||
|
||||
sd = (side or "").strip().lower()
|
||||
if sd not in ("buy", "sell"):
|
||||
raise LiveTradingError(f"Invalid side: {side}")
|
||||
sym = to_kucoin_futures_symbol(symbol)
|
||||
px = float(price or 0.0)
|
||||
if px <= 0:
|
||||
raise LiveTradingError("Invalid price")
|
||||
qty_ct = self._base_to_contracts(symbol=sym, base_size=float(size or 0.0))
|
||||
if qty_ct <= 0:
|
||||
raise LiveTradingError("Invalid size (converted contracts <= 0)")
|
||||
body: Dict[str, Any] = {
|
||||
"clientOid": str(client_order_id or str(int(time.time() * 1000))),
|
||||
"side": sd,
|
||||
"symbol": sym,
|
||||
"type": "limit",
|
||||
"price": str(px),
|
||||
"size": qty_ct,
|
||||
}
|
||||
if reduce_only:
|
||||
body["reduceOnly"] = True
|
||||
if post_only:
|
||||
body["postOnly"] = True
|
||||
raw = self._signed_request("POST", "/api/v1/orders", json_body=body)
|
||||
oid = ""
|
||||
if isinstance(raw, dict):
|
||||
d = raw.get("data")
|
||||
if isinstance(d, dict):
|
||||
oid = str(d.get("orderId") or "")
|
||||
elif isinstance(d, str):
|
||||
oid = str(d)
|
||||
return LiveOrderResult(exchange_id="kucoin", exchange_order_id=oid, filled=0.0, avg_price=0.0, raw=raw if isinstance(raw, dict) else {"raw": raw})
|
||||
|
||||
def cancel_order(self, *, order_id: str = "", client_order_id: str = "") -> Any:
|
||||
if order_id:
|
||||
return self._signed_request("DELETE", f"/api/v1/orders/{str(order_id)}")
|
||||
if client_order_id:
|
||||
return self._signed_request("DELETE", f"/api/v1/orders/client-order/{str(client_order_id)}")
|
||||
raise LiveTradingError("KuCoinFutures cancel_order requires order_id or client_order_id")
|
||||
|
||||
def get_order(self, *, order_id: str = "", client_order_id: str = "") -> Any:
|
||||
if order_id:
|
||||
return self._signed_request("GET", f"/api/v1/orders/{str(order_id)}")
|
||||
if client_order_id:
|
||||
return self._signed_request("GET", f"/api/v1/orders/byClientOid", params={"clientOid": str(client_order_id)})
|
||||
raise LiveTradingError("KuCoinFutures get_order requires order_id or client_order_id")
|
||||
|
||||
def wait_for_fill(self, *, order_id: str, max_wait_sec: float = 3.0, poll_interval_sec: float = 0.5) -> Dict[str, Any]:
|
||||
end_ts = time.time() + float(max_wait_sec or 0.0)
|
||||
last: Dict[str, Any] = {}
|
||||
while True:
|
||||
try:
|
||||
resp = self.get_order(order_id=str(order_id))
|
||||
last = resp if isinstance(resp, dict) else {"raw": resp}
|
||||
except Exception:
|
||||
last = last or {}
|
||||
od = (last.get("data") if isinstance(last, dict) else None) or {}
|
||||
status = str(od.get("status") or "")
|
||||
filled = 0.0
|
||||
avg_price = 0.0
|
||||
try:
|
||||
# dealSize is in contracts; convert back to base using multiplier best-effort.
|
||||
deal_ct = float(od.get("dealSize") or 0.0)
|
||||
except Exception:
|
||||
deal_ct = 0.0
|
||||
try:
|
||||
deal_value = float(od.get("dealValue") or 0.0)
|
||||
except Exception:
|
||||
deal_value = 0.0
|
||||
# Best-effort: infer avg price from dealValue / (deal contracts * multiplier)
|
||||
mult = 1.0
|
||||
try:
|
||||
sym = str(od.get("symbol") or "")
|
||||
meta = self.get_contract(symbol=sym) or {}
|
||||
mult = float(meta.get("multiplier") or meta.get("lotSize") or 1.0)
|
||||
if mult <= 0:
|
||||
mult = 1.0
|
||||
except Exception:
|
||||
mult = 1.0
|
||||
filled = abs(float(deal_ct or 0.0)) * float(mult)
|
||||
if filled > 0 and deal_value > 0:
|
||||
avg_price = float(deal_value) / float(filled)
|
||||
if filled > 0 and avg_price > 0:
|
||||
return {"filled": filled, "avg_price": avg_price, "status": status, "order": last}
|
||||
if status.lower() in ("done", "canceled", "cancelled", "filled"):
|
||||
return {"filled": filled, "avg_price": avg_price, "status": status, "order": last}
|
||||
if time.time() >= end_ts:
|
||||
return {"filled": filled, "avg_price": avg_price, "status": status, "order": last}
|
||||
time.sleep(float(poll_interval_sec or 0.5))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user