implemented tests for Exchangeinterface.py

This commit is contained in:
Rob 2024-08-04 02:06:23 -03:00
parent 917ccedbaf
commit c398a423a3
15 changed files with 375 additions and 254 deletions

View File

@ -6,7 +6,7 @@ from Strategies import Strategies
from backtesting import Backtester
from candles import Candles
from Configuration import Configuration
from exchangeinterface import ExchangeInterface
from ExchangeInterface import ExchangeInterface
from indicators import Indicators
from Signals import Signals
from trade import Trades
@ -429,14 +429,14 @@ class BrighterTrades:
return None
# 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'),
qty=vld('quantity'))
if status == 'Error':
print(f'Error placing the trade: {result}')
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'side={vld("side")}, '
f'type={vld("orderType")}, '

View File

@ -5,8 +5,7 @@ from Database import Database
from shared_utilities import query_satisfied, query_uptodate, unix_time_millis, timeframe_to_minutes
import logging
# Set up logging
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)

View File

@ -1,12 +1,10 @@
import ccxt
import pandas as pd
from datetime import datetime, timedelta
from typing import Tuple, Dict, List, Union
from typing import Tuple, Dict, List, Union, Any
import time
import logging
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
@ -29,8 +27,11 @@ class Exchange:
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.configured = False
self.exchange_id = exchange_id
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()
@ -63,6 +64,17 @@ class Exchange:
'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
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)}")
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]:
"""
Sets the list of available symbols on the exchange.
@ -365,7 +360,7 @@ class Exchange:
"""
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.
@ -374,9 +369,13 @@ class Exchange:
order_id (str): The ID of the order.
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,
quantity: float, price: float = None) -> Tuple[str, object]:
@ -418,7 +417,8 @@ class Exchange:
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) -> 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.
@ -433,6 +433,7 @@ class Exchange:
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}")

214
src/ExchangeInterface.py Normal file
View File

@ -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}')

View File

@ -1,4 +1,6 @@
import json
import logging
from flask import Flask, render_template, request, redirect, jsonify, session, flash
from flask_cors import CORS
from flask_sock import Sock
@ -8,6 +10,9 @@ from email_validator import validate_email, EmailNotValidError
import config
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,
# and manages objects that process trade data.
brighter_trades = BrighterTrades()

View File

@ -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}')

View File

@ -3,7 +3,7 @@
<div class="a1" >
<!-- Chart specific controls -->
<div id="chart_controls">
<!-- Container target for any indicator output -->
<!-- Container exchange_name for any indicator output -->
<div id="indicator_output" ></div>
<!-- Trading pair selector -->
<form id="tp_selector" action="/settings" method="post">

View File

@ -513,7 +513,7 @@ class Trades:
# Required fields.
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.
# It will be rounded up to the minimum required amount by the exchange_interface.

View File

@ -1,5 +1,5 @@
from DataCache import DataCache
from exchangeinterface import ExchangeInterface
from ExchangeInterface import ExchangeInterface
import unittest
import pandas as pd
import datetime as dt

View File

@ -181,13 +181,6 @@ class TestExchange(unittest.TestCase):
self.assertEqual(price, 0.0)
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__':
unittest.main()

View File

@ -2,7 +2,7 @@ import json
from Configuration import Configuration
from DataCache import DataCache
from exchangeinterface import ExchangeInterface
from ExchangeInterface import ExchangeInterface
# Object that interacts and maintains exchange_interface and account data
exchanges = ExchangeInterface()

View File

@ -2,7 +2,7 @@ import datetime
from candles import Candles
from Configuration import Configuration
from exchangeinterface import ExchangeInterface
from ExchangeInterface import ExchangeInterface
def test_sqlite():

View File

@ -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()

View File

@ -3,7 +3,7 @@ import json
from Configuration import Configuration
from DataCache import DataCache
from candles import Candles
from exchangeinterface import ExchangeInterface
from ExchangeInterface import ExchangeInterface
from indicators import Indicators
ALPACA_API_KEY = 'PKPSH7OHWH3Q5AUBZBE5'

View File

@ -1,4 +1,4 @@
from exchangeinterface import ExchangeInterface
from ExchangeInterface import ExchangeInterface
from trade import Trades
@ -103,7 +103,7 @@ def test_load_trades():
print(f'Active trades: {test_trades_obj.active_trades}')
trades = [{
'order_price': 24595.4,
'target': 'backtester',
'exchange_name': 'backtester',
'base_order_qty': 0.05,
'order': None,
'fee': 0.1,