feat: Add backtesting parameter sweep support and related functionality

This commit is contained in:
2026-06-02 08:44:10 +02:00
parent 8ef8dc801d
commit f612c8533a
6 changed files with 685 additions and 7 deletions
+102
View File
@@ -0,0 +1,102 @@
from __future__ import annotations
from datetime import UTC, datetime, timedelta
from arbitrade.backtesting.replay import ReplayBookEvent
from arbitrade.backtesting.sweep import (
PromotionCriteria,
SweepResult,
build_parameter_grid,
run_parameter_search,
split_events_time_windows,
)
from arbitrade.detection.graph import CurrencyGraph
from arbitrade.exchange.models import BookLevel
def _build_cycles() -> dict[str, list]:
graph = CurrencyGraph()
graph.add_pair("USD", "BTC", "BTC/USD")
graph.add_pair("BTC", "ETH", "ETH/BTC")
graph.add_pair("ETH", "USD", "ETH/USD")
return graph.index_cycles_by_pair(graph.triangular_cycles())
def _events() -> list[ReplayBookEvent]:
base_time = datetime(2026, 6, 1, 12, 0, tzinfo=UTC)
rows: list[ReplayBookEvent] = []
for index in range(12):
tick = base_time + timedelta(seconds=index)
rows.extend(
[
ReplayBookEvent(
occurred_at=tick,
symbol="BTC/USD",
bids=(BookLevel(price=99.5, volume=10.0),),
asks=(BookLevel(price=100.0, volume=10.0),),
),
ReplayBookEvent(
occurred_at=tick,
symbol="ETH/BTC",
bids=(BookLevel(price=0.051, volume=10.0),),
asks=(BookLevel(price=0.050, volume=10.0),),
),
ReplayBookEvent(
occurred_at=tick,
symbol="ETH/USD",
bids=(BookLevel(price=110.0, volume=10.0),),
asks=(BookLevel(price=110.5, volume=10.0),),
),
]
)
return rows
def test_split_events_time_windows_returns_non_empty_train_and_test() -> None:
train, test = split_events_time_windows(_events(), train_ratio=0.7)
assert train
assert test
assert train[-1].occurred_at <= test[0].occurred_at
def test_build_parameter_grid_expands_combinations() -> None:
grid = build_parameter_grid(
theta_values=[0.0005, 0.001],
trade_capital_values=[100.0],
pair_universes=[["BTC/USD", "ETH/BTC", "ETH/USD"]],
staleness_threshold_values=[3.0, 5.0],
)
assert len(grid) == 4
def test_run_parameter_search_produces_ranked_results_with_overfit_guard() -> None:
artifacts = run_parameter_search(
events=_events(),
cycles_by_pair=_build_cycles(),
parameter_grid=build_parameter_grid(
theta_values=[0.0005, 0.001],
trade_capital_values=[75.0, 100.0],
pair_universes=[["BTC/USD", "ETH/BTC", "ETH/USD"]],
staleness_threshold_values=[5.0],
),
starting_balances={"USD": 2000.0},
train_ratio=0.7,
promotion_criteria=PromotionCriteria(
min_test_realized_pnl_usd=-1000.0,
min_test_win_rate=0.0,
min_test_fill_rate=0.0,
max_test_drawdown_usd=1_000_000.0,
max_generalization_gap_ratio=0.9,
),
)
assert artifacts.results
assert artifacts.results[0].test_score >= artifacts.results[-1].test_score
first: SweepResult = artifacts.results[0]
assert first.train_event_count > 0
assert first.test_event_count > 0
assert first.generalization_gap_ratio >= 0.0
assert isinstance(first.promotion_ready, bool)