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:
parent
b9730b3a1d
commit
45395c86c5
100
src/Exchange.py
100
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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
22
src/trade.py
22
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:
|
||||
|
|
|
|||
Loading…
Reference in New Issue