Harden trading, training, and monitoring
This commit is contained in:
+158
-3
@@ -9,6 +9,8 @@ from typing import Any
|
||||
from urllib.parse import urlencode
|
||||
|
||||
import requests
|
||||
from requests.adapters import HTTPAdapter
|
||||
from urllib3.util.retry import Retry
|
||||
|
||||
from crypto_spot_bot.config import Settings
|
||||
from crypto_spot_bot.models import Candle, Ticker
|
||||
@@ -41,6 +43,17 @@ class BybitClient:
|
||||
def __init__(self, settings: Settings):
|
||||
self.settings = settings
|
||||
self.session = requests.Session()
|
||||
retry = Retry(
|
||||
total=3,
|
||||
connect=3,
|
||||
read=3,
|
||||
status=3,
|
||||
backoff_factor=0.4,
|
||||
status_forcelist=(429, 500, 502, 503, 504),
|
||||
allowed_methods=frozenset({"GET"}),
|
||||
respect_retry_after_header=True,
|
||||
)
|
||||
self.session.mount("https://", HTTPAdapter(max_retries=retry))
|
||||
|
||||
def public_get(self, path: str, params: dict[str, Any]) -> dict[str, Any]:
|
||||
response = self.session.get(
|
||||
@@ -208,27 +221,165 @@ class BybitClient:
|
||||
"symbol": symbol,
|
||||
"side": side,
|
||||
"orderType": "Market",
|
||||
"qty": f"{qty:.8f}".rstrip("0").rstrip("."),
|
||||
"qty": _decimal_text(qty),
|
||||
"timeInForce": "IOC",
|
||||
"isLeverage": 0,
|
||||
"orderFilter": "Order",
|
||||
"marketUnit": market_unit,
|
||||
"orderLinkId": order_link_id,
|
||||
}
|
||||
slippage_percent = max(0.01, min(10.0, self.settings.slippage_rate * 100.0))
|
||||
payload["slippageToleranceType"] = "Percent"
|
||||
payload["slippageTolerance"] = f"{slippage_percent:.2f}"
|
||||
return self.private_post("/v5/order/create", payload)
|
||||
|
||||
def place_spot_protective_stop(
|
||||
self,
|
||||
*,
|
||||
symbol: str,
|
||||
qty: float,
|
||||
trigger_price: float,
|
||||
order_link_id: str,
|
||||
) -> dict[str, Any]:
|
||||
payload = {
|
||||
"category": "spot",
|
||||
"symbol": symbol,
|
||||
"side": "Sell",
|
||||
"orderType": "Market",
|
||||
"qty": _decimal_text(qty),
|
||||
"triggerPrice": _decimal_text(trigger_price),
|
||||
"timeInForce": "IOC",
|
||||
"isLeverage": 0,
|
||||
"orderFilter": "tpslOrder",
|
||||
"marketUnit": "baseCoin",
|
||||
"orderLinkId": order_link_id,
|
||||
}
|
||||
return self.private_post("/v5/order/create", payload)
|
||||
|
||||
def cancel_spot_order(
|
||||
self,
|
||||
*,
|
||||
symbol: str,
|
||||
order_id: str | None = None,
|
||||
order_link_id: str | None = None,
|
||||
order_filter: str = "Order",
|
||||
) -> dict[str, Any]:
|
||||
if not order_id and not order_link_id:
|
||||
raise ValueError("order_id or order_link_id is required")
|
||||
payload: dict[str, Any] = {
|
||||
"category": "spot",
|
||||
"symbol": symbol,
|
||||
"orderFilter": order_filter,
|
||||
}
|
||||
if order_id:
|
||||
payload["orderId"] = order_id
|
||||
if order_link_id:
|
||||
payload["orderLinkId"] = order_link_id
|
||||
return self.private_post("/v5/order/cancel", payload)
|
||||
|
||||
def wallet_balance(self, account_type: str = "UNIFIED", coin: str | None = None) -> dict[str, Any]:
|
||||
return self.private_get(
|
||||
"/v5/account/wallet-balance",
|
||||
{"accountType": account_type, "coin": coin},
|
||||
)
|
||||
|
||||
def realtime_orders(self, *, category: str = "spot", open_only: int = 0, limit: int = 50) -> dict[str, Any]:
|
||||
def realtime_orders(
|
||||
self,
|
||||
*,
|
||||
category: str = "spot",
|
||||
open_only: int = 0,
|
||||
limit: int = 50,
|
||||
symbol: str | None = None,
|
||||
order_id: str | None = None,
|
||||
order_link_id: str | None = None,
|
||||
order_filter: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
return self.private_get(
|
||||
"/v5/order/realtime",
|
||||
{"category": category, "openOnly": open_only, "limit": max(1, min(limit, 50))},
|
||||
{
|
||||
"category": category,
|
||||
"openOnly": open_only,
|
||||
"limit": max(1, min(limit, 50)),
|
||||
"symbol": symbol,
|
||||
"orderId": order_id,
|
||||
"orderLinkId": order_link_id,
|
||||
"orderFilter": order_filter,
|
||||
},
|
||||
)
|
||||
|
||||
def order_history(
|
||||
self,
|
||||
*,
|
||||
symbol: str | None = None,
|
||||
order_id: str | None = None,
|
||||
order_link_id: str | None = None,
|
||||
limit: int = 50,
|
||||
) -> dict[str, Any]:
|
||||
return self.private_get(
|
||||
"/v5/order/history",
|
||||
{
|
||||
"category": "spot",
|
||||
"symbol": symbol,
|
||||
"orderId": order_id,
|
||||
"orderLinkId": order_link_id,
|
||||
"limit": max(1, min(limit, 50)),
|
||||
},
|
||||
)
|
||||
|
||||
def executions(
|
||||
self,
|
||||
*,
|
||||
symbol: str | None = None,
|
||||
order_id: str | None = None,
|
||||
order_link_id: str | None = None,
|
||||
limit: int = 100,
|
||||
) -> dict[str, Any]:
|
||||
return self.private_get(
|
||||
"/v5/execution/list",
|
||||
{
|
||||
"category": "spot",
|
||||
"symbol": symbol,
|
||||
"orderId": order_id,
|
||||
"orderLinkId": order_link_id,
|
||||
"limit": max(1, min(limit, 100)),
|
||||
},
|
||||
)
|
||||
|
||||
def wait_for_spot_order(
|
||||
self,
|
||||
*,
|
||||
order_id: str,
|
||||
symbol: str,
|
||||
timeout_seconds: float,
|
||||
poll_seconds: float = 0.5,
|
||||
) -> dict[str, Any]:
|
||||
deadline = time.monotonic() + max(1.0, timeout_seconds)
|
||||
latest: dict[str, Any] = {}
|
||||
terminal = {
|
||||
"Filled",
|
||||
"Cancelled",
|
||||
"Rejected",
|
||||
"PartiallyFilledCanceled",
|
||||
"PartillyFilledCancelled",
|
||||
"Deactivated",
|
||||
}
|
||||
while time.monotonic() < deadline:
|
||||
realtime = self.realtime_orders(symbol=symbol, order_id=order_id, open_only=1, limit=1)
|
||||
rows = realtime.get("list") if isinstance(realtime.get("list"), list) else []
|
||||
if rows and isinstance(rows[0], dict):
|
||||
latest = rows[0]
|
||||
if str(latest.get("orderStatus", "")) in terminal:
|
||||
break
|
||||
time.sleep(max(0.1, poll_seconds))
|
||||
if not latest or str(latest.get("orderStatus", "")) not in terminal:
|
||||
history = self.order_history(symbol=symbol, order_id=order_id, limit=1)
|
||||
rows = history.get("list") if isinstance(history.get("list"), list) else []
|
||||
if rows and isinstance(rows[0], dict):
|
||||
latest = rows[0]
|
||||
execution_result = self.executions(symbol=symbol, order_id=order_id)
|
||||
executions = execution_result.get("list") if isinstance(execution_result.get("list"), list) else []
|
||||
return {"order": latest, "executions": executions}
|
||||
|
||||
|
||||
def websocket_subscribe_message(symbols: list[str], interval: str = "1") -> str:
|
||||
args: list[str] = []
|
||||
@@ -254,3 +405,7 @@ def _looks_like_stablecoin(base_coin: str) -> bool:
|
||||
"PYUSD",
|
||||
"USD1",
|
||||
}
|
||||
|
||||
|
||||
def _decimal_text(value: float) -> str:
|
||||
return f"{value:.12f}".rstrip("0").rstrip(".")
|
||||
|
||||
Reference in New Issue
Block a user