Merge branch 'develop' into ta_on_candle

This commit is contained in:
Matthias
2018-09-01 20:01:18 +02:00
30 changed files with 856 additions and 235 deletions

View File

@@ -1,5 +1,5 @@
""" FreqTrade bot """
__version__ = '0.17.1'
__version__ = '0.17.2'
class DependencyException(BaseException):

View File

@@ -36,7 +36,7 @@ SUPPORTED_FIAT = [
"EUR", "GBP", "HKD", "HUF", "IDR", "ILS", "INR", "JPY",
"KRW", "MXN", "MYR", "NOK", "NZD", "PHP", "PKR", "PLN",
"RUB", "SEK", "SGD", "THB", "TRY", "TWD", "ZAR", "USD",
"BTC", "ETH", "XRP", "LTC", "BCH", "USDT"
"BTC", "XBT", "ETH", "XRP", "LTC", "BCH", "USDT"
]
# Required json-schema for user specified config
@@ -45,7 +45,7 @@ CONF_SCHEMA = {
'properties': {
'max_open_trades': {'type': 'integer', 'minimum': 0},
'ticker_interval': {'type': 'string', 'enum': list(TICKER_INTERVAL_MINUTES.keys())},
'stake_currency': {'type': 'string', 'enum': ['BTC', 'ETH', 'USDT', 'EUR', 'USD']},
'stake_currency': {'type': 'string', 'enum': ['BTC', 'XBT', 'ETH', 'USDT', 'EUR', 'USD']},
'stake_amount': {
"type": ["number", "string"],
"minimum": 0.0005,
@@ -79,18 +79,35 @@ CONF_SCHEMA = {
'type': 'number',
'minimum': 0,
'maximum': 1,
'exclusiveMaximum': False
'exclusiveMaximum': False,
'use_order_book': {'type': 'boolean'},
'order_book_top': {'type': 'number', 'maximum': 20, 'minimum': 1},
'check_depth_of_market': {
'type': 'object',
'properties': {
'enabled': {'type': 'boolean'},
'bids_to_ask_delta': {'type': 'number', 'minimum': 0},
}
},
},
},
'required': ['ask_last_balance']
},
'ask_strategy': {
'type': 'object',
'properties': {
'use_order_book': {'type': 'boolean'},
'order_book_min': {'type': 'number', 'minimum': 1},
'order_book_max': {'type': 'number', 'minimum': 1, 'maximum': 50}
}
},
'exchange': {'$ref': '#/definitions/exchange'},
'experimental': {
'type': 'object',
'properties': {
'use_sell_signal': {'type': 'boolean'},
'sell_profit_only': {'type': 'boolean'},
"ignore_roi_if_buy_signal_true": {'type': 'boolean'}
'ignore_roi_if_buy_signal_true': {'type': 'boolean'}
}
},
'telegram': {
@@ -146,7 +163,8 @@ CONF_SCHEMA = {
'pattern': '^[0-9A-Z]+/[0-9A-Z]+$'
},
'uniqueItems': True
}
},
'outdated_offset': {'type': 'integer', 'minimum': 1}
},
'required': ['name', 'key', 'secret', 'pair_whitelist']
}

View File

@@ -116,7 +116,7 @@ class Exchange(object):
api.urls['api'] = api.urls['test']
logger.info("Enabled Sandbox API on %s", name)
else:
logger.warning(self, "No Sandbox URL in CCXT, exiting. "
logger.warning(name, "No Sandbox URL in CCXT, exiting. "
"Please check your config.json")
raise OperationalException(f'Exchange {name} does not provide a sandbox api')
@@ -322,7 +322,7 @@ class Exchange(object):
return data
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Could not load ticker history due to {e.__class__.__name__}. Message: {e}')
f'Could not load ticker due to {e.__class__.__name__}. Message: {e}')
except ccxt.BaseError as e:
raise OperationalException(e)
else:
@@ -409,6 +409,37 @@ class Exchange(object):
except ccxt.BaseError as e:
raise OperationalException(e)
@retrier
def get_order_book(self, pair: str, limit: int = 100) -> dict:
"""
get order book level 2 from exchange
Notes:
20180619: bittrex doesnt support limits -.-
20180619: binance support limits but only on specific range
"""
try:
if self._api.name == 'Binance':
limit_range = [5, 10, 20, 50, 100, 500, 1000]
# get next-higher step in the limit_range list
limit = min(list(filter(lambda x: limit <= x, limit_range)))
# above script works like loop below (but with slightly better performance):
# for limitx in limit_range:
# if limit <= limitx:
# limit = limitx
# break
return self._api.fetch_l2_order_book(pair, limit)
except ccxt.NotSupported as e:
raise OperationalException(
f'Exchange {self._api.name} does not support fetching order book.'
f'Message: {e}')
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Could not get order book due to {e.__class__.__name__}. Message: {e}')
except ccxt.BaseError as e:
raise OperationalException(e)
@retrier
def get_trades_for_order(self, order_id: str, pair: str, since: datetime) -> List:
if self._conf['dry_run']:
@@ -462,12 +493,3 @@ class Exchange(object):
f'Could not get fee info due to {e.__class__.__name__}. Message: {e}')
except ccxt.BaseError as e:
raise OperationalException(e)
def get_amount_lots(self, pair: str, amount: float) -> float:
"""
get buyable amount rounding, ..
"""
# validate that markets are loaded before trying to get fee
if not self._api.markets:
self._api.load_markets()
return self._api.amount_to_lots(pair, amount)

View File

@@ -2,6 +2,7 @@
Functions to analyze ticker data with indicators and produce buy and sell signals
"""
import logging
import pandas as pd
from pandas import DataFrame, to_datetime
logger = logging.getLogger(__name__)
@@ -31,3 +32,27 @@ def parse_ticker_dataframe(ticker: list) -> DataFrame:
})
frame.drop(frame.tail(1).index, inplace=True) # eliminate partial candle
return frame
def order_book_to_dataframe(bids: list, asks: list) -> DataFrame:
"""
Gets order book list, returns dataframe with below format per suggested by creslin
-------------------------------------------------------------------
b_sum b_size bids asks a_size a_sum
-------------------------------------------------------------------
"""
cols = ['bids', 'b_size']
bids_frame = DataFrame(bids, columns=cols)
# add cumulative sum column
bids_frame['b_sum'] = bids_frame['b_size'].cumsum()
cols2 = ['asks', 'a_size']
asks_frame = DataFrame(asks, columns=cols2)
# add cumulative sum column
asks_frame['a_sum'] = asks_frame['a_size'].cumsum()
frame = pd.concat([bids_frame['b_sum'], bids_frame['b_size'], bids_frame['bids'],
asks_frame['asks'], asks_frame['a_size'], asks_frame['a_sum']], axis=1,
keys=['b_sum', 'b_size', 'bids', 'asks', 'a_size', 'a_sum'])
# logger.info('order book %s', frame )
return frame

View File

@@ -21,6 +21,7 @@ from freqtrade.rpc import RPCManager, RPCMessageType
from freqtrade.state import State
from freqtrade.strategy.interface import SellType
from freqtrade.strategy.resolver import IStrategy, StrategyResolver
from freqtrade.exchange.exchange_helpers import order_book_to_dataframe
logger = logging.getLogger(__name__)
@@ -94,6 +95,8 @@ class FreqtradeBot(object):
'status': f'{state.name.lower()}'
})
logger.info('Changing state to: %s', state.name)
if state == State.RUNNING:
self._startup_messages()
if state == State.STOPPED:
time.sleep(1)
@@ -110,6 +113,38 @@ class FreqtradeBot(object):
nb_assets=nb_assets)
return state
def _startup_messages(self) -> None:
if self.config.get('dry_run', False):
self.rpc.send_msg({
'type': RPCMessageType.WARNING_NOTIFICATION,
'status': 'Dry run is enabled. All trades are simulated.'
})
stake_currency = self.config['stake_currency']
stake_amount = self.config['stake_amount']
minimal_roi = self.config['minimal_roi']
ticker_interval = self.config['ticker_interval']
exchange_name = self.config['exchange']['name']
strategy_name = self.config.get('strategy', '')
self.rpc.send_msg({
'type': RPCMessageType.CUSTOM_NOTIFICATION,
'status': f'*Exchange:* `{exchange_name}`\n'
f'*Stake per trade:* `{stake_amount} {stake_currency}`\n'
f'*Minimum ROI:* `{minimal_roi}`\n'
f'*Ticker Interval:* `{ticker_interval}`\n'
f'*Strategy:* `{strategy_name}`'
})
if self.config.get('dynamic_whitelist', False):
top_pairs = 'top ' + str(self.config.get('dynamic_whitelist', 20))
specific_pairs = ''
else:
top_pairs = 'whitelisted'
specific_pairs = '\n' + ', '.join(self.config['exchange'].get('pair_whitelist', ''))
self.rpc.send_msg({
'type': RPCMessageType.STATUS_NOTIFICATION,
'status': f'Searching for {top_pairs} {stake_currency} pairs to buy and sell...'
f'{specific_pairs}'
})
def _throttle(self, func: Callable[..., Any], min_secs: float, *args, **kwargs) -> Any:
"""
Throttles the given callable that it
@@ -233,16 +268,40 @@ class FreqtradeBot(object):
return final_list
def get_target_bid(self, ticker: Dict[str, float]) -> float:
def get_target_bid(self, pair: str, ticker: Dict[str, float]) -> float:
"""
Calculates bid target between current ask price and last price
:param ticker: Ticker to use for getting Ask and Last Price
:return: float: Price
"""
if ticker['ask'] < ticker['last']:
return ticker['ask']
balance = self.config['bid_strategy']['ask_last_balance']
return ticker['ask'] + balance * (ticker['last'] - ticker['ask'])
ticker_rate = ticker['ask']
else:
balance = self.config['bid_strategy']['ask_last_balance']
ticker_rate = ticker['ask'] + balance * (ticker['last'] - ticker['ask'])
used_rate = ticker_rate
config_bid_strategy = self.config.get('bid_strategy', {})
if 'use_order_book' in config_bid_strategy and\
config_bid_strategy.get('use_order_book', False):
logger.info('Getting price from order book')
order_book_top = config_bid_strategy.get('order_book_top', 1)
order_book = self.exchange.get_order_book(pair, order_book_top)
logger.debug('order_book %s', order_book)
# top 1 = index 0
order_book_rate = order_book['bids'][order_book_top - 1][0]
# if ticker has lower rate, then use ticker ( usefull if down trending )
logger.info('...top %s order book buy rate %0.8f', order_book_top, order_book_rate)
if ticker_rate < order_book_rate:
logger.info('...using ticker rate instead %0.8f', ticker_rate)
used_rate = ticker_rate
else:
used_rate = order_book_rate
else:
logger.info('Using Last Ask / Last Price')
used_rate = ticker_rate
return used_rate
def _get_trade_stake_amount(self) -> Optional[float]:
"""
@@ -334,9 +393,34 @@ class FreqtradeBot(object):
(buy, sell) = self.strategy.get_signal(_pair, interval, thistory)
if buy and not sell:
bidstrat_check_depth_of_market = self.config.get('bid_strategy', {}).\
get('check_depth_of_market', {})
if (bidstrat_check_depth_of_market.get('enabled', False)) and\
(bidstrat_check_depth_of_market.get('bids_to_ask_delta', 0) > 0):
if self._check_depth_of_market_buy(_pair, bidstrat_check_depth_of_market):
return self.execute_buy(_pair, stake_amount)
else:
return False
return self.execute_buy(_pair, stake_amount)
return False
def _check_depth_of_market_buy(self, pair: str, conf: Dict) -> bool:
"""
Checks depth of market before executing a buy
"""
conf_bids_to_ask_delta = conf.get('bids_to_ask_delta', 0)
logger.info('checking depth of market for %s', pair)
order_book = self.exchange.get_order_book(pair, 1000)
order_book_data_frame = order_book_to_dataframe(order_book['bids'], order_book['asks'])
order_book_bids = order_book_data_frame['b_size'].sum()
order_book_asks = order_book_data_frame['a_size'].sum()
bids_ask_delta = order_book_bids / order_book_asks
logger.info('bids: %s, asks: %s, delta: %s', order_book_bids,
order_book_asks, bids_ask_delta)
if bids_ask_delta >= conf_bids_to_ask_delta:
return True
return False
def execute_buy(self, pair: str, stake_amount: float) -> bool:
"""
Executes a limit buy for the given pair
@@ -349,7 +433,7 @@ class FreqtradeBot(object):
fiat_currency = self.config.get('fiat_display_currency', None)
# Calculate amount
buy_limit = self.get_target_bid(self.exchange.get_ticker(pair))
buy_limit = self.get_target_bid(pair, self.exchange.get_ticker(pair))
min_stake_amount = self._get_min_pair_stake_amount(pair_s, buy_limit)
if min_stake_amount is not None and min_stake_amount > stake_amount:
@@ -492,7 +576,7 @@ class FreqtradeBot(object):
raise ValueError(f'attempt to handle closed trade: {trade}')
logger.debug('Handling %s ...', trade)
current_rate = self.exchange.get_ticker(trade.pair)['bid']
sell_rate = self.exchange.get_ticker(trade.pair)['bid']
(buy, sell) = (False, False)
experimental = self.config.get('experimental', {})
@@ -501,13 +585,43 @@ class FreqtradeBot(object):
(buy, sell) = self.strategy.get_signal(trade.pair, self.strategy.ticker_interval,
ticker)
should_sell = self.strategy.should_sell(trade, current_rate, datetime.utcnow(), buy, sell)
if should_sell.sell_flag:
self.execute_sell(trade, current_rate, should_sell.sell_type)
return True
config_ask_strategy = self.config.get('ask_strategy', {})
if config_ask_strategy.get('use_order_book', False):
logger.info('Using order book for selling...')
# logger.debug('Order book %s',orderBook)
order_book_min = config_ask_strategy.get('order_book_min', 1)
order_book_max = config_ask_strategy.get('order_book_max', 1)
order_book = self.exchange.get_order_book(trade.pair, order_book_max)
for i in range(order_book_min, order_book_max + 1):
order_book_rate = order_book['asks'][i - 1][0]
# if orderbook has higher rate (high profit),
# use orderbook, otherwise just use bids rate
logger.info(' order book asks top %s: %0.8f', i, order_book_rate)
if sell_rate < order_book_rate:
sell_rate = order_book_rate
if self.check_sell(trade, sell_rate, buy, sell):
return True
break
else:
logger.info('checking sell')
if self.check_sell(trade, sell_rate, buy, sell):
return True
logger.info('Found no sell signals for whitelisted currencies. Trying again..')
return False
def check_sell(self, trade: Trade, sell_rate: float, buy: bool, sell: bool) -> bool:
should_sell = self.strategy.should_sell(trade, sell_rate, datetime.utcnow(), buy, sell)
if should_sell.sell_flag:
self.execute_sell(trade, sell_rate, should_sell.sell_type)
logger.info('excuted sell')
return True
return False
def check_handle_timedout(self) -> None:
"""
Check if any orders are timed out and cancel if neccessary

View File

@@ -1,7 +1,13 @@
# pragma pylint: disable=missing-docstring
import gzip
import json
try:
import ujson as json
_UJSON = True
except ImportError:
# see mypy/issues/1153
import json # type: ignore
_UJSON = False
import logging
import os
from typing import Optional, List, Dict, Tuple, Any
@@ -14,6 +20,14 @@ from freqtrade.arguments import TimeRange
logger = logging.getLogger(__name__)
def json_load(data):
"""Try to load data with ujson"""
if _UJSON:
return json.load(data, precise_float=True)
else:
return json.load(data)
def trim_tickerlist(tickerlist: List[Dict], timerange: TimeRange) -> List[Dict]:
if not tickerlist:
return tickerlist
@@ -163,7 +177,7 @@ def load_cached_data_for_updating(filename: str,
# read the cached file
if os.path.isfile(filename):
with open(filename, "rt") as file:
data = json.load(file)
data = json_load(file)
# remove the last item, because we are not sure if it is correct
# it could be fetched when the candle was incompleted
if data:

View File

@@ -79,10 +79,12 @@ def check_migrate(engine) -> None:
table_back_name = 'trades_bak'
for i, table_back_name in enumerate(tabs):
table_back_name = f'trades_bak{i}'
logger.info(f'trying {table_back_name}')
logger.debug(f'trying {table_back_name}')
# Check for latest column
if not has_column(cols, 'max_rate'):
if not has_column(cols, 'ticker_interval'):
logger.info(f'Running database migration - backup available as {table_back_name}')
fee_open = get_column_def(cols, 'fee_open', 'fee')
fee_close = get_column_def(cols, 'fee_close', 'fee')
open_rate_requested = get_column_def(cols, 'open_rate_requested', 'null')
@@ -157,8 +159,8 @@ class Trade(_DECL_BASE):
id = Column(Integer, primary_key=True)
exchange = Column(String, nullable=False)
pair = Column(String, nullable=False)
is_open = Column(Boolean, nullable=False, default=True)
pair = Column(String, nullable=False, index=True)
is_open = Column(Boolean, nullable=False, default=True, index=True)
fee_open = Column(Float, nullable=False, default=0.0)
fee_close = Column(Float, nullable=False, default=0.0)
open_rate = Column(Float)

View File

@@ -13,6 +13,7 @@ import sqlalchemy as sql
from numpy import mean, nan_to_num
from pandas import DataFrame
from freqtrade import TemporaryError
from freqtrade.fiat_convert import CryptoToFiatConverter
from freqtrade.misc import shorten_date
from freqtrade.persistence import Trade
@@ -24,6 +25,8 @@ logger = logging.getLogger(__name__)
class RPCMessageType(Enum):
STATUS_NOTIFICATION = 'status'
WARNING_NOTIFICATION = 'warning'
CUSTOM_NOTIFICATION = 'custom'
BUY_NOTIFICATION = 'buy'
SELL_NOTIFICATION = 'sell'
@@ -271,10 +274,13 @@ class RPC(object):
if coin == 'BTC':
rate = 1.0
else:
if coin == 'USDT':
rate = 1.0 / self._freqtrade.exchange.get_ticker('BTC/USDT', False)['bid']
else:
rate = self._freqtrade.exchange.get_ticker(coin + '/BTC', False)['bid']
try:
if coin == 'USDT':
rate = 1.0 / self._freqtrade.exchange.get_ticker('BTC/USDT', False)['bid']
else:
rate = self._freqtrade.exchange.get_ticker(coin + '/BTC', False)['bid']
except TemporaryError:
continue
est_btc: float = rate * balance['total']
total = total + est_btc
output.append({

View File

@@ -154,6 +154,12 @@ class Telegram(RPC):
elif msg['type'] == RPCMessageType.STATUS_NOTIFICATION:
message = '*Status:* `{status}`'.format(**msg)
elif msg['type'] == RPCMessageType.WARNING_NOTIFICATION:
message = '*Warning:* `{status}`'.format(**msg)
elif msg['type'] == RPCMessageType.CUSTOM_NOTIFICATION:
message = '{status}'.format(**msg)
else:
raise NotImplementedError('Unknown message type: {}'.format(msg['type']))

View File

@@ -1,4 +1,5 @@
import logging
import sys
from copy import deepcopy
from freqtrade.strategy.interface import IStrategy
@@ -12,8 +13,18 @@ def import_strategy(strategy: IStrategy, config: dict) -> IStrategy:
Imports given Strategy instance to global scope
of freqtrade.strategy and returns an instance of it
"""
# Copy all attributes from base class and class
attr = deepcopy({**strategy.__class__.__dict__, **strategy.__dict__})
comb = {**strategy.__class__.__dict__, **strategy.__dict__}
# Delete '_abc_impl' from dict as deepcopy fails on 3.7 with
# `TypeError: can't pickle _abc_data objects``
# This will only apply to python 3.7
if sys.version_info.major == 3 and sys.version_info.minor == 7 and '_abc_impl' in comb:
del comb['_abc_impl']
attr = deepcopy(comb)
# Adjust module name
attr['__module__'] = 'freqtrade.strategy'

