[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
This commit is contained in:
enenn 2018-04-06 09:57:08 +02:00 committed by Samuel Husso
parent f3847a3a9a
commit 1f75636e56
12 changed files with 586 additions and 486 deletions

View File

@ -1,35 +1,39 @@
# pragma pylint: disable=W0603 # pragma pylint: disable=W0603
""" Cryptocurrency Exchanges support """ """ Cryptocurrency Exchanges support """
import enum
import logging import logging
import ccxt
from random import randint from random import randint
from typing import List, Dict, Any, Optional from typing import List, Dict, Any, Optional
from cachetools import cached, TTLCache
from datetime import datetime
import ccxt
import arrow import arrow
import requests
from freqtrade import OperationalException, NetworkException from freqtrade import OperationalException, DependencyException, NetworkException
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Current selected exchange # Current selected exchange
_API = None _API: ccxt.Exchange = None
_CONF: dict = {} _CONF: dict = {}
API_RETRY_COUNT = 4 API_RETRY_COUNT = 4
# Holds all open sell orders for dry_run # Holds all open sell orders for dry_run
_DRY_RUN_OPEN_ORDERS: Dict[str, Any] = {} _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 retrier(f):
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
count = kwargs.pop('count', API_RETRY_COUNT) count = kwargs.pop('count', API_RETRY_COUNT)
try: try:
return f(*args, **kwargs) return f(*args, **kwargs)
# TODO dont be a gotta-catch-them-all pokemon collector except (NetworkException, DependencyException) as ex:
except Exception as ex:
logger.warning('%s returned exception: "%s"', f, ex) logger.warning('%s returned exception: "%s"', f, ex)
if count > 0: if count > 0:
count -= 1 count -= 1
@ -41,19 +45,6 @@ def retrier(f):
return wrapper 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: def init(config: dict) -> None:
""" """
Initializes this module with the given config, Initializes this module with the given config,
@ -74,18 +65,19 @@ def init(config: dict) -> None:
# Find matching class for the given exchange name # Find matching class for the given exchange name
name = exchange_config['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: try:
_API = getattr(ccxt, name.lower())({ _API = getattr(ccxt, name.lower())({
'apiKey': exchange_config.get('key'), 'apiKey': exchange_config.get('key'),
'secret': exchange_config.get('secret'), '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): except (KeyError, AttributeError):
raise OperationalException('Exchange {} is not supported'.format(name)) raise OperationalException('Exchange {} is not supported'.format(name))
# we need load api markets logger.info('Using Exchange "%s"', get_name())
_API.load_markets()
# Check if all pairs are available # Check if all pairs are available
validate_pairs(config['exchange']['pair_whitelist']) validate_pairs(config['exchange']['pair_whitelist'])
@ -99,14 +91,15 @@ def validate_pairs(pairs: List[str]) -> None:
:return: None :return: None
""" """
if not _API.markets: try:
_API.load_markets() 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'] stake_cur = _CONF['stake_currency']
for pair in pairs: for pair in pairs:
# Note: ccxt has BaseCurrency/QuoteCurrency format for pairs # Note: ccxt has BaseCurrency/QuoteCurrency format for pairs
pair = pair.replace('_', '/')
# TODO: add a support for having coins in BTC/USDT format # TODO: add a support for having coins in BTC/USDT format
if not pair.endswith(stake_cur): if not pair.endswith(stake_cur):
raise OperationalException( raise OperationalException(
@ -114,120 +107,212 @@ def validate_pairs(pairs: List[str]) -> None:
) )
if pair not in markets: if pair not in markets:
raise OperationalException( 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']: if _CONF['dry_run']:
global _DRY_RUN_OPEN_ORDERS global _DRY_RUN_OPEN_ORDERS
order_id = 'dry_run_buy_{}'.format(randint(0, 10**6)) order_id = 'dry_run_buy_{}'.format(randint(0, 10**6))
_DRY_RUN_OPEN_ORDERS[order_id] = { _DRY_RUN_OPEN_ORDERS[order_id] = {
'pair': pair, 'pair': pair,
'rate': rate, 'price': rate,
'amount': amount, 'amount': amount,
'type': 'LIMIT_BUY', 'type': 'limit',
'side': 'buy',
'remaining': 0.0, 'remaining': 0.0,
'opened': arrow.utcnow().datetime, 'datetime': arrow.utcnow().isoformat(),
'closed': arrow.utcnow().datetime, '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']: if _CONF['dry_run']:
global _DRY_RUN_OPEN_ORDERS global _DRY_RUN_OPEN_ORDERS
order_id = 'dry_run_sell_{}'.format(randint(0, 10**6)) order_id = 'dry_run_sell_{}'.format(randint(0, 10**6))
_DRY_RUN_OPEN_ORDERS[order_id] = { _DRY_RUN_OPEN_ORDERS[order_id] = {
'pair': pair, 'pair': pair,
'rate': rate, 'price': rate,
'amount': amount, 'amount': amount,
'type': 'LIMIT_SELL', 'type': 'limit',
'side': 'sell',
'remaining': 0.0, 'remaining': 0.0,
'opened': arrow.utcnow().datetime, 'datetime': arrow.utcnow().isoformat(),
'closed': arrow.utcnow().datetime, '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: def get_balance(currency: str) -> float:
if _CONF['dry_run']: if _CONF['dry_run']:
return 999.9 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']: if _CONF['dry_run']:
return [] 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 []
try: try:
ohlcv = _API.fetch_ohlcv(pair, timeframe=str(tick_interval)+"m") balances = _API.fetch_balance()
return ohlcv # Remove additional info from ccxt results
except IndexError as e: balances.pop("info", None)
logger.warning('Empty ticker history. Msg %s', str(e)) balances.pop("free", None)
balances.pop("total", None)
balances.pop("used", None)
return balances
except ccxt.NetworkError as e: 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: except ccxt.BaseError as e:
logger.warning('Could not fetch ticker data. Msg: %s', str(e)) raise OperationalException(e)
return []
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']: if _CONF['dry_run']:
return 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']: if _CONF['dry_run']:
order = _DRY_RUN_OPEN_ORDERS[order_id] order = _DRY_RUN_OPEN_ORDERS[order_id]
order.update({ order.update({
'id': order_id 'id': order_id
}) })
return order return order
try:
return _API.get_order(order_id) 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: def get_pair_detail_url(pair: str) -> str:
return _get_market_url(_API).format( try:
_API.markets[pair]['id'] 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]: def get_markets() -> List[dict]:
return _API.get_markets() try:
return _API.fetch_markets()
except ccxt.NetworkError as e:
def get_market_summaries() -> List[Dict]: raise NetworkException(
return _API.fetch_tickers() 'Could not load markets due to networking error. Message: {}'.format(e)
)
except ccxt.BaseError as e:
raise OperationalException(e)
def get_name() -> str: def get_name() -> str:
return _API.__class__.__name__.capitalize() return _API.name
def get_id() -> str:
return _API.id
def get_fee_maker() -> float: def get_fee_maker() -> float:
@ -239,11 +324,9 @@ def get_fee_taker() -> float:
def get_fee() -> float: def get_fee() -> float:
return get_fee_taker() # validate that markets are loaded before trying to get fee
if _API.markets is None or len(_API.markets) == 0:
def get_wallet_health() -> List[Dict]:
if not _API.markets:
_API.load_markets() _API.load_markets()
return _API.markets return _API.calculate_fee(symbol='ETH/BTC', type='', side='', amount=1, price=1)['rate']

View File

@ -41,7 +41,7 @@ class FreqtradeBot(object):
self.logger = Logger(name=__name__, level=config.get('loglevel')).get_logger() self.logger = Logger(name=__name__, level=config.get('loglevel')).get_logger()
# Init bot states # Init bot states
self._state = State.STOPPED self.state = State.STOPPED
# Init objects # Init objects
self.config = config self.config = config
@ -71,9 +71,9 @@ class FreqtradeBot(object):
initial_state = self.config.get('initial_state') initial_state = self.config.get('initial_state')
if initial_state: if initial_state:
self.update_state(State[initial_state.upper()]) self.state = State[initial_state.upper()]
else: else:
self.update_state(State.STOPPED) self.state = State.STOPPED
def clean(self) -> bool: def clean(self) -> bool:
""" """
@ -82,41 +82,26 @@ class FreqtradeBot(object):
""" """
self.rpc.send_msg('*Status:* `Stopping trader...`') self.rpc.send_msg('*Status:* `Stopping trader...`')
self.logger.info('Stopping trader and cleaning up modules...') self.logger.info('Stopping trader and cleaning up modules...')
self.update_state(State.STOPPED) self.state = State.STOPPED
self.rpc.cleanup() self.rpc.cleanup()
persistence.cleanup() persistence.cleanup()
return True 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: def worker(self, old_state: None) -> State:
""" """
Trading routine that must be run at each loop Trading routine that must be run at each loop
:param old_state: the previous service state from the previous call :param old_state: the previous service state from the previous call
:return: current service state :return: current service state
""" """
new_state = self.get_state()
# Log state transition # Log state transition
if new_state != old_state: state = self.state
self.rpc.send_msg('*Status:* `{}`'.format(new_state.name.lower())) if state != old_state:
self.logger.info('Changing state to: %s', new_state.name) 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) time.sleep(1)
elif new_state == State.RUNNING: elif state == State.RUNNING:
min_secs = self.config.get('internals', {}).get( min_secs = self.config.get('internals', {}).get(
'process_throttle_secs', 'process_throttle_secs',
Constants.PROCESS_THROTTLE_SECS Constants.PROCESS_THROTTLE_SECS
@ -130,7 +115,7 @@ class FreqtradeBot(object):
self._throttle(func=self._process, self._throttle(func=self._process,
min_secs=min_secs, min_secs=min_secs,
nb_assets=nb_assets) nb_assets=nb_assets)
return new_state return state
def _throttle(self, func: Callable[..., Any], min_secs: float, *args, **kwargs) -> Any: 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.logger.exception('OperationalException. Stopping trader ...')
self.update_state(State.STOPPED) self.state = State.STOPPED
return state_changed return state_changed
@cached(TTLCache(maxsize=1, ttl=1800)) @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) fmt_exp_profit = round(trade.calc_profit_percent(rate=limit) * 100, 2)
profit_trade = trade.calc_profit(rate=limit) profit_trade = trade.calc_profit(rate=limit)
current_rate = exchange.get_ticker(trade.pair, False)['bid'] current_rate = exchange.get_ticker(trade.pair)['bid']
profit = trade.calc_profit_percent(current_rate) profit = trade.calc_profit_percent(limit)
message = "*{exchange}:* Selling\n" \ message = "*{exchange}:* Selling\n" \
"*Current Pair:* [{pair}]({pair_url})\n" \ "*Current Pair:* [{pair}]({pair_url})\n" \

View File

@ -6,6 +6,7 @@ This module contains the backtesting logic
from argparse import Namespace from argparse import Namespace
from typing import Dict, Tuple, Any, List, Optional from typing import Dict, Tuple, Any, List, Optional
import ccxt
import arrow import arrow
from pandas import DataFrame, Series from pandas import DataFrame, Series
from tabulate import tabulate from tabulate import tabulate
@ -16,7 +17,6 @@ from freqtrade import exchange
from freqtrade.analyze import Analyze from freqtrade.analyze import Analyze
from freqtrade.arguments import Arguments from freqtrade.arguments import Arguments
from freqtrade.configuration import Configuration from freqtrade.configuration import Configuration
from freqtrade.logger import Logger from freqtrade.logger import Logger
from freqtrade.misc import file_dump_json from freqtrade.misc import file_dump_json
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
@ -53,7 +53,8 @@ class Backtesting(object):
self.tickerdata_to_dataframe = self.analyze.tickerdata_to_dataframe self.tickerdata_to_dataframe = self.analyze.tickerdata_to_dataframe
self.populate_buy_trend = self.analyze.populate_buy_trend self.populate_buy_trend = self.analyze.populate_buy_trend
self.populate_sell_trend = self.analyze.populate_sell_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']['key'] = ''
self.config['exchange']['secret'] = '' self.config['exchange']['secret'] = ''
exchange.init(self.config) exchange.init(self.config)

View File

@ -41,7 +41,7 @@ class RPC(object):
""" """
# Fetch open trade # Fetch open trade
trades = Trade.query.filter(Trade.is_open.is_(True)).all() 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`' return True, '*Status:* `trader is not running`'
elif not trades: elif not trades:
return True, '*Status:* `no active trade`' return True, '*Status:* `no active trade`'
@ -87,7 +87,7 @@ class RPC(object):
def rpc_status_table(self) -> Tuple[bool, Any]: def rpc_status_table(self) -> Tuple[bool, Any]:
trades = Trade.query.filter(Trade.is_open.is_(True)).all() 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`' return True, '*Status:* `trader is not running`'
elif not trades: elif not trades:
return True, '*Status:* `no active order`' return True, '*Status:* `no active order`'
@ -285,18 +285,18 @@ class RPC(object):
""" """
Handler for start. Handler for start.
""" """
if self.freqtrade.get_state() == State.RUNNING: if self.freqtrade.state == State.RUNNING:
return True, '*Status:* `already running`' return True, '*Status:* `already running`'
self.freqtrade.update_state(State.RUNNING) self.freqtrade.state = State.RUNNING
return False, '`Starting trader ...`' return False, '`Starting trader ...`'
def rpc_stop(self) -> (bool, str): def rpc_stop(self) -> (bool, str):
""" """
Handler for stop. Handler for stop.
""" """
if self.freqtrade.get_state() == State.RUNNING: if self.freqtrade.state == State.RUNNING:
self.freqtrade.update_state(State.STOPPED) self.freqtrade.state = State.STOPPED
return False, '`Stopping trader ...`' return False, '`Stopping trader ...`'
return True, '*Status:* `already stopped`' return True, '*Status:* `already stopped`'
@ -329,7 +329,7 @@ class RPC(object):
self.freqtrade.execute_sell(trade, current_rate) self.freqtrade.execute_sell(trade, current_rate)
# ---- EOF def _exec_forcesell ---- # ---- EOF def _exec_forcesell ----
if self.freqtrade.get_state() != State.RUNNING: if self.freqtrade.state != State.RUNNING:
return True, '`trader is not running`' return True, '`trader is not running`'
if trade_id == 'all': if trade_id == 'all':
@ -357,7 +357,7 @@ class RPC(object):
Handler for performance. Handler for performance.
Shows a performance statistic from finished trades 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`' return True, '`trader is not running`'
pair_rates = Trade.session.query(Trade.pair, pair_rates = Trade.session.query(Trade.pair,
@ -378,7 +378,7 @@ class RPC(object):
Returns the number of trades running Returns the number of trades running
:return: None :return: None
""" """
if self.freqtrade.get_state() != State.RUNNING: if self.freqtrade.state != State.RUNNING:
return True, '`trader is not running`' return True, '`trader is not running`'
trades = Trade.query.filter(Trade.is_open.is_(True)).all() trades = Trade.query.filter(Trade.is_open.is_(True)).all()

View File

@ -72,51 +72,6 @@ def default_conf():
"enabled": True, "enabled": True,
"key": "key", "key": "key",
"secret": "secret", "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": [ "pair_whitelist": [
"ETH/BTC", "ETH/BTC",
"TKN/BTC", "TKN/BTC",
@ -204,13 +159,14 @@ def health():
def limit_buy_order(): def limit_buy_order():
return { return {
'id': 'mocked_limit_buy', 'id': 'mocked_limit_buy',
'type': 'LIMIT_BUY', 'type': 'limit',
'side': 'buy',
'pair': 'mocked', 'pair': 'mocked',
'opened': str(arrow.utcnow().datetime), 'datetime': arrow.utcnow().isoformat(),
'rate': 0.00001099, 'price': 0.00001099,
'amount': 90.99181073, 'amount': 90.99181073,
'remaining': 0.0, 'remaining': 0.0,
'closed': str(arrow.utcnow().datetime), 'status': 'closed'
} }
@ -218,12 +174,14 @@ def limit_buy_order():
def limit_buy_order_old(): def limit_buy_order_old():
return { return {
'id': 'mocked_limit_buy_old', 'id': 'mocked_limit_buy_old',
'type': 'LIMIT_BUY', 'type': 'limit',
'pair': 'ETH/BTC', 'side': 'buy',
'opened': str(arrow.utcnow().shift(minutes=-601).datetime), 'pair': 'mocked',
'rate': 0.00001099, 'datetime': str(arrow.utcnow().shift(minutes=-601).datetime),
'price': 0.00001099,
'amount': 90.99181073, 'amount': 90.99181073,
'remaining': 90.99181073, 'remaining': 90.99181073,
'status': 'open'
} }
@ -231,12 +189,14 @@ def limit_buy_order_old():
def limit_sell_order_old(): def limit_sell_order_old():
return { return {
'id': 'mocked_limit_sell_old', 'id': 'mocked_limit_sell_old',
'type': 'LIMIT_SELL', 'type': 'limit',
'side': 'sell',
'pair': 'ETH/BTC', 'pair': 'ETH/BTC',
'opened': str(arrow.utcnow().shift(minutes=-601).datetime), 'datetime': arrow.utcnow().shift(minutes=-601).isoformat(),
'rate': 0.00001099, 'price': 0.00001099,
'amount': 90.99181073, 'amount': 90.99181073,
'remaining': 90.99181073, 'remaining': 90.99181073,
'status': 'open'
} }
@ -244,12 +204,14 @@ def limit_sell_order_old():
def limit_buy_order_old_partial(): def limit_buy_order_old_partial():
return { return {
'id': 'mocked_limit_buy_old_partial', 'id': 'mocked_limit_buy_old_partial',
'type': 'LIMIT_BUY', 'type': 'limit',
'side': 'buy',
'pair': 'ETH/BTC', 'pair': 'ETH/BTC',
'opened': str(arrow.utcnow().shift(minutes=-601).datetime), 'datetime': arrow.utcnow().shift(minutes=-601).isoformat(),
'rate': 0.00001099, 'price': 0.00001099,
'amount': 90.99181073, 'amount': 90.99181073,
'remaining': 67.99181073, 'remaining': 67.99181073,
'status': 'open'
} }
@ -257,16 +219,47 @@ def limit_buy_order_old_partial():
def limit_sell_order(): def limit_sell_order():
return { return {
'id': 'mocked_limit_sell', 'id': 'mocked_limit_sell',
'type': 'LIMIT_SELL', 'type': 'limit',
'side': 'sell',
'pair': 'mocked', 'pair': 'mocked',
'opened': str(arrow.utcnow().datetime), 'datetime': arrow.utcnow().isoformat(),
'rate': 0.00001173, 'price': 0.00001173,
'amount': 90.99181073, 'amount': 90.99181073,
'remaining': 0.0, '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 @pytest.fixture
def ticker_history(): def ticker_history():
return [ return [
@ -342,158 +335,3 @@ def result():
# that inserts a trade of some type and open-status # that inserts a trade of some type and open-status
# return the open-order-id # return the open-order-id
# See tests in rpc/main that could use this # 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
}
}
}

View File

@ -3,14 +3,15 @@
import logging import logging
from copy import deepcopy from copy import deepcopy
from random import randint from random import randint
from unittest.mock import MagicMock from unittest.mock import MagicMock, PropertyMock
import ccxt
import pytest import pytest
import freqtrade.exchange as exchange from freqtrade import OperationalException, DependencyException, NetworkException
from freqtrade import OperationalException
from freqtrade.exchange import init, validate_pairs, buy, sell, get_balance, get_balances, \ 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 from freqtrade.tests.conftest import log_has
API_INIT = False API_INIT = False
@ -42,7 +43,12 @@ def test_init_exception(default_conf):
def test_validate_pairs(default_conf, mocker): def test_validate_pairs(default_conf, mocker):
api_mock = MagicMock() 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('freqtrade.exchange._API', api_mock)
mocker.patch.dict('freqtrade.exchange._CONF', default_conf) mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
validate_pairs(default_conf['exchange']['pair_whitelist']) 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): def test_validate_pairs_not_available(default_conf, mocker):
api_mock = MagicMock() 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('freqtrade.exchange._API', api_mock)
mocker.patch.dict('freqtrade.exchange._CONF', default_conf) mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
with pytest.raises(OperationalException, match=r'not available'): 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): def test_validate_pairs_not_compatible(default_conf, mocker):
api_mock = MagicMock() api_mock = MagicMock()
api_mock.get_markets = MagicMock( api_mock.load_markets = MagicMock(return_value={
return_value=['BTC/ETH', 'BTC/TKN', 'BTC/TRST', 'BTC/SWT']) 'ETH/BTC': '', 'TKN/BTC': '', 'TRST/BTC': '', 'SWT/BTC': '', 'BCC/BTC': ''
conf = deepcopy(default_conf) })
conf['stake_currency'] = 'ETH' default_conf['stake_currency'] = 'ETH'
mocker.patch('freqtrade.exchange._API', api_mock) mocker.patch('freqtrade.exchange._API', api_mock)
mocker.patch.dict('freqtrade.exchange._CONF', conf) mocker.patch.dict('freqtrade.exchange._CONF', conf)
with pytest.raises(OperationalException, match=r'not compatible'): 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): def test_validate_pairs_exception(default_conf, mocker, caplog):
caplog.set_level(logging.INFO) caplog.set_level(logging.INFO)
api_mock = MagicMock() api_mock = MagicMock()
api_mock.load_markets = MagicMock(side_effect=ccxt.BaseError())
api_mock.name = 'binance' api_mock.name = 'binance'
mocker.patch('freqtrade.exchange._API', api_mock) mocker.patch('freqtrade.exchange._API', api_mock)
mocker.patch.dict('freqtrade.exchange._CONF', default_conf) 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'): 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'])
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): def test_validate_pairs_stake_exception(default_conf, mocker, caplog):
caplog.set_level(logging.INFO) caplog.set_level(logging.INFO)
@ -99,38 +109,99 @@ def test_buy_dry_run(default_conf, mocker):
default_conf['dry_run'] = True default_conf['dry_run'] = True
mocker.patch.dict('freqtrade.exchange._CONF', default_conf) 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): def test_buy_prod(default_conf, mocker):
api_mock = MagicMock() api_mock = MagicMock()
api_mock.buy = MagicMock( order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
return_value='dry_run_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) mocker.patch('freqtrade.exchange._API', api_mock)
default_conf['dry_run'] = False default_conf['dry_run'] = False
mocker.patch.dict('freqtrade.exchange._CONF', default_conf) 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): def test_sell_dry_run(default_conf, mocker):
default_conf['dry_run'] = True default_conf['dry_run'] = True
mocker.patch.dict('freqtrade.exchange._CONF', default_conf) 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): def test_sell_prod(default_conf, mocker):
api_mock = MagicMock() api_mock = MagicMock()
api_mock.sell = MagicMock( order_id = 'test_prod_sell_{}'.format(randint(0, 10 ** 6))
return_value='dry_run_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) mocker.patch('freqtrade.exchange._API', api_mock)
default_conf['dry_run'] = False default_conf['dry_run'] = False
mocker.patch.dict('freqtrade.exchange._CONF', default_conf) 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): 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): def test_get_balance_prod(default_conf, mocker):
api_mock = MagicMock() 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) mocker.patch('freqtrade.exchange._API', api_mock)
default_conf['dry_run'] = False default_conf['dry_run'] = False
@ -150,36 +221,51 @@ def test_get_balance_prod(default_conf, mocker):
assert get_balance(currency='BTC') == 123.4 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): def test_get_balances_dry_run(default_conf, mocker):
default_conf['dry_run'] = True default_conf['dry_run'] = True
mocker.patch.dict('freqtrade.exchange._CONF', default_conf) mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
assert get_balances() == [] assert get_balances() == {}
def test_get_balances_prod(default_conf, mocker): def test_get_balances_prod(default_conf, mocker):
balance_item = { balance_item = {
'Currency': '1ST', 'free': 10.0,
'Balance': 10.0, 'total': 10.0,
'Available': 10.0, 'used': 0.0
'Pending': 0.0,
'CryptoAddress': None
} }
api_mock = MagicMock() api_mock = MagicMock()
api_mock.get_balances = MagicMock( api_mock.fetch_balance = MagicMock(return_value={
return_value=[balance_item, balance_item, balance_item]) '1ST': balance_item,
'2ST': balance_item,
'3ST': balance_item
})
mocker.patch('freqtrade.exchange._API', api_mock) mocker.patch('freqtrade.exchange._API', api_mock)
default_conf['dry_run'] = False default_conf['dry_run'] = False
mocker.patch.dict('freqtrade.exchange._CONF', default_conf) mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
assert len(get_balances()) == 3 assert len(get_balances()) == 3
assert get_balances()[0]['Currency'] == '1ST' assert get_balances()['1ST']['free'] == 10.0
assert get_balances()[0]['Balance'] == 10.0 assert get_balances()['1ST']['total'] == 10.0
assert get_balances()[0]['Available'] == 10.0 assert get_balances()['1ST']['used'] == 0.0
assert get_balances()[0]['Pending'] == 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 # 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): def test_get_ticker(default_conf, mocker):
maybe_init_api(default_conf, mocker) maybe_init_api(default_conf, mocker)
api_mock = MagicMock() api_mock = MagicMock()
tick = {"success": True, 'result': {'Bid': 0.00001098, 'Ask': 0.00001099, 'Last': 0.0001}} tick = {
api_mock.get_ticker = MagicMock(return_value=tick) 'symbol': 'ETH/BTC',
mocker.patch('freqtrade.exchange.bittrex._API', api_mock) '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 # retrieve original ticker
ticker = get_ticker(pair='BTC/ETH') ticker = get_ticker(pair='ETH/BTC')
assert ticker['bid'] == 0.00001098 assert ticker['bid'] == 0.00001098
assert ticker['ask'] == 0.00001099 assert ticker['ask'] == 0.00001099
# change the ticker # change the ticker
tick = {"success": True, 'result': {"Bid": 0.5, "Ask": 1, "Last": 42}} tick = {
api_mock.get_ticker = MagicMock(return_value=tick) 'symbol': 'ETH/BTC',
mocker.patch('freqtrade.exchange.bittrex._API', api_mock) '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 caching the result we should get the same ticker
# if not fetching a new result we should get the cached ticker # if not fetching a new result we should get the cached ticker
ticker = get_ticker(pair='BTC/ETH', refresh=False) ticker = get_ticker(pair='ETH/BTC')
assert ticker['bid'] == 0.00001098
assert ticker['ask'] == 0.00001099
# force ticker refresh
ticker = get_ticker(pair='BTC/ETH', refresh=True)
assert ticker['bid'] == 0.5 assert ticker['bid'] == 0.5
assert ticker['ask'] == 1 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): def test_get_ticker_history(default_conf, mocker):
api_mock = MagicMock() api_mock = MagicMock()
tick = 123 tick = [
api_mock.get_ticker_history = MagicMock(return_value=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', api_mock)
mocker.patch('freqtrade.exchange._API.has', {'fetchOHLCV': True})
mocker.patch('freqtrade.exchange._API.fetch_ohlcv', return_value=tick)
# retrieve original ticker # retrieve original ticker
ticks = get_ticker_history('ETH/BTC', int(default_conf['ticker_interval'])) ticks = get_ticker_history('ETH/BTC', default_conf['ticker_interval'])
assert ticks == 123 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 # change ticker and ensure tick changes
tick = 999 new_tick = [
api_mock.get_ticker_history = MagicMock(return_value=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) mocker.patch('freqtrade.exchange._API', api_mock)
# ensure caching will still return the original ticker ticks = get_ticker_history('ETH/BTC', default_conf['ticker_interval'])
ticks = get_ticker_history('BTC/ETH', int(default_conf['ticker_interval'])) assert ticks[0][0] == 1511686210000
assert ticks == 123 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): def test_cancel_order_dry_run(default_conf, mocker):
default_conf['dry_run'] = True default_conf['dry_run'] = True
mocker.patch.dict('freqtrade.exchange._CONF', default_conf) 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 # 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 = MagicMock()
api_mock.cancel_order = MagicMock(return_value=123) api_mock.cancel_order = MagicMock(return_value=123)
mocker.patch('freqtrade.exchange._API', api_mock) 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): def test_get_order(default_conf, mocker):
@ -257,44 +414,83 @@ def test_get_order(default_conf, mocker):
order = MagicMock() order = MagicMock()
order.myid = 123 order.myid = 123
exchange._DRY_RUN_OPEN_ORDERS['X'] = order exchange._DRY_RUN_OPEN_ORDERS['X'] = order
print(exchange.get_order('X')) print(exchange.get_order('X', 'TKN/BTC'))
assert exchange.get_order('X').myid == 123 assert exchange.get_order('X', 'TKN/BTC').myid == 123
default_conf['dry_run'] = False default_conf['dry_run'] = False
mocker.patch.dict('freqtrade.exchange._CONF', default_conf) mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
api_mock = MagicMock() 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) 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): def test_get_name(default_conf, mocker):
mocker.patch('freqtrade.exchange.validate_pairs', mocker.patch('freqtrade.exchange.validate_pairs',
side_effect=lambda s: True) 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' default_conf['exchange']['name'] = 'bittrex'
init(default_conf) 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): 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 = 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) mocker.patch('freqtrade.exchange._API', api_mock)
exchange.get_markets() assert get_fee() == 0.025
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

View File

@ -41,12 +41,12 @@ def test_rpc_trade_status(default_conf, ticker, mocker) -> None:
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://')) freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
rpc = RPC(freqtradebot) rpc = RPC(freqtradebot)
freqtradebot.update_state(State.STOPPED) freqtradebot.state = State.STOPPED
(error, result) = rpc.rpc_trade_status() (error, result) = rpc.rpc_trade_status()
assert error assert error
assert 'trader is not running' in result assert 'trader is not running' in result
freqtradebot.update_state(State.RUNNING) freqtradebot.state = State.RUNNING
(error, result) = rpc.rpc_trade_status() (error, result) = rpc.rpc_trade_status()
assert error assert error
assert 'no active trade' in result 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://')) freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
rpc = RPC(freqtradebot) rpc = RPC(freqtradebot)
freqtradebot.update_state(State.STOPPED) freqtradebot.state = State.STOPPED
(error, result) = rpc.rpc_status_table() (error, result) = rpc.rpc_status_table()
assert error assert error
assert '*Status:* `trader is not running`' in result assert '*Status:* `trader is not running`' in result
freqtradebot.update_state(State.RUNNING) freqtradebot.state = State.RUNNING
(error, result) = rpc.rpc_status_table() (error, result) = rpc.rpc_status_table()
assert error assert error
assert '*Status:* `no active order`' in result 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://')) freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
rpc = RPC(freqtradebot) rpc = RPC(freqtradebot)
freqtradebot.update_state(State.STOPPED) freqtradebot.state = State.STOPPED
(error, result) = rpc.rpc_start() (error, result) = rpc.rpc_start()
assert not error assert not error
assert '`Starting trader ...`' in result assert '`Starting trader ...`' in result
assert freqtradebot.get_state() == State.RUNNING assert freqtradebot.state == State.RUNNING
(error, result) = rpc.rpc_start() (error, result) = rpc.rpc_start()
assert error assert error
assert '*Status:* `already running`' in result 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: 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://')) freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
rpc = RPC(freqtradebot) rpc = RPC(freqtradebot)
freqtradebot.update_state(State.RUNNING) freqtradebot.state = State.RUNNING
(error, result) = rpc.rpc_stop() (error, result) = rpc.rpc_stop()
assert not error assert not error
assert '`Stopping trader ...`' in result assert '`Stopping trader ...`' in result
assert freqtradebot.get_state() == State.STOPPED assert freqtradebot.state == State.STOPPED
(error, result) = rpc.rpc_stop() (error, result) = rpc.rpc_stop()
assert error assert error
assert '*Status:* `already stopped`' in result 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: 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://')) freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
rpc = RPC(freqtradebot) rpc = RPC(freqtradebot)
freqtradebot.update_state(State.STOPPED) freqtradebot.state = State.STOPPED
(error, res) = rpc.rpc_forcesell(None) (error, res) = rpc.rpc_forcesell(None)
assert error assert error
assert res == '`trader is not running`' assert res == '`trader is not running`'
freqtradebot.update_state(State.RUNNING) freqtradebot.state = State.RUNNING
(error, res) = rpc.rpc_forcesell(None) (error, res) = rpc.rpc_forcesell(None)
assert error assert error
assert res == 'Invalid argument.' assert res == 'Invalid argument.'
@ -433,7 +433,7 @@ def test_rpc_forcesell(default_conf, ticker, mocker) -> None:
assert not error assert not error
assert res == '' assert res == ''
freqtradebot.update_state(State.STOPPED) freqtradebot.state = State.STOPPED
(error, res) = rpc.rpc_forcesell(None) (error, res) = rpc.rpc_forcesell(None)
assert error assert error
assert res == '`trader is not running`' assert res == '`trader is not running`'
@ -442,7 +442,7 @@ def test_rpc_forcesell(default_conf, ticker, mocker) -> None:
assert error assert error
assert res == '`trader is not running`' assert res == '`trader is not running`'
freqtradebot.update_state(State.RUNNING) freqtradebot.state = State.RUNNING
assert cancel_order_mock.call_count == 0 assert cancel_order_mock.call_count == 0
# make an limit-buy open trade # make an limit-buy open trade
mocker.patch( mocker.patch(

View File

@ -302,13 +302,13 @@ def test_status_handle(default_conf, update, ticker, mocker) -> None:
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://')) freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
telegram = Telegram(freqtradebot) telegram = Telegram(freqtradebot)
freqtradebot.update_state(State.STOPPED) freqtradebot.state = State.STOPPED
telegram._status(bot=MagicMock(), update=update) telegram._status(bot=MagicMock(), update=update)
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert 'trader is not running' in msg_mock.call_args_list[0][0][0] assert 'trader is not running' in msg_mock.call_args_list[0][0][0]
msg_mock.reset_mock() msg_mock.reset_mock()
freqtradebot.update_state(State.RUNNING) freqtradebot.state = State.RUNNING
telegram._status(bot=MagicMock(), update=update) telegram._status(bot=MagicMock(), update=update)
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert 'no active trade' in msg_mock.call_args_list[0][0][0] 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://')) freqtradebot = FreqtradeBot(conf, create_engine('sqlite://'))
telegram = Telegram(freqtradebot) telegram = Telegram(freqtradebot)
freqtradebot.update_state(State.STOPPED) freqtradebot.state = State.STOPPED
telegram._status_table(bot=MagicMock(), update=update) telegram._status_table(bot=MagicMock(), update=update)
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert 'trader is not running' in msg_mock.call_args_list[0][0][0] assert 'trader is not running' in msg_mock.call_args_list[0][0][0]
msg_mock.reset_mock() msg_mock.reset_mock()
freqtradebot.update_state(State.RUNNING) freqtradebot.state = State.RUNNING
telegram._status_table(bot=MagicMock(), update=update) telegram._status_table(bot=MagicMock(), update=update)
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert 'no active order' in msg_mock.call_args_list[0][0][0] 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 # Try invalid data
msg_mock.reset_mock() msg_mock.reset_mock()
freqtradebot.update_state(State.RUNNING) freqtradebot.state = State.RUNNING
update.message.text = '/daily -2' update.message.text = '/daily -2'
telegram._daily(bot=MagicMock(), update=update) telegram._daily(bot=MagicMock(), update=update)
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
@ -480,7 +480,7 @@ def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None:
# Try invalid data # Try invalid data
msg_mock.reset_mock() msg_mock.reset_mock()
freqtradebot.update_state(State.RUNNING) freqtradebot.state = State.RUNNING
update.message.text = '/daily today' update.message.text = '/daily today'
telegram._daily(bot=MagicMock(), update=update) telegram._daily(bot=MagicMock(), update=update)
assert str('Daily Profit over the last 7 days') in msg_mock.call_args_list[0][0][0] 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://')) freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
telegram = Telegram(freqtradebot) telegram = Telegram(freqtradebot)
freqtradebot.update_state(State.STOPPED) freqtradebot.state = State.STOPPED
assert freqtradebot.get_state() == State.STOPPED assert freqtradebot.state == State.STOPPED
telegram._start(bot=MagicMock(), update=update) telegram._start(bot=MagicMock(), update=update)
assert freqtradebot.get_state() == State.RUNNING assert freqtradebot.state == State.RUNNING
assert msg_mock.call_count == 0 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://')) freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
telegram = Telegram(freqtradebot) telegram = Telegram(freqtradebot)
freqtradebot.update_state(State.RUNNING) freqtradebot.state = State.RUNNING
assert freqtradebot.get_state() == State.RUNNING assert freqtradebot.state == State.RUNNING
telegram._start(bot=MagicMock(), update=update) telegram._start(bot=MagicMock(), update=update)
assert freqtradebot.get_state() == State.RUNNING assert freqtradebot.state == State.RUNNING
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert 'already running' in msg_mock.call_args_list[0][0][0] 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://')) freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
telegram = Telegram(freqtradebot) telegram = Telegram(freqtradebot)
freqtradebot.update_state(State.RUNNING) freqtradebot.state = State.RUNNING
assert freqtradebot.get_state() == State.RUNNING assert freqtradebot.state == State.RUNNING
telegram._stop(bot=MagicMock(), update=update) telegram._stop(bot=MagicMock(), update=update)
assert freqtradebot.get_state() == State.STOPPED assert freqtradebot.state == State.STOPPED
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert 'Stopping trader' in msg_mock.call_args_list[0][0][0] 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://')) freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
telegram = Telegram(freqtradebot) telegram = Telegram(freqtradebot)
freqtradebot.update_state(State.STOPPED) freqtradebot.state = State.STOPPED
assert freqtradebot.get_state() == State.STOPPED assert freqtradebot.state == State.STOPPED
telegram._stop(bot=MagicMock(), update=update) telegram._stop(bot=MagicMock(), update=update)
assert freqtradebot.get_state() == State.STOPPED assert freqtradebot.state == State.STOPPED
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert 'already stopped' in msg_mock.call_args_list[0][0][0] 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) telegram = Telegram(freqtradebot)
# Trader is not running # Trader is not running
freqtradebot.update_state(State.STOPPED) freqtradebot.state = State.STOPPED
update.message.text = '/forcesell 1' update.message.text = '/forcesell 1'
telegram._forcesell(bot=MagicMock(), update=update) telegram._forcesell(bot=MagicMock(), update=update)
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
@ -892,7 +892,7 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None:
# No argument # No argument
msg_mock.reset_mock() msg_mock.reset_mock()
freqtradebot.update_state(State.RUNNING) freqtradebot.state = State.RUNNING
update.message.text = '/forcesell' update.message.text = '/forcesell'
telegram._forcesell(bot=MagicMock(), update=update) telegram._forcesell(bot=MagicMock(), update=update)
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
@ -900,7 +900,7 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None:
# Invalid argument # Invalid argument
msg_mock.reset_mock() msg_mock.reset_mock()
freqtradebot.update_state(State.RUNNING) freqtradebot.state = State.RUNNING
update.message.text = '/forcesell 123456' update.message.text = '/forcesell 123456'
telegram._forcesell(bot=MagicMock(), update=update) telegram._forcesell(bot=MagicMock(), update=update)
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
@ -965,7 +965,7 @@ def test_performance_handle_invalid(default_conf, update, mocker) -> None:
telegram = Telegram(freqtradebot) telegram = Telegram(freqtradebot)
# Trader is not running # Trader is not running
freqtradebot.update_state(State.STOPPED) freqtradebot.state = State.STOPPED
telegram._performance(bot=MagicMock(), update=update) telegram._performance(bot=MagicMock(), update=update)
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert 'not running' in msg_mock.call_args_list[0][0][0] 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://')) freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
telegram = Telegram(freqtradebot) telegram = Telegram(freqtradebot)
freqtradebot.update_state(State.STOPPED) freqtradebot.state = State.STOPPED
telegram._count(bot=MagicMock(), update=update) telegram._count(bot=MagicMock(), update=update)
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert 'not running' in msg_mock.call_args_list[0][0][0] assert 'not running' in msg_mock.call_args_list[0][0][0]
msg_mock.reset_mock() msg_mock.reset_mock()
freqtradebot.update_state(State.RUNNING) freqtradebot.state = State.RUNNING
# Create some test data # Create some test data
freqtradebot.create_trade() freqtradebot.create_trade()

View File

@ -84,8 +84,6 @@ def test_freqtradebot_object() -> None:
Test the FreqtradeBot object has the mandatory public methods Test the FreqtradeBot object has the mandatory public methods
""" """
assert hasattr(FreqtradeBot, 'worker') assert hasattr(FreqtradeBot, 'worker')
assert hasattr(FreqtradeBot, 'get_state')
assert hasattr(FreqtradeBot, 'update_state')
assert hasattr(FreqtradeBot, 'clean') assert hasattr(FreqtradeBot, 'clean')
assert hasattr(FreqtradeBot, 'create_trade') assert hasattr(FreqtradeBot, 'create_trade')
assert hasattr(FreqtradeBot, 'get_target_bid') 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 Test __init__, _init_modules, update_state, and get_state methods
""" """
freqtrade = get_patched_freqtradebot(mocker, default_conf) 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 = deepcopy(default_conf)
conf.pop('initial_state') conf.pop('initial_state')
freqtrade = FreqtradeBot(conf) freqtrade = FreqtradeBot(conf)
assert freqtrade.get_state() is State.STOPPED assert freqtrade.state is State.STOPPED
def test_clean(mocker, default_conf, caplog) -> None: 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) mocker.patch('freqtrade.persistence.cleanup', mock_cleanup)
freqtrade = get_patched_freqtradebot(mocker, default_conf) freqtrade = get_patched_freqtradebot(mocker, default_conf)
assert freqtrade.get_state() == State.RUNNING assert freqtrade.state == State.RUNNING
assert freqtrade.clean() 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 log_has('Stopping trader and cleaning up modules...', caplog.record_tuples)
assert mock_cleanup.call_count == 1 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) mock_sleep = mocker.patch('time.sleep', return_value=None)
freqtrade = get_patched_freqtradebot(mocker, default_conf) freqtrade = get_patched_freqtradebot(mocker, default_conf)
freqtrade.update_state(State.STOPPED) freqtrade.state = State.STOPPED
state = freqtrade.worker(old_state=State.RUNNING) state = freqtrade.worker(old_state=State.RUNNING)
assert state is State.STOPPED assert state is State.STOPPED
assert log_has('Changing state to: STOPPED', caplog.record_tuples) 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.stake_amount == 0.001
assert trade.is_open assert trade.is_open
assert trade.open_date is not None assert trade.open_date is not None
assert trade.exchange == 'BITTREX' assert trade.exchange == 'bittrex'
# Simulate fulfilled LIMIT_BUY order for trade # Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order) 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.stake_amount == default_conf['stake_amount']
assert trade.is_open assert trade.is_open
assert trade.open_date is not None assert trade.open_date is not None
assert trade.exchange == "BITTREX" assert trade.exchange == 'bittrex'
assert trade.open_rate == 0.00001099 assert trade.open_rate == 0.00001099
assert trade.amount == 90.99181073703367 assert trade.amount == 90.99181073703367
@ -471,11 +469,11 @@ def test_process_operational_exception(default_conf, ticker, health, mocker) ->
buy=MagicMock(side_effect=OperationalException) buy=MagicMock(side_effect=OperationalException)
) )
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://')) freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
assert freqtrade.get_state() == State.RUNNING assert freqtrade.state == State.RUNNING
result = freqtrade._process() result = freqtrade._process()
assert result is False 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] assert 'OperationalException' in msg_mock.call_args_list[-1][0][0]

View File

@ -4,7 +4,6 @@ import os
import pytest import pytest
from sqlalchemy import create_engine from sqlalchemy import create_engine
from freqtrade import exchange
from freqtrade.persistence import Trade, init, clean_dry_run_db from freqtrade.persistence import Trade, init, clean_dry_run_db

View File

@ -6,7 +6,7 @@ import sys
from freqtrade import exchange from freqtrade import exchange
from freqtrade import misc from freqtrade import misc
from freqtrade.exchange import Bittrex from freqtrade.exchange import ccxt
parser = misc.common_args_parser('download utility') parser = misc.common_args_parser('download utility')
parser.add_argument( parser.add_argument(
@ -28,7 +28,7 @@ PAIRS = list(set(PAIRS))
print('About to download pairs:', PAIRS) print('About to download pairs:', PAIRS)
# Init Bittrex exchange # Init Bittrex exchange
exchange._API = Bittrex({'key': '', 'secret': ''}) exchange._API = ccxt.bittrex({'key': '', 'secret': ''})
for pair in PAIRS: for pair in PAIRS:
for tick_interval in TICKER_INTERVALS: for tick_interval in TICKER_INTERVALS:

View File

@ -12,7 +12,7 @@ scipy==1.0.0
jsonschema==2.6.0 jsonschema==2.6.0
numpy==1.14.2 numpy==1.14.2
TA-Lib==0.4.17 TA-Lib==0.4.17
pytest==3.4.2 pytest==3.5.0
pytest-mock==1.7.1 pytest-mock==1.7.1
pytest-cov==2.5.1 pytest-cov==2.5.1
hyperopt==0.1 hyperopt==0.1