191 lines
6.4 KiB
Python
191 lines
6.4 KiB
Python
import abc
|
|
import logging
|
|
from typing import Dict, Any, List, Tuple, Optional
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
class ExchangeInterface(abc.ABC):
|
|
"""Base class for all exchange interfaces.
|
|
|
|
This abstract class defines the required methods that all exchange
|
|
implementations must provide to ensure compatibility with the trading system.
|
|
"""
|
|
|
|
def __init__(self, api_key: str = None, api_secret: str = None, test_mode: bool = True):
|
|
"""Initialize the exchange interface.
|
|
|
|
Args:
|
|
api_key: API key for the exchange
|
|
api_secret: API secret for the exchange
|
|
test_mode: If True, use test/sandbox environment
|
|
"""
|
|
self.api_key = api_key
|
|
self.api_secret = api_secret
|
|
self.test_mode = test_mode
|
|
self.client = None
|
|
self.last_price_cache = {}
|
|
|
|
@abc.abstractmethod
|
|
def connect(self) -> bool:
|
|
"""Connect to the exchange API.
|
|
|
|
Returns:
|
|
bool: True if connection successful, False otherwise
|
|
"""
|
|
pass
|
|
|
|
@abc.abstractmethod
|
|
def get_balance(self, asset: str) -> float:
|
|
"""Get balance of a specific asset.
|
|
|
|
Args:
|
|
asset: Asset symbol (e.g., 'BTC', 'USDT')
|
|
|
|
Returns:
|
|
float: Available balance of the asset
|
|
"""
|
|
pass
|
|
|
|
@abc.abstractmethod
|
|
def get_ticker(self, symbol: str) -> Dict[str, Any]:
|
|
"""Get current ticker data for a symbol.
|
|
|
|
Args:
|
|
symbol: Trading symbol (e.g., 'BTC/USDT')
|
|
|
|
Returns:
|
|
dict: Ticker data including price information
|
|
"""
|
|
pass
|
|
|
|
@abc.abstractmethod
|
|
def place_order(self, symbol: str, side: str, order_type: str,
|
|
quantity: float, price: float = None) -> Dict[str, Any]:
|
|
"""Place an order on the exchange.
|
|
|
|
Args:
|
|
symbol: Trading symbol (e.g., 'BTC/USDT')
|
|
side: Order side ('buy' or 'sell')
|
|
order_type: Order type ('market', 'limit', etc.)
|
|
quantity: Order quantity
|
|
price: Order price (for limit orders)
|
|
|
|
Returns:
|
|
dict: Order information including order ID
|
|
"""
|
|
pass
|
|
|
|
@abc.abstractmethod
|
|
def cancel_order(self, symbol: str, order_id: str) -> bool:
|
|
"""Cancel an existing order.
|
|
|
|
Args:
|
|
symbol: Trading symbol (e.g., 'BTC/USDT')
|
|
order_id: ID of the order to cancel
|
|
|
|
Returns:
|
|
bool: True if cancellation successful, False otherwise
|
|
"""
|
|
pass
|
|
|
|
@abc.abstractmethod
|
|
def get_order_status(self, symbol: str, order_id: str) -> Dict[str, Any]:
|
|
"""Get status of an existing order.
|
|
|
|
Args:
|
|
symbol: Trading symbol (e.g., 'BTC/USDT')
|
|
order_id: ID of the order
|
|
|
|
Returns:
|
|
dict: Order status information
|
|
"""
|
|
pass
|
|
|
|
@abc.abstractmethod
|
|
def get_open_orders(self, symbol: str = None) -> List[Dict[str, Any]]:
|
|
"""Get all open orders, optionally filtered by symbol.
|
|
|
|
Args:
|
|
symbol: Trading symbol (e.g., 'BTC/USDT'), or None for all symbols
|
|
|
|
Returns:
|
|
list: List of open orders
|
|
"""
|
|
pass
|
|
|
|
def get_last_price(self, symbol: str) -> float:
|
|
"""Get last known price for a symbol, may use cached value.
|
|
|
|
Args:
|
|
symbol: Trading symbol (e.g., 'BTC/USDT')
|
|
|
|
Returns:
|
|
float: Last price
|
|
"""
|
|
try:
|
|
ticker = self.get_ticker(symbol)
|
|
price = float(ticker['last'])
|
|
self.last_price_cache[symbol] = price
|
|
return price
|
|
except Exception as e:
|
|
logger.error(f"Error getting price for {symbol}: {str(e)}")
|
|
# Return cached price if available
|
|
return self.last_price_cache.get(symbol, 0.0)
|
|
|
|
def execute_trade(self, symbol: str, action: str, quantity: float = None,
|
|
percent_of_balance: float = None) -> Optional[Dict[str, Any]]:
|
|
"""Execute a trade based on a signal.
|
|
|
|
Args:
|
|
symbol: Trading symbol (e.g., 'BTC/USDT')
|
|
action: Trade action ('BUY', 'SELL')
|
|
quantity: Specific quantity to trade
|
|
percent_of_balance: Alternative to quantity - percentage of available balance to use
|
|
|
|
Returns:
|
|
dict: Order information or None if order failed
|
|
"""
|
|
if action not in ['BUY', 'SELL']:
|
|
logger.error(f"Invalid action: {action}. Must be 'BUY' or 'SELL'")
|
|
return None
|
|
|
|
side = action.lower()
|
|
|
|
try:
|
|
# Determine base and quote assets from symbol (e.g., BTC/USDT -> BTC, USDT)
|
|
base_asset, quote_asset = symbol.split('/')
|
|
|
|
# Calculate quantity if percent_of_balance is provided
|
|
if quantity is None and percent_of_balance is not None:
|
|
if percent_of_balance <= 0 or percent_of_balance > 1:
|
|
logger.error(f"Invalid percent_of_balance: {percent_of_balance}. Must be between 0 and 1")
|
|
return None
|
|
|
|
if side == 'buy':
|
|
# For buy, use quote asset (e.g., USDT)
|
|
balance = self.get_balance(quote_asset)
|
|
price = self.get_last_price(symbol)
|
|
quantity = (balance * percent_of_balance) / price
|
|
else:
|
|
# For sell, use base asset (e.g., BTC)
|
|
balance = self.get_balance(base_asset)
|
|
quantity = balance * percent_of_balance
|
|
|
|
if not quantity or quantity <= 0:
|
|
logger.error(f"Invalid quantity: {quantity}")
|
|
return None
|
|
|
|
# Place market order
|
|
order = self.place_order(
|
|
symbol=symbol,
|
|
side=side,
|
|
order_type='market',
|
|
quantity=quantity
|
|
)
|
|
|
|
logger.info(f"Executed {side.upper()} order for {quantity} {base_asset} at market price")
|
|
return order
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error executing {action} trade for {symbol}: {str(e)}")
|
|
return None |