View File

@@ -182,7 +182,8 @@ class IStrategy(ABC):
# Check if dataframe is out of date
signal_date = arrow.get(latest['date'])
interval_minutes = constants.TICKER_INTERVAL_MINUTES[interval]
if signal_date < (arrow.utcnow().shift(minutes=-(interval_minutes * 2 + 5))):
offset = self.config.get('exchange', {}).get('outdated_offset', 5)
if signal_date < (arrow.utcnow().shift(minutes=-(interval_minutes * 2 + offset))):
logger.warning(
'Outdated history for pair %s. Last tick is %s minutes old',
pair,

View File

@@ -44,7 +44,8 @@ class StrategyResolver(object):
# Check if we need to override configuration
if 'minimal_roi' in config:
self.strategy.minimal_roi = config['minimal_roi']
logger.info("Override strategy 'minimal_roi' with value in config file.")
logger.info("Override strategy 'minimal_roi' with value in config file: %s.",
config['minimal_roi'])
else:
config['minimal_roi'] = self.strategy.minimal_roi

View File

@@ -102,7 +102,18 @@ def default_conf():
"sell": 30
},
"bid_strategy": {
"ask_last_balance": 0.0
"ask_last_balance": 0.0,
"use_order_book": False,
"order_book_top": 1,
"check_depth_of_market": {
"enabled": False,
"bids_to_ask_delta": 1
}
},
"ask_strategy": {
"use_order_book": False,
"order_book_min": 1,
"order_book_max": 1
},
"exchange": {
"name": "bittrex",
@@ -403,6 +414,39 @@ def limit_sell_order():
}
@pytest.fixture
def order_book_l2():
return MagicMock(return_value={
'bids': [
[0.043936, 10.442],
[0.043935, 31.865],
[0.043933, 11.212],
[0.043928, 0.088],
[0.043925, 10.0],
[0.043921, 10.0],
[0.04392, 37.64],
[0.043899, 0.066],
[0.043885, 0.676],
[0.04387, 22.758]
],
'asks': [
[0.043949, 0.346],
[0.04395, 0.608],
[0.043951, 3.948],
[0.043954, 0.288],
[0.043958, 9.277],
[0.043995, 1.566],
[0.044, 0.588],
[0.044002, 0.992],
[0.044003, 0.095],
[0.04402, 37.64]
],
'timestamp': None,
'datetime': None,
'nonce': 288004540
})
@pytest.fixture
def ticker_history():
return [

View File

@@ -515,6 +515,35 @@ def test_get_ticker(default_conf, mocker):
exchange.get_ticker(pair='ETH/BTC', refresh=True)
def test_get_order_book(default_conf, mocker, order_book_l2):
default_conf['exchange']['name'] = 'binance'
api_mock = MagicMock()
api_mock.fetch_l2_order_book = order_book_l2
exchange = get_patched_exchange(mocker, default_conf, api_mock)
order_book = exchange.get_order_book(pair='ETH/BTC', limit=10)
assert 'bids' in order_book
assert 'asks' in order_book
assert len(order_book['bids']) == 10
assert len(order_book['asks']) == 10
def test_get_order_book_exception(default_conf, mocker):
api_mock = MagicMock()
with pytest.raises(OperationalException):
api_mock.fetch_l2_order_book = MagicMock(side_effect=ccxt.NotSupported)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.get_order_book(pair='ETH/BTC', limit=50)
with pytest.raises(TemporaryError):
api_mock.fetch_l2_order_book = MagicMock(side_effect=ccxt.NetworkError)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.get_order_book(pair='ETH/BTC', limit=50)
with pytest.raises(OperationalException):
api_mock.fetch_l2_order_book = MagicMock(side_effect=ccxt.BaseError)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.get_order_book(pair='ETH/BTC', limit=50)
def make_fetch_ohlcv_mock(data):
def fetch_ohlcv_mock(pair, timeframe, since):
if since:
@@ -823,15 +852,3 @@ def test_get_fee(default_conf, mocker):
ccxt_exceptionhandlers(mocker, default_conf, api_mock,
'get_fee', 'calculate_fee')
def test_get_amount_lots(default_conf, mocker):
api_mock = MagicMock()
api_mock.amount_to_lots = MagicMock(return_value=1.0)
api_mock.markets = None
marketmock = MagicMock()
api_mock.load_markets = marketmock
exchange = get_patched_exchange(mocker, default_conf, api_mock)
assert exchange.get_amount_lots('LTC/BTC', 1.54) == 1
assert marketmock.call_count == 1

View File

@@ -6,6 +6,7 @@ from unittest.mock import MagicMock, ANY
import pytest
from freqtrade import TemporaryError
from freqtrade.fiat_convert import CryptoToFiatConverter
from freqtrade.freqtradebot import FreqtradeBot
from freqtrade.persistence import Trade
@@ -285,11 +286,12 @@ def test_rpc_balance_handle(default_conf, mocker):
'used': 2.0,
},
'ETH': {
'free': 0.0,
'total': 0.0,
'used': 0.0,
'free': 1.0,
'total': 5.0,
'used': 4.0,
}
}
# ETH will be skipped due to mocked Error below
mocker.patch.multiple(
'freqtrade.fiat_convert.Market',
@@ -301,7 +303,8 @@ def test_rpc_balance_handle(default_conf, mocker):
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_balances=MagicMock(return_value=mock_balance)
get_balances=MagicMock(return_value=mock_balance),
get_ticker=MagicMock(side_effect=TemporaryError('Could not load ticker due to xxx'))
)
freqtradebot = FreqtradeBot(default_conf)
@@ -320,6 +323,7 @@ def test_rpc_balance_handle(default_conf, mocker):
'pending': 2.0,
'est_btc': 12.0,
}]
assert result['total'] == 12.0
def test_rpc_start(mocker, default_conf) -> None:

