feat: Add backtesting parameter sweep support and related functionality
This commit is contained in:
@@ -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)
|
||||
Reference in New Issue
Block a user