|
@@ -34,6 +34,14 @@ class OkxClient:
|
|
|
session = requests.Session()
|
|
session = requests.Session()
|
|
|
self.session = session
|
|
self.session = session
|
|
|
|
|
|
|
|
|
|
+ def _invalid_payload(self) -> ValueError:
|
|
|
|
|
+ return ValueError("okx response payload is invalid")
|
|
|
|
|
+
|
|
|
|
|
+ def _first_item(self, data: list[dict[str, object]]) -> dict[str, object]:
|
|
|
|
|
+ if not data:
|
|
|
|
|
+ raise self._invalid_payload()
|
|
|
|
|
+ return data[0]
|
|
|
|
|
+
|
|
|
def _request(
|
|
def _request(
|
|
|
self,
|
|
self,
|
|
|
method: str,
|
|
method: str,
|
|
@@ -81,18 +89,21 @@ class OkxClient:
|
|
|
"/api/v5/market/history-candles",
|
|
"/api/v5/market/history-candles",
|
|
|
params={"instId": symbol, "bar": bar, "limit": limit},
|
|
params={"instId": symbol, "bar": bar, "limit": limit},
|
|
|
)
|
|
)
|
|
|
- return [
|
|
|
|
|
- Candle(
|
|
|
|
|
- symbol=symbol,
|
|
|
|
|
- ts=int(entry[0]),
|
|
|
|
|
- open=float(entry[1]),
|
|
|
|
|
- high=float(entry[2]),
|
|
|
|
|
- low=float(entry[3]),
|
|
|
|
|
- close=float(entry[4]),
|
|
|
|
|
- volume=float(entry[5]),
|
|
|
|
|
- )
|
|
|
|
|
- for entry in data
|
|
|
|
|
- ]
|
|
|
|
|
|
|
+ try:
|
|
|
|
|
+ return [
|
|
|
|
|
+ Candle(
|
|
|
|
|
+ symbol=symbol,
|
|
|
|
|
+ ts=int(entry[0]),
|
|
|
|
|
+ open=float(entry[1]),
|
|
|
|
|
+ high=float(entry[2]),
|
|
|
|
|
+ low=float(entry[3]),
|
|
|
|
|
+ close=float(entry[4]),
|
|
|
|
|
+ volume=float(entry[5]),
|
|
|
|
|
+ )
|
|
|
|
|
+ for entry in data
|
|
|
|
|
+ ]
|
|
|
|
|
+ except (IndexError, TypeError, ValueError):
|
|
|
|
|
+ raise self._invalid_payload() from None
|
|
|
|
|
|
|
|
def get_instrument_meta(self, symbol: str) -> InstrumentMeta:
|
|
def get_instrument_meta(self, symbol: str) -> InstrumentMeta:
|
|
|
data = self._request(
|
|
data = self._request(
|
|
@@ -100,20 +111,28 @@ class OkxClient:
|
|
|
"/api/v5/public/instruments",
|
|
"/api/v5/public/instruments",
|
|
|
params={"instType": "SWAP", "instId": symbol},
|
|
params={"instType": "SWAP", "instId": symbol},
|
|
|
)
|
|
)
|
|
|
- instrument = data[0]
|
|
|
|
|
- return InstrumentMeta(
|
|
|
|
|
- ct_val=float(instrument["ctVal"]),
|
|
|
|
|
- lot_sz=float(instrument["lotSz"]),
|
|
|
|
|
- min_sz=float(instrument["minSz"]),
|
|
|
|
|
- )
|
|
|
|
|
|
|
+ instrument = self._first_item(data)
|
|
|
|
|
+ try:
|
|
|
|
|
+ return InstrumentMeta(
|
|
|
|
|
+ ct_val=float(instrument["ctVal"]),
|
|
|
|
|
+ lot_sz=float(instrument["lotSz"]),
|
|
|
|
|
+ min_sz=float(instrument["minSz"]),
|
|
|
|
|
+ )
|
|
|
|
|
+ except (KeyError, TypeError, ValueError):
|
|
|
|
|
+ raise self._invalid_payload() from None
|
|
|
|
|
|
|
|
def get_last_price(self, symbol: str) -> float:
|
|
def get_last_price(self, symbol: str) -> float:
|
|
|
data = self._request("GET", "/api/v5/market/ticker", params={"instId": symbol})
|
|
data = self._request("GET", "/api/v5/market/ticker", params={"instId": symbol})
|
|
|
- return float(data[0]["last"])
|
|
|
|
|
|
|
+ ticker = self._first_item(data)
|
|
|
|
|
+ try:
|
|
|
|
|
+ return float(ticker["last"])
|
|
|
|
|
+ except (KeyError, TypeError, ValueError):
|
|
|
|
|
+ raise self._invalid_payload() from None
|
|
|
|
|
|
|
|
def ensure_hedge_mode(self) -> None:
|
|
def ensure_hedge_mode(self) -> None:
|
|
|
data = self._request("GET", "/api/v5/account/config")
|
|
data = self._request("GET", "/api/v5/account/config")
|
|
|
- if data[0]["posMode"] != "long_short_mode":
|
|
|
|
|
|
|
+ config = self._first_item(data)
|
|
|
|
|
+ if config.get("posMode") != "long_short_mode":
|
|
|
raise ValueError("hedge mode is required")
|
|
raise ValueError("hedge mode is required")
|
|
|
|
|
|
|
|
def set_leverage(self, symbol: str, leverage: int, pos_side: str) -> None:
|
|
def set_leverage(self, symbol: str, leverage: int, pos_side: str) -> None:
|
|
@@ -160,7 +179,8 @@ class OkxClient:
|
|
|
if signal.entry_price is not None:
|
|
if signal.entry_price is not None:
|
|
|
request_body["px"] = _format_number(signal.entry_price)
|
|
request_body["px"] = _format_number(signal.entry_price)
|
|
|
data = self._request("POST", "/api/v5/trade/order", json_body=request_body)
|
|
data = self._request("POST", "/api/v5/trade/order", json_body=request_body)
|
|
|
- order_id = None if not data else str(data[0].get("ordId") or "")
|
|
|
|
|
|
|
+ order = self._first_item(data)
|
|
|
|
|
+ order_id = str(order.get("ordId") or "") or None
|
|
|
return OrderResult(
|
|
return OrderResult(
|
|
|
status="placed",
|
|
status="placed",
|
|
|
order_id=order_id,
|
|
order_id=order_id,
|
|
@@ -173,12 +193,17 @@ class OkxClient:
|
|
|
|
|
|
|
|
def get_positions(self, symbol: str) -> list[Position]:
|
|
def get_positions(self, symbol: str) -> list[Position]:
|
|
|
data = self._request("GET", "/api/v5/account/positions", params={"instId": symbol})
|
|
data = self._request("GET", "/api/v5/account/positions", params={"instId": symbol})
|
|
|
- return [
|
|
|
|
|
- Position(
|
|
|
|
|
- symbol=str(entry["instId"]),
|
|
|
|
|
- pos_side=str(entry["posSide"]),
|
|
|
|
|
- size=float(entry["pos"]),
|
|
|
|
|
- avg_price=float(entry["avgPx"]),
|
|
|
|
|
- )
|
|
|
|
|
- for entry in data
|
|
|
|
|
- ]
|
|
|
|
|
|
|
+ if not data:
|
|
|
|
|
+ raise self._invalid_payload()
|
|
|
|
|
+ try:
|
|
|
|
|
+ return [
|
|
|
|
|
+ Position(
|
|
|
|
|
+ symbol=str(entry["instId"]),
|
|
|
|
|
+ pos_side=str(entry["posSide"]),
|
|
|
|
|
+ size=float(entry["pos"]),
|
|
|
|
|
+ avg_price=float(entry["avgPx"]),
|
|
|
|
|
+ )
|
|
|
|
|
+ for entry in data
|
|
|
|
|
+ ]
|
|
|
|
|
+ except (KeyError, TypeError, ValueError):
|
|
|
|
|
+ raise self._invalid_payload() from None
|