View File

@@ -1095,6 +1095,38 @@ def test_send_msg_status_notification(default_conf, mocker) -> None:
assert msg_mock.call_args[0][0] == '*Status:* `running`'
def test_warning_notification(default_conf, mocker) -> None:
msg_mock = MagicMock()
mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(),
_send_msg=msg_mock
)
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
telegram = Telegram(freqtradebot)
telegram.send_msg({
'type': RPCMessageType.WARNING_NOTIFICATION,
'status': 'message'
})
assert msg_mock.call_args[0][0] == '*Warning:* `message`'
def test_custom_notification(default_conf, mocker) -> None:
msg_mock = MagicMock()
mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(),
_send_msg=msg_mock
)
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
telegram = Telegram(freqtradebot)
telegram.send_msg({
'type': RPCMessageType.CUSTOM_NOTIFICATION,
'status': '*Custom:* `Hello World`'
})
assert msg_mock.call_args[0][0] == '*Custom:* `Hello World`'
def test_send_msg_unknown_type(default_conf, mocker) -> None:
msg_mock = MagicMock()
mocker.patch.multiple(

View File

@@ -8,6 +8,7 @@ from pandas import DataFrame
from freqtrade.arguments import TimeRange
from freqtrade.optimize.__init__ import load_tickerdata_file
from freqtrade.persistence import Trade
from freqtrade.tests.conftest import get_patched_exchange, log_has
from freqtrade.strategy.default_strategy import DefaultStrategy
@@ -107,6 +108,29 @@ def test_tickerdata_to_dataframe(default_conf) -> None:
assert len(data['UNITTEST/BTC']) == 99 # partial candle was removed
def test_min_roi_reached(default_conf, fee) -> None:
strategy = DefaultStrategy(default_conf)
strategy.minimal_roi = {0: 0.1, 20: 0.05, 55: 0.01}
trade = Trade(
pair='ETH/BTC',
stake_amount=0.001,
open_date=arrow.utcnow().shift(hours=-1).datetime,
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='bittrex',
open_rate=1,
)
assert not strategy.min_roi_reached(trade, 0.01, arrow.utcnow().shift(minutes=-55).datetime)
assert strategy.min_roi_reached(trade, 0.12, arrow.utcnow().shift(minutes=-55).datetime)
assert not strategy.min_roi_reached(trade, 0.04, arrow.utcnow().shift(minutes=-39).datetime)
assert strategy.min_roi_reached(trade, 0.06, arrow.utcnow().shift(minutes=-39).datetime)
assert not strategy.min_roi_reached(trade, -0.01, arrow.utcnow().shift(minutes=-1).datetime)
assert strategy.min_roi_reached(trade, 0.02, arrow.utcnow().shift(minutes=-1).datetime)
def test_analyze_ticker_default(ticker_history, mocker, caplog) -> None:
caplog.set_level(logging.DEBUG)
ind_mock = MagicMock(side_effect=lambda x, meta: x)

View File

@@ -130,7 +130,7 @@ def test_strategy_override_minimal_roi(caplog):
assert resolver.strategy.minimal_roi[0] == 0.5
assert ('freqtrade.strategy.resolver',
logging.INFO,
"Override strategy 'minimal_roi' with value in config file."
"Override strategy 'minimal_roi' with value in config file: {'0': 0.5}."
) in caplog.record_tuples

View File

@@ -159,6 +159,15 @@ def test_gen_pair_whitelist(mocker, default_conf, tickers) -> None:
assert whitelist == []
def test_gen_pair_whitelist_not_supported(mocker, default_conf, tickers) -> None:
freqtrade = get_patched_freqtradebot(mocker, default_conf)
mocker.patch('freqtrade.exchange.Exchange.get_tickers', tickers)
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=False))
with pytest.raises(OperationalException):
freqtrade._gen_pair_whitelist(base_currency='BTC')
@pytest.mark.skip(reason="Test not implemented")
def test_refresh_whitelist() -> None:
pass
@@ -664,21 +673,21 @@ def test_balance_fully_ask_side(mocker, default_conf) -> None:
default_conf['bid_strategy']['ask_last_balance'] = 0.0
freqtrade = get_patched_freqtradebot(mocker, default_conf)
assert freqtrade.get_target_bid({'ask': 20, 'last': 10}) == 20
assert freqtrade.get_target_bid('ETH/BTC', {'ask': 20, 'last': 10}) == 20
def test_balance_fully_last_side(mocker, default_conf) -> None:
default_conf['bid_strategy']['ask_last_balance'] = 1.0
freqtrade = get_patched_freqtradebot(mocker, default_conf)
assert freqtrade.get_target_bid({'ask': 20, 'last': 10}) == 10
assert freqtrade.get_target_bid('ETH/BTC', {'ask': 20, 'last': 10}) == 10
def test_balance_bigger_last_ask(mocker, default_conf) -> None:
default_conf['bid_strategy']['ask_last_balance'] = 1.0
freqtrade = get_patched_freqtradebot(mocker, default_conf)
assert freqtrade.get_target_bid({'ask': 5, 'last': 10}) == 5
assert freqtrade.get_target_bid('ETH/BTC', {'ask': 5, 'last': 10}) == 5
def test_process_maybe_execute_buy(mocker, default_conf) -> None:
@@ -1876,3 +1885,194 @@ def test_get_real_amount_open_trade(default_conf, mocker):
freqtrade = FreqtradeBot(default_conf)
patch_get_signal(freqtrade)
assert freqtrade.get_real_amount(trade, order) == amount
def test_order_book_depth_of_market(default_conf, ticker, limit_buy_order, fee, markets, mocker,
order_book_l2):
default_conf['bid_strategy']['check_depth_of_market']['enabled'] = True
default_conf['bid_strategy']['check_depth_of_market']['bids_to_ask_delta'] = 0.1
patch_RPCManager(mocker)
mocker.patch('freqtrade.exchange.Exchange.get_order_book', order_book_l2)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
get_fee=fee,
get_markets=markets
)
# Save state of current whitelist
whitelist = deepcopy(default_conf['exchange']['pair_whitelist'])
freqtrade = FreqtradeBot(default_conf)
patch_get_signal(freqtrade)
freqtrade.create_trade()
trade = Trade.query.first()
assert trade is not None
assert trade.stake_amount == 0.001
assert trade.is_open
assert trade.open_date is not None
assert trade.exchange == 'bittrex'
# Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order)
assert trade.open_rate == 0.00001099
assert whitelist == default_conf['exchange']['pair_whitelist']
def test_order_book_depth_of_market_high_delta(default_conf, ticker, limit_buy_order,
fee, markets, mocker, order_book_l2):
default_conf['bid_strategy']['check_depth_of_market']['enabled'] = True
# delta is 100 which is impossible to reach. hence check_depth_of_market will return false
default_conf['bid_strategy']['check_depth_of_market']['bids_to_ask_delta'] = 100
patch_RPCManager(mocker)
mocker.patch('freqtrade.exchange.Exchange.get_order_book', order_book_l2)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
get_fee=fee,
get_markets=markets
)
# Save state of current whitelist
freqtrade = FreqtradeBot(default_conf)
patch_get_signal(freqtrade)
freqtrade.create_trade()
trade = Trade.query.first()
assert trade is None
def test_order_book_bid_strategy1(mocker, default_conf, order_book_l2, markets) -> None:
"""
test if function get_target_bid will return the order book price
instead of the ask rate
"""
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_markets=markets,
get_order_book=order_book_l2
)
default_conf['exchange']['name'] = 'binance'
default_conf['bid_strategy']['use_order_book'] = True
default_conf['bid_strategy']['order_book_top'] = 2
default_conf['bid_strategy']['ask_last_balance'] = 0
default_conf['telegram']['enabled'] = False
freqtrade = FreqtradeBot(default_conf)
assert freqtrade.get_target_bid('ETH/BTC', {'ask': 0.045, 'last': 0.046}) == 0.043935
def test_order_book_bid_strategy2(mocker, default_conf, order_book_l2, markets) -> None:
"""
test if function get_target_bid will return the ask rate (since its value is lower)
instead of the order book rate (even if enabled)
"""
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_markets=markets,
get_order_book=order_book_l2
)
default_conf['exchange']['name'] = 'binance'
default_conf['bid_strategy']['use_order_book'] = True
default_conf['bid_strategy']['order_book_top'] = 2
default_conf['bid_strategy']['ask_last_balance'] = 0
default_conf['telegram']['enabled'] = False
freqtrade = FreqtradeBot(default_conf)
assert freqtrade.get_target_bid('ETH/BTC', {'ask': 0.042, 'last': 0.046}) == 0.042
def test_order_book_bid_strategy3(default_conf, mocker, order_book_l2, markets) -> None:
"""
test if function get_target_bid will return ask rate instead
of the order book rate
"""
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_markets=markets,
get_order_book=order_book_l2
)
default_conf['exchange']['name'] = 'binance'
default_conf['bid_strategy']['use_order_book'] = True
default_conf['bid_strategy']['order_book_top'] = 1
default_conf['bid_strategy']['ask_last_balance'] = 0
default_conf['telegram']['enabled'] = False
freqtrade = FreqtradeBot(default_conf)
assert freqtrade.get_target_bid('ETH/BTC', {'ask': 0.03, 'last': 0.029}) == 0.03
def test_check_depth_of_market_buy(default_conf, mocker, order_book_l2, markets) -> None:
"""
test check depth of market
"""
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_markets=markets,
get_order_book=order_book_l2
)
default_conf['telegram']['enabled'] = False
default_conf['exchange']['name'] = 'binance'
default_conf['bid_strategy']['check_depth_of_market']['enabled'] = True
# delta is 100 which is impossible to reach. hence function will return false
default_conf['bid_strategy']['check_depth_of_market']['bids_to_ask_delta'] = 100
freqtrade = FreqtradeBot(default_conf)
conf = default_conf['bid_strategy']['check_depth_of_market']
assert freqtrade._check_depth_of_market_buy('ETH/BTC', conf) is False
def test_order_book_ask_strategy(default_conf, limit_buy_order, limit_sell_order,
fee, markets, mocker, order_book_l2) -> None:
"""
test order book ask strategy
"""
mocker.patch('freqtrade.exchange.Exchange.get_order_book', order_book_l2)
default_conf['exchange']['name'] = 'binance'
default_conf['ask_strategy']['use_order_book'] = True
default_conf['ask_strategy']['order_book_min'] = 1
default_conf['ask_strategy']['order_book_max'] = 2
default_conf['telegram']['enabled'] = False
patch_RPCManager(mocker)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=MagicMock(return_value={
'bid': 0.00001172,
'ask': 0.00001173,
'last': 0.00001172
}),
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
sell=MagicMock(return_value={'id': limit_sell_order['id']}),
get_fee=fee,
get_markets=markets
)
freqtrade = FreqtradeBot(default_conf)
patch_get_signal(freqtrade)
freqtrade.create_trade()
trade = Trade.query.first()
assert trade
time.sleep(0.01) # Race condition fix
trade.update(limit_buy_order)
assert trade.is_open is True
patch_get_signal(freqtrade, value=(False, True))
assert freqtrade.handle_trade(trade) is True
def test_startup_messages(default_conf, mocker):
default_conf['dynamic_whitelist'] = 20
freqtrade = get_patched_freqtradebot(mocker, default_conf)
assert freqtrade.state is State.RUNNING

