From 45395c86c5fa97e5fa8994f4d3692b27c3d61b7e Mon Sep 17 00:00:00 2001 From: rob Date: Mon, 2 Mar 2026 16:30:19 -0400 Subject: [PATCH] Implement fee fetching from exchange via ccxt Exchange.py: - Add get_trading_fees(symbol) - fetches fees from market data - Tries user-specific fees for authenticated users (fetch_trading_fees) - Falls back to market data (public) - Returns maker/taker rates with source indicator - Add get_margin_info(symbol) - returns margin trading availability ExchangeInterface.py: - Add get_trading_fees() - routes to appropriate exchange - Add get_margin_info() - routes to appropriate exchange - Both methods handle user/exchange lookup with fallback to defaults trade.py: - Update new_trade() to fetch actual fees from exchange - Uses taker fee for market orders, maker fee for limit orders - Falls back to exchange_fees defaults if fetch fails Fees now come from actual exchange data (0.1% for Binance spot) instead of hardcoded defaults. Co-Authored-By: Claude Opus 4.5 --- src/Exchange.py | 100 +++++++++++++++++++++++++++++++++++++++ src/ExchangeInterface.py | 60 +++++++++++++++++++++++ src/trade.py | 22 ++++++++- 3 files changed, 181 insertions(+), 1 deletion(-) diff --git a/src/Exchange.py b/src/Exchange.py index 1d218ef..9df0cbc 100644 --- a/src/Exchange.py +++ b/src/Exchange.py @@ -396,6 +396,106 @@ class Exchange: """ return self._fetch_min_notional_qty(symbol) + def get_trading_fees(self, symbol: str = None) -> Dict[str, Any]: + """ + Returns the trading fees for a symbol from market info (public data). + + For authenticated users, this will try to fetch user-specific fees first, + falling back to market defaults if that fails. + + Parameters: + symbol (str, optional): The trading symbol (e.g., 'BTC/USDT'). + If None, returns general exchange fees. + + Returns: + Dict[str, Any]: A dictionary containing: + - 'maker': Maker fee rate (e.g., 0.001 for 0.1%) + - 'taker': Taker fee rate (e.g., 0.001 for 0.1%) + - 'source': Where the fees came from ('user', 'market', or 'default') + """ + default_fees = {'maker': 0.001, 'taker': 0.001, 'source': 'default'} + + # Try to get user-specific fees if authenticated + if self.configured: + try: + user_fees = self.client.fetch_trading_fees() + if symbol and symbol in user_fees: + fee_data = user_fees[symbol] + return { + 'maker': float(fee_data.get('maker', 0.001)), + 'taker': float(fee_data.get('taker', 0.001)), + 'source': 'user' + } + elif user_fees: + # Some exchanges return a single fee structure, not per-symbol + # Try to get a representative fee + first_key = next(iter(user_fees), None) + if first_key and isinstance(user_fees[first_key], dict): + fee_data = user_fees[first_key] + return { + 'maker': float(fee_data.get('maker', 0.001)), + 'taker': float(fee_data.get('taker', 0.001)), + 'source': 'user' + } + except ccxt.NotSupported: + logger.debug(f"fetch_trading_fees not supported by {self.exchange_id}") + except ccxt.AuthenticationError: + logger.warning(f"Authentication required for user-specific fees on {self.exchange_id}") + except Exception as e: + logger.debug(f"Could not fetch user-specific fees: {e}") + + # Fall back to market info (public data) + if symbol and symbol in self.exchange_info: + market = self.exchange_info[symbol] + maker = market.get('maker') + taker = market.get('taker') + if maker is not None and taker is not None: + return { + 'maker': float(maker), + 'taker': float(taker), + 'source': 'market' + } + + # Return defaults if nothing else works + return default_fees + + def get_margin_info(self, symbol: str) -> Dict[str, Any]: + """ + Returns margin trading information for a symbol. + + Parameters: + symbol (str): The trading symbol (e.g., 'BTC/USDT'). + + Returns: + Dict[str, Any]: A dictionary containing: + - 'margin_enabled': Whether margin trading is available + - 'max_leverage': Maximum leverage (if available) + - 'margin_modes': Available margin modes (cross/isolated) + """ + result = { + 'margin_enabled': False, + 'max_leverage': 1, + 'margin_modes': [] + } + + if symbol in self.exchange_info: + market = self.exchange_info[symbol] + result['margin_enabled'] = market.get('margin', False) + + # Check for leverage info in market limits + if 'limits' in market and 'leverage' in market['limits']: + leverage_limits = market['limits']['leverage'] + max_lev = leverage_limits.get('max') + result['max_leverage'] = max_lev if max_lev is not None else 1 + + # Check for margin mode info + if market.get('margin'): + result['margin_modes'].append('cross') + if market.get('isolated'): + result['margin_modes'].append('isolated') + + return result + def get_order(self, symbol: str, order_id: str) -> Dict[str, Any] | None: """ Returns an order by its ID for a given symbol. diff --git a/src/ExchangeInterface.py b/src/ExchangeInterface.py index 4b49546..56fdfb1 100644 --- a/src/ExchangeInterface.py +++ b/src/ExchangeInterface.py @@ -331,6 +331,66 @@ class ExchangeInterface: else: raise ValueError(f'No implementation for price source: {price_source}') + def get_trading_fees(self, symbol: str, user_name: str = None, + exchange_name: str = None) -> Dict[str, Any]: + """ + Get trading fees for a symbol. + + This method routes to the appropriate exchange and returns fee information. + For authenticated users, it will try to get user-specific fees (based on + volume/VIP tier). Falls back to market defaults for unauthenticated requests. + + :param symbol: The trading symbol (e.g., 'BTC/USDT'). + :param user_name: Optional user name for user-specific fees. + :param exchange_name: Optional exchange name (defaults to default exchange). + :return: Dict with 'maker', 'taker', and 'source' keys. + """ + default_fees = {'maker': 0.001, 'taker': 0.001, 'source': 'default'} + + try: + # Try to get user-specific exchange first + if user_name and exchange_name: + exchange = self.get_exchange(ename=exchange_name, uname=user_name) + if exchange: + return exchange.get_trading_fees(symbol) + + # Fall back to default exchange + self.connect_default_exchange() + if self.default_exchange: + return self.default_exchange.get_trading_fees(symbol) + + except Exception as e: + logger.warning(f"Could not fetch trading fees for {symbol}: {e}") + + return default_fees + + def get_margin_info(self, symbol: str, user_name: str = None, + exchange_name: str = None) -> Dict[str, Any]: + """ + Get margin trading information for a symbol. + + :param symbol: The trading symbol (e.g., 'BTC/USDT'). + :param user_name: Optional user name. + :param exchange_name: Optional exchange name. + :return: Dict with margin info (margin_enabled, max_leverage, margin_modes). + """ + default_info = {'margin_enabled': False, 'max_leverage': 1, 'margin_modes': []} + + try: + if user_name and exchange_name: + exchange = self.get_exchange(ename=exchange_name, uname=user_name) + if exchange: + return exchange.get_margin_info(symbol) + + self.connect_default_exchange() + if self.default_exchange: + return self.default_exchange.get_margin_info(symbol) + + except Exception as e: + logger.warning(f"Could not fetch margin info for {symbol}: {e}") + + return default_info + def get_trade_status(self, trade) -> str: """ Get the status of a trade order. diff --git a/src/trade.py b/src/trade.py index 32a78d5..85247a8 100644 --- a/src/trade.py +++ b/src/trade.py @@ -460,6 +460,25 @@ class Trades: except Exception as e: logger.warning(f"Could not fetch current price for {symbol}: {e}, using provided price {price}") + # Fetch trading fees from exchange (falls back to defaults if unavailable) + effective_fee = self.exchange_fees.get('taker', 0.001) # Default to taker fee + if self.exchange_interface: + try: + user_name = self._get_user_name(user_id) if user_id else None + fee_info = self.exchange_interface.get_trading_fees( + symbol=symbol, + user_name=user_name, + exchange_name=target if not is_paper else None + ) + # Use taker fee for market orders, maker fee for limit orders + if order_type and order_type.upper() == 'LIMIT': + effective_fee = fee_info.get('maker', 0.001) + else: + effective_fee = fee_info.get('taker', 0.001) + logger.debug(f"Trade fee for {symbol}: {effective_fee} (source: {fee_info.get('source', 'unknown')})") + except Exception as e: + logger.warning(f"Could not fetch trading fees for {symbol}: {e}, using default {effective_fee}") + try: trade = Trade( target=target, @@ -470,7 +489,8 @@ class Trades: order_type=order_type.upper() if order_type else 'MARKET', strategy_id=strategy_id, is_paper=is_paper, - creator=user_id + creator=user_id, + fee=effective_fee ) if is_paper: