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)
|
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.
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
22
src/trade.py
22
src/trade.py
|
|
@ -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:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue