Stage 06.1 - journal management UI, export and system menu redesign
This commit is contained in:
@@ -50,6 +50,89 @@ class ExchangeService:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _classify_error(self, exc: Exception) -> str:
|
||||
text = str(exc).lower()
|
||||
|
||||
if any(
|
||||
marker in text
|
||||
for marker in [
|
||||
"invalid api key",
|
||||
"api key",
|
||||
"api-key",
|
||||
"signature",
|
||||
"unauthorized",
|
||||
"forbidden",
|
||||
"private api error",
|
||||
"expired",
|
||||
]
|
||||
):
|
||||
return "auth"
|
||||
|
||||
if any(
|
||||
marker in text
|
||||
for marker in [
|
||||
"timeout",
|
||||
"timed out",
|
||||
"connection error",
|
||||
"network error",
|
||||
"name or service not known",
|
||||
"nodename nor servname",
|
||||
]
|
||||
):
|
||||
return "network"
|
||||
|
||||
if any(
|
||||
marker in text
|
||||
for marker in [
|
||||
"-1021",
|
||||
"server time",
|
||||
"doesn't match server time",
|
||||
]
|
||||
):
|
||||
return "time"
|
||||
|
||||
return "generic"
|
||||
|
||||
def _log_exchange_error(
|
||||
self,
|
||||
*,
|
||||
endpoint: str,
|
||||
exc: Exception,
|
||||
symbol: str | None = None,
|
||||
extra_payload: dict | None = None,
|
||||
) -> None:
|
||||
payload = {
|
||||
"endpoint": endpoint,
|
||||
"symbol": symbol,
|
||||
"exchange_name": self.settings.exchange_name,
|
||||
"error_type": self._classify_error(exc),
|
||||
"raw_error": str(exc),
|
||||
}
|
||||
|
||||
if extra_payload:
|
||||
payload.update(extra_payload)
|
||||
|
||||
self._log_error(
|
||||
"exchange_request_error",
|
||||
str(exc),
|
||||
payload,
|
||||
)
|
||||
|
||||
def _format_exchange_time(self, raw_timestamp: object) -> str:
|
||||
if not raw_timestamp:
|
||||
return "n/a"
|
||||
|
||||
dt_utc = datetime.fromtimestamp(int(raw_timestamp) / 1000, tz=ZoneInfo("UTC"))
|
||||
dt_local = dt_utc.astimezone(ZoneInfo(self.settings.tz))
|
||||
return dt_local.strftime("%d.%m.%Y %H:%M:%S")
|
||||
|
||||
def _source_name(self) -> str:
|
||||
return (
|
||||
"dzengi-demo-api"
|
||||
if "demo" in self.settings.exchange_base_url.lower()
|
||||
else "dzengi-api"
|
||||
)
|
||||
|
||||
def get_health(self) -> ExchangeHealth:
|
||||
if not self.settings.exchange_enabled:
|
||||
return mock_exchange_health()
|
||||
@@ -94,6 +177,10 @@ class ExchangeService:
|
||||
payload = ExchangePrivateClient().get_account_info(show_zero_balance=False)
|
||||
balances = parse_account_balances(payload)
|
||||
except Exception as exc:
|
||||
self._log_exchange_error(
|
||||
endpoint="private/account_info",
|
||||
exc=exc,
|
||||
)
|
||||
return PrivateAuthHealth(
|
||||
ok=False,
|
||||
message=f"Private API error: {exc}",
|
||||
@@ -134,33 +221,40 @@ class ExchangeService:
|
||||
raise ExchangeError(validation.message)
|
||||
|
||||
client = ExchangeRestClient()
|
||||
payload = client.get_json(
|
||||
"/api/v2/ticker/24hr",
|
||||
params={"symbol": validation.normalized_symbol},
|
||||
)
|
||||
|
||||
try:
|
||||
payload = client.get_json(
|
||||
"/api/v2/ticker/24hr",
|
||||
params={"symbol": validation.normalized_symbol},
|
||||
)
|
||||
except Exception as exc:
|
||||
self._log_exchange_error(
|
||||
endpoint="ticker/24hr",
|
||||
exc=exc,
|
||||
symbol=validation.normalized_symbol,
|
||||
)
|
||||
raise ExchangeError(str(exc)) from exc
|
||||
|
||||
last_raw = payload.get("lastPrice")
|
||||
if last_raw is None:
|
||||
raise ExchangeError("Field 'lastPrice' is missing in ticker response.")
|
||||
exc = ExchangeError("Field 'lastPrice' is missing in ticker response.")
|
||||
self._log_exchange_error(
|
||||
endpoint="ticker/24hr",
|
||||
exc=exc,
|
||||
symbol=validation.normalized_symbol,
|
||||
)
|
||||
raise exc
|
||||
|
||||
bid_raw = payload.get("bidPrice") or last_raw
|
||||
ask_raw = payload.get("askPrice") or last_raw
|
||||
|
||||
close_time = payload.get("closeTime") or payload.get("eventTime") or ""
|
||||
|
||||
if close_time:
|
||||
dt_utc = datetime.fromtimestamp(int(close_time) / 1000, tz=ZoneInfo("UTC"))
|
||||
dt_local = dt_utc.astimezone(ZoneInfo(self.settings.tz))
|
||||
updated_at = dt_local.strftime("%d.%m.%Y %H:%M:%S")
|
||||
else:
|
||||
updated_at = "n/a"
|
||||
|
||||
return {
|
||||
"symbol": validation.normalized_symbol,
|
||||
"last_price": float(last_raw),
|
||||
"bid_price": float(bid_raw),
|
||||
"ask_price": float(ask_raw),
|
||||
"updated_at": updated_at,
|
||||
"updated_at": self._format_exchange_time(close_time),
|
||||
}
|
||||
|
||||
def get_balance_summary(self) -> list[BalanceSummary]:
|
||||
@@ -169,25 +263,24 @@ class ExchangeService:
|
||||
|
||||
auth_health = self.get_private_auth_health()
|
||||
if not auth_health.ok:
|
||||
self._log_error(
|
||||
"balance_summary_error",
|
||||
auth_health.message,
|
||||
{
|
||||
"exchange_name": self.settings.exchange_name,
|
||||
auth_exc = ExchangeError(auth_health.message)
|
||||
self._log_exchange_error(
|
||||
endpoint="private/account_info",
|
||||
exc=auth_exc,
|
||||
extra_payload={
|
||||
"default_symbol": self.settings.default_symbol,
|
||||
},
|
||||
)
|
||||
raise ExchangeError(auth_health.message)
|
||||
raise auth_exc
|
||||
|
||||
try:
|
||||
payload = ExchangePrivateClient().get_account_info(show_zero_balance=False)
|
||||
balances = parse_account_balances(payload)
|
||||
except Exception as exc:
|
||||
self._log_error(
|
||||
"balance_summary_error",
|
||||
f"Не удалось получить баланс: {exc}",
|
||||
{
|
||||
"exchange_name": self.settings.exchange_name,
|
||||
self._log_exchange_error(
|
||||
endpoint="private/account_info",
|
||||
exc=exc,
|
||||
extra_payload={
|
||||
"default_symbol": self.settings.default_symbol,
|
||||
},
|
||||
)
|
||||
@@ -220,7 +313,15 @@ class ExchangeService:
|
||||
return []
|
||||
|
||||
client = ExchangeRestClient()
|
||||
payload = client.get_json("/api/v2/exchangeInfo")
|
||||
|
||||
try:
|
||||
payload = client.get_json("/api/v2/exchangeInfo")
|
||||
except Exception as exc:
|
||||
self._log_exchange_error(
|
||||
endpoint="exchangeInfo",
|
||||
exc=exc,
|
||||
)
|
||||
raise ExchangeError(str(exc)) from exc
|
||||
|
||||
if isinstance(payload.get("symbols"), list):
|
||||
symbols_raw = payload["symbols"]
|
||||
@@ -229,7 +330,12 @@ class ExchangeService:
|
||||
if isinstance(inner, dict) and isinstance(inner.get("symbols"), list):
|
||||
symbols_raw = inner["symbols"]
|
||||
else:
|
||||
raise ExchangeError("Field 'symbols' is missing in exchangeInfo response.")
|
||||
exc = ExchangeError("Field 'symbols' is missing in exchangeInfo response.")
|
||||
self._log_exchange_error(
|
||||
endpoint="exchangeInfo",
|
||||
exc=exc,
|
||||
)
|
||||
raise exc
|
||||
|
||||
def _safe_str(value: object, default: str = "") -> str:
|
||||
if value is None:
|
||||
@@ -394,46 +500,28 @@ class ExchangeService:
|
||||
params={"symbol": symbol},
|
||||
)
|
||||
except Exception as exc:
|
||||
self._log_error(
|
||||
"market_price_error",
|
||||
f"Не удалось получить цену инструмента {symbol}: {exc}",
|
||||
{
|
||||
"symbol": symbol,
|
||||
"exchange_name": self.settings.exchange_name,
|
||||
},
|
||||
self._log_exchange_error(
|
||||
endpoint="ticker/24hr",
|
||||
exc=exc,
|
||||
symbol=symbol,
|
||||
)
|
||||
raise
|
||||
raise ExchangeError(str(exc)) from exc
|
||||
|
||||
price_raw = payload.get("lastPrice")
|
||||
if price_raw is None:
|
||||
self._log_error(
|
||||
"market_price_error",
|
||||
"Field 'lastPrice' is missing in ticker response.",
|
||||
{
|
||||
"symbol": symbol,
|
||||
"exchange_name": self.settings.exchange_name,
|
||||
},
|
||||
exc = ExchangeError("Field 'lastPrice' is missing in ticker response.")
|
||||
self._log_exchange_error(
|
||||
endpoint="ticker/24hr",
|
||||
exc=exc,
|
||||
symbol=symbol,
|
||||
)
|
||||
raise ExchangeError("Field 'lastPrice' is missing in ticker response.")
|
||||
raise exc
|
||||
|
||||
close_time = payload.get("closeTime") or payload.get("eventTime") or ""
|
||||
|
||||
if close_time:
|
||||
dt_utc = datetime.fromtimestamp(int(close_time) / 1000, tz=ZoneInfo("UTC"))
|
||||
dt_local = dt_utc.astimezone(ZoneInfo(self.settings.tz))
|
||||
updated_at = dt_local.strftime("%d.%m.%Y %H:%M:%S")
|
||||
else:
|
||||
updated_at = "n/a"
|
||||
|
||||
source = (
|
||||
"dzengi-demo-api"
|
||||
if "demo" in self.settings.exchange_base_url.lower()
|
||||
else "dzengi-api"
|
||||
)
|
||||
|
||||
return TickerPrice(
|
||||
symbol=symbol,
|
||||
price=float(price_raw),
|
||||
source=source,
|
||||
updated_at=updated_at,
|
||||
source=self._source_name(),
|
||||
updated_at=self._format_exchange_time(close_time),
|
||||
)
|
||||
Reference in New Issue
Block a user