From feb5da0c359a04fc4f6a80365632a286df6a2e52 Mon Sep 17 00:00:00 2001 From: kryofly Date: Thu, 11 Jan 2018 15:49:04 +0100 Subject: [PATCH 1/5] file_dump_json --- freqtrade/misc.py | 5 +++++ freqtrade/optimize/__init__.py | 4 ++-- freqtrade/tests/test_misc.py | 11 ++++++++++- freqtrade/tests/testdata/download_backtest_data.py | 5 ++--- 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/freqtrade/misc.py b/freqtrade/misc.py index afc4334e8..00d431723 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -14,6 +14,11 @@ from freqtrade import __version__ logger = logging.getLogger(__name__) +def file_dump_json(filename, data): + with open(filename, 'w') as fp: + json.dump(data, fp) + + class State(enum.Enum): RUNNING = 0 STOPPED = 1 diff --git a/freqtrade/optimize/__init__.py b/freqtrade/optimize/__init__.py index 2d73c3215..613bb7b17 100644 --- a/freqtrade/optimize/__init__.py +++ b/freqtrade/optimize/__init__.py @@ -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__) @@ -127,7 +128,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']) - with open(filename, "wt") as fp: - json.dump(data, fp) + misc.file_dump_json(filename, data) return True diff --git a/freqtrade/tests/test_misc.py b/freqtrade/tests/test_misc.py index 0b85000cb..e0cd70709 100644 --- a/freqtrade/tests/test_misc.py +++ b/freqtrade/tests/test_misc.py @@ -5,10 +5,11 @@ import argparse from copy import deepcopy import pytest +from unittest.mock import MagicMock from jsonschema import ValidationError from freqtrade.misc import throttle, parse_args, load_config,\ - parse_args_common + parse_args_common, file_dump_json def test_throttle(): @@ -135,6 +136,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_load_config(default_conf, mocker): file_mock = mocker.patch('freqtrade.misc.open', mocker.mock_open( read_data=json.dumps(default_conf) diff --git a/freqtrade/tests/testdata/download_backtest_data.py b/freqtrade/tests/testdata/download_backtest_data.py index 37cd4c95f..32c36073b 100755 --- a/freqtrade/tests/testdata/download_backtest_data.py +++ b/freqtrade/tests/testdata/download_backtest_data.py @@ -1,11 +1,11 @@ #!/usr/bin/env python3 """This script generate json data from bittrex""" -import json from os import path from freqtrade import exchange from freqtrade.exchange import Bittrex +from freqtrade import misc PAIRS = [ 'BTC_BCC', 'BTC_ETH', 'BTC_MER', 'BTC_POWR', 'BTC_ETC', @@ -25,5 +25,4 @@ for pair in PAIRS: pair, TICKER_INTERVAL, )) - with open(filename, 'w') as fp: - json.dump(data, fp) + misc.file_dump_json(filename, data) From 27769f03018f5e0606dac448cf4cfcce32da59dc Mon Sep 17 00:00:00 2001 From: kryofly Date: Thu, 11 Jan 2018 17:45:41 +0100 Subject: [PATCH 2/5] uncomplex backtest --- freqtrade/optimize/backtesting.py | 116 ++++++++++--------- freqtrade/optimize/hyperopt.py | 4 +- freqtrade/tests/optimize/test_backtesting.py | 23 ++-- 3 files changed, 83 insertions(+), 60 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 315f960d8..8296dc900 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -68,17 +68,59 @@ 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.Index, (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) trades = [] trade_count_lock: dict = {} exchange._API = Bittrex({'key': '', 'secret': ''}) @@ -101,41 +143,11 @@ 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: + lock_pair_until, trade_entry = ret + trades.append(trade_entry) labels = ['currency', 'profit_percent', 'profit_BTC', 'duration', 'profit', 'loss'] return DataFrame.from_records(trades, columns=labels) @@ -181,17 +193,17 @@ 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') + }) logger.info( '\n==================================== BACKTESTING REPORT ====================================\n%s', # noqa generate_text_table(data, results, config['stake_currency'], args.ticker_interval) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index cf46b96ad..146874b9c 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -128,7 +128,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() diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index 5f899a48a..a9c58e719 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -43,8 +43,10 @@ def test_backtest(default_conf, mocker): exchange._API = Bittrex({'key': '', 'secret': ''}) data = optimize.load_data(None, ticker_interval=5, pairs=['BTC_ETH']) - 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 @@ -54,8 +56,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']) - 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 @@ -113,7 +117,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 :: assert len(results) == num_results @@ -125,8 +132,10 @@ def simple_backtest(config, contour, num_results): 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']) - 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 From ed47ee4e294ca190e82962d006ef1fc1c3d006b5 Mon Sep 17 00:00:00 2001 From: kryofly Date: Thu, 11 Jan 2018 15:45:39 +0100 Subject: [PATCH 3/5] backtest export json2 --- docs/backtesting.md | 5 +++++ freqtrade/misc.py | 8 ++++++++ freqtrade/optimize/backtesting.py | 16 ++++++++++++++-- freqtrade/tests/optimize/test_backtesting.py | 1 + 4 files changed, 28 insertions(+), 2 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index c426e2b5c..69797cb05 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -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 +``` + For help about backtesting usage, please refer to [Backtesting commands](#backtesting-commands). diff --git a/freqtrade/misc.py b/freqtrade/misc.py index 00d431723..9c581bd47 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -188,6 +188,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', + ) # Add hyperopt subcommand hyperopt_cmd = subparsers.add_parser('hyperopt', help='hyperopt module') diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 8296dc900..dd813cd33 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -1,6 +1,5 @@ # pragma pylint: disable=missing-docstring,W0212 - import logging from typing import Tuple, Dict @@ -121,6 +120,8 @@ def backtest(args) -> 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': ''}) @@ -148,6 +149,16 @@ def backtest(args) -> DataFrame: if ret: lock_pair_until, trade_entry = ret 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.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) @@ -202,7 +213,8 @@ def start(args): 'realistic': args.realistic_simulation, 'sell_profit_only': sell_profit_only, 'use_sell_signal': use_sell_signal, - 'stoploss': config.get('stoploss') + 'stoploss': config.get('stoploss'), + 'record': args.export }) logger.info( '\n==================================== BACKTESTING REPORT ====================================\n%s', # noqa diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index a9c58e719..5c01004b8 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -175,6 +175,7 @@ def test_backtest_start(default_conf, mocker, caplog): args.level = 10 args.live = False args.datadir = None + args.export = None backtesting.start(args) # check the logs, that will contain the backtest result exists = ['Using max_open_trades: 1 ...', From d4008374f622dfa2c31ab131f25cb605d42f7182 Mon Sep 17 00:00:00 2001 From: kryofly Date: Fri, 12 Jan 2018 21:28:56 +0100 Subject: [PATCH 4/5] backtest export: include enter,exit dates --- freqtrade/optimize/backtesting.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index ab12fe559..1c0fcd2a1 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -94,13 +94,13 @@ def get_trade_entry(pair, row, ticker, trade_count_lock, args): (row2.sell == 1 and use_sell_signal) or \ current_profit_percent <= stoploss: current_profit_btc = trade.calc_profit(rate=row2.close) - return row2.Index, (pair, - current_profit_percent, - current_profit_btc, - row2.Index - row.Index, - current_profit_btc > 0, - current_profit_btc < 0 - ) + 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: @@ -146,12 +146,16 @@ def backtest(args) -> DataFrame: ret = get_trade_entry(pair, row, ticker, trade_count_lock, args) if ret: - lock_pair_until, trade_entry = 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 + # 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) From fb110ccfd2fd0d03d0e49ba2d5f1d5d5d80e4690 Mon Sep 17 00:00:00 2001 From: Gerald Lonlas Date: Fri, 19 Jan 2018 22:09:28 -0800 Subject: [PATCH 5/5] Add number of trades in /daily command --- freqtrade/rpc/telegram.py | 18 +++++++++++++----- freqtrade/tests/rpc/test_rpc_telegram.py | 22 ++++++++++++++++++++++ 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 12e701a1c..0fdc734f4 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -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') diff --git a/freqtrade/tests/rpc/test_rpc_telegram.py b/freqtrade/tests/rpc/test_rpc_telegram.py index 992693248..f1ac53f5e 100644 --- a/freqtrade/tests/rpc/test_rpc_telegram.py +++ b/freqtrade/tests/rpc/test_rpc_telegram.py @@ -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()