feat: Add dashboard controls for tradable pairs and strategy configuration
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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 }} />
|
||||
|
||||
Reference in New Issue
Block a user