Stage 06.1 - journal management UI, export and system menu redesign

This commit is contained in:
2026-04-27 15:02:56 +03:00
parent 1fb72ced58
commit f6fc300e84
19 changed files with 1935 additions and 421 deletions

View File

@@ -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),
)