Merge branch 'develop' into split_btanalysis_load_trades

This commit is contained in:
Matthias
2019-06-24 07:15:14 +02:00
15 changed files with 197 additions and 134 deletions

View File

@@ -87,9 +87,9 @@ class Arguments(object):
)
parser.add_argument(
'-c', '--config',
help="Specify configuration file (default: %(default)s). "
"Multiple --config options may be used. "
"Can be set to '-' to read config from stdin.",
help=f'Specify configuration file (default: {constants.DEFAULT_CONFIG}). '
f'Multiple --config options may be used. '
f'Can be set to `-` to read config from stdin.',
dest='config',
action='append',
metavar='PATH',
@@ -122,9 +122,9 @@ class Arguments(object):
)
parser.add_argument(
'--dynamic-whitelist',
help='Dynamically generate and update whitelist'
' based on 24h BaseVolume (default: %(const)s).'
' DEPRECATED.',
help='Dynamically generate and update whitelist '
'based on 24h BaseVolume (default: %(const)s). '
'DEPRECATED.',
dest='dynamic_whitelist',
const=constants.DYNAMIC_WHITELIST,
type=int,
@@ -133,8 +133,8 @@ class Arguments(object):
)
parser.add_argument(
'--db-url',
help='Override trades database URL, this is useful if dry_run is enabled'
' or in custom deployments (default: %(default)s).',
help=f'Override trades database URL, this is useful if dry_run is enabled '
f'or in custom deployments (default: {constants.DEFAULT_DB_DRYRUN_URL}.',
dest='db_url',
metavar='PATH',
)
@@ -228,10 +228,10 @@ class Arguments(object):
)
parser.add_argument(
'--export-filename',
help='Save backtest results to this filename \
requires --export to be set as well\
Example --export-filename=user_data/backtest_data/backtest_today.json\
(default: %(default)s)',
help='Save backtest results to this filename '
'requires --export to be set as well. '
'Example --export-filename=user_data/backtest_data/backtest_today.json '
'(default: %(default)s)',
default=os.path.join('user_data', 'backtest_data', 'backtest-result.json'),
dest='exportfilename',
metavar='PATH',
@@ -246,8 +246,8 @@ class Arguments(object):
parser.add_argument(
'--stoplosses',
help='Defines a range of stoploss against which edge will assess the strategy '
'the format is "min,max,step" (without any space).'
'example: --stoplosses=-0.01,-0.1,-0.001',
'the format is "min,max,step" (without any space). '
'Example: --stoplosses=-0.01,-0.1,-0.001',
dest='stoploss_range',
)
@@ -289,8 +289,8 @@ class Arguments(object):
)
parser.add_argument(
'-s', '--spaces',
help='Specify which parameters to hyperopt. Space separate list. \
Default: %(default)s.',
help='Specify which parameters to hyperopt. Space separate list. '
'Default: %(default)s.',
choices=['all', 'buy', 'sell', 'roi', 'stoploss'],
default='all',
nargs='+',
@@ -467,13 +467,14 @@ class Arguments(object):
)
parser.add_argument(
'--exchange',
help='Exchange name (default: %(default)s). Only valid if no config is provided.',
help=f'Exchange name (default: {constants.DEFAULT_EXCHANGE}). '
f'Only valid if no config is provided.',
dest='exchange',
)
parser.add_argument(
'-t', '--timeframes',
help='Specify which tickers to download. Space separated list. \
Default: %(default)s.',
help=f'Specify which tickers to download. Space separated list. '
f'Default: {constants.DEFAULT_DOWNLOAD_TICKER_INTERVALS}.',
choices=['1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h',
'6h', '8h', '12h', '1d', '3d', '1w'],
nargs='+',

View File

@@ -4,6 +4,7 @@
bot constants
"""
DEFAULT_CONFIG = 'config.json'
DEFAULT_EXCHANGE = 'bittrex'
DYNAMIC_WHITELIST = 20 # pairs
PROCESS_THROTTLE_SECS = 5 # sec
DEFAULT_TICKER_INTERVAL = 5 # min
@@ -21,6 +22,7 @@ ORDERTYPE_POSSIBILITIES = ['limit', 'market']
ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc']
AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList']
DRY_RUN_WALLET = 999.9
DEFAULT_DOWNLOAD_TICKER_INTERVALS = '1m 5m'
TICKER_INTERVALS = [
'1m', '3m', '5m', '15m', '30m',

View File

@@ -23,7 +23,7 @@ def load_backtest_data(filename) -> pd.DataFrame:
"""
Load backtest data file.
:param filename: pathlib.Path object, or string pointing to the file.
:return a dataframe with the analysis results
:return: a dataframe with the analysis results
"""
if isinstance(filename, str):
filename = Path(filename)
@@ -77,7 +77,7 @@ def load_trades_from_db(db_url: str) -> pd.DataFrame:
"""
Load trades from a DB (using dburl)
:param db_url: Sqlite url (default format sqlite:///tradesv3.dry-run.sqlite)
:returns: Dataframe containing Trades
:return: Dataframe containing Trades
"""
trades: pd.DataFrame = pd.DataFrame([], columns=BT_DATA_COLUMNS)
persistence.init(db_url, clean_open_orders=False)

View File

@@ -63,7 +63,7 @@ def load_tickerdata_file(
timerange: Optional[TimeRange] = None) -> Optional[list]:
"""
Load a pair from file, either .json.gz or .json
:return tickerlist or None if unsuccesful
:return: tickerlist or None if unsuccesful
"""
filename = pair_data_filename(datadir, pair, ticker_interval)
pairdata = misc.file_load_json(filename)

View File

@@ -53,8 +53,7 @@ class FreqtradeBot(object):
self.rpc: RPCManager = RPCManager(self)
exchange_name = self.config.get('exchange', {}).get('name').title()
self.exchange = ExchangeResolver(exchange_name, self.config).exchange
self.exchange = ExchangeResolver(self.config['exchange']['name'], self.config).exchange
self.wallets = Wallets(self.config, self.exchange)
self.dataprovider = DataProvider(self.config, self.exchange)
@@ -691,13 +690,22 @@ class FreqtradeBot(object):
# cancelling the current stoploss on exchange first
logger.info('Trailing stoploss: cancelling current stoploss on exchange (id:{%s})'
'in order to add another one ...', order['id'])
if self.exchange.cancel_order(order['id'], trade.pair):
try:
self.exchange.cancel_order(order['id'], trade.pair)
except InvalidOrderException:
logger.exception(f"Could not cancel stoploss order {order['id']} "
f"for pair {trade.pair}")
try:
# creating the new one
stoploss_order_id = self.exchange.stoploss_limit(
pair=trade.pair, amount=trade.amount,
stop_price=trade.stop_loss, rate=trade.stop_loss * 0.99
)['id']
trade.stoploss_order_id = str(stoploss_order_id)
except DependencyException:
logger.exception(f"Could create trailing stoploss order "
f"for pair {trade.pair}.")
def check_sell(self, trade: Trade, sell_rate: float, buy: bool, sell: bool) -> bool:
if self.edge:
@@ -843,7 +851,10 @@ class FreqtradeBot(object):
# First cancelling stoploss on exchange ...
if self.strategy.order_types.get('stoploss_on_exchange') and trade.stoploss_order_id:
self.exchange.cancel_order(trade.stoploss_order_id, trade.pair)
try:
self.exchange.cancel_order(trade.stoploss_order_id, trade.pair)
except InvalidOrderException:
logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}")
# Execute sell and update trade record
order_id = self.exchange.sell(pair=str(trade.pair),

View File

@@ -63,8 +63,7 @@ class Backtesting(object):
self.config['dry_run'] = True
self.strategylist: List[IStrategy] = []
exchange_name = self.config.get('exchange', {}).get('name').title()
self.exchange = ExchangeResolver(exchange_name, self.config).exchange
self.exchange = ExchangeResolver(self.config['exchange']['name'], self.config).exchange
self.fee = self.exchange.get_fee()
if self.config.get('runmode') != RunMode.HYPEROPT:

View File

@@ -22,6 +22,7 @@ class ExchangeResolver(IResolver):
Load the custom class from config parameter
:param config: configuration dictionary
"""
exchange_name = exchange_name.title()
try:
self.exchange = self._load_exchange(exchange_name, kwargs={'config': config})
except ImportError:

View File

@@ -158,7 +158,7 @@ class IStrategy(ABC):
"""
Parses the given ticker history and returns a populated DataFrame
add several TA indicators and buy signal to it
:return DataFrame with ticker data and indicator data
:return: DataFrame with ticker data and indicator data
"""
pair = str(metadata.get('pair'))
@@ -351,7 +351,7 @@ class IStrategy(ABC):
"""
Based an earlier trade and current price and ROI configuration, decides whether bot should
sell. Requires current_profit to be in percent!!
:return True if bot should sell at current rate
:return: True if bot should sell at current rate
"""
# Check if time matches and current rate is above threshold

View File

@@ -5,6 +5,7 @@ import re
from copy import deepcopy
from datetime import datetime
from functools import reduce
from pathlib import Path
from typing import List
from unittest.mock import MagicMock, PropertyMock
@@ -60,7 +61,7 @@ def get_patched_exchange(mocker, config, api_mock=None, id='bittrex') -> Exchang
patch_exchange(mocker, api_mock, id)
config["exchange"]["name"] = id
try:
exchange = ExchangeResolver(id.title(), config).exchange
exchange = ExchangeResolver(id, config).exchange
except ImportError:
exchange = Exchange(config)
return exchange
@@ -110,11 +111,23 @@ def patch_freqtradebot(mocker, config) -> None:
def get_patched_freqtradebot(mocker, config) -> FreqtradeBot:
"""
This function patches _init_modules() to not call dependencies
:param mocker: a Mocker object to apply patches
:param config: Config to pass to the bot
:return: FreqtradeBot
"""
patch_freqtradebot(mocker, config)
return FreqtradeBot(config)
def get_patched_worker(mocker, config) -> Worker:
"""
This function patches _init_modules() to not call dependencies
:param mocker: a Mocker object to apply patches
:param config: Config to pass to the bot
:return: Worker
"""
patch_freqtradebot(mocker, config)
return Worker(args=None, config=config)
@@ -865,7 +878,7 @@ def tickers():
@pytest.fixture
def result():
with open('freqtrade/tests/testdata/UNITTEST_BTC-1m.json') as data_file:
with Path('freqtrade/tests/testdata/UNITTEST_BTC-1m.json').open('r') as data_file:
return parse_ticker_dataframe(json.load(data_file), '1m', fill_missing=True)
# FIX:

View File

@@ -124,14 +124,14 @@ def test_exchange_resolver(default_conf, mocker, caplog):
caplog.record_tuples)
caplog.clear()
exchange = ExchangeResolver('Kraken', default_conf).exchange
exchange = ExchangeResolver('kraken', default_conf).exchange
assert isinstance(exchange, Exchange)
assert isinstance(exchange, Kraken)
assert not isinstance(exchange, Binance)
assert not log_has_re(r"No .* specific subclass found. Using the generic class instead.",
caplog.record_tuples)
exchange = ExchangeResolver('Binance', default_conf).exchange
exchange = ExchangeResolver('binance', default_conf).exchange
assert isinstance(exchange, Exchange)
assert isinstance(exchange, Binance)
assert not isinstance(exchange, Kraken)

View File

@@ -75,14 +75,10 @@ def test_load_strategy_byte64(result):
def test_load_strategy_invalid_directory(result, caplog):
resolver = StrategyResolver()
extra_dir = path.join('some', 'path')
extra_dir = Path.cwd() / 'some/path'
resolver._load_strategy('TestStrategy', config={}, extra_dir=extra_dir)
assert (
'freqtrade.resolvers.strategy_resolver',
logging.WARNING,
'Path "{}" does not exist'.format(extra_dir),
) in caplog.record_tuples
assert log_has_re(r'Path .*' + r'some.*path.*' + r'.* does not exist', caplog.record_tuples)
assert 'adx' in resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'})

View File

@@ -19,47 +19,13 @@ from freqtrade.persistence import Trade
from freqtrade.rpc import RPCMessageType
from freqtrade.state import State
from freqtrade.strategy.interface import SellCheckTuple, SellType
from freqtrade.tests.conftest import (log_has, log_has_re, patch_edge,
patch_exchange, patch_get_signal,
patch_wallet)
from freqtrade.tests.conftest import (get_patched_freqtradebot,
get_patched_worker, log_has, log_has_re,
patch_edge, patch_exchange,
patch_get_signal, patch_wallet)
from freqtrade.worker import Worker
# Functions for recurrent object patching
def patch_freqtradebot(mocker, config) -> None:
"""
This function patches _init_modules() to not call dependencies
:param mocker: a Mocker object to apply patches
:param config: Config to pass to the bot
:return: None
"""
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock())
patch_exchange(mocker)
def get_patched_freqtradebot(mocker, config) -> FreqtradeBot:
"""
This function patches _init_modules() to not call dependencies
:param mocker: a Mocker object to apply patches
:param config: Config to pass to the bot
:return: FreqtradeBot
"""
patch_freqtradebot(mocker, config)
return FreqtradeBot(config)
def get_patched_worker(mocker, config) -> Worker:
"""
This function patches _init_modules() to not call dependencies
:param mocker: a Mocker object to apply patches
:param config: Config to pass to the bot
:return: Worker
"""
patch_freqtradebot(mocker, config)
return Worker(args=None, config=config)
def patch_RPCManager(mocker) -> MagicMock:
"""
This function mock RPC manager to avoid repeating this code in almost every tests
@@ -1176,6 +1142,77 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog,
stop_price=0.00002344 * 0.95)
def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, caplog,
markets, limit_buy_order,
limit_sell_order) -> None:
# When trailing stoploss is set
stoploss_limit = MagicMock(return_value={'id': 13434334})
patch_exchange(mocker)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
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,
markets=PropertyMock(return_value=markets),
stoploss_limit=stoploss_limit
)
# enabling TSL
default_conf['trailing_stop'] = True
freqtrade = get_patched_freqtradebot(mocker, default_conf)
# enabling stoploss on exchange
freqtrade.strategy.order_types['stoploss_on_exchange'] = True
# setting stoploss
freqtrade.strategy.stoploss = -0.05
# setting stoploss_on_exchange_interval to 60 seconds
freqtrade.strategy.order_types['stoploss_on_exchange_interval'] = 60
patch_get_signal(freqtrade)
freqtrade.create_trade()
trade = Trade.query.first()
trade.is_open = True
trade.open_order_id = None
trade.stoploss_order_id = "abcd"
trade.stop_loss = 0.2
trade.stoploss_last_update = arrow.utcnow().shift(minutes=-601).datetime.replace(tzinfo=None)
stoploss_order_hanging = {
'id': "abcd",
'status': 'open',
'type': 'stop_loss_limit',
'price': 3,
'average': 2,
'info': {
'stopPrice': '0.1'
}
}
mocker.patch('freqtrade.exchange.Exchange.cancel_order', side_effect=InvalidOrderException())
mocker.patch('freqtrade.exchange.Exchange.get_order', stoploss_order_hanging)
freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging)
assert log_has_re(r"Could not cancel stoploss order abcd for pair ETH/BTC.*",
caplog.record_tuples)
# Still try to create order
assert stoploss_limit.call_count == 1
# Fail creating stoploss order
caplog.clear()
cancel_mock = mocker.patch("freqtrade.exchange.Exchange.cancel_order", MagicMock())
mocker.patch("freqtrade.exchange.Exchange.stoploss_limit", side_effect=DependencyException())
freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging)
assert cancel_mock.call_count == 1
assert log_has_re(r"Could create trailing stoploss order for pair ETH/BTC\..*",
caplog.record_tuples)
def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog,
markets, limit_buy_order, limit_sell_order) -> None:
@@ -2108,6 +2145,36 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe
} == last_msg
def test_execute_sell_sloe_cancel_exception(mocker, default_conf, ticker, fee,
markets, caplog) -> None:
freqtrade = get_patched_freqtradebot(mocker, default_conf)
mocker.patch('freqtrade.exchange.Exchange.cancel_order', side_effect=InvalidOrderException())
sellmock = MagicMock()
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
_load_markets=MagicMock(return_value={}),
get_ticker=ticker,
get_fee=fee,
markets=PropertyMock(return_value=markets),
sell=sellmock
)
freqtrade.strategy.order_types['stoploss_on_exchange'] = True
patch_get_signal(freqtrade)
freqtrade.create_trade()
trade = Trade.query.first()
Trade.session = MagicMock()
freqtrade.config['dry_run'] = False
trade.stoploss_order_id = "abcd"
freqtrade.execute_sell(trade=trade, limit=1234,
sell_reason=SellType.STOP_LOSS)
assert sellmock.call_count == 1
assert log_has('Could not cancel stoploss order abcd', caplog.record_tuples)
def test_execute_sell_with_stoploss_on_exchange(default_conf,
ticker, fee, ticker_sell_up,
markets, mocker) -> None: