disLodge's picture
Adding fees inclusion
2dee41b
"""
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,
}