refactor: clean up imports and improve code formatting across multiple files
CI / lint-test-build (push) Successful in 2m21s

This commit is contained in:
2026-06-09 10:02:41 +02:00
parent 403daa6cf1
commit e4f5d8dfcc
5 changed files with 75 additions and 116 deletions
+12 -24
View File
@@ -6,51 +6,40 @@ from collections.abc import AsyncIterator
from datetime import UTC, datetime from datetime import UTC, datetime
from importlib import resources from importlib import resources
from pathlib import Path from pathlib import Path
from typing import cast
from urllib.parse import parse_qs
import orjson
from fastapi import APIRouter, Depends, Request, Response from fastapi import APIRouter, Depends, Request, Response
from fastapi.responses import HTMLResponse, JSONResponse, StreamingResponse from fastapi.responses import HTMLResponse, JSONResponse, StreamingResponse
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from arbitrade.alerting.notifier import SupportsAlerts, SupportsAlertStatus
from arbitrade.api.auth import require_dashboard_auth from arbitrade.api.auth import require_dashboard_auth
from arbitrade.api.control_state import DashboardControlState
from arbitrade.config.pairing_sync import sync_pairings_from_kraken from arbitrade.config.pairing_sync import sync_pairings_from_kraken
from arbitrade.config.service import ConfigPairing
from arbitrade.dashboard.context import ( from arbitrade.dashboard.context import (
_backtesting_panel_context, _backtesting_panel_context,
_recent_backtest_reports, _recent_backtest_reports,
) )
from arbitrade.dashboard.dashboard import ( from arbitrade.dashboard.dashboard import (
_alert_status_snapshot, _alert_status_snapshot,
_dashboard_backtesting_job_handler, _dashboard_audit,
_dashboard_backtesting_job_export,
_dashboard_backtesting_handler, _dashboard_backtesting_handler,
_dashboard_backtesting_job_export,
_dashboard_backtesting_job_handler,
_dashboard_charts,
_dashboard_config_context,
_dashboard_controls,
_dashboard_ctl_cfg, _dashboard_ctl_cfg,
_dashboard_ctl_kill,
_dashboard_ctl_start, _dashboard_ctl_start,
_dashboard_ctl_stop, _dashboard_ctl_stop,
_dashboard_ctl_kill,
_dashboard_metrics, _dashboard_metrics,
_dashboard_overview, _dashboard_overview,
_dashboard_config_context,
_dashboard_charts,
_dashboard_controls,
_dashboard_audit,
_dashboard_response,
_dashboard_pairings_response, _dashboard_pairings_response,
_dashboard_response,
_pairing_repo, _pairing_repo,
_toggle_pairing _toggle_pairing,
) )
from arbitrade.detection.graph import CurrencyGraph, TriangularCycle
from arbitrade.storage.pg_store import PgStore
from arbitrade.storage.repositories import ( from arbitrade.storage.repositories import (
AuditRecord,
AuditRepository,
BacktestJobRepository, BacktestJobRepository,
ConfigPairingRepository, ConfigPairingRepository,
KrakenAccountSnapshotRepository,
LogRepository, LogRepository,
) )
@@ -60,8 +49,7 @@ public_router = APIRouter()
def _resolve_templates_directory() -> str: def _resolve_templates_directory() -> str:
# Support source layout, Docker runtime (/app), and installed package data. # Support source layout, Docker runtime (/app), and installed package data.
source_layout_path = Path( source_layout_path = Path(__file__).resolve().parents[3] / "web" / "templates"
__file__).resolve().parents[3] / "web" / "templates"
if source_layout_path.is_dir(): if source_layout_path.is_dir():
return str(source_layout_path) return str(source_layout_path)
@@ -70,8 +58,7 @@ def _resolve_templates_directory() -> str:
return str(docker_runtime_path) return str(docker_runtime_path)
try: try:
package_path = resources.files( package_path = resources.files("arbitrade").joinpath("web", "templates")
"arbitrade").joinpath("web", "templates")
if package_path.is_dir(): if package_path.is_dir():
return str(package_path) return str(package_path)
except (ModuleNotFoundError, AttributeError): except (ModuleNotFoundError, AttributeError):
@@ -375,6 +362,7 @@ async def health() -> JSONResponse:
# ── Pairing API ───────────────────────────────────────────────────────────── # ── Pairing API ─────────────────────────────────────────────────────────────
@router.get("/dashboard/api/pairings", response_class=JSONResponse) @router.get("/dashboard/api/pairings", response_class=JSONResponse)
async def dashboard_api_pairings( async def dashboard_api_pairings(
request: Request, request: Request,
+1 -17
View File
@@ -1,25 +1,9 @@
from __future__ import annotations from __future__ import annotations
import json from fastapi import Request
from asyncio import Lock
from collections.abc import AsyncIterator
from datetime import UTC, datetime
from importlib import resources
from pathlib import Path
from typing import cast
from urllib.parse import parse_qs
import orjson
from fastapi import APIRouter, Depends, Request, Response
from fastapi.responses import HTMLResponse, JSONResponse, StreamingResponse
from fastapi.templating import Jinja2Templates
from arbitrade.storage.repositories import ( from arbitrade.storage.repositories import (
AuditRecord,
AuditRepository,
BacktestJobRepository, BacktestJobRepository,
ConfigPairingRepository,
KrakenAccountSnapshotRepository,
LogRepository,
) )
+18 -41
View File
@@ -1,39 +1,31 @@
from __future__ import annotations from __future__ import annotations
import json import json
from asyncio import Lock
from collections.abc import AsyncIterator
from datetime import UTC, datetime from datetime import UTC, datetime
from importlib import resources from importlib import resources
from pathlib import Path from pathlib import Path
from typing import cast from typing import cast
from urllib.parse import parse_qs from urllib.parse import parse_qs
from httpcore import request
import orjson import orjson
from fastapi import APIRouter, Depends, Request, Response from fastapi import APIRouter, Depends, Request, Response
from fastapi.responses import HTMLResponse, JSONResponse, StreamingResponse from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from arbitrade.alerting.notifier import SupportsAlerts, SupportsAlertStatus from arbitrade.alerting.notifier import SupportsAlerts, SupportsAlertStatus
from arbitrade.api.auth import require_dashboard_auth from arbitrade.api.auth import require_dashboard_auth
from arbitrade.api.control_state import DashboardControlState from arbitrade.api.control_state import DashboardControlState
from arbitrade.api.routes import ( from arbitrade.config.service import ConfigPairing
from arbitrade.dashboard.context import (
_backtesting_panel_context, _backtesting_panel_context,
) )
from arbitrade.config.pairing_sync import sync_pairings_from_kraken
from arbitrade.config.service import ConfigPairing
from arbitrade.api.control_state import DashboardControlState
from arbitrade.storage.pg_store import PgStore from arbitrade.storage.pg_store import PgStore
from arbitrade.detection.graph import CurrencyGraph, TriangularCycle
from arbitrade.storage.repositories import ( from arbitrade.storage.repositories import (
AuditRecord, AuditRecord,
AuditRepository, AuditRepository,
BacktestJobRepository, BacktestJobRepository,
ConfigPairingRepository, ConfigPairingRepository,
KrakenAccountSnapshotRepository, KrakenAccountSnapshotRepository,
LogRepository,
) )
router = APIRouter(dependencies=[Depends(require_dashboard_auth)]) router = APIRouter(dependencies=[Depends(require_dashboard_auth)])
@@ -42,8 +34,7 @@ public_router = APIRouter()
def _resolve_templates_directory() -> str: def _resolve_templates_directory() -> str:
# Support source layout, Docker runtime (/app), and installed package data. # Support source layout, Docker runtime (/app), and installed package data.
source_layout_path = Path( source_layout_path = Path(__file__).resolve().parents[3] / "web" / "templates"
__file__).resolve().parents[3] / "web" / "templates"
if source_layout_path.is_dir(): if source_layout_path.is_dir():
return str(source_layout_path) return str(source_layout_path)
@@ -52,8 +43,7 @@ def _resolve_templates_directory() -> str:
return str(docker_runtime_path) return str(docker_runtime_path)
try: try:
package_path = resources.files( package_path = resources.files("arbitrade").joinpath("web", "templates")
"arbitrade").joinpath("web", "templates")
if package_path.is_dir(): if package_path.is_dir():
return str(package_path) return str(package_path)
except (ModuleNotFoundError, AttributeError): except (ModuleNotFoundError, AttributeError):
@@ -242,11 +232,9 @@ async def _dashboard_pairings_response(
if source: if source:
pairings = [p for p in pairings if p.source.lower() == source.lower()] pairings = [p for p in pairings if p.source.lower() == source.lower()]
if base: if base:
pairings = [p for p in pairings if p.base_asset.lower() == pairings = [p for p in pairings if p.base_asset.lower() == base.lower()]
base.lower()]
if quote: if quote:
pairings = [p for p in pairings if p.quote_asset.lower() == pairings = [p for p in pairings if p.quote_asset.lower() == quote.lower()]
quote.lower()]
# Sort # Sort
reverse = order.lower() == "desc" reverse = order.lower() == "desc"
@@ -391,8 +379,7 @@ async def _dashboard_overview(request: Request) -> dict[str, object]:
LIMIT 1 LIMIT 1
""") """)
if acct_row is not None: if acct_row is not None:
fee_tier = str( fee_tier = str(acct_row["fee_tier"]) if acct_row["fee_tier"] is not None else ""
acct_row["fee_tier"]) if acct_row["fee_tier"] is not None else ""
maker_fee = ( maker_fee = (
f"{float(acct_row['maker_fee']):.4%}" f"{float(acct_row['maker_fee']):.4%}"
if acct_row["maker_fee"] is not None if acct_row["maker_fee"] is not None
@@ -420,8 +407,7 @@ async def _dashboard_overview(request: Request) -> dict[str, object]:
try: try:
parsed = json.loads(balances_raw) parsed = json.loads(balances_raw)
if isinstance(parsed, dict): if isinstance(parsed, dict):
non_zero = {k: float(v) non_zero = {k: float(v) for k, v in parsed.items() if float(v) > 0.0}
for k, v in parsed.items() if float(v) > 0.0}
if non_zero: if non_zero:
balances_value = "<br>".join( balances_value = "<br>".join(
f"{v:.6g} {k}" for k, v in sorted(non_zero.items()) f"{v:.6g} {k}" for k, v in sorted(non_zero.items())
@@ -442,8 +428,7 @@ async def _dashboard_overview(request: Request) -> dict[str, object]:
"trade_ref": str(r["trade_ref"]), "trade_ref": str(r["trade_ref"]),
"status": str(r["status"]), "status": str(r["status"]),
"started_at": ( "started_at": (
r["started_at"].isoformat() if isinstance( r["started_at"].isoformat() if isinstance(r["started_at"], datetime) else ""
r["started_at"], datetime) else ""
), ),
"cycle": str(r["cycle"]) if r["cycle"] is not None else "", "cycle": str(r["cycle"]) if r["cycle"] is not None else "",
} }
@@ -457,8 +442,7 @@ async def _dashboard_overview(request: Request) -> dict[str, object]:
f"{float(r['est_profit']):.2f} USD" if r["est_profit"] is not None else "" f"{float(r['est_profit']):.2f} USD" if r["est_profit"] is not None else ""
), ),
"detected_at": ( "detected_at": (
r["detected_at"].isoformat() if isinstance( r["detected_at"].isoformat() if isinstance(r["detected_at"], datetime) else ""
r["detected_at"], datetime) else ""
), ),
} }
for r in latest_opportunities for r in latest_opportunities
@@ -499,10 +483,8 @@ async def _dashboard_charts(request: Request) -> dict[str, object]:
labels.append(row["detected_at"].isoformat()) labels.append(row["detected_at"].isoformat())
else: else:
labels.append(f"opportunity-{index + 1}") labels.append(f"opportunity-{index + 1}")
np = [float(row["net_pct"]) if row["net_pct"] np = [float(row["net_pct"]) if row["net_pct"] is not None else 0.0 for row in cr]
is not None else 0.0 for row in cr] ep = [float(row["est_profit"]) if row["est_profit"] is not None else 0.0 for row in cr]
ep = [float(row["est_profit"]) if row["est_profit"]
is not None else 0.0 for row in cr]
cycles = [str(row["cycle"]) for row in cr] cycles = [str(row["cycle"]) for row in cr]
return { return {
@@ -576,8 +558,7 @@ async def _dashboard_config_context(request: Request) -> dict[str, object]:
max_consecutive_failures_value = ( max_consecutive_failures_value = (
str(rs.max_consecutive_failures) if rs.max_consecutive_failures is not None else "" str(rs.max_consecutive_failures) if rs.max_consecutive_failures is not None else ""
) )
strategy_stat_arb_enabled = bool( strategy_stat_arb_enabled = bool(getattr(rs, "strategy_enable_stat_arb_experiment", False))
getattr(rs, "strategy_enable_stat_arb_experiment", False))
return { return {
# Runtime # Runtime
@@ -698,8 +679,7 @@ def _dashboard_controls(request: Request) -> dict[str, object]:
alerts_last_channel_results = [ alerts_last_channel_results = [
str(item) for item in cast(list[object], alert_status.get("last_channel_results", [])) str(item) for item in cast(list[object], alert_status.get("last_channel_results", []))
] ]
strategy_stat_arb_enabled = bool( strategy_stat_arb_enabled = bool(getattr(rs, "strategy_enable_stat_arb_experiment", False))
getattr(rs, "strategy_enable_stat_arb_experiment", False))
return { return {
"execution_status": "running" if ctl.is_running else "stopped", "execution_status": "running" if ctl.is_running else "stopped",
@@ -755,8 +735,7 @@ async def _dashboard_backtesting_handler(request: Request) -> HTMLResponse:
try: try:
custom_fee_rate = ( custom_fee_rate = (
float(defaults["custom_fee_rate"] float(defaults["custom_fee_rate"]) if defaults["custom_fee_rate"].strip() else None
) if defaults["custom_fee_rate"].strip() else None
) )
fee_rate = await _fee_rate_for_profile( fee_rate = await _fee_rate_for_profile(
defaults["fee_profile"], custom_fee_rate, request=request defaults["fee_profile"], custom_fee_rate, request=request
@@ -767,8 +746,7 @@ async def _dashboard_backtesting_handler(request: Request) -> HTMLResponse:
if not symbols_str.strip(): if not symbols_str.strip():
pairing_repo = ConfigPairingRepository(request.app.state.store) pairing_repo = ConfigPairingRepository(request.app.state.store)
enabled = await pairing_repo.list_pairings(enabled_only=True) enabled = await pairing_repo.list_pairings(enabled_only=True)
symbols_str = ",".join( symbols_str = ",".join(f"{p.base_asset}/{p.quote_asset}" for p in enabled)
f"{p.base_asset}/{p.quote_asset}" for p in enabled)
config_dict: dict[str, object] = { config_dict: dict[str, object] = {
"source": defaults["source"], "source": defaults["source"],
@@ -882,8 +860,7 @@ async def _dashboard_backtesting_job_export(request: Request, job_id: str) -> Re
return Response( return Response(
content=orjson.dumps(payload).decode("utf-8"), content=orjson.dumps(payload).decode("utf-8"),
media_type="application/x-jsonlines", media_type="application/x-jsonlines",
headers={ headers={"Content-Disposition": f"attachment; filename=backtest_{job_id[:8]}.jsonl"},
"Content-Disposition": f"attachment; filename=backtest_{job_id[:8]}.jsonl"},
) )
+1 -2
View File
@@ -36,8 +36,7 @@ async def run_log_archive(store: PgStore, retention_days: int = _RETENTION_DAYS)
repo = LogArchiveRepository(store) repo = LogArchiveRepository(store)
count = await repo.archive_before(cutoff) count = await repo.archive_before(cutoff)
if count > 0: if count > 0:
_LOG.info("log_archive_complete", _LOG.info("log_archive_complete", cutoff=cutoff.isoformat(), archived=count)
cutoff=cutoff.isoformat(), archived=count)
return count return count
+43 -32
View File
@@ -242,6 +242,12 @@ class AuditRepository:
async def insert(self, record: AuditRecord) -> None: async def insert(self, record: AuditRecord) -> None:
async with self._store.pool.acquire() as conn: async with self._store.pool.acquire() as conn:
payload = None
if record.payload is not None:
try:
payload = orjson.dumps(record.payload).decode("utf-8")
except Exception:
payload = None
await conn.execute( await conn.execute(
""" """
INSERT INTO audit_events ( INSERT INTO audit_events (
@@ -258,8 +264,7 @@ class AuditRepository:
record.actor, record.actor,
record.event_type, record.event_type,
record.decision, record.decision,
(None if record.payload is None else orjson.dumps( payload,
record.payload).decode("utf-8")),
record.correlation_id, record.correlation_id,
) )
@@ -279,6 +284,9 @@ class AuditRepository:
for row in rows: for row in rows:
payload: dict[str, Any] | None = None payload: dict[str, Any] | None = None
raw_payload = row["payload"] raw_payload = row["payload"]
correlation_id = None
if row["correlation_id"] is not None:
correlation_id = str(row["correlation_id"])
if isinstance(raw_payload, str) and raw_payload: if isinstance(raw_payload, str) and raw_payload:
decoded = orjson.loads(raw_payload) decoded = orjson.loads(raw_payload)
if isinstance(decoded, dict): if isinstance(decoded, dict):
@@ -291,10 +299,7 @@ class AuditRepository:
event_type=str(row["event_type"]), event_type=str(row["event_type"]),
decision=str(row["decision"]), decision=str(row["decision"]),
payload=payload, payload=payload,
correlation_id=( correlation_id=correlation_id,
str(row["correlation_id"]
) if row["correlation_id"] is not None else None
),
) )
) )
@@ -354,6 +359,9 @@ class RuntimeStateRepository:
balances: dict[str, Any] | None = None balances: dict[str, Any] | None = None
raw_balances = row["last_known_balances"] raw_balances = row["last_known_balances"]
kill_switch_reason = None
if row["kill_switch_reason"] is not None:
kill_switch_reason = str(row["kill_switch_reason"])
if isinstance(raw_balances, str) and raw_balances: if isinstance(raw_balances, str) and raw_balances:
decoded = orjson.loads(raw_balances) decoded = orjson.loads(raw_balances)
if isinstance(decoded, dict): if isinstance(decoded, dict):
@@ -363,10 +371,7 @@ class RuntimeStateRepository:
snapshot_at=row["snapshot_at"], snapshot_at=row["snapshot_at"],
is_running=bool(row["is_running"]), is_running=bool(row["is_running"]),
kill_switch_active=bool(row["kill_switch_active"]), kill_switch_active=bool(row["kill_switch_active"]),
kill_switch_reason=( kill_switch_reason=kill_switch_reason,
str(row["kill_switch_reason"]
) if row["kill_switch_reason"] is not None else None
),
open_trade_count=int(row["open_trade_count"]), open_trade_count=int(row["open_trade_count"]),
last_known_balances=balances, last_known_balances=balances,
note=str(row["note"]) if row["note"] is not None else None, note=str(row["note"]) if row["note"] is not None else None,
@@ -773,11 +778,12 @@ class ConfigBacktestingDefaultsRepository:
defaults.execution_latency_ms, defaults.execution_latency_ms,
) )
if row: if row:
starting_balances = None
if row["starting_balances"] is not None:
starting_balances = orjson.loads(row["starting_balances"])
return ConfigBacktestingDefaults( return ConfigBacktestingDefaults(
starting_balances=( starting_balances=starting_balances,
orjson.loads(row["starting_balances"]
) if row["starting_balances"] else None
),
trade_capital=row["trade_capital"], trade_capital=row["trade_capital"],
min_profit_threshold=row["min_profit_threshold"], min_profit_threshold=row["min_profit_threshold"],
slippage_bps=row["slippage_bps"], slippage_bps=row["slippage_bps"],
@@ -795,11 +801,11 @@ class ConfigBacktestingDefaultsRepository:
LIMIT 1 LIMIT 1
""") """)
if row: if row:
starting_balances = None
if row["starting_balances"] is not None:
starting_balances = orjson.loads(row["starting_balances"])
return ConfigBacktestingDefaults( return ConfigBacktestingDefaults(
starting_balances=( starting_balances=starting_balances,
orjson.loads(row["starting_balances"]
) if row["starting_balances"] else None
),
trade_capital=row["trade_capital"], trade_capital=row["trade_capital"],
min_profit_threshold=row["min_profit_threshold"], min_profit_threshold=row["min_profit_threshold"],
slippage_bps=row["slippage_bps"], slippage_bps=row["slippage_bps"],
@@ -833,11 +839,11 @@ class ConfigBacktestingDefaultsRepository:
defaults.execution_latency_ms, defaults.execution_latency_ms,
) )
if row: if row:
starting_balances = None
if row["starting_balances"] is not None:
starting_balances = orjson.loads(row["starting_balances"])
return ConfigBacktestingDefaults( return ConfigBacktestingDefaults(
starting_balances=( starting_balances=starting_balances,
orjson.loads(row["starting_balances"]
) if row["starting_balances"] else None
),
trade_capital=row["trade_capital"], trade_capital=row["trade_capital"],
min_profit_threshold=row["min_profit_threshold"], min_profit_threshold=row["min_profit_threshold"],
slippage_bps=row["slippage_bps"], slippage_bps=row["slippage_bps"],
@@ -900,20 +906,20 @@ class KrakenAccountSnapshotRepository:
""") """)
if row is None: if row is None:
return None return None
trade_balance_raw = None
fee_schedule_raw = None
if row["trade_balance_raw"] is not None:
trade_balance_raw = orjson.loads(row["trade_balance_raw"])
if row["fee_schedule_raw"] is not None:
fee_schedule_raw = orjson.loads(row["fee_schedule_raw"])
return KrakenAccountSnapshot( return KrakenAccountSnapshot(
snapshot_at=row["snapshot_at"], snapshot_at=row["snapshot_at"],
fee_tier=row["fee_tier"], fee_tier=row["fee_tier"],
maker_fee=row["maker_fee"], maker_fee=row["maker_fee"],
taker_fee=row["taker_fee"], taker_fee=row["taker_fee"],
thirty_day_volume=row["thirty_day_volume"], thirty_day_volume=row["thirty_day_volume"],
trade_balance_raw=( trade_balance_raw=trade_balance_raw,
orjson.loads(row["trade_balance_raw"] fee_schedule_raw=fee_schedule_raw,
) if row["trade_balance_raw"] else None
),
fee_schedule_raw=(
orjson.loads(row["fee_schedule_raw"]
) if row["fee_schedule_raw"] else None
),
) )
@@ -1073,6 +1079,12 @@ class LogRepository:
async def insert(self, record: LogRecord) -> None: async def insert(self, record: LogRecord) -> None:
async with self._store.pool.acquire() as conn: async with self._store.pool.acquire() as conn:
context = None
if record.context:
try:
context = orjson.dumps(record.context).decode("utf-8")
except Exception:
context = None
await conn.execute( await conn.execute(
""" """
INSERT INTO app_logs (recorded_at, level, logger, message, context) INSERT INTO app_logs (recorded_at, level, logger, message, context)
@@ -1082,8 +1094,7 @@ class LogRepository:
record.level, record.level,
record.logger, record.logger,
record.message, record.message,
orjson.dumps(record.context).decode( context,
"utf-8") if record.context else None,
) )
async def query( async def query(