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 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) 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), ) starting_balances = _parse_balances(args.starting_balances) r = asyncio.run(engine.run(events, starting_balances=starting_balances)) print("Backtest report:") print(f"- processed_events: {r.processed_events}") print(f"- opportunities_seen: {r.opportunities_seen}") print(f"- trades_executed: {r.trades_executed}") print(f"- win_rate: {r.win_rate if r.win_rate is not None else 'n/a'}") print(f"- fill_rate: {r.fill_rate if r.fill_rate is not None else 'n/a'}") print(f"- realized_pnl_usd: {r.realized_pnl_usd:.4f}") print(f"- max_drawdown_usd: {r.max_drawdown_usd:.4f}") print(f"- miss_reasons: {dict(r.miss_reasons)}") print( "- execution_latency_ms: " f"p50={r.execution_latency_p50_ms or 0.0:.4f}, " f"p95={r.execution_latency_p95_ms or 0.0:.4f}, " f"p99={r.execution_latency_p99_ms or 0.0:.4f}" ) return 0 if __name__ == "__main__": raise SystemExit(main())