diff --git a/src/arbitrade/api/routes.py b/src/arbitrade/api/routes.py index 2f5bbf3..6ec3344 100644 --- a/src/arbitrade/api/routes.py +++ b/src/arbitrade/api/routes.py @@ -86,7 +86,7 @@ def _dashboard_overview(request: Request) -> dict[str, object]: ORDER BY started_at DESC LIMIT 5 """).fetchall() - pnl_total_row = conn.execute(""" + rpnl = conn.execute(""" SELECT COALESCE(SUM(COALESCE(realized_pnl, 0)), 0) FROM trades """).fetchone() @@ -131,7 +131,7 @@ def _dashboard_overview(request: Request) -> dict[str, object]: "total_value": total_value, "open_trade_count": len(open_trade_rows), "open_trades": open_trade_rows, - "realized_pnl_total": f"{float(pnl_total_row[0]):.2f} USD" if pnl_total_row else "—", + "realized_pnl_total": f"{float(rpnl[0]):.2f} USD" if rpnl else "—", "opportunities": opportunity_rows, } @@ -146,21 +146,23 @@ def _dashboard_charts(request: Request) -> dict[str, object]: LIMIT 10 """).fetchall() - chart_rows = list(reversed(opportunity_rows)) - labels = [ - row[0].isoformat() if isinstance(row[0], datetime) else f"opportunity-{index + 1}" - for index, row in enumerate(chart_rows) - ] - net_pct_values = [float(row[2]) if row[2] is not None else 0.0 for row in chart_rows] - est_profit_values = [float(row[3]) if row[3] is not None else 0.0 for row in chart_rows] - cycles = [str(row[1]) for row in chart_rows] + cr = list(reversed(opportunity_rows)) + labels = [] + for index, row in enumerate(cr): + if isinstance(row[0], datetime): + labels.append(row[0].isoformat()) + else: + labels.append(f"opportunity-{index + 1}") + np = [float(row[2]) if row[2] is not None else 0.0 for row in cr] + ep = [float(row[3]) if row[3] is not None else 0.0 for row in cr] + cycles = [str(row[1]) for row in cr] return { "labels": labels, - "net_pct_values": net_pct_values, - "est_profit_values": est_profit_values, + "net_pct_values": np, + "est_profit_values": ep, "cycles": cycles, - "has_chart_data": bool(chart_rows), + "has_chart_data": bool(cr), "generated_at": datetime.now(UTC).isoformat(), } @@ -186,13 +188,17 @@ def _record_audit( if repository is None: return correlation_id = request.headers.get("x-request-id") + if payload is not None: + ret_pl = {str(key): payload[key] for key in payload} + else: + ret_pl = None repository.insert( AuditRecord( occurred_at=datetime.now(UTC), actor=actor, event_type=event_type, decision=decision, - payload=None if payload is None else {str(key): payload[key] for key in payload}, + payload=ret_pl, correlation_id=correlation_id, ) ) @@ -254,8 +260,8 @@ def _alert_status_snapshot(request: Request) -> dict[str, object]: def _dashboard_controls(request: Request) -> dict[str, object]: - controls = _dashboard_controls_state(request) - settings = request.app.state.settings + ctl = _dashboard_controls_state(request) + rs = request.app.state.settings alert_status = _alert_status_snapshot(request) last_event = alert_status.get("last_event") last_event_title = "—" @@ -264,44 +270,47 @@ def _dashboard_controls(request: Request) -> dict[str, object]: if isinstance(title_value, str): last_event_title = title_value - configured_channels = alert_status.get("configured_channels") - channels_display = "—" - if isinstance(configured_channels, list) and configured_channels: - channels_display = ", ".join(str(channel) for channel in configured_channels) + cc = alert_status.get("configured_channels") + cd = "—" + if isinstance(cc, list) and cc: + cd = ", ".join(str(channel) for channel in cc) - dedup_seconds_raw = alert_status.get("dedup_seconds", 0.0) - dedup_seconds = float(dedup_seconds_raw) if isinstance(dedup_seconds_raw, int | float) else 0.0 - tradable_pairs_display = ( - ", ".join(controls.tradable_pairs) if controls.tradable_pairs else "All" + ddsr = alert_status.get("dedup_seconds", 0.0) + dds = float(ddsr) if isinstance(ddsr, int | float) else 0.0 + tpd = ", ".join(ctl.tradable_pairs) if ctl.tradable_pairs else "All" + max_trade_capital_usd = ( + f"{float(rs.max_trade_capital_usd):.2f} USD" + if rs.max_trade_capital_usd is not None + else "—" ) + max_trade_capital_usd_value = ( + f"{float(rs.max_trade_capital_usd):.2f}" if rs.max_trade_capital_usd is not None else "" + ) + max_concurrent_trades = ( + str(rs.max_concurrent_trades) if rs.max_concurrent_trades is not None else "—" + ) + max_concurrent_trades_value = ( + str(rs.max_concurrent_trades) if rs.max_concurrent_trades is not None else "" + ) + alerts_last_channel_results = [ + str(item) for item in cast(list[object], alert_status.get("last_channel_results", [])) + ] return { - "execution_status": "running" if controls.is_running else "stopped", - "kill_switch_status": "active" if controls.kill_switch.is_active else "inactive", - "kill_switch_reason": controls.kill_switch.reason or "—", - "paper_trading_mode": "enabled" if settings.paper_trading_mode else "disabled", - "trade_capital_usd": f"{float(settings.trade_capital_usd):.2f} USD", - "trade_capital_usd_value": f"{float(settings.trade_capital_usd):.2f}", - "max_trade_capital_usd": ( - "—" - if settings.max_trade_capital_usd is None - else f"{float(settings.max_trade_capital_usd):.2f} USD" - ), - "max_trade_capital_usd_value": ( - "" - if settings.max_trade_capital_usd is None - else f"{float(settings.max_trade_capital_usd):.2f}" - ), - "max_concurrent_trades": ( - "—" if settings.max_concurrent_trades is None else str(settings.max_concurrent_trades) - ), - "max_concurrent_trades_value": ( - "" if settings.max_concurrent_trades is None else str(settings.max_concurrent_trades) - ), + "execution_status": "running" if ctl.is_running else "stopped", + "kill_switch_status": "active" if ctl.kill_switch.is_active else "inactive", + "kill_switch_reason": ctl.kill_switch.reason or "—", + "paper_trading_mode": "enabled" if rs.paper_trading_mode else "disabled", + "trade_capital_usd": f"{float(rs.trade_capital_usd):.2f} USD", + "trade_capital_usd_value": f"{float(rs.trade_capital_usd):.2f}", + "max_trade_capital_usd": max_trade_capital_usd, + "max_trade_capital_usd_value": max_trade_capital_usd_value, + "max_concurrent_trades": max_concurrent_trades, + "max_concurrent_trades_value": max_concurrent_trades_value, "alerts_enabled": "enabled" if bool(alert_status.get("enabled", False)) else "disabled", - "alerts_channels": channels_display, + "alerts_channels": cd, "alerts_min_severity": str(alert_status.get("min_severity", "—")), - "alerts_dedup_seconds": f"{dedup_seconds:.0f}", + "alerts_dedup_seconds": f"{dds:.0f}", "alerts_last_result": str(alert_status.get("last_result", "unavailable")), "alerts_last_attempted_at": str(alert_status.get("last_attempted_at") or "—"), "alerts_last_success_at": str(alert_status.get("last_success_at") or "—"), @@ -310,12 +319,12 @@ def _dashboard_controls(request: Request) -> dict[str, object]: "alerts_last_channel_results": [ str(item) for item in cast(list[object], alert_status.get("last_channel_results", [])) ], - "tradable_pairs_display": tradable_pairs_display, - "tradable_pairs_value": ", ".join(controls.tradable_pairs), - "strategy_mode": controls.strategy_mode, - "strategy_profit_threshold": f"{controls.strategy_profit_threshold:.6f}", - "strategy_max_depth_levels": str(controls.strategy_max_depth_levels), - "updated_at": controls.updated_at.isoformat(), + "tradable_pairs_display": tpd, + "tradable_pairs_value": ", ".join(ctl.tradable_pairs), + "strategy_mode": ctl.strategy_mode, + "strategy_profit_threshold": f"{ctl.strategy_profit_threshold:.6f}", + "strategy_max_depth_levels": str(ctl.strategy_max_depth_levels), + "updated_at": ctl.updated_at.isoformat(), "start_endpoint": "/dashboard/control/start", "stop_endpoint": "/dashboard/control/stop", "kill_switch_endpoint": "/dashboard/control/kill-switch", @@ -519,34 +528,38 @@ async def dashboard_control_kill_switch(request: Request) -> HTMLResponse: @router.post("/dashboard/control/config", response_class=HTMLResponse) async def dashboard_control_config(request: Request) -> HTMLResponse: - controls = _dashboard_controls_state(request) - settings = request.app.state.settings + ctl = _dashboard_controls_state(request) + rs = request.app.state.settings form = _parse_form_body(await request.body()) if "trade_capital_usd" in form and form["trade_capital_usd"]: - settings.trade_capital_usd = float(form["trade_capital_usd"]) + rs.trade_capital_usd = float(form["trade_capital_usd"]) if "max_trade_capital_usd" in form: - max_trade_capital_value = form["max_trade_capital_usd"].strip() - settings.max_trade_capital_usd = ( - float(max_trade_capital_value) if max_trade_capital_value else None - ) + mtcv = form["max_trade_capital_usd"].strip() + rs.max_trade_capital_usd = float(mtcv) if mtcv else None if "max_concurrent_trades" in form: - max_concurrent_value = form["max_concurrent_trades"].strip() - settings.max_concurrent_trades = int(max_concurrent_value) if max_concurrent_value else None + mcv = form["max_concurrent_trades"].strip() + rs.max_concurrent_trades = int(mcv) if mcv else None - controls.tradable_pairs = _parse_comma_separated_list(form.get("tradable_pairs")) + form_pairs = form.get("tradable_pairs") + ctl.tradable_pairs = _parse_comma_separated_list(form_pairs) if "strategy_mode" in form and form["strategy_mode"].strip(): strategy_mode = form["strategy_mode"].strip().lower() if strategy_mode not in {"incremental", "paper", "live"}: - raise ValueError("strategy_mode must be one of: incremental, paper, live") - controls.strategy_mode = strategy_mode - if "strategy_profit_threshold" in form and form["strategy_profit_threshold"].strip(): - controls.strategy_profit_threshold = float(form["strategy_profit_threshold"]) - if "strategy_max_depth_levels" in form and form["strategy_max_depth_levels"].strip(): - controls.strategy_max_depth_levels = int(form["strategy_max_depth_levels"]) + e = "strategy_mode must be one of: incremental, paper, live" + raise ValueError(e) + ctl.strategy_mode = strategy_mode + if "strategy_profit_threshold" in form: + if form["strategy_profit_threshold"].strip(): + spt = float(form["strategy_profit_threshold"]) + ctl.strategy_profit_threshold = spt + if "strategy_max_depth_levels" in form: + if form["strategy_max_depth_levels"].strip(): + smdl = int(form["strategy_max_depth_levels"]) + ctl.strategy_max_depth_levels = smdl - settings.paper_trading_mode = _form_bool(form.get("paper_trading_mode")) - controls.mark_updated() + rs.paper_trading_mode = _form_bool(form.get("paper_trading_mode")) + ctl.mark_updated() notifier = _alert_notifier(request) if notifier is not None: @@ -556,18 +569,14 @@ async def dashboard_control_config(request: Request) -> HTMLResponse: title="Runtime config updated", message="Dashboard control updated runtime risk and execution settings.", details={ - "trade_capital_usd": f"{settings.trade_capital_usd}", + "trade_capital_usd": f"{rs.trade_capital_usd}", "max_trade_capital_usd": ( - "none" - if settings.max_trade_capital_usd is None - else f"{settings.max_trade_capital_usd}" + "none" if rs.max_trade_capital_usd is None else f"{rs.max_trade_capital_usd}" ), "max_concurrent_trades": ( - "none" - if settings.max_concurrent_trades is None - else f"{settings.max_concurrent_trades}" + "none" if rs.max_concurrent_trades is None else f"{rs.max_concurrent_trades}" ), - "paper_trading_mode": "true" if settings.paper_trading_mode else "false", + "paper_trading_mode": "true" if rs.paper_trading_mode else "false", }, ) _record_audit( @@ -576,14 +585,14 @@ async def dashboard_control_config(request: Request) -> HTMLResponse: event_type="dashboard.control.config", decision="approved", payload={ - "trade_capital_usd": settings.trade_capital_usd, - "max_trade_capital_usd": settings.max_trade_capital_usd, - "max_concurrent_trades": settings.max_concurrent_trades, - "paper_trading_mode": settings.paper_trading_mode, - "tradable_pairs": controls.tradable_pairs, - "strategy_mode": controls.strategy_mode, - "strategy_profit_threshold": controls.strategy_profit_threshold, - "strategy_max_depth_levels": controls.strategy_max_depth_levels, + "trade_capital_usd": rs.trade_capital_usd, + "max_trade_capital_usd": rs.max_trade_capital_usd, + "max_concurrent_trades": rs.max_concurrent_trades, + "paper_trading_mode": rs.paper_trading_mode, + "tradable_pairs": ctl.tradable_pairs, + "strategy_mode": ctl.strategy_mode, + "strategy_profit_threshold": ctl.strategy_profit_threshold, + "strategy_max_depth_levels": ctl.strategy_max_depth_levels, }, ) diff --git a/src/arbitrade/metrics.py b/src/arbitrade/metrics.py index 6273519..aadaf32 100644 --- a/src/arbitrade/metrics.py +++ b/src/arbitrade/metrics.py @@ -24,7 +24,7 @@ class MetricsCalculator: def compute(self) -> PerformanceMetrics: with self._store.connect() as conn: - trade_metrics = conn.execute(""" + tm = conn.execute(""" SELECT COALESCE(SUM(COALESCE(realized_pnl, 0)), 0) AS realized_pnl_usd, COUNT(*) AS total_trades, @@ -46,7 +46,7 @@ class MetricsCalculator: WHERE finished_at IS NOT NULL """).fetchone() - opportunity_metrics = conn.execute(""" + om = conn.execute(""" SELECT COUNT(*) AS opportunity_count, MIN(detected_at) AS first_detected_at, @@ -54,79 +54,47 @@ class MetricsCalculator: FROM opportunities """).fetchone() - fill_metrics = conn.execute(""" + fm = conn.execute(""" SELECT AVG(filled_volume / volume) AS fill_rate FROM orders WHERE volume > 0 AND filled_volume IS NOT NULL """).fetchone() - realized_pnl_usd = ( - float(trade_metrics[0]) if trade_metrics and trade_metrics[0] is not None else 0.0 - ) - total_trades = ( - int(trade_metrics[1]) if trade_metrics and trade_metrics[1] is not None else 0 - ) - winning_trades = ( - int(trade_metrics[2]) if trade_metrics and trade_metrics[2] is not None else 0 - ) - win_rate = winning_trades / total_trades if total_trades > 0 else None + r_pnl_usd = float(tm[0]) if tm and tm[0] is not None else 0.0 + tt = int(tm[1]) if tm and tm[1] is not None else 0 + wt = int(tm[2]) if tm and tm[2] is not None else 0 + wr = wt / tt if tt > 0 else None - avg_trade_duration_seconds = ( - float(trade_metrics[3]) if trade_metrics and trade_metrics[3] is not None else None - ) + atd = float(tm[3]) if tm and tm[3] is not None else None - opportunity_count = ( - int(opportunity_metrics[0]) - if opportunity_metrics is not None and opportunity_metrics[0] is not None - else 0 - ) - first_detected_at = ( - opportunity_metrics[1] - if opportunity_metrics is not None and isinstance(opportunity_metrics[1], datetime) - else None - ) - last_detected_at = ( - opportunity_metrics[2] - if opportunity_metrics is not None and isinstance(opportunity_metrics[2], datetime) - else None - ) + oc = int(om[0]) if om is not None and om[0] is not None else 0 + fo = om[1] if om is not None and isinstance(om[1], datetime) else None + lo = om[2] if om is not None and isinstance(om[2], datetime) else None opportunities_per_minute: float | None - if ( - opportunity_count >= 2 - and first_detected_at is not None - and last_detected_at is not None - ): - span_seconds = (last_detected_at - first_detected_at).total_seconds() + if oc >= 2 and fo is not None and lo is not None: + span_seconds = (lo - fo).total_seconds() opportunities_per_minute = ( - opportunity_count / (span_seconds / 60.0) - if span_seconds > 0.0 - else float(opportunity_count) + oc / (span_seconds / 60.0) if span_seconds > 0.0 else float(oc) ) - elif opportunity_count == 1: + elif oc == 1: opportunities_per_minute = 60.0 else: opportunities_per_minute = None - fill_rate = float(fill_metrics[0]) if fill_metrics and fill_metrics[0] is not None else None + fill_rate = float(fm[0]) if fm and fm[0] is not None else None - latency_p50_seconds = ( - float(trade_metrics[4]) if trade_metrics and trade_metrics[4] is not None else None - ) - latency_p95_seconds = ( - float(trade_metrics[5]) if trade_metrics and trade_metrics[5] is not None else None - ) - latency_p99_seconds = ( - float(trade_metrics[6]) if trade_metrics and trade_metrics[6] is not None else None - ) + lp50 = float(tm[4]) if tm and tm[4] is not None else None + lp95 = float(tm[5]) if tm and tm[5] is not None else None + lp99 = float(tm[6]) if tm and tm[6] is not None else None return PerformanceMetrics( - realized_pnl_usd=realized_pnl_usd, - win_rate=win_rate, - avg_trade_duration_seconds=avg_trade_duration_seconds, + realized_pnl_usd=r_pnl_usd, + win_rate=wr, + avg_trade_duration_seconds=atd, opportunities_per_minute=opportunities_per_minute, fill_rate=fill_rate, - latency_p50_seconds=latency_p50_seconds, - latency_p95_seconds=latency_p95_seconds, - latency_p99_seconds=latency_p99_seconds, + latency_p50_seconds=lp50, + latency_p95_seconds=lp95, + latency_p99_seconds=lp99, ) diff --git a/src/arbitrade/runtime/lifecycle.py b/src/arbitrade/runtime/lifecycle.py index 124611d..c00a0e0 100644 --- a/src/arbitrade/runtime/lifecycle.py +++ b/src/arbitrade/runtime/lifecycle.py @@ -127,31 +127,32 @@ def persist_runtime_snapshot(app: FastAPI, *, note: str | None = None) -> Runtim async def restore_runtime_state(app: FastAPI) -> RuntimeRecoveryReport: - controls = _controls(app) + ctl = _controls(app) store = _store(app) - runtime_repository = _runtime_repository(app) + repo = _runtime_repository(app) restored_from_snapshot = False snapshot_at: str | None = None - latest = runtime_repository.latest() if runtime_repository is not None else None + latest = repo.latest() if repo is not None else None if latest is not None: restored_from_snapshot = True snapshot_at = latest.snapshot_at.isoformat() - controls.is_running = latest.is_running + ctl.is_running = latest.is_running if latest.kill_switch_active: - controls.kill_switch.activate(reason=latest.kill_switch_reason or "recovered") + r = latest.kill_switch_reason or "recovered" + ctl.kill_switch.activate(reason=r) else: - controls.kill_switch.deactivate() - controls.mark_updated() + ctl.kill_switch.deactivate() + ctl.mark_updated() open_trades = _open_trade_count(store) restart_guard_active = False if open_trades > 0: - controls.is_running = False - if not controls.kill_switch.is_active: - controls.kill_switch.activate(reason="recovery_open_trades_detected") - controls.mark_updated() + ctl.is_running = False + if not ctl.kill_switch.is_active: + ctl.kill_switch.activate(reason="recovery_open_trades_detected") + ctl.mark_updated() restart_guard_active = True report = RuntimeRecoveryReport(