Merge branch 'develop' into test_coverage
This commit is contained in:
commit
cf266a67ad
@ -51,6 +51,11 @@ python3 ./freqtrade/main.py backtesting --realistic-simulation --live
|
||||
python3 ./freqtrade/main.py backtesting --datadir freqtrade/tests/testdata-20180101
|
||||
```
|
||||
|
||||
**Exporting trades to file**
|
||||
```bash
|
||||
freqtrade backtesting --export trades
|
||||
```
|
||||
|
||||
**Running backtest with smaller testset**
|
||||
Use the `--timerange` argument to change how much of the testset
|
||||
you want to use. The last N ticks/timeframes will be used.
|
||||
|
@ -143,7 +143,7 @@ def _process(nb_assets: Optional[int] = 0) -> bool:
|
||||
# FIX: 20180110, why is cancel.order unconditionally here, whereas
|
||||
# it is conditionally called in the
|
||||
# handle_timedout_limit_sell()?
|
||||
def handle_timedout_limit_buy(trade, order):
|
||||
def handle_timedout_limit_buy(trade: Trade, order: Dict) -> bool:
|
||||
"""Buy timeout - cancel order
|
||||
:return: True if order was fully cancelled
|
||||
"""
|
||||
@ -171,7 +171,7 @@ def handle_timedout_limit_buy(trade, order):
|
||||
|
||||
|
||||
# FIX: 20180110, should cancel_order() be cond. or unconditionally called?
|
||||
def handle_timedout_limit_sell(trade, order):
|
||||
def handle_timedout_limit_sell(trade: Trade, order: Dict) -> bool:
|
||||
"""
|
||||
Sell timeout - cancel order and update trade
|
||||
:return: True if order was fully cancelled
|
||||
|
@ -191,6 +191,14 @@ def build_subcommands(parser: argparse.ArgumentParser) -> None:
|
||||
action='store_true',
|
||||
dest='refresh_pairs',
|
||||
)
|
||||
backtesting_cmd.add_argument(
|
||||
'--export',
|
||||
help='Export backtest results, argument are: trades\
|
||||
Example --export trades',
|
||||
type=str,
|
||||
default=None,
|
||||
dest='export',
|
||||
)
|
||||
backtesting_cmd.add_argument(
|
||||
'--timerange',
|
||||
help='Specify what timerange of data to use.',
|
||||
|
@ -8,6 +8,7 @@ from pandas import DataFrame
|
||||
from freqtrade.exchange import get_ticker_history
|
||||
from freqtrade.optimize.hyperopt_conf import hyperopt_optimize_conf
|
||||
from freqtrade.analyze import populate_indicators, parse_ticker_dataframe
|
||||
from freqtrade import misc
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -155,6 +156,6 @@ def download_backtesting_testdata(datadir: str, pair: str, interval: int = 5) ->
|
||||
logger.debug("New End: {}".format(data[-1:][0]['T']))
|
||||
data = sorted(data, key=lambda data: data['T'])
|
||||
|
||||
file_dump_json(filename, data)
|
||||
misc.file_dump_json(filename, data)
|
||||
|
||||
return True
|
||||
|
@ -66,17 +66,60 @@ def generate_text_table(
|
||||
return tabulate(tabular_data, headers=headers, floatfmt=floatfmt)
|
||||
|
||||
|
||||
def backtest(stake_amount: float, processed: Dict[str, DataFrame],
|
||||
max_open_trades: int = 0, realistic: bool = True, sell_profit_only: bool = False,
|
||||
stoploss: int = -1.00, use_sell_signal: bool = False) -> DataFrame:
|
||||
def get_trade_entry(pair, row, ticker, trade_count_lock, args):
|
||||
stake_amount = args['stake_amount']
|
||||
max_open_trades = args.get('max_open_trades', 0)
|
||||
sell_profit_only = args.get('sell_profit_only', False)
|
||||
stoploss = args.get('stoploss', -1)
|
||||
use_sell_signal = args.get('use_sell_signal', False)
|
||||
trade = Trade(open_rate=row.close,
|
||||
open_date=row.date,
|
||||
stake_amount=stake_amount,
|
||||
amount=stake_amount / row.open,
|
||||
fee=exchange.get_fee()
|
||||
)
|
||||
|
||||
# calculate win/lose forwards from buy point
|
||||
sell_subset = ticker[row.Index + 1:][['close', 'date', 'sell']]
|
||||
for row2 in sell_subset.itertuples(index=True):
|
||||
if max_open_trades > 0:
|
||||
# Increase trade_count_lock for every iteration
|
||||
trade_count_lock[row2.date] = trade_count_lock.get(row2.date, 0) + 1
|
||||
|
||||
current_profit_percent = trade.calc_profit_percent(rate=row2.close)
|
||||
if (sell_profit_only and current_profit_percent < 0):
|
||||
continue
|
||||
if min_roi_reached(trade, row2.close, row2.date) or \
|
||||
(row2.sell == 1 and use_sell_signal) or \
|
||||
current_profit_percent <= stoploss:
|
||||
current_profit_btc = trade.calc_profit(rate=row2.close)
|
||||
return row2, (pair,
|
||||
current_profit_percent,
|
||||
current_profit_btc,
|
||||
row2.Index - row.Index,
|
||||
current_profit_btc > 0,
|
||||
current_profit_btc < 0
|
||||
)
|
||||
|
||||
|
||||
def backtest(args) -> DataFrame:
|
||||
"""
|
||||
Implements backtesting functionality
|
||||
:param stake_amount: btc amount to use for each trade
|
||||
:param processed: a processed dictionary with format {pair, data}
|
||||
:param max_open_trades: maximum number of concurrent trades (default: 0, disabled)
|
||||
:param realistic: do we try to simulate realistic trades? (default: True)
|
||||
:param args: a dict containing:
|
||||
stake_amount: btc amount to use for each trade
|
||||
processed: a processed dictionary with format {pair, data}
|
||||
max_open_trades: maximum number of concurrent trades (default: 0, disabled)
|
||||
realistic: do we try to simulate realistic trades? (default: True)
|
||||
sell_profit_only: sell if profit only
|
||||
use_sell_signal: act on sell-signal
|
||||
stoploss: use stoploss
|
||||
:return: DataFrame
|
||||
"""
|
||||
processed = args['processed']
|
||||
max_open_trades = args.get('max_open_trades', 0)
|
||||
realistic = args.get('realistic', True)
|
||||
record = args.get('record', None)
|
||||
records = []
|
||||
trades = []
|
||||
trade_count_lock: dict = {}
|
||||
exchange._API = Bittrex({'key': '', 'secret': ''})
|
||||
@ -99,41 +142,25 @@ def backtest(stake_amount: float, processed: Dict[str, DataFrame],
|
||||
# Increase lock
|
||||
trade_count_lock[row.date] = trade_count_lock.get(row.date, 0) + 1
|
||||
|
||||
trade = Trade(
|
||||
open_rate=row.close,
|
||||
open_date=row.date,
|
||||
stake_amount=stake_amount,
|
||||
amount=stake_amount / row.open,
|
||||
fee=exchange.get_fee()
|
||||
)
|
||||
|
||||
# calculate win/lose forwards from buy point
|
||||
sell_subset = ticker[row.Index + 1:][['close', 'date', 'sell']]
|
||||
for row2 in sell_subset.itertuples(index=True):
|
||||
if max_open_trades > 0:
|
||||
# Increase trade_count_lock for every iteration
|
||||
trade_count_lock[row2.date] = trade_count_lock.get(row2.date, 0) + 1
|
||||
|
||||
current_profit_percent = trade.calc_profit_percent(rate=row2.close)
|
||||
if (sell_profit_only and current_profit_percent < 0):
|
||||
continue
|
||||
if min_roi_reached(trade, row2.close, row2.date) or \
|
||||
(row2.sell == 1 and use_sell_signal) or \
|
||||
current_profit_percent <= stoploss:
|
||||
current_profit_btc = trade.calc_profit(rate=row2.close)
|
||||
lock_pair_until = row2.Index
|
||||
|
||||
trades.append(
|
||||
(
|
||||
pair,
|
||||
current_profit_percent,
|
||||
current_profit_btc,
|
||||
row2.Index - row.Index,
|
||||
current_profit_btc > 0,
|
||||
current_profit_btc < 0
|
||||
)
|
||||
)
|
||||
break
|
||||
ret = get_trade_entry(pair, row, ticker,
|
||||
trade_count_lock, args)
|
||||
if ret:
|
||||
row2, trade_entry = ret
|
||||
lock_pair_until = row2.Index
|
||||
trades.append(trade_entry)
|
||||
if record:
|
||||
# Note, need to be json.dump friendly
|
||||
# record a tuple of pair, current_profit_percent,
|
||||
# entry-date, duration
|
||||
records.append((pair, trade_entry[1],
|
||||
row.date.strftime('%s'),
|
||||
row2.date.strftime('%s'),
|
||||
row.Index, trade_entry[3]))
|
||||
# For now export inside backtest(), maybe change so that backtest()
|
||||
# returns a tuple like: (dataframe, records, logs, etc)
|
||||
if record and record.find('trades') >= 0:
|
||||
logger.info('Dumping backtest results')
|
||||
misc.file_dump_json('backtest-result.json', records)
|
||||
labels = ['currency', 'profit_percent', 'profit_BTC', 'duration', 'profit', 'loss']
|
||||
return DataFrame.from_records(trades, columns=labels)
|
||||
|
||||
@ -180,17 +207,18 @@ def start(args):
|
||||
# Print timeframe
|
||||
min_date, max_date = get_timeframe(preprocessed)
|
||||
logger.info('Measuring data from %s up to %s ...', min_date.isoformat(), max_date.isoformat())
|
||||
|
||||
# Execute backtest and print results
|
||||
results = backtest(
|
||||
stake_amount=config['stake_amount'],
|
||||
processed=preprocessed,
|
||||
max_open_trades=max_open_trades,
|
||||
realistic=args.realistic_simulation,
|
||||
sell_profit_only=config.get('experimental', {}).get('sell_profit_only', False),
|
||||
stoploss=config.get('stoploss'),
|
||||
use_sell_signal=config.get('experimental', {}).get('use_sell_signal', False)
|
||||
)
|
||||
sell_profit_only = config.get('experimental', {}).get('sell_profit_only', False)
|
||||
use_sell_signal = config.get('experimental', {}).get('use_sell_signal', False)
|
||||
results = backtest({'stake_amount': config['stake_amount'],
|
||||
'processed': preprocessed,
|
||||
'max_open_trades': max_open_trades,
|
||||
'realistic': args.realistic_simulation,
|
||||
'sell_profit_only': sell_profit_only,
|
||||
'use_sell_signal': use_sell_signal,
|
||||
'stoploss': config.get('stoploss'),
|
||||
'record': args.export
|
||||
})
|
||||
logger.info(
|
||||
'\n==================================== BACKTESTING REPORT ====================================\n%s', # noqa
|
||||
generate_text_table(data, results, config['stake_currency'], args.ticker_interval)
|
||||
|
@ -164,7 +164,9 @@ def optimizer(params):
|
||||
from freqtrade.optimize import backtesting
|
||||
backtesting.populate_buy_trend = buy_strategy_generator(params)
|
||||
|
||||
results = backtest(OPTIMIZE_CONFIG['stake_amount'], PROCESSED, stoploss=params['stoploss'])
|
||||
results = backtest({'stake_amount': OPTIMIZE_CONFIG['stake_amount'],
|
||||
'processed': PROCESSED,
|
||||
'stoploss': params['stoploss']})
|
||||
result_explanation = format_results(results)
|
||||
|
||||
total_profit = results.profit_percent.sum()
|
||||
|
@ -241,20 +241,27 @@ def _daily(bot: Bot, update: Update) -> None:
|
||||
.order_by(Trade.close_date)\
|
||||
.all()
|
||||
curdayprofit = sum(trade.calc_profit() for trade in trades)
|
||||
profit_days[profitday] = format(curdayprofit, '.8f')
|
||||
profit_days[profitday] = {
|
||||
'amount': format(curdayprofit, '.8f'),
|
||||
'trades': len(trades)
|
||||
}
|
||||
|
||||
stats = [
|
||||
[
|
||||
key,
|
||||
'{value:.8f} {symbol}'.format(value=float(value), symbol=_CONF['stake_currency']),
|
||||
'{value:.8f} {symbol}'.format(
|
||||
value=float(value['amount']),
|
||||
symbol=_CONF['stake_currency']
|
||||
),
|
||||
'{value:.3f} {symbol}'.format(
|
||||
value=_FIAT_CONVERT.convert_amount(
|
||||
value,
|
||||
value['amount'],
|
||||
_CONF['stake_currency'],
|
||||
_CONF['fiat_display_currency']
|
||||
),
|
||||
symbol=_CONF['fiat_display_currency']
|
||||
)
|
||||
),
|
||||
'{value} trade{s}'.format(value=value['trades'], s='' if value['trades'] < 2 else 's'),
|
||||
]
|
||||
for key, value in profit_days.items()
|
||||
]
|
||||
@ -262,7 +269,8 @@ def _daily(bot: Bot, update: Update) -> None:
|
||||
headers=[
|
||||
'Day',
|
||||
'Profit {}'.format(_CONF['stake_currency']),
|
||||
'Profit {}'.format(_CONF['fiat_display_currency'])
|
||||
'Profit {}'.format(_CONF['fiat_display_currency']),
|
||||
'# Trades'
|
||||
],
|
||||
tablefmt='simple')
|
||||
|
||||
|
@ -51,8 +51,10 @@ def test_backtest(default_conf, mocker):
|
||||
|
||||
data = optimize.load_data(None, ticker_interval=5, pairs=['BTC_ETH'])
|
||||
data = trim_dictlist(data, -200)
|
||||
results = backtest(default_conf['stake_amount'],
|
||||
optimize.preprocess(data), 10, True)
|
||||
results = backtest({'stake_amount': default_conf['stake_amount'],
|
||||
'processed': optimize.preprocess(data),
|
||||
'max_open_trades': 10,
|
||||
'realistic': True})
|
||||
assert not results.empty
|
||||
|
||||
|
||||
@ -63,8 +65,10 @@ def test_backtest_1min_ticker_interval(default_conf, mocker):
|
||||
# Run a backtesting for an exiting 5min ticker_interval
|
||||
data = optimize.load_data(None, ticker_interval=1, pairs=['BTC_UNITEST'])
|
||||
data = trim_dictlist(data, -200)
|
||||
results = backtest(default_conf['stake_amount'],
|
||||
optimize.preprocess(data), 1, True)
|
||||
results = backtest({'stake_amount': default_conf['stake_amount'],
|
||||
'processed': optimize.preprocess(data),
|
||||
'max_open_trades': 1,
|
||||
'realistic': True})
|
||||
assert not results.empty
|
||||
|
||||
|
||||
@ -115,7 +119,10 @@ def simple_backtest(config, contour, num_results):
|
||||
data = load_data_test(contour)
|
||||
processed = optimize.preprocess(data)
|
||||
assert isinstance(processed, dict)
|
||||
results = backtest(config['stake_amount'], processed, 1, True)
|
||||
results = backtest({'stake_amount': config['stake_amount'],
|
||||
'processed': processed,
|
||||
'max_open_trades': 1,
|
||||
'realistic': True})
|
||||
# results :: <class 'pandas.core.frame.DataFrame'>
|
||||
assert len(results) == num_results
|
||||
|
||||
@ -128,8 +135,10 @@ def test_backtest2(default_conf, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
data = optimize.load_data(None, ticker_interval=5, pairs=['BTC_ETH'])
|
||||
data = trim_dictlist(data, -200)
|
||||
results = backtest(default_conf['stake_amount'],
|
||||
optimize.preprocess(data), 10, True)
|
||||
results = backtest({'stake_amount': default_conf['stake_amount'],
|
||||
'processed': optimize.preprocess(data),
|
||||
'max_open_trades': 10,
|
||||
'realistic': True})
|
||||
assert not results.empty
|
||||
|
||||
|
||||
@ -169,6 +178,7 @@ def test_backtest_start(default_conf, mocker, caplog):
|
||||
args.level = 10
|
||||
args.live = False
|
||||
args.datadir = None
|
||||
args.export = None
|
||||
args.timerange = '-100' # needed due to MagicMock malleability
|
||||
backtesting.start(args)
|
||||
# check the logs, that will contain the backtest result
|
||||
|
@ -173,7 +173,7 @@ def test_download_backtesting_testdata(default_conf, ticker_history, mocker):
|
||||
|
||||
def test_download_backtesting_testdata2(default_conf, mocker):
|
||||
tick = [{'T': 'bar'}, {'T': 'foo'}]
|
||||
mocker.patch('freqtrade.optimize.__init__.file_dump_json', return_value=None)
|
||||
mocker.patch('freqtrade.misc.file_dump_json', return_value=None)
|
||||
mocker.patch('freqtrade.optimize.__init__.get_ticker_history', return_value=tick)
|
||||
assert download_backtesting_testdata(None, pair="BTC-UNITEST", interval=1)
|
||||
assert download_backtesting_testdata(None, pair="BTC-UNITEST", interval=3)
|
||||
|
@ -448,6 +448,28 @@ def test_daily_handle(
|
||||
assert str(datetime.utcnow().date()) in msg_mock.call_args_list[0][0][0]
|
||||
assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0]
|
||||
assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0]
|
||||
assert str(' 1 trade') in msg_mock.call_args_list[0][0][0]
|
||||
assert str(' 0 trade') in msg_mock.call_args_list[0][0][0]
|
||||
|
||||
# Reset msg_mock
|
||||
msg_mock.reset_mock()
|
||||
# Add two other trades
|
||||
create_trade(0.001)
|
||||
create_trade(0.001)
|
||||
|
||||
trades = Trade.query.all()
|
||||
for trade in trades:
|
||||
trade.update(limit_buy_order)
|
||||
trade.update(limit_sell_order)
|
||||
trade.close_date = datetime.utcnow()
|
||||
trade.is_open = False
|
||||
|
||||
update.message.text = '/daily 1'
|
||||
|
||||
_daily(bot=MagicMock(), update=update)
|
||||
assert str(' 0.00018651 BTC') in msg_mock.call_args_list[0][0][0]
|
||||
assert str(' 2.798 USD') in msg_mock.call_args_list[0][0][0]
|
||||
assert str(' 3 trades') in msg_mock.call_args_list[0][0][0]
|
||||
|
||||
# Try invalid data
|
||||
msg_mock.reset_mock()
|
||||
|
@ -47,18 +47,6 @@ def test_main_start_hyperopt(mocker):
|
||||
assert call_args.func is not None
|
||||
|
||||
|
||||
# def test_main_trader(mocker):
|
||||
# mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
||||
# mocker.patch('freqtrade.misc.get_state', return_value=True)
|
||||
# mocker.patch.multiple('freqtrade.main',
|
||||
# init=MagicMock(),
|
||||
# cleanup=MagicMock(),
|
||||
# throttle=MagicMock()
|
||||
# )
|
||||
# Cant run this yet because we have an unconditional while loop in main
|
||||
# assert 0 == main.main([])
|
||||
|
||||
|
||||
def test_process_maybe_execute_buy(default_conf, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
mocker.patch('freqtrade.main.create_trade', return_value=True)
|
||||
|
@ -5,10 +5,11 @@ import time
|
||||
from copy import deepcopy
|
||||
|
||||
import pytest
|
||||
from unittest.mock import MagicMock
|
||||
from jsonschema import ValidationError
|
||||
|
||||
from freqtrade.misc import (common_args_parser, load_config, parse_args,
|
||||
throttle, parse_timerange)
|
||||
throttle, file_dump_json, parse_timerange)
|
||||
|
||||
|
||||
def test_throttle():
|
||||
@ -133,6 +134,14 @@ def test_parse_args_hyperopt_custom(mocker):
|
||||
assert call_args.func is not None
|
||||
|
||||
|
||||
def test_file_dump_json(default_conf, mocker):
|
||||
file_open = mocker.patch('freqtrade.misc.open', MagicMock())
|
||||
json_dump = mocker.patch('json.dump', MagicMock())
|
||||
file_dump_json('somefile', [1, 2, 3])
|
||||
assert file_open.call_count == 1
|
||||
assert json_dump.call_count == 1
|
||||
|
||||
|
||||
def test_parse_timerange_incorrect():
|
||||
assert ((None, 'line'), None, -200) == parse_timerange('-200')
|
||||
assert (('line', None), 200, None) == parse_timerange('200-')
|
||||
|
Loading…
Reference in New Issue
Block a user