feat: add backtesting functionality with UI and API endpoints
CI / lint-test-build (push) Successful in 2m31s
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:
+63
-6
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user