|
@@ -4,12 +4,16 @@ import hmac
|
|
|
import json
|
|
import json
|
|
|
from datetime import UTC, datetime
|
|
from datetime import UTC, datetime
|
|
|
from decimal import Decimal, ROUND_DOWN
|
|
from decimal import Decimal, ROUND_DOWN
|
|
|
|
|
+from typing import TypeAlias
|
|
|
from urllib.parse import urlencode
|
|
from urllib.parse import urlencode
|
|
|
|
|
|
|
|
from okx_codex_trader.config import Config
|
|
from okx_codex_trader.config import Config
|
|
|
from okx_codex_trader.models import Candle, InstrumentMeta, OrderResult, Position, TradeSignal
|
|
from okx_codex_trader.models import Candle, InstrumentMeta, OrderResult, Position, TradeSignal
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+OkxRow: TypeAlias = dict[str, object] | list[object]
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
def build_contract_size(notional: float, price: float, metadata: InstrumentMeta) -> float:
|
|
def build_contract_size(notional: float, price: float, metadata: InstrumentMeta) -> float:
|
|
|
raw_size = Decimal(str(notional)) / (Decimal(str(price)) * Decimal(str(metadata.ct_val)))
|
|
raw_size = Decimal(str(notional)) / (Decimal(str(price)) * Decimal(str(metadata.ct_val)))
|
|
|
lot_size = Decimal(str(metadata.lot_sz))
|
|
lot_size = Decimal(str(metadata.lot_sz))
|
|
@@ -37,10 +41,16 @@ class OkxClient:
|
|
|
def _invalid_payload(self) -> ValueError:
|
|
def _invalid_payload(self) -> ValueError:
|
|
|
return ValueError("okx response payload is invalid")
|
|
return ValueError("okx response payload is invalid")
|
|
|
|
|
|
|
|
- def _first_item(self, data: list[dict[str, object]]) -> dict[str, object]:
|
|
|
|
|
|
|
+ def _transport_error(self) -> ValueError:
|
|
|
|
|
+ return ValueError("okx transport error")
|
|
|
|
|
+
|
|
|
|
|
+ def _first_item(self, data: list[OkxRow]) -> dict[str, object]:
|
|
|
if not data:
|
|
if not data:
|
|
|
raise self._invalid_payload()
|
|
raise self._invalid_payload()
|
|
|
- return data[0]
|
|
|
|
|
|
|
+ item = data[0]
|
|
|
|
|
+ if not isinstance(item, dict):
|
|
|
|
|
+ raise self._invalid_payload()
|
|
|
|
|
+ return item
|
|
|
|
|
|
|
|
def _request(
|
|
def _request(
|
|
|
self,
|
|
self,
|
|
@@ -49,7 +59,7 @@ class OkxClient:
|
|
|
*,
|
|
*,
|
|
|
params: dict[str, object] | None = None,
|
|
params: dict[str, object] | None = None,
|
|
|
json_body: dict[str, object] | None = None,
|
|
json_body: dict[str, object] | None = None,
|
|
|
- ) -> list[dict[str, object]]:
|
|
|
|
|
|
|
+ ) -> list[OkxRow]:
|
|
|
timestamp = datetime.now(UTC).isoformat(timespec="milliseconds").replace("+00:00", "Z")
|
|
timestamp = datetime.now(UTC).isoformat(timespec="milliseconds").replace("+00:00", "Z")
|
|
|
query = urlencode(params or {})
|
|
query = urlencode(params or {})
|
|
|
path_with_query = path if not query else f"{path}?{query}"
|
|
path_with_query = path if not query else f"{path}?{query}"
|
|
@@ -68,14 +78,22 @@ class OkxClient:
|
|
|
"OK-ACCESS-PASSPHRASE": self.config.api_passphrase,
|
|
"OK-ACCESS-PASSPHRASE": self.config.api_passphrase,
|
|
|
"x-simulated-trading": "1",
|
|
"x-simulated-trading": "1",
|
|
|
}
|
|
}
|
|
|
- response = self.session.request(
|
|
|
|
|
- method.upper(),
|
|
|
|
|
- f"{self.base_url}{path}",
|
|
|
|
|
- headers=headers,
|
|
|
|
|
- params=params,
|
|
|
|
|
- json=json_body,
|
|
|
|
|
- )
|
|
|
|
|
- payload = response.json()
|
|
|
|
|
|
|
+ try:
|
|
|
|
|
+ response = self.session.request(
|
|
|
|
|
+ method.upper(),
|
|
|
|
|
+ f"{self.base_url}{path}",
|
|
|
|
|
+ headers=headers,
|
|
|
|
|
+ params=params,
|
|
|
|
|
+ json=json_body,
|
|
|
|
|
+ )
|
|
|
|
|
+ except Exception:
|
|
|
|
|
+ raise self._transport_error() from None
|
|
|
|
|
+ try:
|
|
|
|
|
+ payload = response.json()
|
|
|
|
|
+ except Exception:
|
|
|
|
|
+ raise self._invalid_payload() from None
|
|
|
|
|
+ if not isinstance(payload, dict):
|
|
|
|
|
+ raise self._invalid_payload()
|
|
|
if getattr(response, "status_code", 200) >= 400:
|
|
if getattr(response, "status_code", 200) >= 400:
|
|
|
raise ValueError(str(payload.get("msg") or "okx http error"))
|
|
raise ValueError(str(payload.get("msg") or "okx http error"))
|
|
|
if payload.get("code") != "0":
|
|
if payload.get("code") != "0":
|
|
@@ -105,7 +123,7 @@ class OkxClient:
|
|
|
for entry in data
|
|
for entry in data
|
|
|
]
|
|
]
|
|
|
return sorted(candles, key=lambda candle: candle.ts)
|
|
return sorted(candles, key=lambda candle: candle.ts)
|
|
|
- except (IndexError, TypeError, ValueError):
|
|
|
|
|
|
|
+ except (IndexError, KeyError, TypeError, ValueError):
|
|
|
raise self._invalid_payload() from None
|
|
raise self._invalid_payload() from None
|
|
|
|
|
|
|
|
def get_instrument_meta(self, symbol: str) -> InstrumentMeta:
|
|
def get_instrument_meta(self, symbol: str) -> InstrumentMeta:
|