""" Fee calculation module for triangular arbitrage. Handles trading fees (maker/taker) and slippage for different exchanges. """ from typing import Optional, Dict from dataclasses import dataclass import ccxt.async_support as ccxt @dataclass class ExchangeFees: """Fee structure for an exchange.""" maker_fee: float # Maker fee as decimal (e.g., 0.001 for 0.1%) taker_fee: float # Taker fee as decimal (e.g., 0.001 for 0.1%) name: str # Standard fee structures for exchanges EXCHANGE_FEES: Dict[str, ExchangeFees] = { "binance": ExchangeFees( maker_fee=0.001, # 0.1% taker_fee=0.001, # 0.1% name="Binance" ), "binanceus": ExchangeFees( maker_fee=0.001, # 0.1% taker_fee=0.001, # 0.1% name="Binance US" ), "huobi": ExchangeFees( maker_fee=0.002, # 0.2% taker_fee=0.002, # 0.2% name="Huobi" ), "htx": ExchangeFees( # Huobi rebranded as HTX maker_fee=0.002, # 0.2% taker_fee=0.002, # 0.2% name="HTX" ), } def get_exchange_fees(exchange_name: str) -> ExchangeFees: """ Get fee structure for an exchange. Args: exchange_name: Name of the exchange (e.g., 'binance', 'huobi') Returns: ExchangeFees object with maker and taker fees """ # Normalize exchange name exchange_name_lower = exchange_name.lower() # Check direct match if exchange_name_lower in EXCHANGE_FEES: return EXCHANGE_FEES[exchange_name_lower] # Check partial matches for key, fees in EXCHANGE_FEES.items(): if key in exchange_name_lower or exchange_name_lower in key: return fees # Default fees (conservative estimate) return ExchangeFees( maker_fee=0.002, # 0.2% default taker_fee=0.002, # 0.2% default name=exchange_name ) async def fetch_user_fee_tier( exchange_name: str, api_key: Optional[str] = None, api_secret: Optional[str] = None ) -> Optional[ExchangeFees]: """ Fetch actual fee tier from exchange API if API keys are provided. This allows for VIP tier discounts and BNB/HT holdings discounts. Args: exchange_name: Name of the exchange api_key: API key for authenticated requests api_secret: API secret for authenticated requests Returns: ExchangeFees with actual user fees, or None if not available """ if not api_key or not api_secret: return None try: exchange_class = getattr(ccxt, exchange_name) exchange = exchange_class({ 'apiKey': api_key, 'secret': api_secret, 'enableRateLimit': True, 'options': { 'defaultType': 'spot', # Ensure we're using spot trading } }) # Try to fetch fee information exchange_name_lower = exchange_name.lower() if exchange_name_lower in ['binance', 'binanceus']: try: # Binance: fetch trading fees if hasattr(exchange, 'fetch_trading_fees'): fees = await exchange.fetch_trading_fees() if fees and 'trading' in fees: trading_fees = fees['trading'] # Get fees for a common trading pair (e.g., BTC/USDT) if 'BTC/USDT' in trading_fees: btc_fees = trading_fees['BTC/USDT'] maker_fee = btc_fees.get('maker', 0.001) taker_fee = btc_fees.get('taker', 0.001) await exchange.close() return ExchangeFees( maker_fee=maker_fee, taker_fee=taker_fee, name=f"{exchange_name} (User Tier)" ) # Alternative: try fetchBalance which sometimes includes fee info balance = await exchange.fetch_balance() if 'info' in balance: info = balance['info'] # Binance account info might contain fee tier # This varies by exchange API version pass except Exception as e: # If specific method fails, try general approach pass elif exchange_name_lower in ['huobi', 'htx']: try: # Huobi: fetch trading fees if hasattr(exchange, 'fetch_trading_fees'): fees = await exchange.fetch_trading_fees() if fees and 'trading' in fees: trading_fees = fees['trading'] if 'BTC/USDT' in trading_fees: btc_fees = trading_fees['BTC/USDT'] maker_fee = btc_fees.get('maker', 0.002) taker_fee = btc_fees.get('taker', 0.002) await exchange.close() return ExchangeFees( maker_fee=maker_fee, taker_fee=taker_fee, name=f"{exchange_name} (User Tier)" ) except Exception: pass # Try generic fetchTradingFees if available try: if hasattr(exchange, 'fetch_trading_fees'): fees = await exchange.fetch_trading_fees() if fees: # Extract maker/taker from first available trading pair if isinstance(fees, dict) and 'trading' in fees: trading_fees = fees['trading'] for pair, pair_fees in trading_fees.items(): if isinstance(pair_fees, dict): maker_fee = pair_fees.get('maker') taker_fee = pair_fees.get('taker') if maker_fee is not None and taker_fee is not None: await exchange.close() return ExchangeFees( maker_fee=maker_fee, taker_fee=taker_fee, name=f"{exchange_name} (User Tier)" ) except Exception: pass await exchange.close() except Exception: # If fetching fails, return None to use default fees pass return None def calculate_trading_fees( amount: float, fee_rate: float, num_trades: int = 3 ) -> float: """ Calculate total trading fees for a triangular arbitrage cycle. Args: amount: Starting amount (e.g., 1.0 for calculating profit ratio) fee_rate: Fee rate per trade (as decimal, e.g., 0.001 for 0.1%) num_trades: Number of trades in the cycle (default 3 for triangular) Returns: Total fees as a multiplier (e.g., 0.003 for 0.3% total fees) """ # For triangular arbitrage, we pay fees on each of the 3 trades # Fee is deducted from the amount received, so we multiply by (1 - fee_rate) for each trade return (1 - fee_rate) ** num_trades def calculate_slippage( trade_size_usd: float = 1000.0, liquidity_factor: float = 0.0005 # 0.05% slippage per $1000 traded ) -> float: """ Calculate slippage as a percentage based on trade size and liquidity. Args: trade_size_usd: Estimated trade size in USD (default 1000) liquidity_factor: Slippage factor per $1000 (default 0.0005 = 0.05%) Higher liquidity = lower slippage Returns: Slippage multiplier (e.g., 0.9995 for 0.05% slippage) """ # Slippage increases with trade size # Formula: slippage = 1 - (trade_size / 1000) * liquidity_factor slippage_percentage = min((trade_size_usd / 1000.0) * liquidity_factor, 0.01) # Cap at 1% return 1 - slippage_percentage def calculate_net_profit( gross_profit: float, exchange_fees: ExchangeFees, num_trades: int = 3, use_taker: bool = True, trade_size_usd: float = 1000.0, slippage_factor: float = 0.0005 ) -> float: """ Calculate net profit after fees and slippage. Args: gross_profit: Gross profit multiplier (e.g., 1.002 for 0.2% profit) exchange_fees: ExchangeFees object with maker/taker rates num_trades: Number of trades in the cycle (default 3) use_taker: Whether to use taker fees (True) or maker fees (False) trade_size_usd: Estimated trade size for slippage calculation slippage_factor: Slippage factor per $1000 traded Returns: Net profit multiplier after fees and slippage """ # Select appropriate fee rate fee_rate = exchange_fees.taker_fee if use_taker else exchange_fees.maker_fee # Calculate fee impact fee_multiplier = calculate_trading_fees(1.0, fee_rate, num_trades) # Calculate slippage impact (applied to each trade) slippage_per_trade = calculate_slippage(trade_size_usd, slippage_factor) slippage_multiplier = slippage_per_trade ** num_trades # Net profit = gross profit * fee multiplier * slippage multiplier net_profit = gross_profit * fee_multiplier * slippage_multiplier return net_profit def get_fee_breakdown( gross_profit: float, exchange_fees: ExchangeFees, num_trades: int = 3, use_taker: bool = True, trade_size_usd: float = 1000.0, slippage_factor: float = 0.0005 ) -> Dict[str, float]: """ Get detailed breakdown of fees and slippage. Returns: Dictionary with gross_profit, total_fees, total_slippage, net_profit """ fee_rate = exchange_fees.taker_fee if use_taker else exchange_fees.maker_fee fee_multiplier = calculate_trading_fees(1.0, fee_rate, num_trades) slippage_per_trade = calculate_slippage(trade_size_usd, slippage_factor) slippage_multiplier = slippage_per_trade ** num_trades total_fees = 1 - fee_multiplier total_slippage = 1 - slippage_multiplier net_profit = gross_profit * fee_multiplier * slippage_multiplier return { "gross_profit": gross_profit - 1, "total_fees": total_fees, "total_slippage": total_slippage, "net_profit": net_profit - 1, "fee_rate_per_trade": fee_rate, "slippage_per_trade": 1 - slippage_per_trade, }