feat: add backtesting functionality with UI and API endpoints
CI / lint-test-build (push) Successful in 2m31s

- Introduced backtesting page and fragment in the dashboard for running backtests and viewing recent reports.
- Implemented backtest run logic with configuration options including event path, starting balances, trade capital, and fee profiles.
- Added recent backtest reports storage and retrieval.
- Created a new strategy module for statistical arbitrage experiments with validation on configuration parameters.
- Updated settings to include parameters for the statistical arbitrage strategy.
- Enhanced dashboard controls to support the new strategy mode.
- Added unit tests for backtesting functionality and strategy validation.
- Updated templates for backtesting UI integration.
This commit is contained in:
2026-06-02 09:28:22 +02:00
parent f612c8533a
commit 38e1d64437
17 changed files with 1089 additions and 165 deletions
+63 -6
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
@@ -261,7 +262,8 @@ async def test_dashboard_controls_update_runtime_state_and_config(tmp_path) -> N
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.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
@@ -273,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:
@@ -333,3 +339,54 @@ async def test_dashboard_alert_status_api_exposes_notifier_snapshot(tmp_path) ->
assert payload["enabled"] is True
assert "configured_channels" in payload
assert "last_result" in payload
async def test_backtesting_page_run_and_recent_reports_api(tmp_path) -> None:
app = create_app(Settings(DUCKDB_PATH=tmp_path / "backtesting-ui.duckdb"))
events_file = tmp_path / "replay.jsonl"
events_file.write_text(
"\n".join(
[
'{"timestamp":"2026-06-01T12:00:00Z","symbol":"BTC/USD","bids":[[99.5,10.0]],"asks":[[100.0,10.0]]}',
'{"timestamp":"2026-06-01T12:00:01Z","symbol":"ETH/BTC","bids":[[0.051,10.0]],"asks":[[0.050,10.0]]}',
'{"timestamp":"2026-06-01T12:00:02Z","symbol":"ETH/USD","bids":[[110.0,10.0]],"asks":[[110.5,10.0]]}',
]
),
encoding="utf-8",
)
transport = httpx.ASGITransport(app=app)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
page = await client.get("/dashboard/backtesting")
fragment = await client.get("/dashboard/fragment/backtesting")
run = await client.post(
"/dashboard/backtesting/run",
data={
"events_path": str(events_file),
"starting_balances": "USD=1000.0",
"trade_capital": "100.0",
"min_profit_threshold": "0.0005",
"fee_profile": "standard",
"slippage_bps": "4.0",
"execution_latency_ms": "20.0",
},
)
reports = await client.get("/dashboard/api/backtesting/reports")
assert page.status_code == 200
assert "Backtesting" in page.text
assert "/dashboard/fragment/backtesting" in page.text
assert fragment.status_code == 200
assert "Run Backtest" in fragment.text
assert "Recent Runs" in fragment.text
assert run.status_code == 200
assert "completed" in run.text
assert "Processed:" in run.text
assert reports.status_code == 200
payload = reports.json()
assert len(payload["reports"]) >= 1
assert payload["reports"][0]["status"] == "completed"