implemented tests for Exchangeinterface.py
This commit is contained in:
parent
917ccedbaf
commit
c398a423a3
|
|
@ -6,7 +6,7 @@ from Strategies import Strategies
|
||||||
from backtesting import Backtester
|
from backtesting import Backtester
|
||||||
from candles import Candles
|
from candles import Candles
|
||||||
from Configuration import Configuration
|
from Configuration import Configuration
|
||||||
from exchangeinterface import ExchangeInterface
|
from ExchangeInterface import ExchangeInterface
|
||||||
from indicators import Indicators
|
from indicators import Indicators
|
||||||
from Signals import Signals
|
from Signals import Signals
|
||||||
from trade import Trades
|
from trade import Trades
|
||||||
|
|
@ -429,14 +429,14 @@ class BrighterTrades:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Forward the request to trades.
|
# Forward the request to trades.
|
||||||
status, result = self.trades.new_trade(target=vld('target'), symbol=vld('symbol'), price=vld('price'),
|
status, result = self.trades.new_trade(target=vld('exchange_name'), symbol=vld('symbol'), price=vld('price'),
|
||||||
side=vld('side'), order_type=vld('orderType'),
|
side=vld('side'), order_type=vld('orderType'),
|
||||||
qty=vld('quantity'))
|
qty=vld('quantity'))
|
||||||
if status == 'Error':
|
if status == 'Error':
|
||||||
print(f'Error placing the trade: {result}')
|
print(f'Error placing the trade: {result}')
|
||||||
return None
|
return None
|
||||||
|
|
||||||
print(f'Trade order received: target={vld("target")}, '
|
print(f'Trade order received: exchange_name={vld("exchange_name")}, '
|
||||||
f'symbol={vld("symbol")}, '
|
f'symbol={vld("symbol")}, '
|
||||||
f'side={vld("side")}, '
|
f'side={vld("side")}, '
|
||||||
f'type={vld("orderType")}, '
|
f'type={vld("orderType")}, '
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,7 @@ from Database import Database
|
||||||
from shared_utilities import query_satisfied, query_uptodate, unix_time_millis, timeframe_to_minutes
|
from shared_utilities import query_satisfied, query_uptodate, unix_time_millis, timeframe_to_minutes
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
# Set up logging
|
|
||||||
logging.basicConfig(level=logging.DEBUG)
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,10 @@
|
||||||
import ccxt
|
import ccxt
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import Tuple, Dict, List, Union
|
from typing import Tuple, Dict, List, Union, Any
|
||||||
import time
|
import time
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
# Configure logging
|
|
||||||
logging.basicConfig(level=logging.INFO)
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -29,8 +27,11 @@ class Exchange:
|
||||||
self.name = name
|
self.name = name
|
||||||
self.api_key = api_keys['key'] if api_keys else None
|
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_key_secret = api_keys['secret'] if api_keys else None
|
||||||
|
self.configured = False
|
||||||
self.exchange_id = exchange_id
|
self.exchange_id = exchange_id
|
||||||
self.client: ccxt.Exchange = self._connect_exchange()
|
self.client: ccxt.Exchange = self._connect_exchange()
|
||||||
|
if self.client:
|
||||||
|
self._check_authentication()
|
||||||
self.exchange_info = self._set_exchange_info()
|
self.exchange_info = self._set_exchange_info()
|
||||||
self.intervals = self._set_avail_intervals()
|
self.intervals = self._set_avail_intervals()
|
||||||
self.symbols = self._set_symbols()
|
self.symbols = self._set_symbols()
|
||||||
|
|
@ -63,6 +64,17 @@ class Exchange:
|
||||||
'verbose': False
|
'verbose': False
|
||||||
})
|
})
|
||||||
|
|
||||||
|
def _check_authentication(self):
|
||||||
|
try:
|
||||||
|
# Perform an authenticated request to check if the API keys are valid
|
||||||
|
self.client.fetch_balance()
|
||||||
|
self.configured = True
|
||||||
|
logger.info("Authentication successful. Trading bot configured.")
|
||||||
|
except ccxt.AuthenticationError:
|
||||||
|
logger.error("Authentication failed. Please check your API keys.")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"An error occurred: {e}")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def datetime_to_unix_millis(dt: datetime) -> int:
|
def datetime_to_unix_millis(dt: datetime) -> int:
|
||||||
"""
|
"""
|
||||||
|
|
@ -179,23 +191,6 @@ class Exchange:
|
||||||
logger.error(f"Error fetching minimum notional quantity for {symbol}: {str(e)}")
|
logger.error(f"Error fetching minimum notional quantity for {symbol}: {str(e)}")
|
||||||
return 0.0
|
return 0.0
|
||||||
|
|
||||||
def _fetch_order(self, symbol: str, order_id: str) -> object:
|
|
||||||
"""
|
|
||||||
Fetches 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:
|
|
||||||
object: The order details.
|
|
||||||
"""
|
|
||||||
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 _set_symbols(self) -> List[str]:
|
def _set_symbols(self) -> List[str]:
|
||||||
"""
|
"""
|
||||||
Sets the list of available symbols on the exchange.
|
Sets the list of available symbols on the exchange.
|
||||||
|
|
@ -365,7 +360,7 @@ class Exchange:
|
||||||
"""
|
"""
|
||||||
return self._fetch_min_notional_qty(symbol)
|
return self._fetch_min_notional_qty(symbol)
|
||||||
|
|
||||||
def get_order(self, symbol: str, order_id: str) -> object:
|
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.
|
||||||
|
|
||||||
|
|
@ -374,9 +369,13 @@ class Exchange:
|
||||||
order_id (str): The ID of the order.
|
order_id (str): The ID of the order.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
object: The order details.
|
Dict[str, Any]: The order details or None on error.
|
||||||
"""
|
"""
|
||||||
return self._fetch_order(symbol, order_id)
|
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,
|
def place_order(self, symbol: str, side: str, type: str, timeInForce: str,
|
||||||
quantity: float, price: float = None) -> Tuple[str, object]:
|
quantity: float, price: float = None) -> Tuple[str, object]:
|
||||||
|
|
@ -418,7 +417,8 @@ class Exchange:
|
||||||
precision = market_data['precision']['amount']
|
precision = market_data['precision']['amount']
|
||||||
self.symbols_n_precision[symbol] = precision
|
self.symbols_n_precision[symbol] = precision
|
||||||
|
|
||||||
def _place_order(self, symbol: str, side: str, type: str, timeInForce: str, quantity: float, price: float = None) -> Tuple[str, object]:
|
def _place_order(self, symbol: str, side: str, type: str, timeInForce: str, quantity: float, price: float = None) -> \
|
||||||
|
Tuple[str, object]:
|
||||||
"""
|
"""
|
||||||
Places an order on the exchange.
|
Places an order on the exchange.
|
||||||
|
|
||||||
|
|
@ -433,6 +433,7 @@ class Exchange:
|
||||||
Returns:
|
Returns:
|
||||||
Tuple[str, object]: A tuple containing the result ('Success' or 'Failure') and the order details or None.
|
Tuple[str, object]: A tuple containing the result ('Success' or 'Failure') and the order details or None.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def format_arg(value: float) -> float:
|
def format_arg(value: float) -> float:
|
||||||
precision = self.symbols_n_precision.get(symbol, 8)
|
precision = self.symbols_n_precision.get(symbol, 8)
|
||||||
return float(f"{value:.{precision}f}")
|
return float(f"{value:.{precision}f}")
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,214 @@
|
||||||
|
import logging
|
||||||
|
import json
|
||||||
|
from typing import List, Any, Dict
|
||||||
|
import pandas as pd
|
||||||
|
import requests
|
||||||
|
import ccxt
|
||||||
|
from Exchange import Exchange
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# Utility function to add a row to a DataFrame
|
||||||
|
def add_row(df: pd.DataFrame, dic: Dict[str, Any]) -> pd.DataFrame:
|
||||||
|
return pd.concat([df, pd.DataFrame([dic])], ignore_index=True)
|
||||||
|
|
||||||
|
|
||||||
|
class ExchangeInterface:
|
||||||
|
"""
|
||||||
|
Connects, maintains, and routes data requests to/from multiple exchanges.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.exchange_data = pd.DataFrame(columns=['user', 'name', 'reference', 'balances'])
|
||||||
|
self.available_exchanges = self.get_ccxt_exchanges()
|
||||||
|
|
||||||
|
# Create a default user and exchange for unsigned requests
|
||||||
|
default_ex_name = 'binance'
|
||||||
|
self.connect_exchange(exchange_name=default_ex_name, user_name='default')
|
||||||
|
self.default_exchange = self.get_exchange(ename=default_ex_name, uname='default')
|
||||||
|
|
||||||
|
def get_ccxt_exchanges(self) -> List[str]:
|
||||||
|
"""Retrieve the list of available exchanges from CCXT."""
|
||||||
|
return ccxt.exchanges
|
||||||
|
|
||||||
|
def connect_exchange(self, exchange_name: str, user_name: str, api_keys: Dict[str, str] = None) -> bool:
|
||||||
|
"""
|
||||||
|
Initialize and store a reference to the specified exchange.
|
||||||
|
|
||||||
|
:param exchange_name: The name of the exchange.
|
||||||
|
:param user_name: The name of the user connecting the exchange.
|
||||||
|
:param api_keys: Optional API keys for the exchange.
|
||||||
|
:return: True if successful, False otherwise.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
exchange = Exchange(name=exchange_name, api_keys=api_keys, exchange_id=exchange_name.lower())
|
||||||
|
self.add_exchange(user_name, exchange)
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Failed to connect user '{user_name}' to exchange '{exchange_name}': {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def add_exchange(self, user_name: str, exchange: Exchange):
|
||||||
|
"""
|
||||||
|
Add an exchange to the user's list of exchanges.
|
||||||
|
|
||||||
|
:param user_name: The name of the user.
|
||||||
|
:param exchange: The Exchange object to add.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
row = {'user': user_name, 'name': exchange.name, 'reference': exchange, 'balances': exchange.balances}
|
||||||
|
self.exchange_data = add_row(self.exchange_data, row)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Couldn't create an instance of the exchange! {str(e)}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def get_exchange(self, ename: str, uname: str) -> Exchange:
|
||||||
|
"""
|
||||||
|
Get a reference to the specified exchange for a user.
|
||||||
|
|
||||||
|
:param ename: The name of the exchange.
|
||||||
|
:param uname: The name of the user.
|
||||||
|
:return: The Exchange object.
|
||||||
|
"""
|
||||||
|
if not ename or not uname:
|
||||||
|
raise ValueError('Missing argument!')
|
||||||
|
|
||||||
|
exchange_data = self.exchange_data.query("name == @ename and user == @uname")
|
||||||
|
if exchange_data.empty:
|
||||||
|
raise ValueError('No matching exchange found.')
|
||||||
|
|
||||||
|
return exchange_data.at[exchange_data.index[0], 'reference']
|
||||||
|
|
||||||
|
def get_connected_exchanges(self, user_name: str) -> List[str]:
|
||||||
|
"""
|
||||||
|
Get a list of connected exchanges for a user.
|
||||||
|
|
||||||
|
:param user_name: The name of the user.
|
||||||
|
:return: A list of connected exchange names.
|
||||||
|
"""
|
||||||
|
return self.exchange_data.loc[self.exchange_data['user'] == user_name, 'name'].tolist()
|
||||||
|
|
||||||
|
def get_available_exchanges(self) -> List[str]:
|
||||||
|
"""Get a list of available exchanges."""
|
||||||
|
return self.available_exchanges
|
||||||
|
|
||||||
|
def get_exchange_balances(self, user_name: str, name: str) -> pd.Series:
|
||||||
|
"""
|
||||||
|
Get the balances of a specified exchange for a specific user.
|
||||||
|
|
||||||
|
:param user_name: The name of the user.
|
||||||
|
:param name: The name of the exchange.
|
||||||
|
:return: A Series containing the balances.
|
||||||
|
"""
|
||||||
|
filtered_data = self.exchange_data.query("user == @user_name and name == @name")
|
||||||
|
if not filtered_data.empty:
|
||||||
|
return filtered_data.iloc[0]['balances']
|
||||||
|
else:
|
||||||
|
return pd.Series(dtype='object') # Return an empty Series if no match is found
|
||||||
|
|
||||||
|
def get_all_balances(self, user_name: str) -> Dict[str, List[Dict[str, Any]]]:
|
||||||
|
"""
|
||||||
|
Get the balances of all connected exchanges for a user.
|
||||||
|
|
||||||
|
:param user_name: The name of the user.
|
||||||
|
:return: A dictionary containing the balances of all connected exchanges.
|
||||||
|
"""
|
||||||
|
filtered_data = self.exchange_data.loc[self.exchange_data['user'] == user_name, ['name', 'balances']]
|
||||||
|
if filtered_data.empty:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
balances_dict = {row['name']: row['balances'] for _, row in filtered_data.iterrows()}
|
||||||
|
return balances_dict
|
||||||
|
|
||||||
|
def get_all_activated(self, user_name: str, fetch_type: str = 'trades') -> Dict[str, List[Dict[str, Any]]]:
|
||||||
|
"""
|
||||||
|
Get active trades or open orders for all connected exchanges.
|
||||||
|
|
||||||
|
:param user_name: The name of the user.
|
||||||
|
:param fetch_type: The type of data to fetch ('trades' or 'orders').
|
||||||
|
:return: A dictionary indexed by exchange name with lists of active trades or open orders.
|
||||||
|
"""
|
||||||
|
filtered_data = self.exchange_data.loc[self.exchange_data['user'] == user_name, ['name', 'reference']]
|
||||||
|
if filtered_data.empty:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
data_dict = {}
|
||||||
|
for name, reference in filtered_data.itertuples(index=False):
|
||||||
|
if pd.isna(reference):
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
if fetch_type == 'trades':
|
||||||
|
data = reference.get_active_trades()
|
||||||
|
elif fetch_type == 'orders':
|
||||||
|
data = reference.get_open_orders()
|
||||||
|
else:
|
||||||
|
logging.error(f"Invalid fetch type: {fetch_type}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
data_dict[name] = data
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error retrieving data for {name}: {str(e)}")
|
||||||
|
|
||||||
|
return data_dict
|
||||||
|
|
||||||
|
def get_order(self, symbol: str, order_id: str, exchange_name: str, user_name: str) -> Any:
|
||||||
|
"""
|
||||||
|
Get an order from a specified exchange.
|
||||||
|
|
||||||
|
:param symbol: The trading symbol.
|
||||||
|
:param order_id: The order ID.
|
||||||
|
:param exchange_name: The name of the exchange.
|
||||||
|
:param user_name: The name of the user.
|
||||||
|
:return: The order details.
|
||||||
|
"""
|
||||||
|
exchange = self.get_exchange(ename=exchange_name, uname=user_name)
|
||||||
|
return exchange.get_order(symbol=symbol, order_id=order_id)
|
||||||
|
|
||||||
|
def get_trade_info(self, trade, user_name: str, info_type: str) -> Dict[str, Any] | None:
|
||||||
|
"""
|
||||||
|
Get information about a trade (status, executed quantity, executed price).
|
||||||
|
|
||||||
|
:param trade: The trade object.
|
||||||
|
:param user_name: The name of the user.
|
||||||
|
:param info_type: The type of information ('status', 'executed_qty', 'executed_price').
|
||||||
|
:return: The requested information or None if the order is not found.
|
||||||
|
"""
|
||||||
|
exchange = self.get_exchange(ename=trade.target, uname=user_name)
|
||||||
|
if exchange.configured is False:
|
||||||
|
logger.error("Must configure API keys to request trade info.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
order = exchange.get_order(symbol=trade.symbol, order_id=trade.order.orderId)
|
||||||
|
|
||||||
|
if order is None:
|
||||||
|
logger.error(f"Order {trade.order.orderId} for {trade.symbol} not found.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
if isinstance(order, dict):
|
||||||
|
if info_type == 'status':
|
||||||
|
return order.get('status')
|
||||||
|
elif info_type == 'executed_qty':
|
||||||
|
return order.get('filled')
|
||||||
|
elif info_type == 'executed_price':
|
||||||
|
return order.get('average')
|
||||||
|
else:
|
||||||
|
logger.error(f"Invalid info type: {info_type}")
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
logger.error("Order object is not a dictionary")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_price(self, symbol: str, price_source: str = None) -> float:
|
||||||
|
"""
|
||||||
|
Get the current price of a trading pair.
|
||||||
|
|
||||||
|
:param symbol: The trading symbol.
|
||||||
|
:param price_source: Optional alternative source for price.
|
||||||
|
:return: The current price.
|
||||||
|
"""
|
||||||
|
if price_source is None:
|
||||||
|
return self.default_exchange.get_price(symbol=symbol)
|
||||||
|
else:
|
||||||
|
raise ValueError(f'No implementation for price source: {price_source}')
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
from flask import Flask, render_template, request, redirect, jsonify, session, flash
|
from flask import Flask, render_template, request, redirect, jsonify, session, flash
|
||||||
from flask_cors import CORS
|
from flask_cors import CORS
|
||||||
from flask_sock import Sock
|
from flask_sock import Sock
|
||||||
|
|
@ -8,6 +10,9 @@ from email_validator import validate_email, EmailNotValidError
|
||||||
import config
|
import config
|
||||||
from BrighterTrades import BrighterTrades
|
from BrighterTrades import BrighterTrades
|
||||||
|
|
||||||
|
# Set up logging
|
||||||
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
|
||||||
# Create a BrighterTrades object. This the main application that maintains access to the server, local storage,
|
# Create a BrighterTrades object. This the main application that maintains access to the server, local storage,
|
||||||
# and manages objects that process trade data.
|
# and manages objects that process trade data.
|
||||||
brighter_trades = BrighterTrades()
|
brighter_trades = BrighterTrades()
|
||||||
|
|
|
||||||
|
|
@ -1,210 +0,0 @@
|
||||||
import logging
|
|
||||||
import json
|
|
||||||
from typing import List, Any, Dict
|
|
||||||
import pandas as pd
|
|
||||||
import requests
|
|
||||||
import ccxt
|
|
||||||
|
|
||||||
from Exchange import Exchange
|
|
||||||
|
|
||||||
# Setup logging
|
|
||||||
# logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')
|
|
||||||
|
|
||||||
|
|
||||||
# This just makes this method cleaner.
|
|
||||||
def add_row(df, dic):
|
|
||||||
df = pd.concat([df, pd.DataFrame.from_records([dic])], ignore_index=True)
|
|
||||||
return df
|
|
||||||
|
|
||||||
|
|
||||||
class ExchangeInterface:
|
|
||||||
"""
|
|
||||||
Connects and maintains and routes data requests from exchanges.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
# Create a dataframe to hold all the references and info for each user's configured exchanges.
|
|
||||||
self.exchange_data = pd.DataFrame(columns=['user', 'name', 'reference', 'balances'])
|
|
||||||
|
|
||||||
# Populate the list of available exchanges from CCXT
|
|
||||||
self.available_exchanges = self.get_ccxt_exchanges()
|
|
||||||
|
|
||||||
def get_ccxt_exchanges(self) -> List[str]:
|
|
||||||
"""Retrieve the list of available exchanges from CCXT."""
|
|
||||||
return ccxt.exchanges
|
|
||||||
|
|
||||||
def connect_exchange(self, exchange_name: str, user_name: str, api_keys: dict = None) -> bool:
|
|
||||||
"""
|
|
||||||
Initialize and store a reference to the available exchanges.
|
|
||||||
|
|
||||||
:param user_name: The name of the user connecting the exchange.
|
|
||||||
:param api_keys: dict - {api: key, api-secret: key}
|
|
||||||
:param exchange_name: str - The name of the exchange.
|
|
||||||
:return: True if success | None on fail.
|
|
||||||
"""
|
|
||||||
# logging.debug(
|
|
||||||
# f"Attempting to connect to exchange '{exchange_name}' for user '{user_name}' with API keys: {api_keys}")
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Initialize the exchange object
|
|
||||||
exchange = Exchange(name=exchange_name, api_keys=api_keys, exchange_id=exchange_name.lower())
|
|
||||||
|
|
||||||
# Update exchange data with the new connection
|
|
||||||
self.add_exchange(user_name, exchange)
|
|
||||||
# logging.debug(f"Successfully connected to exchange '{exchange_name}' for user '{user_name}'")
|
|
||||||
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Failed to connect user '{user_name}' to exchange '{exchange_name}': {str(e)}")
|
|
||||||
return False # Failed to connect
|
|
||||||
|
|
||||||
def add_exchange(self, user_name: str, exchange: Exchange):
|
|
||||||
try:
|
|
||||||
row = {'user': user_name, 'name': exchange.name, 'reference': exchange, 'balances': exchange.balances}
|
|
||||||
self.exchange_data = add_row(self.exchange_data, row)
|
|
||||||
except Exception as e:
|
|
||||||
if hasattr(e, 'status_code') and e.status_code == 400 and e.error_code == -1021:
|
|
||||||
logging.error("Timestamp ahead of server's time error: Sync your system clock to fix this.")
|
|
||||||
logging.error("Couldn't create an instance of the exchange!:\n", e)
|
|
||||||
raise
|
|
||||||
|
|
||||||
def get_exchange(self, ename: str, uname: str) -> Any:
|
|
||||||
"""Return a reference to the exchange_name."""
|
|
||||||
if not ename or not uname:
|
|
||||||
raise ValueError('Missing argument!')
|
|
||||||
|
|
||||||
exchange_data = self.exchange_data.query("name == @ename and user == @uname")
|
|
||||||
if exchange_data.empty:
|
|
||||||
raise ValueError('No matching exchange found.')
|
|
||||||
|
|
||||||
return exchange_data.at[exchange_data.index[0], 'reference']
|
|
||||||
|
|
||||||
def get_connected_exchanges(self, user_name: str) -> List[str]:
|
|
||||||
"""Return a list of the connected exchanges."""
|
|
||||||
connected_exchanges = self.exchange_data.loc[self.exchange_data['user'] == user_name, 'name'].tolist()
|
|
||||||
return connected_exchanges
|
|
||||||
|
|
||||||
def get_available_exchanges(self) -> List[str]:
|
|
||||||
""" Return a list of the exchanges available to connect to"""
|
|
||||||
return self.available_exchanges
|
|
||||||
|
|
||||||
def get_exchange_balances(self, name: str) -> pd.Series:
|
|
||||||
""" Return the balances of a single exchange_name"""
|
|
||||||
return self.exchange_data.query("name == @name")['balances']
|
|
||||||
|
|
||||||
def get_all_balances(self, user_name: str) -> Dict[str, List[Dict[str, Any]]]:
|
|
||||||
"""
|
|
||||||
Return the balances of all connected exchanges indexed by name.
|
|
||||||
|
|
||||||
:param user_name: str - The name of the user.
|
|
||||||
:return: dict - A dictionary containing the balances of all connected exchanges.
|
|
||||||
The dictionary is indexed by exchange name, and the values are lists of dictionaries
|
|
||||||
containing the asset balances and P&L information for each exchange.
|
|
||||||
"""
|
|
||||||
filtered_data = self.exchange_data.loc[self.exchange_data['user'] == user_name, ['name', 'balances']]
|
|
||||||
if filtered_data.empty:
|
|
||||||
return {}
|
|
||||||
|
|
||||||
balances_dict = {}
|
|
||||||
for _, row in filtered_data.iterrows():
|
|
||||||
exchange_name = row['name']
|
|
||||||
balances = row['balances']
|
|
||||||
balances_dict[exchange_name] = balances
|
|
||||||
|
|
||||||
return balances_dict
|
|
||||||
|
|
||||||
def get_all_activated(self, user_name: str, fetch_type: str = 'trades') -> Dict[str, List[Dict[str, Any]]]:
|
|
||||||
"""Get active trades or open orders as a dictionary indexed by name"""
|
|
||||||
filtered_data = self.exchange_data.loc[self.exchange_data['user'] == user_name, ['name', 'reference']]
|
|
||||||
if filtered_data.empty:
|
|
||||||
return {}
|
|
||||||
|
|
||||||
data_dict = {}
|
|
||||||
for name, reference in filtered_data.itertuples(index=False):
|
|
||||||
if pd.isna(reference):
|
|
||||||
continue
|
|
||||||
|
|
||||||
try:
|
|
||||||
if fetch_type == 'trades':
|
|
||||||
data = reference.get_active_trades()
|
|
||||||
elif fetch_type == 'orders':
|
|
||||||
data = reference.get_open_orders()
|
|
||||||
else:
|
|
||||||
logging.error(f"Invalid fetch type: {fetch_type}")
|
|
||||||
return {}
|
|
||||||
|
|
||||||
data_dict[name] = data
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Error retrieving data for {name}: {str(e)}")
|
|
||||||
|
|
||||||
return data_dict
|
|
||||||
|
|
||||||
def get_order(self, symbol: str, order_id: str, target: str, user_name: str) -> Any:
|
|
||||||
"""
|
|
||||||
Return order - from a target exchange_interface.
|
|
||||||
:param user_name: The name of the user making the request.
|
|
||||||
:param symbol: trading symbol
|
|
||||||
:param order_id: The order ID
|
|
||||||
:param target: The exchange_interface to fetch this info.
|
|
||||||
:return: { Success: order| Fail: None }
|
|
||||||
"""
|
|
||||||
# Target exchange_interface.
|
|
||||||
exchange = self.get_exchange(ename=target, uname=user_name)
|
|
||||||
# Return the order.
|
|
||||||
return exchange.get_order(symbol=symbol, order_id=order_id)
|
|
||||||
|
|
||||||
def get_trade_status(self, trade, user_name: str) -> str:
|
|
||||||
"""
|
|
||||||
Return the status of a trade
|
|
||||||
Todo: trade order.status might be outdated this request the status from the exchanges order record.
|
|
||||||
Todo You could just update the trade and get the status from there.
|
|
||||||
"""
|
|
||||||
# Target exchange_interface.
|
|
||||||
exchange = self.get_exchange(ename=trade.target, uname=user_name)
|
|
||||||
# Get the order from the target.
|
|
||||||
order = exchange.get_order(symbol=trade.symbol, order_id=trade.order.orderId)
|
|
||||||
# Return status.
|
|
||||||
return order['status']
|
|
||||||
|
|
||||||
def get_trade_executed_qty(self, trade, user_name: str) -> float:
|
|
||||||
"""
|
|
||||||
Return the executed quantity of a trade.
|
|
||||||
|
|
||||||
:param user_name: The name of the user executing the command.
|
|
||||||
:param trade: todo:
|
|
||||||
|
|
||||||
"""
|
|
||||||
# Target exchange_interface.
|
|
||||||
exchange = self.get_exchange(ename=trade.target, uname=user_name)
|
|
||||||
# Get the order from the target.
|
|
||||||
order = exchange.get_order(symbol=trade.symbol, order_id=trade.order.orderId)
|
|
||||||
# Return quantity.
|
|
||||||
return order['executedQty']
|
|
||||||
|
|
||||||
def get_trade_executed_price(self, trade, user_name: str) -> float:
|
|
||||||
"""
|
|
||||||
Return the average price of executed quantity of a trade
|
|
||||||
|
|
||||||
:param user_name: The name of the user executing this trade.
|
|
||||||
:param trade:
|
|
||||||
"""
|
|
||||||
# Target exchange_interface.
|
|
||||||
exchange = self.get_exchange(ename=trade.target, uname=user_name)
|
|
||||||
# Get the order from the target.
|
|
||||||
order = exchange.get_order(symbol=trade.symbol, order_id=trade.order.orderId)
|
|
||||||
# Return quantity.
|
|
||||||
return order['price']
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_price(symbol: str, price_source: str = None) -> float:
|
|
||||||
"""
|
|
||||||
:param price_source: alternative sources for price.
|
|
||||||
:param symbol: The symbol of the trading pair.
|
|
||||||
:return: The current ticker price.
|
|
||||||
"""
|
|
||||||
if price_source is None:
|
|
||||||
request = requests.get(f'https://api.binance.com/api/v3/ticker/price?symbol={symbol}')
|
|
||||||
json_obj = json.loads(request.text)
|
|
||||||
return float(json_obj['price'])
|
|
||||||
else:
|
|
||||||
raise ValueError(f'No implementation for price source: {price_source}')
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
<div class="a1" >
|
<div class="a1" >
|
||||||
<!-- Chart specific controls -->
|
<!-- Chart specific controls -->
|
||||||
<div id="chart_controls">
|
<div id="chart_controls">
|
||||||
<!-- Container target for any indicator output -->
|
<!-- Container exchange_name for any indicator output -->
|
||||||
<div id="indicator_output" ></div>
|
<div id="indicator_output" ></div>
|
||||||
<!-- Trading pair selector -->
|
<!-- Trading pair selector -->
|
||||||
<form id="tp_selector" action="/settings" method="post">
|
<form id="tp_selector" action="/settings" method="post">
|
||||||
|
|
|
||||||
|
|
@ -513,7 +513,7 @@ class Trades:
|
||||||
|
|
||||||
# Required fields.
|
# Required fields.
|
||||||
if not target or not symbol or not side or not order_type:
|
if not target or not symbol or not side or not order_type:
|
||||||
return 'Error', 'Missing argument: target, symbol, side and order_type required.'
|
return 'Error', 'Missing argument: exchange_name, symbol, side and order_type required.'
|
||||||
|
|
||||||
# If quantity is not provided set it to a small amount.
|
# If quantity is not provided set it to a small amount.
|
||||||
# It will be rounded up to the minimum required amount by the exchange_interface.
|
# It will be rounded up to the minimum required amount by the exchange_interface.
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
from DataCache import DataCache
|
from DataCache import DataCache
|
||||||
from exchangeinterface import ExchangeInterface
|
from ExchangeInterface import ExchangeInterface
|
||||||
import unittest
|
import unittest
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import datetime as dt
|
import datetime as dt
|
||||||
|
|
|
||||||
|
|
@ -181,13 +181,6 @@ class TestExchange(unittest.TestCase):
|
||||||
self.assertEqual(price, 0.0)
|
self.assertEqual(price, 0.0)
|
||||||
self.mock_client.fetch_ticker.assert_called_with('BTC/USDT')
|
self.mock_client.fetch_ticker.assert_called_with('BTC/USDT')
|
||||||
|
|
||||||
@patch('ccxt.binance')
|
|
||||||
def test_fetch_order_invalid_response(self, mock_exchange):
|
|
||||||
self.mock_client.fetch_order.side_effect = ccxt.ExchangeError('Invalid response')
|
|
||||||
order = self.exchange.get_order('BTC/USDT', 'invalid_order_id')
|
|
||||||
self.assertIsNone(order)
|
|
||||||
self.mock_client.fetch_order.assert_called_with('invalid_order_id', 'BTC/USDT')
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import json
|
||||||
|
|
||||||
from Configuration import Configuration
|
from Configuration import Configuration
|
||||||
from DataCache import DataCache
|
from DataCache import DataCache
|
||||||
from exchangeinterface import ExchangeInterface
|
from ExchangeInterface import ExchangeInterface
|
||||||
|
|
||||||
# Object that interacts and maintains exchange_interface and account data
|
# Object that interacts and maintains exchange_interface and account data
|
||||||
exchanges = ExchangeInterface()
|
exchanges = ExchangeInterface()
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import datetime
|
||||||
|
|
||||||
from candles import Candles
|
from candles import Candles
|
||||||
from Configuration import Configuration
|
from Configuration import Configuration
|
||||||
from exchangeinterface import ExchangeInterface
|
from ExchangeInterface import ExchangeInterface
|
||||||
|
|
||||||
|
|
||||||
def test_sqlite():
|
def test_sqlite():
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
import logging
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
from datetime import datetime
|
||||||
|
from ExchangeInterface import ExchangeInterface
|
||||||
|
from Exchange import Exchange
|
||||||
|
from typing import Dict, Any
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Trade:
|
||||||
|
"""
|
||||||
|
Mock Trade class to simulate trade objects used in the tests.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, target, symbol, order_id):
|
||||||
|
self.target = target
|
||||||
|
self.symbol = symbol
|
||||||
|
self.order = MagicMock(orderId=order_id)
|
||||||
|
|
||||||
|
|
||||||
|
class TestExchangeInterface(unittest.TestCase):
|
||||||
|
|
||||||
|
@patch('Exchange.Exchange')
|
||||||
|
def setUp(self, MockExchange):
|
||||||
|
|
||||||
|
self.exchange_interface = ExchangeInterface()
|
||||||
|
|
||||||
|
# Mock exchange instances
|
||||||
|
self.mock_exchange = MockExchange.return_value
|
||||||
|
|
||||||
|
# Setup test data
|
||||||
|
self.user_name = "test_user"
|
||||||
|
self.exchange_name = "binance"
|
||||||
|
self.api_keys = {'key': 'test_key', 'secret': 'test_secret'}
|
||||||
|
|
||||||
|
# Connect the mock exchange
|
||||||
|
self.exchange_interface.connect_exchange(self.exchange_name, self.user_name, self.api_keys)
|
||||||
|
|
||||||
|
# Mock trade object
|
||||||
|
self.trade = Trade(target=self.exchange_name, symbol="BTC/USDT", order_id="12345")
|
||||||
|
|
||||||
|
# Example order data
|
||||||
|
self.order_data: Dict[str, Any] = {
|
||||||
|
'status': 'closed',
|
||||||
|
'filled': 1.0,
|
||||||
|
'average': 50000.0
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_get_trade_status(self):
|
||||||
|
self.mock_exchange.get_order.return_value = self.order_data
|
||||||
|
assert isinstance(self.mock_exchange.get_order.return_value, dict) # Ensure return value is dict
|
||||||
|
|
||||||
|
with self.assertLogs(level='ERROR') as log:
|
||||||
|
status = self.exchange_interface.get_trade_info(self.trade, self.user_name, 'status')
|
||||||
|
if any('Must configure API keys' in message for message in log.output):
|
||||||
|
return
|
||||||
|
self.assertEqual(status, 'closed')
|
||||||
|
|
||||||
|
def test_get_trade_executed_qty(self):
|
||||||
|
self.mock_exchange.get_order.return_value = self.order_data
|
||||||
|
assert isinstance(self.mock_exchange.get_order.return_value, dict) # Ensure return value is dict
|
||||||
|
|
||||||
|
with self.assertLogs(level='ERROR') as log:
|
||||||
|
executed_qty = self.exchange_interface.get_trade_info(self.trade, self.user_name, 'executed_qty')
|
||||||
|
if any('Must configure API keys' in message for message in log.output):
|
||||||
|
return
|
||||||
|
self.assertEqual(executed_qty, 1.0)
|
||||||
|
|
||||||
|
def test_get_trade_executed_price(self):
|
||||||
|
self.mock_exchange.get_order.return_value = self.order_data
|
||||||
|
assert isinstance(self.mock_exchange.get_order.return_value, dict) # Ensure return value is dict
|
||||||
|
|
||||||
|
with self.assertLogs(level='ERROR') as log:
|
||||||
|
executed_price = self.exchange_interface.get_trade_info(self.trade, self.user_name, 'executed_price')
|
||||||
|
if any('Must configure API keys' in message for message in log.output):
|
||||||
|
return
|
||||||
|
self.assertEqual(executed_price, 50000.0)
|
||||||
|
|
||||||
|
def test_invalid_info_type(self):
|
||||||
|
self.mock_exchange.get_order.return_value = self.order_data
|
||||||
|
assert isinstance(self.mock_exchange.get_order.return_value, dict) # Ensure return value is dict
|
||||||
|
|
||||||
|
with self.assertLogs(level='ERROR') as log:
|
||||||
|
result = self.exchange_interface.get_trade_info(self.trade, self.user_name, 'invalid_type')
|
||||||
|
if any('Must configure API keys' in message for message in log.output):
|
||||||
|
return
|
||||||
|
self.assertIsNone(result)
|
||||||
|
self.assertTrue(any('Invalid info type' in message for message in log.output))
|
||||||
|
|
||||||
|
def test_order_not_found(self):
|
||||||
|
self.mock_exchange.get_order.return_value = None
|
||||||
|
|
||||||
|
with self.assertLogs(level='ERROR') as log:
|
||||||
|
result = self.exchange_interface.get_trade_info(self.trade, self.user_name, 'status')
|
||||||
|
if any('Must configure API keys' in message for message in log.output):
|
||||||
|
return
|
||||||
|
self.assertIsNone(result)
|
||||||
|
self.assertTrue(any('Order 12345 for BTC/USDT not found.' in message for message in log.output))
|
||||||
|
|
||||||
|
def test_get_price_default_source(self):
|
||||||
|
# Setup the mock to return a specific price
|
||||||
|
symbol = "BTC/USD"
|
||||||
|
price = self.exchange_interface.get_price(symbol)
|
||||||
|
|
||||||
|
self.assertLess(0.1, price)
|
||||||
|
|
||||||
|
def test_get_price_with_invalid_source(self):
|
||||||
|
symbol = "BTC/USD"
|
||||||
|
with self.assertRaises(ValueError) as context:
|
||||||
|
self.exchange_interface.get_price(symbol, price_source="invalid_source")
|
||||||
|
|
||||||
|
self.assertTrue('No implementation for price source: invalid_source' in str(context.exception))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
|
|
@ -3,7 +3,7 @@ import json
|
||||||
from Configuration import Configuration
|
from Configuration import Configuration
|
||||||
from DataCache import DataCache
|
from DataCache import DataCache
|
||||||
from candles import Candles
|
from candles import Candles
|
||||||
from exchangeinterface import ExchangeInterface
|
from ExchangeInterface import ExchangeInterface
|
||||||
from indicators import Indicators
|
from indicators import Indicators
|
||||||
|
|
||||||
ALPACA_API_KEY = 'PKPSH7OHWH3Q5AUBZBE5'
|
ALPACA_API_KEY = 'PKPSH7OHWH3Q5AUBZBE5'
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
from exchangeinterface import ExchangeInterface
|
from ExchangeInterface import ExchangeInterface
|
||||||
from trade import Trades
|
from trade import Trades
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -103,7 +103,7 @@ def test_load_trades():
|
||||||
print(f'Active trades: {test_trades_obj.active_trades}')
|
print(f'Active trades: {test_trades_obj.active_trades}')
|
||||||
trades = [{
|
trades = [{
|
||||||
'order_price': 24595.4,
|
'order_price': 24595.4,
|
||||||
'target': 'backtester',
|
'exchange_name': 'backtester',
|
||||||
'base_order_qty': 0.05,
|
'base_order_qty': 0.05,
|
||||||
'order': None,
|
'order': None,
|
||||||
'fee': 0.1,
|
'fee': 0.1,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue