654 lines
25 KiB
Python
654 lines
25 KiB
Python
import ccxt
|
|
import pandas as pd
|
|
from datetime import datetime, timedelta, timezone
|
|
from typing import Tuple, Dict, List, Union, Any
|
|
import time
|
|
import logging
|
|
from shared_utilities import timeframe_to_minutes
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class Exchange:
|
|
"""
|
|
A class to interact with a cryptocurrency exchange using the CCXT library.
|
|
"""
|
|
|
|
_market_cache = {}
|
|
|
|
def __init__(self, name: str, api_keys: Dict[str, str] | None, exchange_id: str,
|
|
testnet: bool = False):
|
|
"""
|
|
Initializes the Exchange object.
|
|
|
|
Parameters:
|
|
name (str): The name of this exchange instance.
|
|
api_keys (Dict[str, str]): Dictionary containing API credentials.
|
|
Supports:
|
|
- key / secret
|
|
- optional passphrase (stored as 'passphrase' or legacy 'password')
|
|
exchange_id (str): The ID of the exchange as recognized by CCXT. Example('binance')
|
|
testnet (bool): Whether to use testnet/sandbox mode. Defaults to False.
|
|
"""
|
|
self.name = name
|
|
self.api_key = api_keys['key'] if api_keys else None
|
|
self.api_key_secret = api_keys['secret'] if api_keys else None
|
|
self.api_passphrase = (
|
|
api_keys.get('passphrase') or api_keys.get('password')
|
|
) if api_keys else None
|
|
self.configured = False
|
|
self.exchange_id = exchange_id
|
|
self.testnet = testnet
|
|
self.edm_session_id = None # EDM session for authenticated candle access
|
|
self.client: ccxt.Exchange = self._connect_exchange()
|
|
if self.client:
|
|
self._check_authentication()
|
|
self.exchange_info = self._set_exchange_info()
|
|
self.intervals = self._set_avail_intervals()
|
|
self.symbols = self._set_symbols()
|
|
self.balances = self._set_balances()
|
|
self.symbols_n_precision = {}
|
|
|
|
def _connect_exchange(self) -> ccxt.Exchange:
|
|
"""
|
|
Connects to the exchange using the CCXT library.
|
|
|
|
Returns:
|
|
ccxt.Exchange: An instance of the CCXT exchange class.
|
|
"""
|
|
exchange_class = getattr(ccxt, self.exchange_id)
|
|
if not exchange_class:
|
|
logger.error(f"Exchange {self.exchange_id} is not supported by CCXT.")
|
|
raise ValueError(f"Exchange {self.exchange_id} is not supported by CCXT.")
|
|
|
|
mode_str = "testnet" if self.testnet else "production"
|
|
logger.info(f"Connecting to exchange {self.exchange_id} ({mode_str} mode).")
|
|
|
|
config = {
|
|
'enableRateLimit': True,
|
|
'verbose': False,
|
|
'options': {'warnOnFetchOpenOrdersWithoutSymbol': False}
|
|
}
|
|
|
|
if self.api_key and self.api_key_secret:
|
|
config['apiKey'] = self.api_key
|
|
config['secret'] = self.api_key_secret
|
|
if self.api_passphrase:
|
|
# CCXT uses `password` for exchange passphrases (e.g., KuCoin).
|
|
config['password'] = self.api_passphrase
|
|
|
|
client = exchange_class(config)
|
|
|
|
# Enable sandbox/testnet mode if requested
|
|
if self.testnet:
|
|
try:
|
|
client.set_sandbox_mode(True)
|
|
logger.info(f"Sandbox mode enabled for {self.exchange_id}")
|
|
except Exception as e:
|
|
# CRITICAL: Do NOT continue with production if testnet was requested
|
|
# This prevents users from accidentally trading real money
|
|
logger.error(f"TESTNET UNAVAILABLE: {self.exchange_id} does not support sandbox mode: {e}")
|
|
raise ValueError(
|
|
f"Testnet/sandbox mode is not available for {self.exchange_id}. "
|
|
f"Please use paper trading mode instead, or trade on production with caution."
|
|
)
|
|
|
|
return client
|
|
|
|
def _check_authentication(self):
|
|
if not (self.api_key and self.api_key_secret):
|
|
self.configured = False
|
|
return
|
|
try:
|
|
self.client.fetch_open_orders() # Much faster than fetch_balance
|
|
self.configured = True
|
|
logger.info("Authentication successful.")
|
|
except ccxt.AuthenticationError:
|
|
logger.error("Authentication failed. Please check your API keys.")
|
|
except Exception as e:
|
|
logger.error(f"An error occurred: {e}")
|
|
|
|
@staticmethod
|
|
def datetime_to_unix_millis(dt: datetime) -> int:
|
|
"""
|
|
Converts a datetime object to Unix milliseconds.
|
|
Parameters:
|
|
dt (datetime): The datetime object to convert.
|
|
Returns:
|
|
int: The Unix timestamp in milliseconds.
|
|
"""
|
|
if dt.tzinfo is None:
|
|
raise ValueError("datetime object must be timezone-aware or in UTC.")
|
|
return int(dt.timestamp() * 1000)
|
|
|
|
|
|
def _fetch_price(self, symbol: str) -> float:
|
|
"""
|
|
Fetches the current price for a given symbol.
|
|
|
|
Parameters:
|
|
symbol (str): The trading symbol (e.g., 'BTC/USDT').
|
|
|
|
Returns:
|
|
float: The current price.
|
|
"""
|
|
try:
|
|
ticker = self.client.fetch_ticker(symbol)
|
|
return float(ticker['last'])
|
|
except ccxt.BaseError as e:
|
|
logger.error(f"Error fetching price for {symbol}: {str(e)}")
|
|
return 0.0
|
|
|
|
def _fetch_min_qty(self, symbol: str) -> float:
|
|
"""
|
|
Fetches the minimum quantity for a given symbol.
|
|
|
|
Parameters:
|
|
symbol (str): The trading symbol (e.g., 'BTC/USDT').
|
|
|
|
Returns:
|
|
float: The minimum quantity.
|
|
"""
|
|
try:
|
|
market_data = self.exchange_info[symbol]
|
|
return float(market_data['limits']['amount']['min'])
|
|
except KeyError as e:
|
|
logger.error(f"Error fetching minimum quantity for {symbol}: {str(e)}")
|
|
return 0.0
|
|
|
|
def _fetch_min_notional_qty(self, symbol: str) -> float:
|
|
"""
|
|
Fetches the minimum notional quantity for a given symbol.
|
|
|
|
Parameters:
|
|
symbol (str): The trading symbol (e.g., 'BTC/USDT').
|
|
|
|
Returns:
|
|
float: The minimum notional quantity.
|
|
"""
|
|
try:
|
|
market_data = self.exchange_info[symbol]
|
|
return float(market_data['limits']['cost']['min'])
|
|
except KeyError as e:
|
|
logger.error(f"Error fetching minimum notional quantity for {symbol}: {str(e)}")
|
|
return 0.0
|
|
|
|
def _set_symbols(self) -> List[str]:
|
|
"""
|
|
Sets the list of available symbols on the exchange.
|
|
|
|
Returns:
|
|
List[str]: A list of trading symbols.
|
|
"""
|
|
try:
|
|
markets = self.client.fetch_markets()
|
|
symbols = [market['symbol'] for market in markets if market['active']]
|
|
return symbols
|
|
except ccxt.BaseError as e:
|
|
logger.error(f"Error fetching symbols: {str(e)}")
|
|
return []
|
|
|
|
def _set_balances(self) -> List[Dict[str, Union[str, float]]]:
|
|
"""
|
|
Sets the balances of the account.
|
|
|
|
Returns:
|
|
List[Dict[str, Union[str, float]]]: A list of balances with asset, balance, and PnL.
|
|
"""
|
|
if self.api_key and self.api_key_secret:
|
|
try:
|
|
# KuCoin has separate accounts (main, trade, margin, futures)
|
|
# We need to explicitly request the 'trade' account for spot trading
|
|
params = {}
|
|
if self.exchange_id.lower() == 'kucoin':
|
|
params['type'] = 'trade'
|
|
logger.info(f"Fetching KuCoin balance with params: {params}, testnet={self.testnet}")
|
|
|
|
account_info = self.client.fetch_balance(params)
|
|
balances = []
|
|
for asset, balance in account_info['total'].items():
|
|
asset_balance = float(balance)
|
|
if asset_balance > 0:
|
|
balances.append({'asset': asset, 'balance': asset_balance, 'pnl': 0})
|
|
logger.info(f"Fetched balances for {self.exchange_id}: {balances}")
|
|
return balances
|
|
except ccxt.BaseError as e:
|
|
logger.error(f"Error fetching balances: {str(e)}")
|
|
return [{'asset': 'N/A', 'balance': 0, 'pnl': 0}]
|
|
else:
|
|
return [{'asset': 'N/A', 'balance': 0, 'pnl': 0}]
|
|
|
|
def _set_exchange_info(self) -> dict:
|
|
"""
|
|
Sets the exchange information and caches it.
|
|
|
|
Returns:
|
|
dict: The exchange information.
|
|
"""
|
|
if self.exchange_id in Exchange._market_cache:
|
|
return Exchange._market_cache[self.exchange_id]
|
|
|
|
try:
|
|
markets_info = self.client.load_markets()
|
|
Exchange._market_cache[self.exchange_id] = markets_info
|
|
return markets_info
|
|
except ccxt.BaseError as e:
|
|
logger.error(f"Error fetching market info: {str(e)}")
|
|
return {}
|
|
|
|
def get_client(self) -> object:
|
|
"""
|
|
Returns the CCXT client instance.
|
|
|
|
Returns:
|
|
object: The CCXT client.
|
|
"""
|
|
return self.client
|
|
|
|
def get_avail_intervals(self) -> Tuple[str, ...]:
|
|
"""
|
|
Returns the available time intervals for OHLCV data.
|
|
|
|
Returns:
|
|
Tuple[str, ...]: A tuple of available intervals.
|
|
"""
|
|
return self.intervals
|
|
|
|
def get_exchange_info(self) -> dict:
|
|
"""
|
|
Returns the exchange information.
|
|
|
|
Returns:
|
|
dict: The exchange information.
|
|
"""
|
|
return self.exchange_info
|
|
|
|
def get_symbols(self) -> List[str]:
|
|
"""
|
|
Returns the list of available symbols.
|
|
|
|
Returns:
|
|
List[str]: A list of trading symbols.
|
|
"""
|
|
return self.symbols
|
|
|
|
def get_balances(self) -> List[Dict[str, Union[str, float]]]:
|
|
"""
|
|
Returns the balances of the account.
|
|
|
|
Returns:
|
|
List[Dict[str, Union[str, float]]]: A list of balances with asset, balance, and PnL.
|
|
"""
|
|
return self.balances
|
|
|
|
def refresh_balances(self) -> List[Dict[str, Union[str, float]]]:
|
|
"""
|
|
Refreshes and returns the balances from the exchange.
|
|
Reinitializes the ccxt client to handle pickle corruption issues.
|
|
|
|
Returns:
|
|
List[Dict[str, Union[str, float]]]: Updated list of balances.
|
|
"""
|
|
# Reinitialize the ccxt client to fix any pickle corruption
|
|
# (ccxt clients don't survive pickle/unpickle properly)
|
|
self.client = self._connect_exchange()
|
|
self.balances = self._set_balances()
|
|
return self.balances
|
|
|
|
def get_symbol_precision_rule(self, symbol: str) -> int:
|
|
"""
|
|
Returns the precision rule for a given symbol.
|
|
|
|
Parameters:
|
|
symbol (str): The trading symbol (e.g., 'BTC/USDT').
|
|
|
|
Returns:
|
|
int: The precision rule.
|
|
"""
|
|
r_value = self.symbols_n_precision.get(symbol)
|
|
if r_value is None:
|
|
self._set_precision_rule(symbol)
|
|
r_value = self.symbols_n_precision.get(symbol)
|
|
return r_value
|
|
|
|
|
|
def get_price(self, symbol: str) -> float:
|
|
"""
|
|
Returns the current price for a given symbol.
|
|
|
|
Parameters:
|
|
symbol (str): The trading symbol (e.g., 'BTC/USDT').
|
|
|
|
Returns:
|
|
float: The current price.
|
|
"""
|
|
return self._fetch_price(symbol)
|
|
|
|
def get_min_qty(self, symbol: str) -> float:
|
|
"""
|
|
Returns the minimum quantity for a given symbol.
|
|
|
|
Parameters:
|
|
symbol (str): The trading symbol (e.g., 'BTC/USDT').
|
|
|
|
Returns:
|
|
float: The minimum quantity.
|
|
"""
|
|
return self._fetch_min_qty(symbol)
|
|
|
|
def get_min_notional_qty(self, symbol: str) -> float:
|
|
"""
|
|
Returns the minimum notional quantity for a given symbol.
|
|
|
|
Parameters:
|
|
symbol (str): The trading symbol (e.g., 'BTC/USDT').
|
|
|
|
Returns:
|
|
float: The minimum notional quantity.
|
|
"""
|
|
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.
|
|
|
|
Parameters:
|
|
symbol (str): The trading symbol (e.g., 'BTC/USDT').
|
|
order_id (str): The ID of the order.
|
|
|
|
Returns:
|
|
Dict[str, Any]: The order details or None on error.
|
|
"""
|
|
try:
|
|
return self.client.fetch_order(order_id, symbol)
|
|
except ccxt.BaseError as e:
|
|
logger.error(f"Error fetching order {order_id} for {symbol}: {str(e)}")
|
|
return None
|
|
|
|
def place_order(self, symbol: str, side: str, type: str, timeInForce: str,
|
|
quantity: float, price: float = None,
|
|
client_order_id: str = None) -> Tuple[str, object]:
|
|
"""
|
|
Places an order on the exchange.
|
|
|
|
Parameters:
|
|
symbol (str): The trading symbol (e.g., 'BTC/USDT').
|
|
side (str): The side of the order ('buy' or 'sell').
|
|
type (str): The type of the order ('limit' or 'market').
|
|
timeInForce (str): The time-in-force policy ('GTC', 'IOC', etc.).
|
|
quantity (float): The quantity of the order.
|
|
price (float, optional): The price of the order for limit orders.
|
|
client_order_id (str, optional): Client-provided order ID for idempotency.
|
|
|
|
Returns:
|
|
Tuple[str, object]: A tuple containing the result ('Success' or 'Failure') and the order details or None.
|
|
"""
|
|
result, msg = self._place_order(symbol=symbol, side=side, type=type,
|
|
timeInForce=timeInForce, quantity=quantity, price=price,
|
|
client_order_id=client_order_id)
|
|
return result, msg
|
|
|
|
def _set_avail_intervals(self) -> Tuple[str, ...]:
|
|
"""
|
|
Sets the available intervals for OHLCV data.
|
|
|
|
Returns:
|
|
Tuple[str, ...]: A tuple of available intervals.
|
|
"""
|
|
return tuple(self.client.timeframes.keys())
|
|
|
|
def _set_precision_rule(self, symbol: str) -> None:
|
|
"""
|
|
Sets the precision rule for a given symbol.
|
|
|
|
Parameters:
|
|
symbol (str): The trading symbol (e.g., 'BTC/USDT').
|
|
"""
|
|
market_data = self.exchange_info[symbol]
|
|
precision = market_data['precision']['amount']
|
|
self.symbols_n_precision[symbol] = precision
|
|
|
|
def _place_order(self, symbol: str, side: str, type: str, timeInForce: str, quantity: float,
|
|
price: float = None, client_order_id: str = None) -> Tuple[str, object]:
|
|
"""
|
|
Places an order on the exchange.
|
|
|
|
Parameters:
|
|
symbol (str): The trading symbol (e.g., 'BTC/USDT').
|
|
side (str): The side of the order ('buy' or 'sell').
|
|
type (str): The type of the order ('limit' or 'market').
|
|
timeInForce (str): The time-in-force policy ('GTC', 'IOC', etc.).
|
|
quantity (float): The quantity of the order.
|
|
price (float, optional): The price of the order for limit orders.
|
|
client_order_id (str, optional): Client-provided order ID for idempotency.
|
|
|
|
Returns:
|
|
Tuple[str, object]: A tuple containing the result ('Success' or 'Failure') and the order details or None.
|
|
"""
|
|
|
|
def format_arg(value: float) -> float:
|
|
precision = self.symbols_n_precision.get(symbol, 8)
|
|
return float(f"{value:.{precision}f}")
|
|
|
|
quantity = format_arg(quantity)
|
|
if price is not None:
|
|
price = format_arg(price)
|
|
|
|
order_params = {
|
|
'symbol': symbol,
|
|
'type': type,
|
|
'side': side,
|
|
'amount': quantity,
|
|
'params': {}
|
|
}
|
|
|
|
# Only include timeInForce for non-market orders (Binance rejects it for market orders)
|
|
if type != 'market' and timeInForce:
|
|
order_params['params']['timeInForce'] = timeInForce
|
|
|
|
if price is not None:
|
|
order_params['price'] = price
|
|
|
|
# Add client order ID for idempotency (exchange-specific param names)
|
|
if client_order_id:
|
|
# newClientOrderId for Binance, clientOrderId for many others
|
|
order_params['params']['newClientOrderId'] = client_order_id
|
|
order_params['params']['clientOrderId'] = client_order_id
|
|
|
|
try:
|
|
order = self.client.create_order(**order_params)
|
|
return 'Success', order
|
|
except ccxt.BaseError as e:
|
|
logger.error(f"Error placing order for {symbol}: {str(e)}")
|
|
return 'Failure', None
|
|
|
|
def get_active_trades(self) -> List[Dict[str, Union[str, float]]]:
|
|
"""
|
|
Returns a list of active trades/positions.
|
|
|
|
Note: This method uses fetch_positions() which is only available on futures/derivatives
|
|
exchanges. For spot exchanges, this will return an empty list since spot trading
|
|
doesn't have the concept of "positions" - only balances.
|
|
|
|
Returns:
|
|
List[Dict[str, Union[str, float]]]: A list of active trades with symbol, side, quantity, and price.
|
|
"""
|
|
if not self.api_key or not self.api_key_secret:
|
|
return []
|
|
|
|
# Check if the exchange supports fetching positions (futures/derivatives feature)
|
|
if not self.client.has.get('fetchPositions'):
|
|
logger.debug(f"Exchange {self.exchange_id} does not support fetchPositions (spot exchange)")
|
|
return []
|
|
|
|
try:
|
|
positions = self.client.fetch_positions()
|
|
formatted_trades = []
|
|
for position in positions:
|
|
# Get position size - ccxt uses 'contracts' or 'contractSize', fallback to legacy 'quantity'
|
|
quantity = position.get('contracts') or position.get('contractSize') or position.get('quantity', 0)
|
|
if quantity is None:
|
|
quantity = 0
|
|
quantity = float(quantity)
|
|
|
|
# Skip positions with zero size
|
|
if quantity == 0:
|
|
continue
|
|
|
|
# Get entry price - ccxt uses 'entryPrice', fallback to legacy 'entry_price'
|
|
entry_price = position.get('entryPrice') or position.get('entry_price', 0)
|
|
if entry_price is None:
|
|
entry_price = 0
|
|
|
|
active_trade = {
|
|
'symbol': position['symbol'],
|
|
'side': position.get('side') or ('buy' if quantity > 0 else 'sell'),
|
|
'quantity': abs(quantity),
|
|
'price': float(entry_price)
|
|
}
|
|
formatted_trades.append(active_trade)
|
|
return formatted_trades
|
|
except ccxt.PermissionDenied as e:
|
|
# Error -2015: API key doesn't have permission for this endpoint
|
|
logger.debug(f"API key lacks permission for fetchPositions on {self.exchange_id}: {e}")
|
|
return []
|
|
except ccxt.NotSupported as e:
|
|
logger.debug(f"fetchPositions not supported on {self.exchange_id}: {e}")
|
|
return []
|
|
except ccxt.BaseError as e:
|
|
logger.error(f"Error fetching active trades from {self.exchange_id}: {str(e)}")
|
|
return []
|
|
|
|
def get_open_orders(self) -> List[Dict[str, Union[str, float]]]:
|
|
"""
|
|
Returns a list of open orders.
|
|
|
|
Returns:
|
|
List[Dict[str, Union[str, float]]]: A list of open orders with id, symbol, side, type, quantity, price, status.
|
|
"""
|
|
if self.api_key and self.api_key_secret:
|
|
try:
|
|
open_orders = self.client.fetch_open_orders()
|
|
formatted_orders = []
|
|
for order in open_orders:
|
|
open_order = {
|
|
'id': order.get('id'), # Exchange order ID - critical for reconciliation
|
|
'clientOrderId': order.get('clientOrderId'), # Client order ID if available
|
|
'symbol': order['symbol'],
|
|
'side': order['side'],
|
|
'type': order.get('type', 'limit'),
|
|
'quantity': order['amount'],
|
|
'price': order.get('price'),
|
|
'status': order.get('status', 'open'),
|
|
'filled': order.get('filled', 0),
|
|
'remaining': order.get('remaining', order['amount']),
|
|
'timestamp': order.get('timestamp'),
|
|
}
|
|
formatted_orders.append(open_order)
|
|
return formatted_orders
|
|
except ccxt.BaseError as e:
|
|
logger.error(f"Error fetching open orders: {str(e)}")
|
|
return []
|
|
else:
|
|
return []
|