feat: Add deterministic replay backtesting engine and related documentation
CI / lint-test-build (push) Failing after 14s
CI / lint-test-build (push) Failing after 14s
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
from arbitrade.backtesting.replay import (
|
||||
BacktestConfig,
|
||||
BacktestReplayEngine,
|
||||
ReplayBookEvent,
|
||||
load_replay_events,
|
||||
)
|
||||
from arbitrade.detection.graph import CurrencyGraph
|
||||
from arbitrade.exchange.models import BookLevel
|
||||
|
||||
|
||||
def _build_cycles() -> tuple[dict[str, list], list[str]]:
|
||||
graph = CurrencyGraph()
|
||||
graph.add_pair("USD", "BTC", "BTC/USD")
|
||||
graph.add_pair("BTC", "ETH", "ETH/BTC")
|
||||
graph.add_pair("ETH", "USD", "ETH/USD")
|
||||
cycles = graph.triangular_cycles()
|
||||
return graph.index_cycles_by_pair(cycles), ["BTC/USD", "ETH/BTC", "ETH/USD"]
|
||||
|
||||
|
||||
def test_load_replay_events_orders_jsonl_by_timestamp(tmp_path: Path) -> None:
|
||||
path = tmp_path / "replay.jsonl"
|
||||
path.write_text(
|
||||
"\n".join(
|
||||
[
|
||||
'{"timestamp":"2026-06-01T12:00:02Z","symbol":"ETH/USD","bids":[[100.0,1.0]],"asks":[[101.0,1.0]]}',
|
||||
'{"timestamp":"2026-06-01T12:00:01Z","symbol":"BTC/USD","bids":[[10.0,1.0]],"asks":[[11.0,1.0]]}',
|
||||
]
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
events = load_replay_events(path)
|
||||
|
||||
assert [event.symbol for event in events] == ["BTC/USD", "ETH/USD"]
|
||||
|
||||
|
||||
def test_backtest_replay_engine_runs_deterministically() -> None:
|
||||
cycles_by_pair, available_pairs = _build_cycles()
|
||||
started_at = datetime(2026, 6, 1, 12, 0, tzinfo=UTC)
|
||||
replay_events = [
|
||||
ReplayBookEvent(
|
||||
occurred_at=started_at,
|
||||
symbol="BTC/USD",
|
||||
bids=(BookLevel(price=99.5, volume=10.0),),
|
||||
asks=(BookLevel(price=100.0, volume=10.0),),
|
||||
),
|
||||
ReplayBookEvent(
|
||||
occurred_at=started_at + timedelta(seconds=1),
|
||||
symbol="ETH/BTC",
|
||||
bids=(BookLevel(price=0.051, volume=10.0),),
|
||||
asks=(BookLevel(price=0.050, volume=10.0),),
|
||||
),
|
||||
ReplayBookEvent(
|
||||
occurred_at=started_at + timedelta(seconds=2),
|
||||
symbol="ETH/USD",
|
||||
bids=(BookLevel(price=110.0, volume=10.0),),
|
||||
asks=(BookLevel(price=110.5, volume=10.0),),
|
||||
),
|
||||
]
|
||||
|
||||
engine = BacktestReplayEngine(
|
||||
cycles_by_pair=cycles_by_pair,
|
||||
available_pairs=available_pairs,
|
||||
config=BacktestConfig(trade_capital=100.0,
|
||||
slippage_bps=5.0, execution_latency_ms=10.0),
|
||||
started_at=started_at,
|
||||
)
|
||||
|
||||
report = asyncio.run(engine.run(
|
||||
replay_events, starting_balances={"USD": 1000.0}))
|
||||
|
||||
assert report.processed_events == 3
|
||||
assert report.opportunities_seen >= 0
|
||||
assert report.trades_executed >= 0
|
||||
assert isinstance(report.realized_pnl_usd, float)
|
||||
assert report.max_drawdown_usd >= 0.0
|
||||
assert report.execution_latency_p50_ms is None
|
||||
assert report.execution_latency_p95_ms is None
|
||||
assert report.execution_latency_p99_ms is None
|
||||
Reference in New Issue
Block a user