From 1f75636e56132f09a08032d3ef71d319d711164e Mon Sep 17 00:00:00 2001 From: enenn Date: Fri, 6 Apr 2018 09:57:08 +0200 Subject: [PATCH] [1/3] Add support for multiple exchanges with ccxt (objectified version) (#585) * remove obsolete helper functions and make _state a public member. * remove function assertions * revert worker() changes * Update pytest from 3.4.2 to 3.5.0 * Adapt exchange functions to ccxt API Remove get_market_summaries and get_wallet_health, add exception handling * Add NetworkException * Change pair format in constants.py * Add tests for exchange functions that comply with ccxt * Remove bittrex tests * Remove Bittrex and Interface classes * Add retrier decorator * Remove cache from get_ticker * Remove unused and duplicate imports * Add keyword arguments for get_fee * Implement 'get_pair_detail_url' * Change get_ticker_history format to ccxt format * Fix exchange urls dict, don't need to initialize exchanges * Add "Using Exchange ..." logging line --- freqtrade/exchange/__init__.py | 271 ++++++++----- freqtrade/freqtradebot.py | 43 +-- freqtrade/optimize/backtesting.py | 5 +- freqtrade/rpc/rpc.py | 18 +- freqtrade/tests/conftest.py | 274 +++----------- freqtrade/tests/exchange/test_exchange.py | 358 ++++++++++++++---- freqtrade/tests/rpc/test_rpc.py | 28 +- freqtrade/tests/rpc/test_rpc_telegram.py | 48 +-- freqtrade/tests/test_freqtradebot.py | 20 +- freqtrade/tests/test_persistence.py | 1 - .../tests/testdata/download_backtest_data.py | 4 +- requirements.txt | 2 +- 12 files changed, 586 insertions(+), 486 deletions(-) diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 088128dab..b177de962 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -1,35 +1,39 @@ # pragma pylint: disable=W0603 """ Cryptocurrency Exchanges support """ -import enum import logging -import ccxt from random import randint from typing import List, Dict, Any, Optional -from cachetools import cached, TTLCache -from datetime import datetime +import ccxt import arrow -import requests -from freqtrade import OperationalException, NetworkException +from freqtrade import OperationalException, DependencyException, NetworkException + logger = logging.getLogger(__name__) # Current selected exchange -_API = None +_API: ccxt.Exchange = None + _CONF: dict = {} API_RETRY_COUNT = 4 # Holds all open sell orders for dry_run _DRY_RUN_OPEN_ORDERS: Dict[str, Any] = {} +# Urls to exchange markets, insert quote and base with .format() +_EXCHANGE_URLS = { + ccxt.bittrex.__name__: '/Market/Index?MarketName={quote}-{base}', + ccxt.binance.__name__: '/tradeDetail.html?symbol={base}_{quote}' +} + + def retrier(f): def wrapper(*args, **kwargs): count = kwargs.pop('count', API_RETRY_COUNT) try: return f(*args, **kwargs) - # TODO dont be a gotta-catch-them-all pokemon collector - except Exception as ex: + except (NetworkException, DependencyException) as ex: logger.warning('%s returned exception: "%s"', f, ex) if count > 0: count -= 1 @@ -41,19 +45,6 @@ def retrier(f): return wrapper -def _get_market_url(exchange): - "get market url for exchange" - # TODO: PR to ccxt - base = exchange.urls.get('www') - market = "" - if 'bittrex' in get_name(): - market = base + '/Market/Index?MarketName={}' - if 'binance' in get_name(): - market = base + '/trade.html?symbol={}' - - return market - - def init(config: dict) -> None: """ Initializes this module with the given config, @@ -74,18 +65,19 @@ def init(config: dict) -> None: # Find matching class for the given exchange name name = exchange_config['name'] - # Init the exchange if the exchange name passed is supported + if name not in ccxt.exchanges: + raise OperationalException('Exchange {} is not supported'.format(name)) try: _API = getattr(ccxt, name.lower())({ 'apiKey': exchange_config.get('key'), 'secret': exchange_config.get('secret'), + 'password': exchange_config.get('password'), + 'uid': exchange_config.get('uid'), }) - logger.info('Using Exchange %s', name.capitalize()) except (KeyError, AttributeError): raise OperationalException('Exchange {} is not supported'.format(name)) - # we need load api markets - _API.load_markets() + logger.info('Using Exchange "%s"', get_name()) # Check if all pairs are available validate_pairs(config['exchange']['pair_whitelist']) @@ -99,14 +91,15 @@ def validate_pairs(pairs: List[str]) -> None: :return: None """ - if not _API.markets: - _API.load_markets() + try: + markets = _API.load_markets() + except ccxt.BaseError as e: + logger.warning('Unable to validate pairs (assuming they are correct). Reason: %s', e) + return - markets = _API.markets stake_cur = _CONF['stake_currency'] for pair in pairs: # Note: ccxt has BaseCurrency/QuoteCurrency format for pairs - pair = pair.replace('_', '/') # TODO: add a support for having coins in BTC/USDT format if not pair.endswith(stake_cur): raise OperationalException( @@ -114,120 +107,212 @@ def validate_pairs(pairs: List[str]) -> None: ) if pair not in markets: raise OperationalException( - 'Pair {} is not available at {}'.format(pair, _API.name.lower())) + 'Pair {} is not available at {}'.format(pair, _API.id.lower())) -def buy(pair: str, rate: float, amount: float) -> str: +def buy(pair: str, rate: float, amount: float) -> Dict: if _CONF['dry_run']: global _DRY_RUN_OPEN_ORDERS order_id = 'dry_run_buy_{}'.format(randint(0, 10**6)) _DRY_RUN_OPEN_ORDERS[order_id] = { 'pair': pair, - 'rate': rate, + 'price': rate, 'amount': amount, - 'type': 'LIMIT_BUY', + 'type': 'limit', + 'side': 'buy', 'remaining': 0.0, - 'opened': arrow.utcnow().datetime, - 'closed': arrow.utcnow().datetime, + 'datetime': arrow.utcnow().isoformat(), + 'status': 'closed' } - return order_id + return {'id': order_id} - return _API.buy(pair, rate, amount) + try: + return _API.create_limit_buy_order(pair, amount, rate) + except ccxt.InsufficientFunds as e: + raise DependencyException( + 'Insufficient funds to create limit buy order on market {}.' + 'Tried to buy amount {} at rate {} (total {}).' + 'Message: {}'.format(pair, amount, rate, rate*amount, e) + ) + except ccxt.InvalidOrder as e: + raise DependencyException( + 'Could not create limit buy order on market {}.' + 'Tried to buy amount {} at rate {} (total {}).' + 'Message: {}'.format(pair, amount, rate, rate*amount, e) + ) + except ccxt.NetworkError as e: + raise NetworkException( + 'Could not place buy order due to networking error. Message: {}'.format(e) + ) + except ccxt.BaseError as e: + raise OperationalException(e) -def sell(pair: str, rate: float, amount: float) -> str: +def sell(pair: str, rate: float, amount: float) -> Dict: if _CONF['dry_run']: global _DRY_RUN_OPEN_ORDERS order_id = 'dry_run_sell_{}'.format(randint(0, 10**6)) _DRY_RUN_OPEN_ORDERS[order_id] = { 'pair': pair, - 'rate': rate, + 'price': rate, 'amount': amount, - 'type': 'LIMIT_SELL', + 'type': 'limit', + 'side': 'sell', 'remaining': 0.0, - 'opened': arrow.utcnow().datetime, - 'closed': arrow.utcnow().datetime, + 'datetime': arrow.utcnow().isoformat(), + 'status': 'closed' } - return order_id + return {'id': order_id} - return _API.sell(pair, rate, amount) + try: + return _API.create_limit_sell_order(pair, amount, rate) + except ccxt.InsufficientFunds as e: + raise DependencyException( + 'Insufficient funds to create limit sell order on market {}.' + 'Tried to sell amount {} at rate {} (total {}).' + 'Message: {}'.format(pair, amount, rate, rate*amount, e) + ) + except ccxt.InvalidOrder as e: + raise DependencyException( + 'Could not create limit sell order on market {}.' + 'Tried to sell amount {} at rate {} (total {}).' + 'Message: {}'.format(pair, amount, rate, rate*amount, e) + ) + except ccxt.NetworkError as e: + raise NetworkException( + 'Could not place sell order due to networking error. Message: {}'.format(e) + ) + except ccxt.BaseError as e: + raise OperationalException(e) def get_balance(currency: str) -> float: if _CONF['dry_run']: return 999.9 - return _API.fetch_balance()[currency] + # ccxt exception is already handled by get_balances + balances = get_balances() + return balances[currency]['free'] -def get_balances(): +def get_balances() -> dict: if _CONF['dry_run']: - return [] - - return _API.fetch_balance() - -# @cached(TTLCache(maxsize=100, ttl=30)) -@retrier -def get_ticker(pair: str, refresh: Optional[bool] = True) -> dict: - return _API.fetch_ticker(pair) - - -# @cached(TTLCache(maxsize=100, ttl=30)) -@retrier -def get_ticker_history(pair: str, tick_interval) -> List[List]: - # TODO: tickers need to be in format 1m,5m - # fetch_ohlcv returns an [[datetime,o,h,l,c,v]] - if 'fetchOHLCV' not in _API.has or not _API.has['fetchOHLCV']: - logger.warning('Exhange %s does not support fetching historical candlestick data.', - _API.name) - return [] + return {} try: - ohlcv = _API.fetch_ohlcv(pair, timeframe=str(tick_interval)+"m") - return ohlcv - except IndexError as e: - logger.warning('Empty ticker history. Msg %s', str(e)) + balances = _API.fetch_balance() + # Remove additional info from ccxt results + balances.pop("info", None) + balances.pop("free", None) + balances.pop("total", None) + balances.pop("used", None) + + return balances except ccxt.NetworkError as e: - logger.warning('Could not load ticker history due to networking error. Message: %s', str(e)) + raise NetworkException( + 'Could not get balance due to networking error. Message: {}'.format(e) + ) except ccxt.BaseError as e: - logger.warning('Could not fetch ticker data. Msg: %s', str(e)) - return [] + raise OperationalException(e) -def cancel_order(order_id: str) -> None: +# TODO: remove refresh argument, keeping it to keep track of where it was intended to be used +@retrier +def get_ticker(pair: str, refresh: Optional[bool] = True) -> dict: + try: + return _API.fetch_ticker(pair) + except ccxt.NetworkError as e: + raise NetworkException( + 'Could not load tickers due to networking error. Message: {}'.format(e) + ) + except ccxt.BaseError as e: + raise OperationalException(e) + + +@retrier +def get_ticker_history(pair: str, tick_interval: str) -> List[Dict]: + if 'fetchOHLCV' not in _API.has or not _API.has['fetchOHLCV']: + raise OperationalException( + 'Exchange {} does not support fetching historical candlestick data.'.format(_API.name) + ) + + try: + return _API.fetch_ohlcv(pair, timeframe=tick_interval) + except ccxt.NetworkError as e: + raise NetworkException( + 'Could not load ticker history due to networking error. Message: {}'.format(e) + ) + except ccxt.BaseError as e: + raise OperationalException('Could not fetch ticker data. Msg: {}'.format(e)) + + +def cancel_order(order_id: str, pair: str) -> None: if _CONF['dry_run']: return - return _API.cancel_order(order_id) + try: + return _API.cancel_order(order_id, pair) + except ccxt.NetworkError as e: + raise NetworkException( + 'Could not get order due to networking error. Message: {}'.format(e) + ) + except ccxt.InvalidOrder as e: + raise DependencyException( + 'Could not cancel order. Message: {}'.format(e) + ) + except ccxt.BaseError as e: + raise OperationalException(e) -def get_order(order_id: str) -> Dict: +def get_order(order_id: str, pair: str) -> Dict: if _CONF['dry_run']: order = _DRY_RUN_OPEN_ORDERS[order_id] order.update({ 'id': order_id }) return order - - return _API.get_order(order_id) + try: + return _API.fetch_order(order_id, pair) + except ccxt.NetworkError as e: + raise NetworkException( + 'Could not get order due to networking error. Message: {}'.format(e) + ) + except ccxt.InvalidOrder as e: + raise DependencyException( + 'Could not get order. Message: {}'.format(e) + ) + except ccxt.BaseError as e: + raise OperationalException(e) def get_pair_detail_url(pair: str) -> str: - return _get_market_url(_API).format( - _API.markets[pair]['id'] - ) + try: + url_base = _API.urls.get('www') + base, quote = pair.split('/') + + return url_base + _EXCHANGE_URLS[_API.id].format(base=base, quote=quote) + except KeyError: + logger.warning('Could not get exchange url for %s', get_name()) + return "" -def get_markets() -> List[str]: - return _API.get_markets() - - -def get_market_summaries() -> List[Dict]: - return _API.fetch_tickers() +def get_markets() -> List[dict]: + try: + return _API.fetch_markets() + except ccxt.NetworkError as e: + raise NetworkException( + 'Could not load markets due to networking error. Message: {}'.format(e) + ) + except ccxt.BaseError as e: + raise OperationalException(e) def get_name() -> str: - return _API.__class__.__name__.capitalize() + return _API.name + + +def get_id() -> str: + return _API.id def get_fee_maker() -> float: @@ -239,11 +324,9 @@ def get_fee_taker() -> float: def get_fee() -> float: - return get_fee_taker() - - -def get_wallet_health() -> List[Dict]: - if not _API.markets: + # validate that markets are loaded before trying to get fee + if _API.markets is None or len(_API.markets) == 0: _API.load_markets() - return _API.markets + return _API.calculate_fee(symbol='ETH/BTC', type='', side='', amount=1, price=1)['rate'] + diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index ef9bd593b..b3f0b8144 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -41,7 +41,7 @@ class FreqtradeBot(object): self.logger = Logger(name=__name__, level=config.get('loglevel')).get_logger() # Init bot states - self._state = State.STOPPED + self.state = State.STOPPED # Init objects self.config = config @@ -71,9 +71,9 @@ class FreqtradeBot(object): initial_state = self.config.get('initial_state') if initial_state: - self.update_state(State[initial_state.upper()]) + self.state = State[initial_state.upper()] else: - self.update_state(State.STOPPED) + self.state = State.STOPPED def clean(self) -> bool: """ @@ -82,41 +82,26 @@ class FreqtradeBot(object): """ self.rpc.send_msg('*Status:* `Stopping trader...`') self.logger.info('Stopping trader and cleaning up modules...') - self.update_state(State.STOPPED) + self.state = State.STOPPED self.rpc.cleanup() persistence.cleanup() return True - def update_state(self, state: State) -> None: - """ - Updates the application state - :param state: new state - :return: None - """ - self._state = state - - def get_state(self) -> State: - """ - Gets the current application state - :return: - """ - return self._state - def worker(self, old_state: None) -> State: """ Trading routine that must be run at each loop :param old_state: the previous service state from the previous call :return: current service state """ - new_state = self.get_state() # Log state transition - if new_state != old_state: - self.rpc.send_msg('*Status:* `{}`'.format(new_state.name.lower())) - self.logger.info('Changing state to: %s', new_state.name) + state = self.state + if state != old_state: + self.rpc.send_msg('*Status:* `{}`'.format(state.name.lower())) + self.logger.info('Changing state to: %s', state.name) - if new_state == State.STOPPED: + if state == State.STOPPED: time.sleep(1) - elif new_state == State.RUNNING: + elif state == State.RUNNING: min_secs = self.config.get('internals', {}).get( 'process_throttle_secs', Constants.PROCESS_THROTTLE_SECS @@ -130,7 +115,7 @@ class FreqtradeBot(object): self._throttle(func=self._process, min_secs=min_secs, nb_assets=nb_assets) - return new_state + return state def _throttle(self, func: Callable[..., Any], min_secs: float, *args, **kwargs) -> Any: """ @@ -196,7 +181,7 @@ class FreqtradeBot(object): ) ) self.logger.exception('OperationalException. Stopping trader ...') - self.update_state(State.STOPPED) + self.state = State.STOPPED return state_changed @cached(TTLCache(maxsize=1, ttl=1800)) @@ -483,8 +468,8 @@ class FreqtradeBot(object): fmt_exp_profit = round(trade.calc_profit_percent(rate=limit) * 100, 2) profit_trade = trade.calc_profit(rate=limit) - current_rate = exchange.get_ticker(trade.pair, False)['bid'] - profit = trade.calc_profit_percent(current_rate) + current_rate = exchange.get_ticker(trade.pair)['bid'] + profit = trade.calc_profit_percent(limit) message = "*{exchange}:* Selling\n" \ "*Current Pair:* [{pair}]({pair_url})\n" \ diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 2b583da3c..6a06c6616 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -6,6 +6,7 @@ This module contains the backtesting logic from argparse import Namespace from typing import Dict, Tuple, Any, List, Optional +import ccxt import arrow from pandas import DataFrame, Series from tabulate import tabulate @@ -16,7 +17,6 @@ from freqtrade import exchange from freqtrade.analyze import Analyze from freqtrade.arguments import Arguments from freqtrade.configuration import Configuration - from freqtrade.logger import Logger from freqtrade.misc import file_dump_json from freqtrade.persistence import Trade @@ -53,7 +53,8 @@ class Backtesting(object): self.tickerdata_to_dataframe = self.analyze.tickerdata_to_dataframe self.populate_buy_trend = self.analyze.populate_buy_trend self.populate_sell_trend = self.analyze.populate_sell_trend - # Reest keys for backtesting + + # Reset keys for backtesting self.config['exchange']['key'] = '' self.config['exchange']['secret'] = '' exchange.init(self.config) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index b4592f78a..db6ff69d1 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -41,7 +41,7 @@ class RPC(object): """ # Fetch open trade trades = Trade.query.filter(Trade.is_open.is_(True)).all() - if self.freqtrade.get_state() != State.RUNNING: + if self.freqtrade.state != State.RUNNING: return True, '*Status:* `trader is not running`' elif not trades: return True, '*Status:* `no active trade`' @@ -87,7 +87,7 @@ class RPC(object): def rpc_status_table(self) -> Tuple[bool, Any]: trades = Trade.query.filter(Trade.is_open.is_(True)).all() - if self.freqtrade.get_state() != State.RUNNING: + if self.freqtrade.state != State.RUNNING: return True, '*Status:* `trader is not running`' elif not trades: return True, '*Status:* `no active order`' @@ -285,18 +285,18 @@ class RPC(object): """ Handler for start. """ - if self.freqtrade.get_state() == State.RUNNING: + if self.freqtrade.state == State.RUNNING: return True, '*Status:* `already running`' - self.freqtrade.update_state(State.RUNNING) + self.freqtrade.state = State.RUNNING return False, '`Starting trader ...`' def rpc_stop(self) -> (bool, str): """ Handler for stop. """ - if self.freqtrade.get_state() == State.RUNNING: - self.freqtrade.update_state(State.STOPPED) + if self.freqtrade.state == State.RUNNING: + self.freqtrade.state = State.STOPPED return False, '`Stopping trader ...`' return True, '*Status:* `already stopped`' @@ -329,7 +329,7 @@ class RPC(object): self.freqtrade.execute_sell(trade, current_rate) # ---- EOF def _exec_forcesell ---- - if self.freqtrade.get_state() != State.RUNNING: + if self.freqtrade.state != State.RUNNING: return True, '`trader is not running`' if trade_id == 'all': @@ -357,7 +357,7 @@ class RPC(object): Handler for performance. Shows a performance statistic from finished trades """ - if self.freqtrade.get_state() != State.RUNNING: + if self.freqtrade.state != State.RUNNING: return True, '`trader is not running`' pair_rates = Trade.session.query(Trade.pair, @@ -378,7 +378,7 @@ class RPC(object): Returns the number of trades running :return: None """ - if self.freqtrade.get_state() != State.RUNNING: + if self.freqtrade.state != State.RUNNING: return True, '`trader is not running`' trades = Trade.query.filter(Trade.is_open.is_(True)).all() diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index be21ef9f6..3f14ef87b 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -72,51 +72,6 @@ def default_conf(): "enabled": True, "key": "key", "secret": "secret", - "pair_whitelist": [ - "ETH/BTC", - "NEO/BTC", - "LTC/BTC", - "XRP/BTC" - ] - }, - "telegram": { - "enabled": True, - "token": "token", - "chat_id": "0" - }, - "initial_state": "running", - "loglevel": logging.DEBUG - } - validate(configuration, Constants.CONF_SCHEMA) - return configuration - - -@pytest.fixture(scope="module") -def default_conf_ccxt(): - """ Returns validated configuration suitable for most tests """ - configuration = { - "max_open_trades": 1, - "stake_currency": "BTC", - "stake_amount": 0.001, - "fiat_display_currency": "USD", - "ticker_interval": 5, - "dry_run": True, - "minimal_roi": { - "40": 0.0, - "30": 0.01, - "20": 0.02, - "0": 0.04 - }, - "stoploss": -0.10, - "unfilledtimeout": 600, - "bid_strategy": { - "ask_last_balance": 0.0 - }, - "exchange": { - "name": "ccxt-unittest", - "enabled": True, - "key": "key", - "secret": "secret", "pair_whitelist": [ "ETH/BTC", "TKN/BTC", @@ -204,13 +159,14 @@ def health(): def limit_buy_order(): return { 'id': 'mocked_limit_buy', - 'type': 'LIMIT_BUY', + 'type': 'limit', + 'side': 'buy', 'pair': 'mocked', - 'opened': str(arrow.utcnow().datetime), - 'rate': 0.00001099, + 'datetime': arrow.utcnow().isoformat(), + 'price': 0.00001099, 'amount': 90.99181073, 'remaining': 0.0, - 'closed': str(arrow.utcnow().datetime), + 'status': 'closed' } @@ -218,12 +174,14 @@ def limit_buy_order(): def limit_buy_order_old(): return { 'id': 'mocked_limit_buy_old', - 'type': 'LIMIT_BUY', - 'pair': 'ETH/BTC', - 'opened': str(arrow.utcnow().shift(minutes=-601).datetime), - 'rate': 0.00001099, + 'type': 'limit', + 'side': 'buy', + 'pair': 'mocked', + 'datetime': str(arrow.utcnow().shift(minutes=-601).datetime), + 'price': 0.00001099, 'amount': 90.99181073, 'remaining': 90.99181073, + 'status': 'open' } @@ -231,12 +189,14 @@ def limit_buy_order_old(): def limit_sell_order_old(): return { 'id': 'mocked_limit_sell_old', - 'type': 'LIMIT_SELL', + 'type': 'limit', + 'side': 'sell', 'pair': 'ETH/BTC', - 'opened': str(arrow.utcnow().shift(minutes=-601).datetime), - 'rate': 0.00001099, + 'datetime': arrow.utcnow().shift(minutes=-601).isoformat(), + 'price': 0.00001099, 'amount': 90.99181073, 'remaining': 90.99181073, + 'status': 'open' } @@ -244,12 +204,14 @@ def limit_sell_order_old(): def limit_buy_order_old_partial(): return { 'id': 'mocked_limit_buy_old_partial', - 'type': 'LIMIT_BUY', + 'type': 'limit', + 'side': 'buy', 'pair': 'ETH/BTC', - 'opened': str(arrow.utcnow().shift(minutes=-601).datetime), - 'rate': 0.00001099, + 'datetime': arrow.utcnow().shift(minutes=-601).isoformat(), + 'price': 0.00001099, 'amount': 90.99181073, 'remaining': 67.99181073, + 'status': 'open' } @@ -257,16 +219,47 @@ def limit_buy_order_old_partial(): def limit_sell_order(): return { 'id': 'mocked_limit_sell', - 'type': 'LIMIT_SELL', + 'type': 'limit', + 'side': 'sell', 'pair': 'mocked', - 'opened': str(arrow.utcnow().datetime), - 'rate': 0.00001173, + 'datetime': arrow.utcnow().isoformat(), + 'price': 0.00001173, 'amount': 90.99181073, 'remaining': 0.0, - 'closed': str(arrow.utcnow().datetime), + 'status': 'closed' } +@pytest.fixture +def ticker_history_api(): + return [ + [ + 1511686200000, # unix timestamp ms + 8.794e-05, # open + 8.948e-05, # high + 8.794e-05, # low + 8.88e-05, # close + 0.0877869, # volume (in quote currency) + ], + [ + 1511686500000, + 8.88e-05, + 8.942e-05, + 8.88e-05, + 8.893e-05, + 0.05874751, + ], + [ + 1511686800, + 8.891e-05, + 8.893e-05, + 8.875e-05, + 8.877e-05, + 0.7039405 + ] + ] + + @pytest.fixture def ticker_history(): return [ @@ -342,158 +335,3 @@ def result(): # that inserts a trade of some type and open-status # return the open-order-id # See tests in rpc/main that could use this - - -@pytest.fixture -def get_market_summaries_data(): - """ - This fixture is a real result from exchange.get_market_summaries() but reduced to only - 8 entries. 4 BTC, 4 USTD - :return: JSON market summaries - """ - return { - 'XWC/BTC': { - 'symbol': 'XWC/BTC', - 'info': { - 'Ask': 1.316e-05, - 'BaseVolume': 5.72599471, - 'Bid': 1.3e-05, - 'Created': '2014-04-14T00:00:00', - 'High': 1.414e-05, - 'Last': 1.298e-05, - 'Low': 1.282e-05, - 'MarketName': 'BTC-XWC', - 'OpenBuyOrders': 2000, - 'OpenSellOrders': 1484, - 'PrevDay': 1.376e-05, - 'TimeStamp': '2018-02-05T01:32:40.493', - 'Volume': 424041.21418375 - } - }, - 'XZC/BTC': { - 'symbol': 'XZC/BTC', - 'info': { - 'Ask': 0.00627051, - 'BaseVolume': 93.23302388, - 'Bid': 0.00618192, - 'Created': '2016-10-20T04:48:30.387', - 'High': 0.00669897, - 'Last': 0.00618192, - 'Low': 0.006, - 'MarketName': 'BTC-XZC', - 'OpenBuyOrders': 343, - 'OpenSellOrders': 2037, - 'PrevDay': 0.00668229, - 'TimeStamp': '2018-02-05T01:32:43.383', - 'Volume': 14863.60730702 - } - }, - 'ZCL/BTC': { - 'symbol': 'ZCL/BTC', - 'info': { - 'Ask': 0.01137247, - 'BaseVolume': 383.55922657, - 'Bid': 0.01136006, - 'Created': '2016-11-15T20:29:59.73', - 'High': 0.012, - 'Last': 0.01137247, - 'Low': 0.01119883, - 'MarketName': 'BTC-ZCL', - 'OpenBuyOrders': 1332, - 'OpenSellOrders': 5317, - 'PrevDay': 0.01179603, - 'TimeStamp': '2018-02-05T01:32:42.773', - 'Volume': 33308.07358285 - } - }, - 'ZEC/BTC': { - 'symbol': 'ZEC/BTC', - 'info': { - 'Ask': 0.04155821, - 'BaseVolume': 274.75369074, - 'Bid': 0.04130002, - 'Created': '2016-10-28T17:13:10.833', - 'High': 0.04354429, - 'Last': 0.041585, - 'Low': 0.0413, - 'MarketName': 'BTC-ZEC', - 'OpenBuyOrders': 863, - 'OpenSellOrders': 5579, - 'PrevDay': 0.0429, - 'TimeStamp': '2018-02-05T01:32:43.21', - 'Volume': 6479.84033259 - } - }, - 'XMR/USDT': { - 'symbol': 'XMR/USDT', - 'info': { - 'Ask': 210.99999999, - 'BaseVolume': 615132.70989532, - 'Bid': 210.05503736, - 'Created': '2017-07-21T01:08:49.397', - 'High': 257.396, - 'Last': 211.0, - 'Low': 209.05333589, - 'MarketName': 'USDT-XMR', - 'OpenBuyOrders': 180, - 'OpenSellOrders': 1203, - 'PrevDay': 247.93528899, - 'TimeStamp': '2018-02-05T01:32:43.117', - 'Volume': 2688.17410793 - } - }, - 'XRP/USDT': { - 'symbol': 'XRP/USDT', - 'info': { - 'Ask': 0.79589979, - 'BaseVolume': 9349557.01853031, - 'Bid': 0.789226, - 'Created': '2017-07-14T17:10:10.737', - 'High': 0.977, - 'Last': 0.79589979, - 'Low': 0.781, - 'MarketName': 'USDT-XRP', - 'OpenBuyOrders': 1075, - 'OpenSellOrders': 6508, - 'PrevDay': 0.93300218, - 'TimeStamp': '2018-02-05T01:32:42.383', - 'Volume': 10801663.00788851 - } - }, - 'XVG/USDT': { - 'symbol': 'XVG/USDT', - 'info': { - 'Ask': 0.05154982, - 'BaseVolume': 2311087.71232136, - 'Bid': 0.05040107, - 'Created': '2017-12-29T19:29:18.357', - 'High': 0.06668561, - 'Last': 0.0508, - 'Low': 0.05006731, - 'MarketName': 'USDT-XVG', - 'OpenBuyOrders': 655, - 'OpenSellOrders': 5544, - 'PrevDay': 0.0627, - 'TimeStamp': '2018-02-05T01:32:41.507', - 'Volume': 40031424.2152716 - } - }, - 'ZEC/USDT': { - 'symbol': 'ZEC/USDT', - 'info': { - 'Ask': 332.65500022, - 'BaseVolume': 562911.87455665, - 'Bid': 330.00000001, - 'Created': '2017-07-14T17:10:10.673', - 'High': 401.59999999, - 'Last': 332.65500019, - 'Low': 330.0, - 'MarketName': 'USDT-ZEC', - 'OpenBuyOrders': 161, - 'OpenSellOrders': 1731, - 'PrevDay': 391.42, - 'TimeStamp': '2018-02-05T01:32:42.947', - 'Volume': 1571.09647946 - } - } - } diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 7a4d0483e..b45573d6a 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -3,14 +3,15 @@ import logging from copy import deepcopy from random import randint -from unittest.mock import MagicMock +from unittest.mock import MagicMock, PropertyMock +import ccxt import pytest -import freqtrade.exchange as exchange -from freqtrade import OperationalException +from freqtrade import OperationalException, DependencyException, NetworkException from freqtrade.exchange import init, validate_pairs, buy, sell, get_balance, get_balances, \ - get_ticker, get_ticker_history, cancel_order, get_name, get_fee + get_ticker, get_ticker_history, cancel_order, get_name, get_fee, get_id, get_pair_detail_url +import freqtrade.exchange as exchange from freqtrade.tests.conftest import log_has API_INIT = False @@ -42,7 +43,12 @@ def test_init_exception(default_conf): def test_validate_pairs(default_conf, mocker): api_mock = MagicMock() - api_mock.markets = ["ETH/BTC", "NEO/BTC", "LTC/BTC", "XRP/BTC"] + api_mock.load_markets = MagicMock(return_value={ + 'ETH/BTC': '', 'TKN/BTC': '', 'TRST/BTC': '', 'SWT/BTC': '', 'BCC/BTC': '' + }) + id_mock = PropertyMock(return_value='test_exchange') + type(api_mock).id = id_mock + mocker.patch('freqtrade.exchange._API', api_mock) mocker.patch.dict('freqtrade.exchange._CONF', default_conf) validate_pairs(default_conf['exchange']['pair_whitelist']) @@ -50,7 +56,7 @@ def test_validate_pairs(default_conf, mocker): def test_validate_pairs_not_available(default_conf, mocker): api_mock = MagicMock() - api_mock.get_markets = MagicMock(return_value=[]) + api_mock.load_markets = MagicMock(return_value={}) mocker.patch('freqtrade.exchange._API', api_mock) mocker.patch.dict('freqtrade.exchange._CONF', default_conf) with pytest.raises(OperationalException, match=r'not available'): @@ -59,10 +65,10 @@ def test_validate_pairs_not_available(default_conf, mocker): def test_validate_pairs_not_compatible(default_conf, mocker): api_mock = MagicMock() - api_mock.get_markets = MagicMock( - return_value=['BTC/ETH', 'BTC/TKN', 'BTC/TRST', 'BTC/SWT']) - conf = deepcopy(default_conf) - conf['stake_currency'] = 'ETH' + api_mock.load_markets = MagicMock(return_value={ + 'ETH/BTC': '', 'TKN/BTC': '', 'TRST/BTC': '', 'SWT/BTC': '', 'BCC/BTC': '' + }) + default_conf['stake_currency'] = 'ETH' mocker.patch('freqtrade.exchange._API', api_mock) mocker.patch.dict('freqtrade.exchange._CONF', conf) with pytest.raises(OperationalException, match=r'not compatible'): @@ -72,6 +78,7 @@ def test_validate_pairs_not_compatible(default_conf, mocker): def test_validate_pairs_exception(default_conf, mocker, caplog): caplog.set_level(logging.INFO) api_mock = MagicMock() + api_mock.load_markets = MagicMock(side_effect=ccxt.BaseError()) api_mock.name = 'binance' mocker.patch('freqtrade.exchange._API', api_mock) mocker.patch.dict('freqtrade.exchange._CONF', default_conf) @@ -79,6 +86,9 @@ def test_validate_pairs_exception(default_conf, mocker, caplog): with pytest.raises(OperationalException, match=r'Pair ETH/BTC is not available at binance'): validate_pairs(default_conf['exchange']['pair_whitelist']) + validate_pairs(default_conf['exchange']['pair_whitelist']) + assert log_has('Unable to validate pairs (assuming they are correct). Reason: ', + caplog.record_tuples) def test_validate_pairs_stake_exception(default_conf, mocker, caplog): caplog.set_level(logging.INFO) @@ -99,38 +109,99 @@ def test_buy_dry_run(default_conf, mocker): default_conf['dry_run'] = True mocker.patch.dict('freqtrade.exchange._CONF', default_conf) - assert 'dry_run_buy_' in buy(pair='BTC/ETH', rate=200, amount=1) - + order = buy(pair='ETH/BTC', rate=200, amount=1) + assert 'id' in order + assert 'dry_run_buy_' in order['id'] def test_buy_prod(default_conf, mocker): api_mock = MagicMock() - api_mock.buy = MagicMock( - return_value='dry_run_buy_{}'.format(randint(0, 10**6))) + order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) + api_mock.create_limit_buy_order = MagicMock(return_value={ + 'id': order_id, + 'info': { + 'foo': 'bar' + } + }) mocker.patch('freqtrade.exchange._API', api_mock) default_conf['dry_run'] = False mocker.patch.dict('freqtrade.exchange._CONF', default_conf) - assert 'dry_run_buy_' in buy(pair='BTC/ETH', rate=200, amount=1) + order = buy(pair='ETH/BTC', rate=200, amount=1) + assert 'id' in order + assert 'info' in order + assert order['id'] == order_id + + # test exception handling + with pytest.raises(DependencyException): + api_mock.create_limit_buy_order = MagicMock(side_effect=ccxt.InsufficientFunds) + mocker.patch('freqtrade.exchange._API', api_mock) + buy(pair='ETH/BTC', rate=200, amount=1) + + with pytest.raises(DependencyException): + api_mock.create_limit_buy_order = MagicMock(side_effect=ccxt.InvalidOrder) + mocker.patch('freqtrade.exchange._API', api_mock) + buy(pair='ETH/BTC', rate=200, amount=1) + + with pytest.raises(NetworkException): + api_mock.create_limit_buy_order = MagicMock(side_effect=ccxt.NetworkError) + mocker.patch('freqtrade.exchange._API', api_mock) + buy(pair='ETH/BTC', rate=200, amount=1) + + with pytest.raises(OperationalException): + api_mock.create_limit_buy_order = MagicMock(side_effect=ccxt.BaseError) + mocker.patch('freqtrade.exchange._API', api_mock) + buy(pair='ETH/BTC', rate=200, amount=1) def test_sell_dry_run(default_conf, mocker): default_conf['dry_run'] = True mocker.patch.dict('freqtrade.exchange._CONF', default_conf) - assert 'dry_run_sell_' in sell(pair='BTC/ETH', rate=200, amount=1) + order = sell(pair='ETH/BTC', rate=200, amount=1) + assert 'id' in order + assert 'dry_run_sell_' in order['id'] def test_sell_prod(default_conf, mocker): api_mock = MagicMock() - api_mock.sell = MagicMock( - return_value='dry_run_sell_{}'.format(randint(0, 10**6))) + order_id = 'test_prod_sell_{}'.format(randint(0, 10 ** 6)) + api_mock.create_limit_sell_order = MagicMock(return_value={ + 'id': order_id, + 'info': { + 'foo': 'bar' + } + }) mocker.patch('freqtrade.exchange._API', api_mock) default_conf['dry_run'] = False mocker.patch.dict('freqtrade.exchange._CONF', default_conf) - assert 'dry_run_sell_' in sell(pair='BTC/ETH', rate=200, amount=1) + order = sell(pair='ETH/BTC', rate=200, amount=1) + assert 'id' in order + assert 'info' in order + assert order['id'] == order_id + + # test exception handling + with pytest.raises(DependencyException): + api_mock.create_limit_sell_order = MagicMock(side_effect=ccxt.InsufficientFunds) + mocker.patch('freqtrade.exchange._API', api_mock) + sell(pair='ETH/BTC', rate=200, amount=1) + + with pytest.raises(DependencyException): + api_mock.create_limit_sell_order = MagicMock(side_effect=ccxt.InvalidOrder) + mocker.patch('freqtrade.exchange._API', api_mock) + sell(pair='ETH/BTC', rate=200, amount=1) + + with pytest.raises(NetworkException): + api_mock.create_limit_sell_order = MagicMock(side_effect=ccxt.NetworkError) + mocker.patch('freqtrade.exchange._API', api_mock) + sell(pair='ETH/BTC', rate=200, amount=1) + + with pytest.raises(OperationalException): + api_mock.create_limit_sell_order = MagicMock(side_effect=ccxt.BaseError) + mocker.patch('freqtrade.exchange._API', api_mock) + sell(pair='ETH/BTC', rate=200, amount=1) def test_get_balance_dry_run(default_conf, mocker): @@ -142,7 +213,7 @@ def test_get_balance_dry_run(default_conf, mocker): def test_get_balance_prod(default_conf, mocker): api_mock = MagicMock() - api_mock.get_balance = MagicMock(return_value=123.4) + api_mock.fetch_balance = MagicMock(return_value={'BTC': {'free': 123.4}}) mocker.patch('freqtrade.exchange._API', api_mock) default_conf['dry_run'] = False @@ -150,36 +221,51 @@ def test_get_balance_prod(default_conf, mocker): assert get_balance(currency='BTC') == 123.4 + with pytest.raises(OperationalException): + api_mock.fetch_balance = MagicMock(side_effect=ccxt.BaseError) + mocker.patch('freqtrade.exchange._API', api_mock) + get_balance(currency='BTC') + def test_get_balances_dry_run(default_conf, mocker): default_conf['dry_run'] = True mocker.patch.dict('freqtrade.exchange._CONF', default_conf) - assert get_balances() == [] + assert get_balances() == {} def test_get_balances_prod(default_conf, mocker): balance_item = { - 'Currency': '1ST', - 'Balance': 10.0, - 'Available': 10.0, - 'Pending': 0.0, - 'CryptoAddress': None + 'free': 10.0, + 'total': 10.0, + 'used': 0.0 } api_mock = MagicMock() - api_mock.get_balances = MagicMock( - return_value=[balance_item, balance_item, balance_item]) + api_mock.fetch_balance = MagicMock(return_value={ + '1ST': balance_item, + '2ST': balance_item, + '3ST': balance_item + }) mocker.patch('freqtrade.exchange._API', api_mock) default_conf['dry_run'] = False mocker.patch.dict('freqtrade.exchange._CONF', default_conf) assert len(get_balances()) == 3 - assert get_balances()[0]['Currency'] == '1ST' - assert get_balances()[0]['Balance'] == 10.0 - assert get_balances()[0]['Available'] == 10.0 - assert get_balances()[0]['Pending'] == 0.0 + assert get_balances()['1ST']['free'] == 10.0 + assert get_balances()['1ST']['total'] == 10.0 + assert get_balances()['1ST']['used'] == 0.0 + + with pytest.raises(NetworkException): + api_mock.fetch_balance = MagicMock(side_effect=ccxt.NetworkError) + mocker.patch('freqtrade.exchange._API', api_mock) + get_balances() + + with pytest.raises(OperationalException): + api_mock.fetch_balance = MagicMock(side_effect=ccxt.BaseError) + mocker.patch('freqtrade.exchange._API', api_mock) + get_balances() # This test is somewhat redundant with @@ -187,58 +273,114 @@ def test_get_balances_prod(default_conf, mocker): def test_get_ticker(default_conf, mocker): maybe_init_api(default_conf, mocker) api_mock = MagicMock() - tick = {"success": True, 'result': {'Bid': 0.00001098, 'Ask': 0.00001099, 'Last': 0.0001}} - api_mock.get_ticker = MagicMock(return_value=tick) - mocker.patch('freqtrade.exchange.bittrex._API', api_mock) + tick = { + 'symbol': 'ETH/BTC', + 'bid': 0.00001098, + 'ask': 0.00001099, + 'last': 0.0001, + } + api_mock.fetch_ticker = MagicMock(return_value=tick) + mocker.patch('freqtrade.exchange._API', api_mock) # retrieve original ticker - ticker = get_ticker(pair='BTC/ETH') + ticker = get_ticker(pair='ETH/BTC') + assert ticker['bid'] == 0.00001098 assert ticker['ask'] == 0.00001099 # change the ticker - tick = {"success": True, 'result': {"Bid": 0.5, "Ask": 1, "Last": 42}} - api_mock.get_ticker = MagicMock(return_value=tick) - mocker.patch('freqtrade.exchange.bittrex._API', api_mock) + tick = { + 'symbol': 'ETH/BTC', + 'bid': 0.5, + 'ask': 1, + 'last': 42, + } + api_mock.fetch_ticker = MagicMock(return_value=tick) + mocker.patch('freqtrade.exchange._API', api_mock) # if not caching the result we should get the same ticker # if not fetching a new result we should get the cached ticker - ticker = get_ticker(pair='BTC/ETH', refresh=False) - assert ticker['bid'] == 0.00001098 - assert ticker['ask'] == 0.00001099 + ticker = get_ticker(pair='ETH/BTC') - # force ticker refresh - ticker = get_ticker(pair='BTC/ETH', refresh=True) assert ticker['bid'] == 0.5 assert ticker['ask'] == 1 + with pytest.raises(OperationalException): # test retrier + api_mock.fetch_ticker = MagicMock(side_effect=ccxt.NetworkError) + mocker.patch('freqtrade.exchange._API', api_mock) + get_ticker(pair='ETH/BTC', refresh=True) + + with pytest.raises(OperationalException): + api_mock.fetch_ticker = MagicMock(side_effect=ccxt.BaseError) + mocker.patch('freqtrade.exchange._API', api_mock) + get_ticker(pair='ETH/BTC', refresh=True) + def test_get_ticker_history(default_conf, mocker): api_mock = MagicMock() - tick = 123 - api_mock.get_ticker_history = MagicMock(return_value=tick) + tick = [ + [ + 1511686200000, # unix timestamp ms + 1, # open + 2, # high + 3, # low + 4, # close + 5, # volume (in quote currency) + ] + ] + type(api_mock).has = PropertyMock(return_value={'fetchOHLCV': True}) + api_mock.fetch_ohlcv = MagicMock(return_value=tick) mocker.patch('freqtrade.exchange._API', api_mock) - mocker.patch('freqtrade.exchange._API.has', {'fetchOHLCV': True}) - mocker.patch('freqtrade.exchange._API.fetch_ohlcv', return_value=tick) + # retrieve original ticker - ticks = get_ticker_history('ETH/BTC', int(default_conf['ticker_interval'])) - assert ticks == 123 + ticks = get_ticker_history('ETH/BTC', default_conf['ticker_interval']) + assert ticks[0][0] == 1511686200000 + assert ticks[0][1] == 1 + assert ticks[0][2] == 2 + assert ticks[0][3] == 3 + assert ticks[0][4] == 4 + assert ticks[0][5] == 5 - # change the ticker - tick = 999 - api_mock.get_ticker_history = MagicMock(return_value=tick) + # change ticker and ensure tick changes + new_tick = [ + [ + 1511686210000, # unix timestamp ms + 6, # open + 7, # high + 8, # low + 9, # close + 10, # volume (in quote currency) + ] + ] + api_mock.fetch_ohlcv = MagicMock(return_value=new_tick) mocker.patch('freqtrade.exchange._API', api_mock) - # ensure caching will still return the original ticker - ticks = get_ticker_history('BTC/ETH', int(default_conf['ticker_interval'])) - assert ticks == 123 + ticks = get_ticker_history('ETH/BTC', default_conf['ticker_interval']) + assert ticks[0][0] == 1511686210000 + assert ticks[0][1] == 6 + assert ticks[0][2] == 7 + assert ticks[0][3] == 8 + assert ticks[0][4] == 9 + assert ticks[0][5] == 10 + + with pytest.raises(OperationalException): # test retrier + api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.NetworkError) + mocker.patch('freqtrade.exchange._API', api_mock) + # new symbol to get around cache + get_ticker_history('ABCD/BTC', default_conf['ticker_interval']) + + with pytest.raises(OperationalException): + api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.BaseError) + mocker.patch('freqtrade.exchange._API', api_mock) + # new symbol to get around cache + get_ticker_history('EFGH/BTC', default_conf['ticker_interval']) def test_cancel_order_dry_run(default_conf, mocker): default_conf['dry_run'] = True mocker.patch.dict('freqtrade.exchange._CONF', default_conf) - assert cancel_order(order_id='123') is None + assert cancel_order(order_id='123', pair='TKN/BTC') is None # Ensure that if not dry_run, we should call API @@ -248,7 +390,22 @@ def test_cancel_order(default_conf, mocker): api_mock = MagicMock() api_mock.cancel_order = MagicMock(return_value=123) mocker.patch('freqtrade.exchange._API', api_mock) - assert cancel_order(order_id='_') == 123 + assert cancel_order(order_id='_', pair='TKN/BTC') == 123 + + with pytest.raises(NetworkException): + api_mock.cancel_order = MagicMock(side_effect=ccxt.NetworkError) + mocker.patch('freqtrade.exchange._API', api_mock) + cancel_order(order_id='_', pair='TKN/BTC') + + with pytest.raises(DependencyException): + api_mock.cancel_order = MagicMock(side_effect=ccxt.InvalidOrder) + mocker.patch('freqtrade.exchange._API', api_mock) + cancel_order(order_id='_', pair='TKN/BTC') + + with pytest.raises(OperationalException): + api_mock.cancel_order = MagicMock(side_effect=ccxt.BaseError) + mocker.patch('freqtrade.exchange._API', api_mock) + cancel_order(order_id='_', pair='TKN/BTC') def test_get_order(default_conf, mocker): @@ -257,44 +414,83 @@ def test_get_order(default_conf, mocker): order = MagicMock() order.myid = 123 exchange._DRY_RUN_OPEN_ORDERS['X'] = order - print(exchange.get_order('X')) - assert exchange.get_order('X').myid == 123 + print(exchange.get_order('X', 'TKN/BTC')) + assert exchange.get_order('X', 'TKN/BTC').myid == 123 default_conf['dry_run'] = False mocker.patch.dict('freqtrade.exchange._CONF', default_conf) api_mock = MagicMock() - api_mock.get_order = MagicMock(return_value=456) + api_mock.fetch_order = MagicMock(return_value=456) mocker.patch('freqtrade.exchange._API', api_mock) - assert exchange.get_order('X') == 456 + assert exchange.get_order('X', 'TKN/BTC') == 456 + + with pytest.raises(NetworkException): + api_mock.fetch_order = MagicMock(side_effect=ccxt.NetworkError) + mocker.patch('freqtrade.exchange._API', api_mock) + exchange.get_order(order_id='_', pair='TKN/BTC') + + with pytest.raises(DependencyException): + api_mock.fetch_order = MagicMock(side_effect=ccxt.InvalidOrder) + mocker.patch('freqtrade.exchange._API', api_mock) + exchange.get_order(order_id='_', pair='TKN/BTC') + + with pytest.raises(OperationalException): + api_mock.fetch_order = MagicMock(side_effect=ccxt.BaseError) + mocker.patch('freqtrade.exchange._API', api_mock) + exchange.get_order(order_id='_', pair='TKN/BTC') def test_get_name(default_conf, mocker): mocker.patch('freqtrade.exchange.validate_pairs', side_effect=lambda s: True) + default_conf['exchange']['name'] = 'binance' + init(default_conf) + + assert get_name() == 'Binance' + + +def test_get_id(default_conf, mocker): + mocker.patch('freqtrade.exchange.validate_pairs', + side_effect=lambda s: True) + default_conf['exchange']['name'] = 'binance' + init(default_conf) + + assert get_id() == 'binance' + + +def test_get_pair_detail_url(default_conf, mocker): + mocker.patch('freqtrade.exchange.validate_pairs', + side_effect=lambda s: True) + default_conf['exchange']['name'] = 'binance' + init(default_conf) + + url = get_pair_detail_url('TKN/ETH') + assert 'TKN' in url + assert 'ETH' in url + + url = get_pair_detail_url('LOOONG/BTC') + assert 'LOOONG' in url + assert 'BTC' in url + default_conf['exchange']['name'] = 'bittrex' init(default_conf) - assert get_name() == 'Bittrex' + url = get_pair_detail_url('TKN/ETH') + assert 'TKN' in url + assert 'ETH' in url + + url = get_pair_detail_url('LOOONG/BTC') + assert 'LOOONG' in url + assert 'BTC' in url def test_get_fee(default_conf, mocker): - mocker.patch('freqtrade.exchange.validate_pairs', - side_effect=lambda s: True) - init(default_conf) - - assert get_fee() == 0.0025 - - -def test_exchange_misc(mocker): api_mock = MagicMock() + api_mock.calculate_fee = MagicMock(return_value={ + 'type': 'taker', + 'currency': 'BTC', + 'rate': 0.025, + 'cost': 0.05 + }) mocker.patch('freqtrade.exchange._API', api_mock) - exchange.get_markets() - assert api_mock.get_markets.call_count == 1 - exchange.get_market_summaries() - assert api_mock.get_market_summaries.call_count == 1 - api_mock.name = 123 - assert exchange.get_name() == 123 - api_mock.fee = 456 - assert exchange.get_fee() == 456 - exchange.get_wallet_health() - assert api_mock.get_wallet_health.call_count == 1 + assert get_fee() == 0.025 diff --git a/freqtrade/tests/rpc/test_rpc.py b/freqtrade/tests/rpc/test_rpc.py index 191916488..bbaa2385f 100644 --- a/freqtrade/tests/rpc/test_rpc.py +++ b/freqtrade/tests/rpc/test_rpc.py @@ -41,12 +41,12 @@ def test_rpc_trade_status(default_conf, ticker, mocker) -> None: freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://')) rpc = RPC(freqtradebot) - freqtradebot.update_state(State.STOPPED) + freqtradebot.state = State.STOPPED (error, result) = rpc.rpc_trade_status() assert error assert 'trader is not running' in result - freqtradebot.update_state(State.RUNNING) + freqtradebot.state = State.RUNNING (error, result) = rpc.rpc_trade_status() assert error assert 'no active trade' in result @@ -89,12 +89,12 @@ def test_rpc_status_table(default_conf, ticker, mocker) -> None: freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://')) rpc = RPC(freqtradebot) - freqtradebot.update_state(State.STOPPED) + freqtradebot.state = State.STOPPED (error, result) = rpc.rpc_status_table() assert error assert '*Status:* `trader is not running`' in result - freqtradebot.update_state(State.RUNNING) + freqtradebot.state = State.RUNNING (error, result) = rpc.rpc_status_table() assert error assert '*Status:* `no active order`' in result @@ -344,17 +344,17 @@ def test_rpc_start(mocker, default_conf) -> None: freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://')) rpc = RPC(freqtradebot) - freqtradebot.update_state(State.STOPPED) + freqtradebot.state = State.STOPPED (error, result) = rpc.rpc_start() assert not error assert '`Starting trader ...`' in result - assert freqtradebot.get_state() == State.RUNNING + assert freqtradebot.state == State.RUNNING (error, result) = rpc.rpc_start() assert error assert '*Status:* `already running`' in result - assert freqtradebot.get_state() == State.RUNNING + assert freqtradebot.state == State.RUNNING def test_rpc_stop(mocker, default_conf) -> None: @@ -372,17 +372,17 @@ def test_rpc_stop(mocker, default_conf) -> None: freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://')) rpc = RPC(freqtradebot) - freqtradebot.update_state(State.RUNNING) + freqtradebot.state = State.RUNNING (error, result) = rpc.rpc_stop() assert not error assert '`Stopping trader ...`' in result - assert freqtradebot.get_state() == State.STOPPED + assert freqtradebot.state == State.STOPPED (error, result) = rpc.rpc_stop() assert error assert '*Status:* `already stopped`' in result - assert freqtradebot.get_state() == State.STOPPED + assert freqtradebot.state == State.STOPPED def test_rpc_forcesell(default_conf, ticker, mocker) -> None: @@ -410,12 +410,12 @@ def test_rpc_forcesell(default_conf, ticker, mocker) -> None: freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://')) rpc = RPC(freqtradebot) - freqtradebot.update_state(State.STOPPED) + freqtradebot.state = State.STOPPED (error, res) = rpc.rpc_forcesell(None) assert error assert res == '`trader is not running`' - freqtradebot.update_state(State.RUNNING) + freqtradebot.state = State.RUNNING (error, res) = rpc.rpc_forcesell(None) assert error assert res == 'Invalid argument.' @@ -433,7 +433,7 @@ def test_rpc_forcesell(default_conf, ticker, mocker) -> None: assert not error assert res == '' - freqtradebot.update_state(State.STOPPED) + freqtradebot.state = State.STOPPED (error, res) = rpc.rpc_forcesell(None) assert error assert res == '`trader is not running`' @@ -442,7 +442,7 @@ def test_rpc_forcesell(default_conf, ticker, mocker) -> None: assert error assert res == '`trader is not running`' - freqtradebot.update_state(State.RUNNING) + freqtradebot.state = State.RUNNING assert cancel_order_mock.call_count == 0 # make an limit-buy open trade mocker.patch( diff --git a/freqtrade/tests/rpc/test_rpc_telegram.py b/freqtrade/tests/rpc/test_rpc_telegram.py index 2df43f0fc..6dbe6c710 100644 --- a/freqtrade/tests/rpc/test_rpc_telegram.py +++ b/freqtrade/tests/rpc/test_rpc_telegram.py @@ -302,13 +302,13 @@ def test_status_handle(default_conf, update, ticker, mocker) -> None: freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://')) telegram = Telegram(freqtradebot) - freqtradebot.update_state(State.STOPPED) + freqtradebot.state = State.STOPPED telegram._status(bot=MagicMock(), update=update) assert msg_mock.call_count == 1 assert 'trader is not running' in msg_mock.call_args_list[0][0][0] msg_mock.reset_mock() - freqtradebot.update_state(State.RUNNING) + freqtradebot.state = State.RUNNING telegram._status(bot=MagicMock(), update=update) assert msg_mock.call_count == 1 assert 'no active trade' in msg_mock.call_args_list[0][0][0] @@ -348,13 +348,13 @@ def test_status_table_handle(default_conf, update, ticker, mocker) -> None: freqtradebot = FreqtradeBot(conf, create_engine('sqlite://')) telegram = Telegram(freqtradebot) - freqtradebot.update_state(State.STOPPED) + freqtradebot.state = State.STOPPED telegram._status_table(bot=MagicMock(), update=update) assert msg_mock.call_count == 1 assert 'trader is not running' in msg_mock.call_args_list[0][0][0] msg_mock.reset_mock() - freqtradebot.update_state(State.RUNNING) + freqtradebot.state = State.RUNNING telegram._status_table(bot=MagicMock(), update=update) assert msg_mock.call_count == 1 assert 'no active order' in msg_mock.call_args_list[0][0][0] @@ -472,7 +472,7 @@ def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None: # Try invalid data msg_mock.reset_mock() - freqtradebot.update_state(State.RUNNING) + freqtradebot.state = State.RUNNING update.message.text = '/daily -2' telegram._daily(bot=MagicMock(), update=update) assert msg_mock.call_count == 1 @@ -480,7 +480,7 @@ def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None: # Try invalid data msg_mock.reset_mock() - freqtradebot.update_state(State.RUNNING) + freqtradebot.state = State.RUNNING update.message.text = '/daily today' telegram._daily(bot=MagicMock(), update=update) assert str('Daily Profit over the last 7 days') in msg_mock.call_args_list[0][0][0] @@ -667,10 +667,10 @@ def test_start_handle(default_conf, update, mocker) -> None: freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://')) telegram = Telegram(freqtradebot) - freqtradebot.update_state(State.STOPPED) - assert freqtradebot.get_state() == State.STOPPED + freqtradebot.state = State.STOPPED + assert freqtradebot.state == State.STOPPED telegram._start(bot=MagicMock(), update=update) - assert freqtradebot.get_state() == State.RUNNING + assert freqtradebot.state == State.RUNNING assert msg_mock.call_count == 0 @@ -691,10 +691,10 @@ def test_start_handle_already_running(default_conf, update, mocker) -> None: freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://')) telegram = Telegram(freqtradebot) - freqtradebot.update_state(State.RUNNING) - assert freqtradebot.get_state() == State.RUNNING + freqtradebot.state = State.RUNNING + assert freqtradebot.state == State.RUNNING telegram._start(bot=MagicMock(), update=update) - assert freqtradebot.get_state() == State.RUNNING + assert freqtradebot.state == State.RUNNING assert msg_mock.call_count == 1 assert 'already running' in msg_mock.call_args_list[0][0][0] @@ -716,10 +716,10 @@ def test_stop_handle(default_conf, update, mocker) -> None: freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://')) telegram = Telegram(freqtradebot) - freqtradebot.update_state(State.RUNNING) - assert freqtradebot.get_state() == State.RUNNING + freqtradebot.state = State.RUNNING + assert freqtradebot.state == State.RUNNING telegram._stop(bot=MagicMock(), update=update) - assert freqtradebot.get_state() == State.STOPPED + assert freqtradebot.state == State.STOPPED assert msg_mock.call_count == 1 assert 'Stopping trader' in msg_mock.call_args_list[0][0][0] @@ -741,10 +741,10 @@ def test_stop_handle_already_stopped(default_conf, update, mocker) -> None: freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://')) telegram = Telegram(freqtradebot) - freqtradebot.update_state(State.STOPPED) - assert freqtradebot.get_state() == State.STOPPED + freqtradebot.state = State.STOPPED + assert freqtradebot.state == State.STOPPED telegram._stop(bot=MagicMock(), update=update) - assert freqtradebot.get_state() == State.STOPPED + assert freqtradebot.state == State.STOPPED assert msg_mock.call_count == 1 assert 'already stopped' in msg_mock.call_args_list[0][0][0] @@ -884,7 +884,7 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None: telegram = Telegram(freqtradebot) # Trader is not running - freqtradebot.update_state(State.STOPPED) + freqtradebot.state = State.STOPPED update.message.text = '/forcesell 1' telegram._forcesell(bot=MagicMock(), update=update) assert msg_mock.call_count == 1 @@ -892,7 +892,7 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None: # No argument msg_mock.reset_mock() - freqtradebot.update_state(State.RUNNING) + freqtradebot.state = State.RUNNING update.message.text = '/forcesell' telegram._forcesell(bot=MagicMock(), update=update) assert msg_mock.call_count == 1 @@ -900,7 +900,7 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None: # Invalid argument msg_mock.reset_mock() - freqtradebot.update_state(State.RUNNING) + freqtradebot.state = State.RUNNING update.message.text = '/forcesell 123456' telegram._forcesell(bot=MagicMock(), update=update) assert msg_mock.call_count == 1 @@ -965,7 +965,7 @@ def test_performance_handle_invalid(default_conf, update, mocker) -> None: telegram = Telegram(freqtradebot) # Trader is not running - freqtradebot.update_state(State.STOPPED) + freqtradebot.state = State.STOPPED telegram._performance(bot=MagicMock(), update=update) assert msg_mock.call_count == 1 assert 'not running' in msg_mock.call_args_list[0][0][0] @@ -992,12 +992,12 @@ def test_count_handle(default_conf, update, ticker, mocker) -> None: freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://')) telegram = Telegram(freqtradebot) - freqtradebot.update_state(State.STOPPED) + freqtradebot.state = State.STOPPED telegram._count(bot=MagicMock(), update=update) assert msg_mock.call_count == 1 assert 'not running' in msg_mock.call_args_list[0][0][0] msg_mock.reset_mock() - freqtradebot.update_state(State.RUNNING) + freqtradebot.state = State.RUNNING # Create some test data freqtradebot.create_trade() diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 02edf3322..3792fd88d 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -84,8 +84,6 @@ def test_freqtradebot_object() -> None: Test the FreqtradeBot object has the mandatory public methods """ assert hasattr(FreqtradeBot, 'worker') - assert hasattr(FreqtradeBot, 'get_state') - assert hasattr(FreqtradeBot, 'update_state') assert hasattr(FreqtradeBot, 'clean') assert hasattr(FreqtradeBot, 'create_trade') assert hasattr(FreqtradeBot, 'get_target_bid') @@ -103,12 +101,12 @@ def test_freqtradebot(mocker, default_conf) -> None: Test __init__, _init_modules, update_state, and get_state methods """ freqtrade = get_patched_freqtradebot(mocker, default_conf) - assert freqtrade.get_state() is State.RUNNING + assert freqtrade.state is State.RUNNING conf = deepcopy(default_conf) conf.pop('initial_state') freqtrade = FreqtradeBot(conf) - assert freqtrade.get_state() is State.STOPPED + assert freqtrade.state is State.STOPPED def test_clean(mocker, default_conf, caplog) -> None: @@ -119,10 +117,10 @@ def test_clean(mocker, default_conf, caplog) -> None: mocker.patch('freqtrade.persistence.cleanup', mock_cleanup) freqtrade = get_patched_freqtradebot(mocker, default_conf) - assert freqtrade.get_state() == State.RUNNING + assert freqtrade.state == State.RUNNING assert freqtrade.clean() - assert freqtrade.get_state() == State.STOPPED + assert freqtrade.state == State.STOPPED assert log_has('Stopping trader and cleaning up modules...', caplog.record_tuples) assert mock_cleanup.call_count == 1 @@ -151,7 +149,7 @@ def test_worker_stopped(mocker, default_conf, caplog) -> None: mock_sleep = mocker.patch('time.sleep', return_value=None) freqtrade = get_patched_freqtradebot(mocker, default_conf) - freqtrade.update_state(State.STOPPED) + freqtrade.state = State.STOPPED state = freqtrade.worker(old_state=State.RUNNING) assert state is State.STOPPED assert log_has('Changing state to: STOPPED', caplog.record_tuples) @@ -262,7 +260,7 @@ def test_create_trade(default_conf, ticker, limit_buy_order, mocker) -> None: assert trade.stake_amount == 0.001 assert trade.is_open assert trade.open_date is not None - assert trade.exchange == 'BITTREX' + assert trade.exchange == 'bittrex' # Simulate fulfilled LIMIT_BUY order for trade trade.update(limit_buy_order) @@ -424,7 +422,7 @@ def test_process_trade_creation(default_conf, ticker, limit_buy_order, assert trade.stake_amount == default_conf['stake_amount'] assert trade.is_open assert trade.open_date is not None - assert trade.exchange == "BITTREX" + assert trade.exchange == 'bittrex' assert trade.open_rate == 0.00001099 assert trade.amount == 90.99181073703367 @@ -471,11 +469,11 @@ def test_process_operational_exception(default_conf, ticker, health, mocker) -> buy=MagicMock(side_effect=OperationalException) ) freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://')) - assert freqtrade.get_state() == State.RUNNING + assert freqtrade.state == State.RUNNING result = freqtrade._process() assert result is False - assert freqtrade.get_state() == State.STOPPED + assert freqtrade.state == State.STOPPED assert 'OperationalException' in msg_mock.call_args_list[-1][0][0] diff --git a/freqtrade/tests/test_persistence.py b/freqtrade/tests/test_persistence.py index c09774a37..5e80c632b 100644 --- a/freqtrade/tests/test_persistence.py +++ b/freqtrade/tests/test_persistence.py @@ -4,7 +4,6 @@ import os import pytest from sqlalchemy import create_engine -from freqtrade import exchange from freqtrade.persistence import Trade, init, clean_dry_run_db diff --git a/freqtrade/tests/testdata/download_backtest_data.py b/freqtrade/tests/testdata/download_backtest_data.py index ceb8388a1..46efe4511 100755 --- a/freqtrade/tests/testdata/download_backtest_data.py +++ b/freqtrade/tests/testdata/download_backtest_data.py @@ -6,7 +6,7 @@ import sys from freqtrade import exchange from freqtrade import misc -from freqtrade.exchange import Bittrex +from freqtrade.exchange import ccxt parser = misc.common_args_parser('download utility') parser.add_argument( @@ -28,7 +28,7 @@ PAIRS = list(set(PAIRS)) print('About to download pairs:', PAIRS) # Init Bittrex exchange -exchange._API = Bittrex({'key': '', 'secret': ''}) +exchange._API = ccxt.bittrex({'key': '', 'secret': ''}) for pair in PAIRS: for tick_interval in TICKER_INTERVALS: diff --git a/requirements.txt b/requirements.txt index e11ff8680..6295164bb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ scipy==1.0.0 jsonschema==2.6.0 numpy==1.14.2 TA-Lib==0.4.17 -pytest==3.4.2 +pytest==3.5.0 pytest-mock==1.7.1 pytest-cov==2.5.1 hyperopt==0.1