[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:
parent
f3847a3a9a
commit
1f75636e56
@ -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']
|
||||||
|
|
||||||
|
@ -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" \
|
||||||
|
@ -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)
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -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
|
|
||||||
|
@ -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(
|
||||||
|
@ -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()
|
||||||
|
@ -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]
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user