Stage 03.2 - market timestamp in local timezone
This commit is contained in:
@@ -1,12 +1,18 @@
|
|||||||
from __future__ import annotations
|
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.core.config import load_settings
|
||||||
|
from src.integrations.exchange.exceptions import ExchangeError
|
||||||
from src.integrations.exchange.mock_data import (
|
from src.integrations.exchange.mock_data import (
|
||||||
mock_balance_summary,
|
mock_balance_summary,
|
||||||
mock_exchange_health,
|
mock_exchange_health,
|
||||||
mock_ticker_price,
|
mock_ticker_price,
|
||||||
)
|
)
|
||||||
from src.integrations.exchange.models import BalanceSummary, ExchangeHealth, TickerPrice
|
from src.integrations.exchange.models import BalanceSummary, ExchangeHealth, TickerPrice
|
||||||
|
from src.integrations.exchange.rest_client import ExchangeRestClient
|
||||||
|
|
||||||
|
|
||||||
class ExchangeService:
|
class ExchangeService:
|
||||||
@@ -17,22 +23,57 @@ class ExchangeService:
|
|||||||
if not self.settings.exchange_enabled:
|
if not self.settings.exchange_enabled:
|
||||||
return mock_exchange_health()
|
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(
|
return ExchangeHealth(
|
||||||
ok=False,
|
ok=False,
|
||||||
mode="configured_without_keys",
|
mode="real_error",
|
||||||
message="Интеграция включена, но API key / secret не заданы.",
|
message=f"Ошибка подключения к API: {exc}",
|
||||||
)
|
)
|
||||||
|
|
||||||
return ExchangeHealth(
|
return ExchangeHealth(
|
||||||
ok=False,
|
ok=True,
|
||||||
mode="real_pending",
|
mode="real_public_api",
|
||||||
message="Реальный REST client еще не подключен. Пока доступен только mock mode.",
|
message=f"Public API OK. Последняя цена {ticker.symbol}: {ticker.price:.2f}",
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_price(self, symbol: str | None = None) -> TickerPrice:
|
def get_price(self, symbol: str | None = None) -> TickerPrice:
|
||||||
symbol_to_use = symbol or self.settings.default_symbol
|
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]:
|
def get_balance_summary(self) -> list[BalanceSummary]:
|
||||||
return mock_balance_summary()
|
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,
|
||||||
|
)
|
||||||
|
|||||||
15
docs/decisions/0006-public-rest-client.md
Normal file
15
docs/decisions/0006-public-rest-client.md
Normal file
@@ -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
|
||||||
|
- архитектура интеграции остается чистой
|
||||||
34
docs/stages/stage-03-2-real-rest.md
Normal file
34
docs/stages/stage-03-2-real-rest.md
Normal file
@@ -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
|
||||||
Reference in New Issue
Block a user