diff --git a/CHANGELOG.md b/CHANGELOG.md index 81bdfa5..4edc7b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ - Added synthetic latency profiler scenarios and CLI scripts for baseline generation and regression checks. - Added latency baseline/threshold artifacts and CI latency guardrail enforcement. - Added deterministic replay backtesting engine, CLI script, and unit coverage for JSONL event replay. +- Added dashboard controls for tradable pair universe selection and strategy mode/parameter configuration. ### Changed @@ -27,6 +28,7 @@ - Added explicit Kraken API key permission configuration (`KRAKEN_API_KEY_PERMISSIONS`) and docs for least-privilege key usage. - Optimized dashboard metrics aggregation to use DuckDB SQL aggregates/quantiles instead of Python row scans. - Added backtesting usage and replay format documentation to README. +- Dashboard controls now surface tradable pairs and strategy config snapshot values. ### Removed diff --git a/src/arbitrade/api/control_state.py b/src/arbitrade/api/control_state.py index 9c6d875..b715dcc 100644 --- a/src/arbitrade/api/control_state.py +++ b/src/arbitrade/api/control_state.py @@ -10,6 +10,10 @@ from arbitrade.risk.kill_switch import KillSwitch class DashboardControlState: is_running: bool = True kill_switch: KillSwitch = field(default_factory=KillSwitch) + tradable_pairs: list[str] = field(default_factory=list) + strategy_mode: str = "incremental" + strategy_profit_threshold: float = 0.0005 + strategy_max_depth_levels: int = 10 updated_at: datetime = field(default_factory=lambda: datetime.now(UTC)) def mark_updated(self) -> None: diff --git a/src/arbitrade/api/routes.py b/src/arbitrade/api/routes.py index 312b888..2f5bbf3 100644 --- a/src/arbitrade/api/routes.py +++ b/src/arbitrade/api/routes.py @@ -271,6 +271,9 @@ def _dashboard_controls(request: Request) -> dict[str, object]: 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" + ) return { "execution_status": "running" if controls.is_running else "stopped", @@ -307,6 +310,11 @@ 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(), "start_endpoint": "/dashboard/control/start", "stop_endpoint": "/dashboard/control/stop", @@ -327,6 +335,18 @@ def _form_bool(value: str | None) -> bool: return value.lower() in {"1", "true", "yes", "on"} +def _parse_comma_separated_list(value: str | None) -> list[str]: + if value is None: + return [] + + items: list[str] = [] + for raw_item in value.split(","): + item = raw_item.strip().upper() + if item and item not in items: + items.append(item) + return items + + async def _dashboard_response( request: Request, template_name: str = "dashboard.html" ) -> HTMLResponse: @@ -514,6 +534,17 @@ async def dashboard_control_config(request: Request) -> HTMLResponse: max_concurrent_value = form["max_concurrent_trades"].strip() settings.max_concurrent_trades = int(max_concurrent_value) if max_concurrent_value else None + controls.tradable_pairs = _parse_comma_separated_list(form.get("tradable_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"]) + settings.paper_trading_mode = _form_bool(form.get("paper_trading_mode")) controls.mark_updated() @@ -549,6 +580,10 @@ async def dashboard_control_config(request: Request) -> HTMLResponse: "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, }, ) diff --git a/tests/test_dashboard.py b/tests/test_dashboard.py index c2f5b18..4e99090 100644 --- a/tests/test_dashboard.py +++ b/tests/test_dashboard.py @@ -191,7 +191,8 @@ async def test_dashboard_page_and_fragment_and_sse(tmp_path) -> None: assert "trade-open" in overview.text assert overview_stream.status_code == 200 - assert overview_stream.headers["content-type"].startswith("text/event-stream") + assert overview_stream.headers["content-type"].startswith( + "text/event-stream") assert "event: overview" in overview_stream.text assert "trade-open" in overview_stream.text @@ -202,6 +203,8 @@ async def test_dashboard_page_and_fragment_and_sse(tmp_path) -> None: assert "Last result" in controls.text assert "Paper trading mode" in controls.text assert "Trade capital USD" in controls.text + assert "Tradable pairs" in controls.text + assert "Strategy mode" in controls.text assert charts.status_code == 200 assert "Opportunity Trend" in charts.text @@ -229,6 +232,10 @@ async def test_dashboard_controls_update_runtime_state_and_config(tmp_path) -> N "trade_capital_usd": "250.50", "max_trade_capital_usd": "300.00", "max_concurrent_trades": "4", + "tradable_pairs": "BTC/USD, ETH/BTC, BTC/USD", + "strategy_mode": "paper", + "strategy_profit_threshold": "0.0025", + "strategy_max_depth_levels": "7", "paper_trading_mode": "on", }, ) @@ -247,10 +254,19 @@ async def test_dashboard_controls_update_runtime_state_and_config(tmp_path) -> N assert "250.50 USD" in config_response.text assert "300.00 USD" in config_response.text assert "4" in config_response.text + assert "BTC/USD, ETH/BTC" in config_response.text + assert "paper" in config_response.text + assert "0.002500" in config_response.text + assert "7" in config_response.text assert app.state.settings.trade_capital_usd == 250.5 assert app.state.settings.max_trade_capital_usd == 300.0 assert app.state.settings.max_concurrent_trades == 4 assert app.state.settings.paper_trading_mode is True + assert app.state.dashboard_controls.tradable_pairs == [ + "BTC/USD", "ETH/BTC"] + assert app.state.dashboard_controls.strategy_mode == "paper" + assert app.state.dashboard_controls.strategy_profit_threshold == 0.0025 + assert app.state.dashboard_controls.strategy_max_depth_levels == 7 transport = httpx.ASGITransport(app=app) async with httpx.AsyncClient(transport=transport, base_url="http://test") as client: @@ -259,10 +275,14 @@ async def test_dashboard_controls_update_runtime_state_and_config(tmp_path) -> N assert audit_recent.status_code == 200 entries = audit_recent.json()["entries"] assert len(entries) >= 4 - assert any(entry["event_type"] == "dashboard.control.stop" for entry in entries) - assert any(entry["event_type"] == "dashboard.control.start" for entry in entries) - assert any(entry["event_type"] == "dashboard.control.kill_switch" for entry in entries) - assert any(entry["event_type"] == "dashboard.control.config" for entry in entries) + assert any(entry["event_type"] == + "dashboard.control.stop" for entry in entries) + assert any(entry["event_type"] == + "dashboard.control.start" for entry in entries) + assert any(entry["event_type"] == + "dashboard.control.kill_switch" for entry in entries) + assert any(entry["event_type"] == + "dashboard.control.config" for entry in entries) async def test_dashboard_controls_emit_alerts(tmp_path) -> None: diff --git a/web/templates/partials/controls.html b/web/templates/partials/controls.html index 515c247..fadfd56 100644 --- a/web/templates/partials/controls.html +++ b/web/templates/partials/controls.html @@ -16,6 +16,10 @@
Trade capital: {{ trade_capital_usd }}
Max trade capital: {{ max_trade_capital_usd }}
Max concurrent trades: {{ max_concurrent_trades }}
+
Tradable pairs: {{ tradable_pairs_display }}
+
Strategy mode: {{ strategy_mode }}
+
Profit threshold: {{ strategy_profit_threshold }}
+
Max depth levels: {{ strategy_max_depth_levels }}
Alerting
@@ -109,6 +113,76 @@ value="{{ max_concurrent_trades_value }}" /> + + + +