""" Arbitrage Scanner ================= This script scans multiple cryptocurrency exchanges for arbitrage opportunities between common USDT/USD trading pairs. It currently supports the following exchanges: * Binance – uses the 24‑hour ticker endpoint (`/api/v3/ticker/24hr`) and cross‑checks with the 5‑minute average price to detect anomalies. Fees are assumed to be 0.1 % per trade (taker), as noted in Binance’s fee schedule【648781846870325†L100-L105】. * Kraken – uses the public ticker endpoint (`/0/public/Ticker`). Fees are assumed to be 0.26 % per trade (taker) based on Kraken’s volume‑tiered fee model【169809465437320†L404-L405】. * Coinbase – uses the Coinbase Exchange (formerly Coinbase Pro) ticker endpoint (`/products//ticker`). A default 0.6 % taker fee is used; adjust this according to your account tier. * Bitstamp – uses the Bitstamp ticker endpoint (`/api/v2/ticker/`). A default 0.4 % taker fee is assumed based on Bitstamp’s standard fee schedule【169809465437320†L383-L390】. The scanner iterates over a list of base assets (e.g. BTC, ETH, BNB, AVAX, LINK) and attempts to fetch price/volume data from each exchange. If a pair exists on at least two exchanges, it calculates two arbitrage directions (buy A → sell B and buy B → sell A). The gross and net spreads (after subtracting fees) are computed, and opportunities with a positive net spread are reported. Only assets with a Binance quote volume above a configurable threshold (default 10 million USDT) are considered to help avoid illiquid pairs. Usage ----- python arbitrage_scanner.py Adjust the `ASSETS` list, fee assumptions, and volume threshold as necessary. Always perform your own due diligence (including checking withdrawal fees, network delays and liquidity) before executing live trades. This script is for educational purposes only and comes with no warranty. """ import json import logging import sys from dataclasses import dataclass from typing import Dict, Callable, List, Optional, Tuple import requests logging.basicConfig( level=logging.INFO, format="[%(asctime)s] %(levelname)s: %(message)s", datefmt="%Y-%m-%d %H:%M:%S", ) logger = logging.getLogger(__name__) ################################################################################ # Exchange abstraction ################################################################################ @dataclass class Ticker: """Represents ticker data with ask, bid, and volume.""" ask: float bid: float volume: float class Exchange: """Base class for an exchange.""" def __init__(self, name: str, fee: float): self.name = name self.fee = fee # taker fee as a fraction (e.g. 0.001) def get_pair(self, base: str) -> Optional[str]: """Return the exchange-specific trading pair for a given base asset. Should return None if the pair does not exist on this exchange. """ raise NotImplementedError def fetch_ticker(self, base: str) -> Optional[Ticker]: """Fetch ticker data (ask, bid, volume) for the given base asset. Returns None if the pair is not available or an error occurs. """ raise NotImplementedError ################################################################################ # Binance implementation ################################################################################ class Binance(Exchange): """Binance exchange implementation.""" BASE_URL = "https://api.binance.com/api/v3" def __init__(self, fee: float = 0.001): super().__init__("binance", fee) def get_pair(self, base: str) -> Optional[str]: return f"{base}USDT" def fetch_ticker(self, base: str) -> Optional[Ticker]: symbol = self.get_pair(base) if not symbol: return None try: url24 = f"{self.BASE_URL}/ticker/24hr" r24 = requests.get(url24, params={"symbol": symbol}, timeout=10) if r24.status_code != 200: logger.warning("Binance 24hr ticker request failed for %s: %s", symbol, r24.text) return None data24 = r24.json() # Ask price and bid price may occasionally be zero or stale; cross‑check with 5‑minute average ask_price = float(data24.get("askPrice", 0) or 0) bid_price = float(data24.get("bidPrice", 0) or 0) volume = float(data24.get("quoteVolume", 0) or 0) # Fetch 5‑minute average price to validate url_avg = f"{self.BASE_URL}/avgPrice" ravg = requests.get(url_avg, params={"symbol": symbol}, timeout=10) avg_price = None if ravg.status_code == 200: avg_price = float(ravg.json().get("price", 0) or 0) # If ask is zero or deviates by >5 % from avg price, use avg price for ask/bid if avg_price and (ask_price <= 0 or abs(ask_price - avg_price) / avg_price > 0.05): ask_price = avg_price bid_price = avg_price # approximate # If still zero, bail out if ask_price == 0 or bid_price == 0: logger.warning("Binance returned zero ask/bid for %s", symbol) return None return Ticker(ask=ask_price, bid=bid_price, volume=volume) except Exception as e: logger.error("Error fetching Binance ticker for %s: %s", symbol, e) return None ################################################################################ # Kraken implementation ################################################################################ class Kraken(Exchange): """Kraken exchange implementation.""" BASE_URL = "https://api.kraken.com/0/public" # Mapping of base asset to Kraken pair name (USDT quote) PAIR_MAP: Dict[str, str] = { "BTC": "XBTUSDT", "ETH": "ETHUSDT", "BNB": "BNBUSDT", "AVAX": "AVAXUSDT", "LINK": "LINKUSDT", "LTC": "LTCUSDT", "DOT": "DOTUSDT", "SOL": "SOLUSDT", "ADA": "ADAUSDT", "XRP": "XRPUSDT", "DOGE": "XDGUSDT", "BCH": "BCHUSDT", } def __init__(self, fee: float = 0.0026): super().__init__("kraken", fee) def get_pair(self, base: str) -> Optional[str]: return self.PAIR_MAP.get(base) def fetch_ticker(self, base: str) -> Optional[Ticker]: pair = self.get_pair(base) if not pair: return None try: r = requests.get(f"{self.BASE_URL}/Ticker", params={"pair": pair}, timeout=10) if r.status_code != 200: logger.warning("Kraken ticker request failed for %s: %s", pair, r.text) return None data = r.json() result = data.get("result", {}) if not result: return None ticker_data = result[pair] ask = float(ticker_data["a"][0]) bid = float(ticker_data["b"][0]) volume = float(ticker_data["v"][1]) # 24h volume in base asset return Ticker(ask=ask, bid=bid, volume=volume) except Exception as e: logger.error("Error fetching Kraken ticker for %s: %s", pair, e) return None ################################################################################ # Coinbase implementation ################################################################################ class Coinbase(Exchange): """Coinbase exchange implementation.""" BASE_URL = "https://api.exchange.coinbase.com" PAIR_MAP: Dict[str, Optional[str]] = { "BTC": "BTC-USD", "ETH": "ETH-USD", "BNB": None, # BNB not traded on Coinbase "AVAX": "AVAX-USD", "LINK": "LINK-USD", "LTC": "LTC-USD", "DOT": "DOT-USD", "SOL": "SOL-USD", "ADA": "ADA-USD", "XRP": "XRP-USD", "DOGE": "DOGE-USD", "BCH": "BCH-USD", } def __init__(self, fee: float = 0.006): super().__init__("coinbase", fee) def get_pair(self, base: str) -> Optional[str]: return self.PAIR_MAP.get(base) def fetch_ticker(self, base: str) -> Optional[Ticker]: product_id = self.get_pair(base) if not product_id: return None try: r = requests.get(f"{self.BASE_URL}/products/{product_id}/ticker", timeout=10) if r.status_code != 200: logger.warning("Coinbase ticker request failed for %s: %s", product_id, r.text) return None data = r.json() ask = float(data.get("ask", 0) or 0) bid = float(data.get("bid", 0) or 0) volume = float(data.get("volume", 0) or 0) if ask == 0 or bid == 0: return None return Ticker(ask=ask, bid=bid, volume=volume) except Exception as e: logger.error("Error fetching Coinbase ticker for %s: %s", product_id, e) return None ################################################################################ # Bitstamp implementation ################################################################################ class Bitstamp(Exchange): """Bitstamp exchange implementation.""" BASE_URL = "https://www.bitstamp.net/api/v2" PAIR_MAP: Dict[str, Optional[str]] = { "BTC": "btcusd", "ETH": "ethusd", "BNB": None, # BNB not listed on Bitstamp "AVAX": "avaxusd", "LINK": "linkusd", "LTC": "ltcusd", "DOT": "dotusd", "SOL": "solusd", "ADA": "adausd", "XRP": "xrpusd", "DOGE": "dogeusd", "BCH": "bchusd", } def __init__(self, fee: float = 0.004): super().__init__("bitstamp", fee) def get_pair(self, base: str) -> Optional[str]: return self.PAIR_MAP.get(base) def fetch_ticker(self, base: str) -> Optional[Ticker]: pair = self.get_pair(base) if not pair: return None try: r = requests.get(f"{self.BASE_URL}/ticker/{pair}", timeout=10) if r.status_code != 200: logger.warning("Bitstamp ticker request failed for %s: %s", pair, r.text) return None data = r.json() ask = float(data.get("ask", 0) or 0) bid = float(data.get("bid", 0) or 0) volume = float(data.get("volume", 0) or 0) if ask == 0 or bid == 0: return None return Ticker(ask=ask, bid=bid, volume=volume) except Exception as e: logger.error("Error fetching Bitstamp ticker for %s: %s", pair, e) return None ################################################################################ # Arbitrage scanner ################################################################################ # List of base assets to scan. Extend this list as needed. ASSETS = [ "BTC", "ETH", "BNB", "AVAX", "LINK", "LTC", "DOT", "SOL", "ADA", "XRP", "DOGE", "BCH", ] # Minimum Binance quote volume (USDT) for a pair to be considered (helps avoid # illiquid pairs). MIN_BINANCE_VOLUME_USDT = 10_000_000 def find_arbitrage(exchanges: List[Exchange]) -> List[Tuple[str, str, str, float]]: """ Scan for arbitrage opportunities across all provided exchanges. Returns a list of tuples: (base_asset, buy_exchange, sell_exchange, net_spread_percent). """ opportunities = [] # Cache Binance volumes for volume filter binance = next((e for e in exchanges if isinstance(e, Binance)), None) binance_volumes: Dict[str, float] = {} if binance: for base in ASSETS: ticker = binance.fetch_ticker(base) if ticker: binance_volumes[base] = ticker.volume # Pre-fetch ticker data for all exchanges to avoid repeated requests data: Dict[str, Dict[str, Ticker]] = {ex.name: {} for ex in exchanges} for ex in exchanges: for base in ASSETS: if ex.name == "binance": # Use cached ticker for Binance if already fetched ticker = binance.fetch_ticker(base) if base not in data[ex.name] else data[ex.name][base] else: ticker = ex.fetch_ticker(base) if ticker: data[ex.name][base] = ticker # Iterate over base assets and exchange pairs for base in ASSETS: # Skip if Binance volume below threshold if binance and binance_volumes.get(base, 0) < MIN_BINANCE_VOLUME_USDT: continue for i, ex_a in enumerate(exchanges): t_a = data[ex_a.name].get(base) if not t_a: continue for j, ex_b in enumerate(exchanges): if i == j: continue t_b = data[ex_b.name].get(base) if not t_b: continue # Option 1: buy on ex_a, sell on ex_b buy_price = t_a.ask sell_price = t_b.bid gross = (sell_price - buy_price) / buy_price net = gross - (ex_a.fee + ex_b.fee) if net > 0: opportunities.append( (base, ex_a.name, ex_b.name, net * 100) ) # Sort by net spread descending opportunities.sort(key=lambda x: x[3], reverse=True) return opportunities def main() -> None: exchanges: List[Exchange] = [ Binance(), Kraken(), Coinbase(), Bitstamp(), ] opps = find_arbitrage(exchanges) if not opps: print("No arbitrage opportunities found.") return print("Profitable arbitrage opportunities (net spread > 0 after fees):") for base, buy_ex, sell_ex, net_spread in opps: print(f" {base}: buy on {buy_ex}, sell on {sell_ex}, net spread ≈ {net_spread:.2f}%") if __name__ == "__main__": try: main() except KeyboardInterrupt: sys.exit(0)