feat: Add dashboard controls for tradable pairs and strategy configuration

This commit is contained in:
2026-06-01 15:13:05 +02:00
parent 7c86e838fa
commit d742577484
5 changed files with 140 additions and 5 deletions
+2
View File
@@ -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
+4
View File
@@ -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:
+35
View File
@@ -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,
},
)
+25 -5
View File
@@ -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:
+74
View File
@@ -16,6 +16,10 @@
<div class="meta">Trade capital: {{ trade_capital_usd }}</div>
<div class="meta">Max trade capital: {{ max_trade_capital_usd }}</div>
<div class="meta">Max concurrent trades: {{ max_concurrent_trades }}</div>
<div class="meta">Tradable pairs: {{ tradable_pairs_display }}</div>
<div class="meta">Strategy mode: {{ strategy_mode }}</div>
<div class="meta">Profit threshold: {{ strategy_profit_threshold }}</div>
<div class="meta">Max depth levels: {{ strategy_max_depth_levels }}</div>
</article>
<article class="card">
<div class="label">Alerting</div>
@@ -109,6 +113,76 @@
value="{{ max_concurrent_trades_value }}"
/>
</label>
<label class="field">
<span>Tradable pairs</span>
<input
name="tradable_pairs"
type="text"
placeholder="BTC/USD, ETH/BTC"
value="{{ tradable_pairs_value }}"
/>
</label>
<label class="field">
<span>Strategy mode</span>
<select name="strategy_mode">
<option
value="incremental"
{%
if
strategy_mode=""
="incremental"
%}selected{%
endif
%}
>
incremental
</option>
<option
value="paper"
{%
if
strategy_mode=""
="paper"
%}selected{%
endif
%}
>
paper
</option>
<option
value="live"
{%
if
strategy_mode=""
="live"
%}selected{%
endif
%}
>
live
</option>
</select>
</label>
<label class="field">
<span>Strategy profit threshold</span>
<input
name="strategy_profit_threshold"
type="number"
min="0"
step="0.0001"
value="{{ strategy_profit_threshold }}"
/>
</label>
<label class="field">
<span>Max depth levels</span>
<input
name="strategy_max_depth_levels"
type="number"
min="1"
step="1"
value="{{ strategy_max_depth_levels }}"
/>
</label>
<label class="field checkbox">
{% set check = "checked" if paper_trading_mode == "enabled" else "" %}
<input name="paper_trading_mode" type="checkbox" {{ check }} />