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 <noreply@anthropic.com>
This commit is contained in:
rob 2026-03-02 16:30:19 -04:00
parent b9730b3a1d
commit 45395c86c5
3 changed files with 181 additions and 1 deletions

View File

@ -396,6 +396,106 @@ class Exchange:
""" """
return self._fetch_min_notional_qty(symbol) 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: def get_order(self, symbol: str, order_id: str) -> Dict[str, Any] | None:
""" """
Returns an order by its ID for a given symbol. Returns an order by its ID for a given symbol.

View File

@ -331,6 +331,66 @@ class ExchangeInterface:
else: else:
raise ValueError(f'No implementation for price source: {price_source}') 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: def get_trade_status(self, trade) -> str:
""" """
Get the status of a trade order. Get the status of a trade order.

View File

@ -460,6 +460,25 @@ class Trades:
except Exception as e: except Exception as e:
logger.warning(f"Could not fetch current price for {symbol}: {e}, using provided price {price}") 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: try:
trade = Trade( trade = Trade(
target=target, target=target,
@ -470,7 +489,8 @@ class Trades:
order_type=order_type.upper() if order_type else 'MARKET', order_type=order_type.upper() if order_type else 'MARKET',
strategy_id=strategy_id, strategy_id=strategy_id,
is_paper=is_paper, is_paper=is_paper,
creator=user_id creator=user_id,
fee=effective_fee
) )
if is_paper: if is_paper: