4c59a0e4cb
- Added `sync_pairings_from_kraken` function to fetch and upsert asset pairs into the config_pairings table. - Introduced `run_pairing_sync_loop` for periodic synchronization of pairings. - Enhanced `KrakenWsClient` to manage subscribed symbols for market data feeds. - Created `build_detector_from_enabled_pairings` to initialize cycle detection based on enabled pairings. - Updated FastAPI app to start market data feed and pairing synchronization tasks. - Added new API routes for managing pairings, including listing, toggling, and syncing from Kraken. - Improved dashboard templates to display pairing options and allow user interaction for backtesting. - Refactored database queries to streamline fetching and updating of pairing data.
102 lines
3.7 KiB
Python
102 lines
3.7 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 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())
|