View File

@@ -1,5 +1,6 @@
# pragma pylint: disable=missing-docstring, C0103
from unittest.mock import MagicMock
import logging
import pytest
from sqlalchemy import create_engine
@@ -403,7 +404,9 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
"""
Test Database migration (starting with new pairformat)
"""
caplog.set_level(logging.DEBUG)
amount = 103.223
# Always create all columns apart from the last!
create_table_old = """CREATE TABLE IF NOT EXISTS "trades" (
id INTEGER NOT NULL,
exchange VARCHAR NOT NULL,
@@ -418,14 +421,21 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
open_date DATETIME NOT NULL,
close_date DATETIME,
open_order_id VARCHAR,
stop_loss FLOAT,
initial_stop_loss FLOAT,
max_rate FLOAT,
sell_reason VARCHAR,
strategy VARCHAR,
PRIMARY KEY (id),
CHECK (is_open IN (0, 1))
);"""
insert_table_old = """INSERT INTO trades (exchange, pair, is_open, fee,
open_rate, stake_amount, amount, open_date)
open_rate, stake_amount, amount, open_date,
stop_loss, initial_stop_loss, max_rate)
VALUES ('binance', 'ETC/BTC', 1, {fee},
0.00258580, {stake}, {amount},
'2019-11-28 12:44:24.000000')
'2019-11-28 12:44:24.000000',
0.0, 0.0, 0.0)
""".format(fee=fee.return_value,
stake=default_conf.get("stake_amount"),
amount=amount
@@ -463,12 +473,15 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
assert trade.ticker_interval is None
assert log_has("trying trades_bak1", caplog.record_tuples)
assert log_has("trying trades_bak2", caplog.record_tuples)
assert log_has("Running database migration - backup available as trades_bak2",
caplog.record_tuples)
def test_migrate_mid_state(mocker, default_conf, fee, caplog):
"""
Test Database migration (starting with new pairformat)
"""
caplog.set_level(logging.DEBUG)
amount = 103.223
create_table_old = """CREATE TABLE IF NOT EXISTS "trades" (
id INTEGER NOT NULL,
@@ -522,6 +535,8 @@ def test_migrate_mid_state(mocker, default_conf, fee, caplog):
assert trade.stop_loss == 0.0
assert trade.initial_stop_loss == 0.0
assert log_has("trying trades_bak0", caplog.record_tuples)
assert log_has("Running database migration - backup available as trades_bak0",
caplog.record_tuples)
def test_adjust_stop_loss(limit_buy_order, limit_sell_order, fee):

View File

@@ -0,0 +1,16 @@
import talib.abstract as ta
import pandas as pd
def test_talib_bollingerbands_near_zero_values():
inputs = pd.DataFrame([
{'close': 0.00000010},
{'close': 0.00000011},
{'close': 0.00000012},
{'close': 0.00000013},
{'close': 0.00000014}
])
bollinger = ta.BBANDS(inputs, matype=0, timeperiod=2)
assert (bollinger['upperband'][3] != bollinger['middleband'][3])