diff --git a/app/src/integrations/exchange/service.py b/app/src/integrations/exchange/service.py index b05758d..e11c043 100644 --- a/app/src/integrations/exchange/service.py +++ b/app/src/integrations/exchange/service.py @@ -1,12 +1,18 @@ from __future__ import annotations +from datetime import datetime +from zoneinfo import ZoneInfo from src.core.config import load_settings + +from src.core.config import load_settings +from src.integrations.exchange.exceptions import ExchangeError from src.integrations.exchange.mock_data import ( mock_balance_summary, mock_exchange_health, mock_ticker_price, ) from src.integrations.exchange.models import BalanceSummary, ExchangeHealth, TickerPrice +from src.integrations.exchange.rest_client import ExchangeRestClient class ExchangeService: @@ -17,22 +23,57 @@ class ExchangeService: if not self.settings.exchange_enabled: return mock_exchange_health() - if not self.settings.exchange_api_key or not self.settings.exchange_api_secret: + try: + ticker = self._get_real_price(self.settings.default_symbol) + except ExchangeError as exc: return ExchangeHealth( ok=False, - mode="configured_without_keys", - message="Интеграция включена, но API key / secret не заданы.", + mode="real_error", + message=f"Ошибка подключения к API: {exc}", ) return ExchangeHealth( - ok=False, - mode="real_pending", - message="Реальный REST client еще не подключен. Пока доступен только mock mode.", + ok=True, + mode="real_public_api", + message=f"Public API OK. Последняя цена {ticker.symbol}: {ticker.price:.2f}", ) def get_price(self, symbol: str | None = None) -> TickerPrice: symbol_to_use = symbol or self.settings.default_symbol - return mock_ticker_price(symbol_to_use) + + if not self.settings.exchange_enabled: + return mock_ticker_price(symbol_to_use) + + return self._get_real_price(symbol_to_use) def get_balance_summary(self) -> list[BalanceSummary]: return mock_balance_summary() + + def _get_real_price(self, symbol: str) -> TickerPrice: + client = ExchangeRestClient() + payload = client.get_json( + "/api/v2/ticker/24hr", + params={"symbol": symbol}, + ) + + price_raw = payload.get("lastPrice") + if price_raw is None: + raise ExchangeError("Field 'lastPrice' is missing in ticker response.") + + close_time = payload.get("closeTime") or payload.get("eventTime") or "" + + settings = load_settings() + + if close_time: + dt_utc = datetime.fromtimestamp(int(close_time) / 1000, tz=ZoneInfo("UTC")) + dt_local = dt_utc.astimezone(ZoneInfo(settings.tz)) + updated_at = dt_local.strftime("%d/%m/%Y %H:%M:%S") + else: + updated_at = "n/a" + + return TickerPrice( + symbol=symbol, + price=float(price_raw), + source="dzengi-demo-api", + updated_at=updated_at, + ) diff --git a/docs/decisions/0006-public-rest-client.md b/docs/decisions/0006-public-rest-client.md new file mode 100644 index 0000000..4b92b38 --- /dev/null +++ b/docs/decisions/0006-public-rest-client.md @@ -0,0 +1,15 @@ +# 0006 — Public REST Client First + +## Решение +Подключать первую реальную интеграцию с биржей через public readonly endpoint, а не через private auth и не через ордера. + +## Причины +- это безопаснее +- это быстрее дает полезный результат в UI +- это позволяет стабилизировать transport / error handling +- это не требует сразу заводить реальные торговые операции + +## Последствия +- рынок получает реальные данные раньше, чем private account functions +- system screen начинает показывать реальный API health +- архитектура интеграции остается чистой diff --git a/docs/stages/stage-03-2-real-rest.md b/docs/stages/stage-03-2-real-rest.md new file mode 100644 index 0000000..d56a61e --- /dev/null +++ b/docs/stages/stage-03-2-real-rest.md @@ -0,0 +1,34 @@ +# Stage 03.2 — Public REST client + +## Цель +Перевести экран `📈 Рынок` с mock-цены на реальную публичную цену из Dzengi demo API. + +## Что добавляется +- `exceptions.py` +- `rest_client.py` +- real path в `service.py` +- обновленный `market.py` +- обновленный `system_status.py` + +## Что используется +- Base demo URL: `https://demo-api-adapter.dzengi.com` +- Endpoint: `GET /api/v2/ticker/24hr` +- Symbol: `BTC/USD_LEVERAGE` + +## Как работает +### Если `EXCHANGE_ENABLED=false` +- используется mock mode + +### Если `EXCHANGE_ENABLED=true` +- выполняется реальный public GET request +- `📈 Рынок` показывает реальную цену +- `⚙️ Система` показывает статус API + +## Что пока НЕ делается +- private auth +- баланс через реальные ключи +- создание ордеров +- websocket +- retry logic +- backoff +- rate-limit handling