84 lines
2.8 KiB
Python
84 lines
2.8 KiB
Python
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
|