Harden trading, training, and monitoring
This commit is contained in:
@@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import threading
|
||||
from dataclasses import asdict
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
@@ -56,6 +57,9 @@ class MarketData:
|
||||
self.last_ws_message_at: datetime | None = None
|
||||
self.ws_connected = False
|
||||
self._stop_event = asyncio.Event()
|
||||
self._refresh_lock = threading.Lock()
|
||||
self.rest_error_count = 0
|
||||
self.last_rest_error = ""
|
||||
|
||||
async def bootstrap(self) -> None:
|
||||
self.instruments = await asyncio.to_thread(self.client.instruments)
|
||||
@@ -76,47 +80,60 @@ class MarketData:
|
||||
if symbol in self.instruments
|
||||
]
|
||||
self.storage.event("Торговые пары: " + ", ".join(self.symbols))
|
||||
await asyncio.to_thread(self.refresh_rest)
|
||||
await asyncio.to_thread(self.refresh_rest, True)
|
||||
|
||||
def refresh_rest(self) -> None:
|
||||
ticker_map = {ticker.symbol: ticker for ticker in self.client.spot_tickers()}
|
||||
for symbol in self.symbols:
|
||||
ticker = ticker_map.get(symbol)
|
||||
if ticker:
|
||||
self.tickers[symbol] = ticker
|
||||
try:
|
||||
candles = self.client.klines(
|
||||
symbol=symbol,
|
||||
interval=self.settings.base_interval,
|
||||
limit=self.settings.kline_limit,
|
||||
)
|
||||
candles = _closed_candles(candles, self.settings.base_interval)
|
||||
add_indicators(candles)
|
||||
self.candles[symbol] = candles
|
||||
trend_candles = self.client.klines(
|
||||
symbol=symbol,
|
||||
interval=self.settings.trend_interval,
|
||||
limit=self.settings.trend_kline_limit,
|
||||
)
|
||||
trend_candles = _closed_candles(trend_candles, self.settings.trend_interval)
|
||||
add_indicators(trend_candles)
|
||||
self.trend_candles[symbol] = trend_candles
|
||||
bid, ask = self.client.orderbook_top(symbol)
|
||||
self.orderbook_top[symbol] = (bid, ask)
|
||||
if symbol in self.tickers:
|
||||
current = self.tickers[symbol]
|
||||
self.tickers[symbol] = Ticker(
|
||||
symbol=current.symbol,
|
||||
last_price=current.last_price,
|
||||
bid=bid or current.bid,
|
||||
ask=ask or current.ask,
|
||||
turnover_24h=current.turnover_24h,
|
||||
volume_24h=current.volume_24h,
|
||||
change_24h=current.change_24h,
|
||||
)
|
||||
except Exception as exc:
|
||||
self.storage.event(f"{symbol}: ошибка обновления REST данных: {exc}", "ERROR")
|
||||
self.last_rest_refresh_at = utc_now()
|
||||
def refresh_rest(self, force_candles: bool = False) -> None:
|
||||
if not self._refresh_lock.acquire(blocking=False):
|
||||
return
|
||||
try:
|
||||
ticker_map = {ticker.symbol: ticker for ticker in self.client.spot_tickers()}
|
||||
for symbol in self.symbols:
|
||||
ticker = ticker_map.get(symbol)
|
||||
if ticker:
|
||||
self.tickers[symbol] = ticker
|
||||
try:
|
||||
if force_candles or _candles_due(self.candles.get(symbol, []), self.settings.base_interval):
|
||||
candles = self.client.klines(
|
||||
symbol=symbol,
|
||||
interval=self.settings.base_interval,
|
||||
limit=self.settings.kline_limit,
|
||||
)
|
||||
candles = _closed_candles(candles, self.settings.base_interval)
|
||||
add_indicators(candles)
|
||||
self.candles[symbol] = candles
|
||||
if force_candles or _candles_due(
|
||||
self.trend_candles.get(symbol, []), self.settings.trend_interval
|
||||
):
|
||||
trend_candles = self.client.klines(
|
||||
symbol=symbol,
|
||||
interval=self.settings.trend_interval,
|
||||
limit=self.settings.trend_kline_limit,
|
||||
)
|
||||
trend_candles = _closed_candles(trend_candles, self.settings.trend_interval)
|
||||
add_indicators(trend_candles)
|
||||
self.trend_candles[symbol] = trend_candles
|
||||
bid, ask = self.client.orderbook_top(symbol)
|
||||
self.orderbook_top[symbol] = (bid, ask)
|
||||
if symbol in self.tickers:
|
||||
current = self.tickers[symbol]
|
||||
self.tickers[symbol] = Ticker(
|
||||
symbol=current.symbol,
|
||||
last_price=current.last_price,
|
||||
bid=bid or current.bid,
|
||||
ask=ask or current.ask,
|
||||
turnover_24h=current.turnover_24h,
|
||||
volume_24h=current.volume_24h,
|
||||
change_24h=current.change_24h,
|
||||
)
|
||||
except Exception as exc:
|
||||
self.rest_error_count += 1
|
||||
self.last_rest_error = str(exc)
|
||||
self.storage.event(f"{symbol}: ошибка обновления REST данных: {exc}", "ERROR")
|
||||
self.last_rest_refresh_at = utc_now()
|
||||
if ticker_map:
|
||||
self.last_rest_error = ""
|
||||
finally:
|
||||
self._refresh_lock.release()
|
||||
|
||||
async def websocket_loop(self) -> None:
|
||||
if not self.settings.websocket_enabled:
|
||||
@@ -227,10 +244,36 @@ class MarketData:
|
||||
def prices(self) -> dict[str, float]:
|
||||
return {symbol: ticker.last_price for symbol, ticker in self.tickers.items()}
|
||||
|
||||
def symbol_freshness(self, symbol: str) -> dict[str, Any]:
|
||||
ticker = self.tickers.get(symbol)
|
||||
candles = self.candles.get(symbol, [])
|
||||
ticker_age = (utc_now() - ticker.updated_at).total_seconds() if ticker else None
|
||||
interval_ms = _interval_ms(self.settings.base_interval)
|
||||
candle_age = (
|
||||
max(0.0, (utc_now().timestamp() * 1000 - candles[-1].timestamp) / 1000)
|
||||
if candles
|
||||
else None
|
||||
)
|
||||
ticker_ok = ticker_age is not None and ticker_age <= self.settings.market_ticker_max_age_seconds
|
||||
candle_ok = bool(
|
||||
candle_age is not None
|
||||
and interval_ms > 0
|
||||
and candle_age <= (interval_ms / 1000) * 2.5
|
||||
)
|
||||
return {
|
||||
"ok": bool(ticker_ok and candle_ok),
|
||||
"ticker_ok": ticker_ok,
|
||||
"candle_ok": candle_ok,
|
||||
"ticker_age_seconds": round(ticker_age, 3) if ticker_age is not None else None,
|
||||
"candle_age_seconds": round(candle_age, 3) if candle_age is not None else None,
|
||||
}
|
||||
|
||||
def snapshot(self) -> dict[str, Any]:
|
||||
return {
|
||||
"symbols": self.symbols,
|
||||
"ws_connected": self.ws_connected,
|
||||
"rest_error_count": self.rest_error_count,
|
||||
"last_rest_error": self.last_rest_error,
|
||||
"quality": market_quality_snapshot(
|
||||
symbols=self.symbols,
|
||||
candles_by_symbol=self.candles,
|
||||
@@ -291,3 +334,14 @@ def _interval_ms(interval: str) -> int:
|
||||
if normalized.isdigit():
|
||||
return int(normalized) * 60 * 1000
|
||||
return 0
|
||||
|
||||
|
||||
def _candles_due(candles: list[Candle], interval: str, now_ms: int | None = None) -> bool:
|
||||
if not candles:
|
||||
return True
|
||||
interval_ms = _interval_ms(interval)
|
||||
if interval_ms <= 0:
|
||||
return True
|
||||
now_ms = now_ms if now_ms is not None else int(utc_now().timestamp() * 1000)
|
||||
expected_latest_start = (now_ms // interval_ms - 1) * interval_ms
|
||||
return candles[-1].timestamp < expected_latest_start
|
||||
|
||||
Reference in New Issue
Block a user