from __future__ import annotations import pytest from arbitrade.detection.engine import IncrementalCycleDetector from arbitrade.detection.graph import CurrencyGraph from arbitrade.exchange.models import BookLevel from arbitrade.market_data.order_book import OrderBook def _make_book(*, bid: float, ask: float) -> OrderBook: book = OrderBook() book.apply_bids([BookLevel(price=bid, volume=2_000.0)]) book.apply_asks([BookLevel(price=ask, volume=2_000.0)]) return book def _make_book_levels( *, bids: list[tuple[float, float]], asks: list[tuple[float, float]] ) -> OrderBook: book = OrderBook() book.apply_bids([BookLevel(price=price, volume=volume) for price, volume in bids]) book.apply_asks([BookLevel(price=price, volume=volume) for price, volume in asks]) return book def test_synthetic_single_cycle_known_exact_rates() -> None: asset_pairs = { "XXBTZUSD": {"wsname": "BTC/USD"}, "XETHXXBT": {"wsname": "ETH/BTC"}, "XETHZUSD": {"wsname": "ETH/USD"}, } graph = CurrencyGraph.from_kraken_asset_pairs(asset_pairs) detector = IncrementalCycleDetector( graph.index_cycles_by_pair(graph.triangular_cycles()), fee_rate=0.001, max_depth_levels=1, ) books = { "BTC/USD": _make_book(bid=99.9, ask=100.0), "ETH/BTC": _make_book(bid=0.049, ask=0.05), "ETH/USD": _make_book(bid=5.20, ask=5.21), } scores = detector.score_updated_pair("ETH/BTC", books) assert len(scores) == 1 score = scores[0] # Known path result: 1 USD -> 0.01 BTC -> 0.2 ETH -> 1.04 USD gross. assert score.gross_rate == pytest.approx(1.04) assert score.net_rate == pytest.approx(1.04 * (1 - 0.001) ** 3) def test_synthetic_two_cycles_known_filtering_and_pair_index() -> None: asset_pairs = { "XXBTZUSD": {"wsname": "BTC/USD"}, "XETHXXBT": {"wsname": "ETH/BTC"}, "XETHZUSD": {"wsname": "ETH/USD"}, "XLTCXXBT": {"wsname": "LTC/BTC"}, "XLTCZUSD": {"wsname": "LTC/USD"}, } graph = CurrencyGraph.from_kraken_asset_pairs(asset_pairs) detector = IncrementalCycleDetector( graph.index_cycles_by_pair(graph.triangular_cycles()), min_profit_threshold=0.02, max_depth_levels=3, ) books = { # ETH cycle expected to pass threshold (~4% gross) "BTC/USD": _make_book(bid=99.9, ask=100.0), "ETH/BTC": _make_book(bid=0.049, ask=0.05), "ETH/USD": _make_book(bid=5.20, ask=5.21), # LTC cycle expected to fail threshold (<2% gross) "LTC/BTC": _make_book(bid=0.01005, ask=0.0101), "LTC/USD": _make_book(bid=1.01, ask=1.011), } scores = detector.score_updated_pair("BTC/USD", books) assert len(scores) == 1 assert scores[0].cycle.currencies == ("BTC", "ETH", "USD") assert (scores[0].net_rate - 1.0) >= 0.02 def test_synthetic_depth_scenario_known_no_result_when_not_fillable() -> None: asset_pairs = { "XXBTZUSD": {"wsname": "BTC/USD"}, "XETHXXBT": {"wsname": "ETH/BTC"}, "XETHZUSD": {"wsname": "ETH/USD"}, } graph = CurrencyGraph.from_kraken_asset_pairs(asset_pairs) detector = IncrementalCycleDetector( graph.index_cycles_by_pair(graph.triangular_cycles()), max_depth_levels=2, ) books = { # Not enough ask-side BTC/USD capacity inside top-2 levels for initial USD->BTC conversion. "BTC/USD": _make_book_levels( bids=[(99.9, 10.0)], asks=[(100.0, 0.003), (101.0, 0.003)], ), "ETH/BTC": _make_book_levels( bids=[(0.049, 10.0)], asks=[(0.05, 10.0)], ), "ETH/USD": _make_book_levels( bids=[(5.20, 10.0)], asks=[(5.21, 10.0)], ), } scores = detector.score_updated_pair("BTC/USD", books) assert scores == []