Files
arbitrade/scripts/backtest_replay.py
T
zwitschi c8e3daeb57
CI / lint-test-build (push) Failing after 12s
Refactor code for improved readability and consistency
- Consolidated multiline string formatting into single-line for SQL queries in multiple files.
- Adjusted argument formatting in function calls for better alignment and readability.
- Removed unnecessary line breaks and improved spacing in various sections of the codebase.
- Updated test cases to maintain consistency in formatting and improve clarity.
2026-06-04 19:04:30 +02:00

106 lines
3.8 KiB
Python

from __future__ import annotations
import argparse
import asyncio
from collections.abc import Mapping
from datetime import UTC, datetime
from pathlib import Path
import duckdb
from arbitrade.backtesting.replay import BacktestConfig, BacktestReplayEngine, load_replay_events
from arbitrade.detection.graph import CurrencyGraph, TriangularCycle
def _resolve_fee_rate(fee_rate: float | None, db_path: str | None = None) -> float:
"""Resolve fee rate from arg or DB snapshot. Falls back to 0.0026."""
if fee_rate is not None:
return fee_rate
if db_path is not None:
try:
conn = duckdb.connect(db_path)
row = conn.execute(
"""
SELECT maker_fee FROM kraken_account_snapshots
ORDER BY snapshot_at DESC LIMIT 1
"""
).fetchone()
conn.close()
if row is not None and row[0] is not None:
return float(row[0])
except Exception:
pass
return 0.0026 # ultimate fallback
def _build_graph() -> tuple[dict[str, list[TriangularCycle]], 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 _parse_balances(raw: str) -> Mapping[str, float]:
balances: dict[str, float] = {}
for entry in raw.split(","):
if not entry.strip():
continue
asset, value = entry.split("=", 1)
balances[asset.strip().upper()] = float(value)
return balances
def main() -> int:
parser = argparse.ArgumentParser(description="Run a deterministic replay backtest.")
parser.add_argument("--events", type=Path, required=True)
parser.add_argument("--starting-balances", type=str, default="USD=1000.0")
parser.add_argument("--trade-capital", type=float, default=100.0)
parser.add_argument("--fee-rate", type=float, default=None)
parser.add_argument("--slippage-bps", type=float, default=4.0)
parser.add_argument("--execution-latency-ms", type=float, default=20.0)
parser.add_argument("--db-path", type=str, default=None, help="DuckDB path for fee lookup")
args = parser.parse_args()
cycles_by_pair, available_pairs = _build_graph()
events = load_replay_events(args.events)
fee_rate = _resolve_fee_rate(args.fee_rate, args.db_path)
config = BacktestConfig(
fee_rate=fee_rate,
trade_capital=args.trade_capital,
slippage_bps=args.slippage_bps,
execution_latency_ms=args.execution_latency_ms,
)
engine = BacktestReplayEngine(
cycles_by_pair=cycles_by_pair,
available_pairs=available_pairs,
config=config,
started_at=events[0].occurred_at if events else datetime.now(UTC),
)
report = asyncio.run(
engine.run(events, starting_balances=_parse_balances(args.starting_balances))
)
print("Backtest report:")
print(f"- processed_events: {report.processed_events}")
print(f"- opportunities_seen: {report.opportunities_seen}")
print(f"- trades_executed: {report.trades_executed}")
print(f"- win_rate: {report.win_rate if report.win_rate is not None else 'n/a'}")
print(f"- fill_rate: {report.fill_rate if report.fill_rate is not None else 'n/a'}")
print(f"- realized_pnl_usd: {report.realized_pnl_usd:.4f}")
print(f"- max_drawdown_usd: {report.max_drawdown_usd:.4f}")
print(f"- miss_reasons: {dict(report.miss_reasons)}")
print(
"- execution_latency_ms: "
f"p50={report.execution_latency_p50_ms or 0.0:.4f}, "
f"p95={report.execution_latency_p95_ms or 0.0:.4f}, "
f"p99={report.execution_latency_p99_ms or 0.0:.4f}"
)
return 0
if __name__ == "__main__":
raise SystemExit(main())