From feca87345f241bc906d8f40a578c8a5ec79a0756 Mon Sep 17 00:00:00 2001 From: kryofly Date: Wed, 10 Jan 2018 23:00:40 +0100 Subject: [PATCH 01/27] refactor --- freqtrade/optimize/__init__.py | 5 +++++ freqtrade/optimize/backtesting.py | 9 ++++----- freqtrade/optimize/hyperopt.py | 4 ++-- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/freqtrade/optimize/__init__.py b/freqtrade/optimize/__init__.py index 2d73c3215..aff4c372d 100644 --- a/freqtrade/optimize/__init__.py +++ b/freqtrade/optimize/__init__.py @@ -61,6 +61,11 @@ def load_data(datadir: str, ticker_interval: int = 5, pairs: Optional[List[str]] return result +def tickerdata_to_dataframe(data): + preprocessed = preprocess(data) + return preprocessed + + def preprocess(tickerdata: Dict[str, List]) -> Dict[str, DataFrame]: """Creates a dataframe and populates indicators for given ticker data""" return {pair: populate_indicators(parse_ticker_dataframe(pair_data)) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 315f960d8..5521ee98f 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -13,7 +13,6 @@ from freqtrade.analyze import populate_buy_trend, populate_sell_trend from freqtrade.exchange import Bittrex from freqtrade.main import min_roi_reached import freqtrade.misc as misc -from freqtrade.optimize import preprocess import freqtrade.optimize as optimize from freqtrade.persistence import Trade @@ -162,12 +161,12 @@ def start(args): data[pair] = exchange.get_ticker_history(pair, args.ticker_interval) else: logger.info('Using local backtesting data (using whitelist in given config) ...') - data = optimize.load_data(args.datadir, pairs=pairs, ticker_interval=args.ticker_interval, - refresh_pairs=args.refresh_pairs) - logger.info('Using stake_currency: %s ...', config['stake_currency']) logger.info('Using stake_amount: %s ...', config['stake_amount']) + data = optimize.load_data(args.datadir, pairs=pairs, ticker_interval=args.ticker_interval, + refresh_pairs=args.refresh_pairs) + max_open_trades = 0 if args.realistic_simulation: logger.info('Using max_open_trades: %s ...', config['max_open_trades']) @@ -177,7 +176,7 @@ def start(args): from freqtrade import main main._CONF = config - preprocessed = preprocess(data) + preprocessed = optimize.tickerdata_to_dataframe(data) # Print timeframe min_date, max_date = get_timeframe(preprocessed) logger.info('Measuring data from %s up to %s ...', min_date.isoformat(), max_date.isoformat()) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index cf46b96ad..b5490d72e 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -230,8 +230,8 @@ def start(args): logger.info('Using config: %s ...', args.config) config = load_config(args.config) pairs = config['exchange']['pair_whitelist'] - PROCESSED = optimize.preprocess(optimize.load_data( - args.datadir, pairs=pairs, ticker_interval=args.ticker_interval)) + data = optimize.load_data(args.datadir, pairs=pairs, ticker_interval=args.ticker_interval) + PROCESSED = optimize.tickerdata_to_dataframe(data) if args.mongodb: logger.info('Using mongodb ...') From b0f3fd7ffb434c472a61fadb421f46839e3d8f2a Mon Sep 17 00:00:00 2001 From: kryofly Date: Wed, 10 Jan 2018 23:03:05 +0100 Subject: [PATCH 02/27] timeperiod argument to backtesting and hyperopt --- freqtrade/misc.py | 14 ++++++++++++++ freqtrade/optimize/__init__.py | 11 ++++++++++- freqtrade/optimize/backtesting.py | 2 +- freqtrade/optimize/hyperopt.py | 2 +- freqtrade/tests/optimize/test_backtesting.py | 14 ++++---------- freqtrade/tests/optimize/test_hyperopt.py | 8 ++++---- freqtrade/tests/optimize/test_optimize.py | 7 +++++++ 7 files changed, 41 insertions(+), 17 deletions(-) diff --git a/freqtrade/misc.py b/freqtrade/misc.py index afc4334e8..97e1eca36 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -183,6 +183,13 @@ def build_subcommands(parser: argparse.ArgumentParser) -> None: action='store_true', dest='refresh_pairs', ) + backtesting_cmd.add_argument( + '-tp', '--timeperiod', + help='Use the last N ticks of data.', + default=None, + type=int, + dest='timeperiod', + ) # Add hyperopt subcommand hyperopt_cmd = subparsers.add_parser('hyperopt', help='hyperopt module') @@ -209,6 +216,13 @@ def build_subcommands(parser: argparse.ArgumentParser) -> None: type=int, metavar='INT', ) + hyperopt_cmd.add_argument( + '-tp', '--timeperiod', + help='Use the last N ticks of data.', + default=None, + type=int, + dest='timeperiod', + ) # Required json-schema for user specified config diff --git a/freqtrade/optimize/__init__.py b/freqtrade/optimize/__init__.py index aff4c372d..3f6a98ac1 100644 --- a/freqtrade/optimize/__init__.py +++ b/freqtrade/optimize/__init__.py @@ -12,6 +12,13 @@ from freqtrade.analyze import populate_indicators, parse_ticker_dataframe logger = logging.getLogger(__name__) +def trim_tickerlist(dl, num): + new = {} + for pair, pair_data in dl.items(): + new[pair] = pair_data[num:] + return new + + def load_tickerdata_file(datadir, pair, ticker_interval): """ Load a pair from file, @@ -61,7 +68,9 @@ def load_data(datadir: str, ticker_interval: int = 5, pairs: Optional[List[str]] return result -def tickerdata_to_dataframe(data): +def tickerdata_to_dataframe(data, timeperiod=None): + if timeperiod: + data = trim_tickerlist(data, timeperiod) preprocessed = preprocess(data) return preprocessed diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 5521ee98f..35b2c6d27 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -176,7 +176,7 @@ def start(args): from freqtrade import main main._CONF = config - preprocessed = optimize.tickerdata_to_dataframe(data) + preprocessed = optimize.tickerdata_to_dataframe(data, timeperiod=args.timeperiod) # Print timeframe min_date, max_date = get_timeframe(preprocessed) logger.info('Measuring data from %s up to %s ...', min_date.isoformat(), max_date.isoformat()) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index b5490d72e..a556258d0 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -231,7 +231,7 @@ def start(args): config = load_config(args.config) pairs = config['exchange']['pair_whitelist'] data = optimize.load_data(args.datadir, pairs=pairs, ticker_interval=args.ticker_interval) - PROCESSED = optimize.tickerdata_to_dataframe(data) + PROCESSED = optimize.tickerdata_to_dataframe(data, timeperiod=args.timeperiod) if args.mongodb: logger.info('Using mongodb ...') diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index 5f899a48a..d4172d983 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -6,7 +6,7 @@ import pandas as pd from unittest.mock import MagicMock from freqtrade import exchange, optimize from freqtrade.exchange import Bittrex -from freqtrade.optimize import preprocess +from freqtrade.optimize import preprocess, trim_tickerlist from freqtrade.optimize.backtesting import backtest, generate_text_table, get_timeframe import freqtrade.optimize.backtesting as backtesting @@ -59,16 +59,9 @@ def test_backtest_1min_ticker_interval(default_conf, mocker): assert not results.empty -def trim_dictlist(dl, num): - new = {} - for pair, pair_data in dl.items(): - new[pair] = pair_data[num:] - return new - - def load_data_test(what): data = optimize.load_data(None, ticker_interval=1, pairs=['BTC_UNITEST']) - data = trim_dictlist(data, -100) + data = trim_tickerlist(data, -100) pair = data['BTC_UNITEST'] datalen = len(pair) # Depending on the what parameter we now adjust the @@ -152,7 +145,7 @@ def test_backtest_pricecontours(default_conf, mocker): def mocked_load_data(datadir, pairs=[], ticker_interval=0, refresh_pairs=False): tickerdata = optimize.load_tickerdata_file(datadir, 'BTC_UNITEST', 1) pairdata = {'BTC_UNITEST': tickerdata} - return trim_dictlist(pairdata, -100) + return trim_tickerlist(pairdata, -100) def test_backtest_start(default_conf, mocker, caplog): @@ -166,6 +159,7 @@ def test_backtest_start(default_conf, mocker, caplog): args.level = 10 args.live = False args.datadir = None + args.timeperiod = None # needed due to MagicMock malleability backtesting.start(args) # check the logs, that will contain the backtest result exists = ['Using max_open_trades: 1 ...', diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index a309af7fe..f8feda0f8 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -37,7 +37,7 @@ def create_trials(mocker): def test_start_calls_fmin(mocker): mocker.patch('freqtrade.optimize.hyperopt.Trials', return_value=create_trials(mocker)) - mocker.patch('freqtrade.optimize.preprocess') + mocker.patch('freqtrade.optimize.tickerdata_to_dataframe') mocker.patch('freqtrade.optimize.load_data') mock_fmin = mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value={}) @@ -50,7 +50,7 @@ def test_start_calls_fmin(mocker): def test_start_uses_mongotrials(mocker): mock_mongotrials = mocker.patch('freqtrade.optimize.hyperopt.MongoTrials', return_value=create_trials(mocker)) - mocker.patch('freqtrade.optimize.preprocess') + mocker.patch('freqtrade.optimize.tickerdata_to_dataframe') mocker.patch('freqtrade.optimize.load_data') mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value={}) @@ -104,7 +104,7 @@ def test_fmin_best_results(mocker, caplog): } mocker.patch('freqtrade.optimize.hyperopt.MongoTrials', return_value=create_trials(mocker)) - mocker.patch('freqtrade.optimize.preprocess') + mocker.patch('freqtrade.optimize.tickerdata_to_dataframe') mocker.patch('freqtrade.optimize.load_data') mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value=fmin_result) @@ -126,7 +126,7 @@ def test_fmin_best_results(mocker, caplog): def test_fmin_throw_value_error(mocker, caplog): mocker.patch('freqtrade.optimize.hyperopt.MongoTrials', return_value=create_trials(mocker)) - mocker.patch('freqtrade.optimize.preprocess') + mocker.patch('freqtrade.optimize.tickerdata_to_dataframe') mocker.patch('freqtrade.optimize.load_data') mocker.patch('freqtrade.optimize.hyperopt.fmin', side_effect=ValueError()) diff --git a/freqtrade/tests/optimize/test_optimize.py b/freqtrade/tests/optimize/test_optimize.py index a5892f278..07e9a46b6 100644 --- a/freqtrade/tests/optimize/test_optimize.py +++ b/freqtrade/tests/optimize/test_optimize.py @@ -174,3 +174,10 @@ def test_load_tickerdata_file(): assert not load_tickerdata_file(None, 'BTC_UNITEST', 7) tickerdata = load_tickerdata_file(None, 'BTC_UNITEST', 1) assert _btc_unittest_length == len(tickerdata) + + +def test_tickerdata_to_dataframe(): + tick = load_tickerdata_file(None, 'BTC_UNITEST', 1) + tickerlist = {'BTC_UNITEST': tick} + data = optimize.tickerdata_to_dataframe(tickerlist, timeperiod=-100) + assert 100 == len(data['BTC_UNITEST']) From 94883202b8e108ab0e5244e866638dc2d579a019 Mon Sep 17 00:00:00 2001 From: kryofly Date: Thu, 11 Jan 2018 00:14:36 +0100 Subject: [PATCH 03/27] docs: --timeperiod argument --- docs/backtesting.md | 10 ++++++++++ docs/hyperopt.md | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/docs/backtesting.md b/docs/backtesting.md index c426e2b5c..31745877f 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -51,6 +51,16 @@ python3 ./freqtrade/main.py backtesting --realistic-simulation --live python3 ./freqtrade/main.py backtesting --datadir freqtrade/tests/testdata-20180101 ``` +**Running backtest with smaller testset** +Use the --timeperiod argument to change how much of the testset +you want to use. The last N ticks/timeframes will be used. +Example: + +```bash +python3 ./freqtrade/main.py backtesting --timeperiod -200 +``` + + For help about backtesting usage, please refer to [Backtesting commands](#backtesting-commands). diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 24a9dbc51..af564f0b6 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -168,6 +168,16 @@ If you would like to learn parameters using an alternate ticke-data that you have on-disk, use the --datadir PATH option. Default hyperopt will use data from directory freqtrade/tests/testdata. +### Running hyperopt with smaller testset + +Use the --timeperiod argument to change how much of the testset +you want to use. The last N ticks/timeframes will be used. +Example: + +```bash +python3 ./freqtrade/main.py hyperopt --timeperiod -200 +``` + ### Hyperopt with MongoDB Hyperopt with MongoDB, is like Hyperopt under steroids. As you saw by executing the previous command is the execution takes a long time. From feb5da0c359a04fc4f6a80365632a286df6a2e52 Mon Sep 17 00:00:00 2001 From: kryofly Date: Thu, 11 Jan 2018 15:49:04 +0100 Subject: [PATCH 04/27] 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 05/27] 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 06/27] 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 829da096e23072269562da4e57a42e199a292161 Mon Sep 17 00:00:00 2001 From: kryofly Date: Fri, 12 Jan 2018 11:49:50 +0100 Subject: [PATCH 07/27] plotting docs --- docs/plotting.md | 18 ++++++++++++++++++ scripts/plot_dataframe.py | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 docs/plotting.md diff --git a/docs/plotting.md b/docs/plotting.md new file mode 100644 index 000000000..56b44400c --- /dev/null +++ b/docs/plotting.md @@ -0,0 +1,18 @@ +# Plotting +This page explains how to plot prices, indicator, profits. + +## Table of Contents +- [Plot price and indicators](#plot-price-and-indicators) + +## Plot price and indicators +Usage for the price plotter: +script/plot_dataframe.py [-h] [-p pair] + +Example +``` +python script/plot_dataframe.py -p BTC_ETH +``` + +The -p pair argument, can be used to specify what +pair you would like to plot. + diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index f07033637..e9bf65f47 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -21,7 +21,7 @@ def plot_parse_args(args ): return parser.parse_args(args) -def plot_analyzed_dataframe(args) -> None: +def plot_analyzed_dataframe(args): """ Calls analyze() and plots the returned dataframe :param pair: pair as str From 98cf98693468c57335ad38fa9e5b6b9ac02ec66f Mon Sep 17 00:00:00 2001 From: kryofly Date: Fri, 12 Jan 2018 10:52:44 +0100 Subject: [PATCH 08/27] misc options parsing split up --- freqtrade/misc.py | 122 ++++++++++++++++++++++++---------------------- 1 file changed, 65 insertions(+), 57 deletions(-) diff --git a/freqtrade/misc.py b/freqtrade/misc.py index 3d70f6b25..92fe39ef2 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -110,6 +110,14 @@ def common_args_parser(description: str): type=str, metavar='PATH', ) + parser.add_argument( + '-dd', '--datadir', + help='path to backtest data (default freqdata/tests/testdata', + dest='datadir', + default=os.path.join('freqtrade', 'tests', 'testdata'), + type=str, + metavar='PATH', + ) return parser @@ -126,14 +134,6 @@ def parse_args(args: List[str], description: str): action='store_true', dest='dry_run_db', ) - parser.add_argument( - '-dd', '--datadir', - help='path to backtest data (default freqdata/tests/testdata', - dest='datadir', - default=os.path.join('freqtrade', 'tests', 'testdata'), - type=str, - metavar='PATH', - ) parser.add_argument( '--dynamic-whitelist', help='dynamically generate and update whitelist \ @@ -149,6 +149,61 @@ def parse_args(args: List[str], description: str): return parser.parse_args(args) +def backtesting_options(parser: argparse.ArgumentParser) -> None: + parser.add_argument( + '-l', '--live', + action='store_true', + dest='live', + help='using live data', + ) + parser.add_argument( + '-i', '--ticker-interval', + help='specify ticker interval in minutes (default: 5)', + dest='ticker_interval', + default=5, + type=int, + metavar='INT', + ) + parser.add_argument( + '--realistic-simulation', + help='uses max_open_trades from config to simulate real world limitations', + action='store_true', + dest='realistic_simulation', + ) + parser.add_argument( + '-r', '--refresh-pairs-cached', + help='refresh the pairs files in tests/testdata with the latest data from Bittrex. \ + Use it if you want to run your backtesting with up-to-date data.', + action='store_true', + dest='refresh_pairs', + ) + + +def hyperopt_options(parser: argparse.ArgumentParser) -> None: + parser.add_argument( + '-e', '--epochs', + help='specify number of epochs (default: 100)', + dest='epochs', + default=100, + type=int, + metavar='INT', + ) + parser.add_argument( + '--use-mongodb', + help='parallelize evaluations with mongodb (requires mongod in PATH)', + dest='mongodb', + action='store_true', + ) + parser.add_argument( + '-i', '--ticker-interval', + help='specify ticker interval in minutes (default: 5)', + dest='ticker_interval', + default=5, + type=int, + metavar='INT', + ) + + def build_subcommands(parser: argparse.ArgumentParser) -> None: """ Builds and attaches all subcommands """ from freqtrade.optimize import backtesting, hyperopt @@ -158,59 +213,12 @@ def build_subcommands(parser: argparse.ArgumentParser) -> None: # Add backtesting subcommand backtesting_cmd = subparsers.add_parser('backtesting', help='backtesting module') backtesting_cmd.set_defaults(func=backtesting.start) - backtesting_cmd.add_argument( - '-l', '--live', - action='store_true', - dest='live', - help='using live data', - ) - backtesting_cmd.add_argument( - '-i', '--ticker-interval', - help='specify ticker interval in minutes (default: 5)', - dest='ticker_interval', - default=5, - type=int, - metavar='INT', - ) - backtesting_cmd.add_argument( - '--realistic-simulation', - help='uses max_open_trades from config to simulate real world limitations', - action='store_true', - dest='realistic_simulation', - ) - backtesting_cmd.add_argument( - '-r', '--refresh-pairs-cached', - help='refresh the pairs files in tests/testdata with the latest data from Bittrex. \ - Use it if you want to run your backtesting with up-to-date data.', - action='store_true', - dest='refresh_pairs', - ) + backtesting_options(backtesting_cmd) # Add hyperopt subcommand hyperopt_cmd = subparsers.add_parser('hyperopt', help='hyperopt module') hyperopt_cmd.set_defaults(func=hyperopt.start) - hyperopt_cmd.add_argument( - '-e', '--epochs', - help='specify number of epochs (default: 100)', - dest='epochs', - default=100, - type=int, - metavar='INT', - ) - hyperopt_cmd.add_argument( - '--use-mongodb', - help='parallelize evaluations with mongodb (requires mongod in PATH)', - dest='mongodb', - action='store_true', - ) - hyperopt_cmd.add_argument( - '-i', '--ticker-interval', - help='specify ticker interval in minutes (default: 5)', - dest='ticker_interval', - default=5, - type=int, - metavar='INT', - ) + hyperopt_options(hyperopt_cmd) # Required json-schema for user specified config From d8d46890b396a17f51099e9d29860820f7f40af8 Mon Sep 17 00:00:00 2001 From: kryofly Date: Fri, 12 Jan 2018 10:55:49 +0100 Subject: [PATCH 09/27] script: plot profit --- docs/plotting.md | 30 ++++++++ scripts/plot_profit.py | 158 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 188 insertions(+) create mode 100755 scripts/plot_profit.py diff --git a/docs/plotting.md b/docs/plotting.md index 56b44400c..f0df496ac 100644 --- a/docs/plotting.md +++ b/docs/plotting.md @@ -3,6 +3,7 @@ This page explains how to plot prices, indicator, profits. ## Table of Contents - [Plot price and indicators](#plot-price-and-indicators) +- [Plot profit](#plot-profit) ## Plot price and indicators Usage for the price plotter: @@ -16,3 +17,32 @@ python script/plot_dataframe.py -p BTC_ETH The -p pair argument, can be used to specify what pair you would like to plot. + +## Plot profit + +The profit plotter show a picture with three plots: +1) Average closing price for all pairs +2) The summarized profit made by backtesting. + Note that this is not the real-world profit, but + more of an estimate. +3) Each pair individually profit + +The first graph is good to get a grip of how the overall market +progresses. + +The second graph will show how you algorithm works or doesnt. +Perhaps you want an algorithm that steadily makes small profits, +or one that acts less seldom, but makes big swings. + +The third graph can be useful to spot outliers, events in pairs +that makes profit spikes. + +Usage for the profit plotter: +script/plot_profit.py [-h] [-p pair] [--datadir directory] [--ticker_interval num] + +The -p pair argument, can be used to plot a single pair + +Example +``` +python python scripts/plot_profit.py --datadir ../freqtrade/freqtrade/tests/testdata-20171221/ -p BTC_LTC +``` diff --git a/scripts/plot_profit.py b/scripts/plot_profit.py new file mode 100755 index 000000000..ad8346455 --- /dev/null +++ b/scripts/plot_profit.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python3 + +import sys +import argparse +import json +import matplotlib.pyplot as plt +import numpy as np + +import freqtrade.optimize as optimize +import freqtrade.misc as misc +import freqtrade.exchange as exchange +import freqtrade.analyze as analyze + + +def plot_parse_args(args ): + parser = misc.common_args_parser('Graph utility') + # FIX: perhaps delete those backtesting options that are not feasible + misc.backtesting_options(parser) + # TODO: Make the pair argument take a comma separated list + parser.add_argument( + '-p', '--pair', + help = 'Show profits for only this pair', + dest = 'pair', + default = None + ) + + return parser.parse_args(args) + + +def make_profit_array(data, filter_pair): + xmin = 0 + xmax = 0 + + # pair profit-% time duration + # ['BTC_XMR', 0.00537847, 5057, 1] + for trade in data: + pair = trade[0] + profit = trade[1] + x = trade[2] + dur = trade[3] + xmax = max(xmax, x + dur) + + pg = np.zeros(xmax) + + # Go through the trades + # and make an total profit + # array + for trade in data: + pair = trade[0] + if filter_pair and pair != filter_pair: + continue + profit = trade[1] + tim = trade[2] + dur = trade[3] + pg[tim+dur-1] += profit + + # rewrite the pg array to go from + # total profits at each timeframe + # to accumulated profits + pa = 0 + for x in range(0,len(pg)): + p = pg[x] # Get current total percent + pa += p # Add to the accumulated percent + pg[x] = pa # write back to save memory + + return pg + + +def plot_profit(args) -> None: + """ + Plots the total profit for all pairs. + Note, the profit calculation isn't realistic. + But should be somewhat proportional, and therefor useful + in helping out to find a good algorithm. + """ + + # We need to use the same pairs, same tick_interval + # and same timeperiod as used in backtesting + # to match the tickerdata against the profits-results + + filter_pair = args.pair + + config = misc.load_config(args.config) + pairs = config['exchange']['pair_whitelist'] + if filter_pair: + print('Filtering out pair %s' % filter_pair) + pairs = list(filter(lambda pair: pair == filter_pair, pairs)) + + tickers = optimize.load_data(args.datadir, pairs=pairs, + ticker_interval=args.ticker_interval, + refresh_pairs=False) + dataframes = optimize.preprocess(tickers) + + # Make an average close price of all the pairs that was involved. + # this could be useful to gauge the overall market trend + + # FIX: since the dataframes are of unequal length, + # andor has different dates, we need to merge them + # But we dont have the date information in the + # backtesting results, this is needed to match the dates + # For now, assume the dataframes are aligned. + + # We are essentially saying: + # array <- sum dataframes[*]['close'] / num_items dataframes + # FIX: there should be some onliner numpy/panda for this + + first = True + avgclose = None + num = 0 + for pair, pair_data in dataframes.items(): + close = pair_data['close'] + print('Pair %s has length %s' %(pair, len(close))) + num += 1 + if first: + first = False + avgclose = np.copy(close) + else: + avgclose += close + avgclose /= num + + # Load the profits results + # And make an profits-growth array + + filename = 'backtest-result.json' + with open(filename) as file: + data = json.load(file) + pg = make_profit_array(data, filter_pair) + + # + # Plot the pairs average close prices, and total profit growth + # + + fig, (ax1, ax2, ax3) = plt.subplots(3, sharex=True) + fig.suptitle('total profit') + ax1.plot(avgclose, label='avgclose') + ax2.plot(pg, label='profit') + ax1.legend() + ax2.legend() + + # FIX if we have one line pair in paris + # then skip the plotting of the third graph, + # or change what we plot + # In third graph, we plot each profit separately + for pair in pairs: + pg = make_profit_array(data, pair) + ax3.plot(pg, label=pair) + ax3.legend() + + # Fine-tune figure; make subplots close to each other and hide x ticks for + # all but bottom plot. + fig.subplots_adjust(hspace=0) + plt.setp([a.get_xticklabels() for a in fig.axes[:-1]], visible=False) + plt.show() + + +if __name__ == '__main__': + args = plot_parse_args(sys.argv[1:]) + plot_profit(args) From 167483f7771c87767501e63279e701491426c829 Mon Sep 17 00:00:00 2001 From: kryofly Date: Fri, 12 Jan 2018 19:18:31 +0100 Subject: [PATCH 10/27] plot profit: filter multiple pairs, misc fixes --- docs/plotting.md | 2 +- freqtrade/misc.py | 2 +- scripts/plot_profit.py | 75 ++++++++++++++++++++---------------------- 3 files changed, 37 insertions(+), 42 deletions(-) diff --git a/docs/plotting.md b/docs/plotting.md index f0df496ac..598443e12 100644 --- a/docs/plotting.md +++ b/docs/plotting.md @@ -11,7 +11,7 @@ script/plot_dataframe.py [-h] [-p pair] Example ``` -python script/plot_dataframe.py -p BTC_ETH +python script/plot_dataframe.py -p BTC_ETH,BTC_LTC ``` The -p pair argument, can be used to specify what diff --git a/freqtrade/misc.py b/freqtrade/misc.py index 92fe39ef2..97e885e01 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -111,7 +111,7 @@ def common_args_parser(description: str): metavar='PATH', ) parser.add_argument( - '-dd', '--datadir', + '--datadir', help='path to backtest data (default freqdata/tests/testdata', dest='datadir', default=os.path.join('freqtrade', 'tests', 'testdata'), diff --git a/scripts/plot_profit.py b/scripts/plot_profit.py index ad8346455..a61335318 100755 --- a/scripts/plot_profit.py +++ b/scripts/plot_profit.py @@ -14,40 +14,27 @@ import freqtrade.analyze as analyze def plot_parse_args(args ): parser = misc.common_args_parser('Graph utility') - # FIX: perhaps delete those backtesting options that are not feasible + # FIX: perhaps delete those backtesting options that are not feasible (shows up in -h) misc.backtesting_options(parser) - # TODO: Make the pair argument take a comma separated list parser.add_argument( '-p', '--pair', - help = 'Show profits for only this pair', + help = 'Show profits for only this pairs. Pairs are comma-separated.', dest = 'pair', default = None ) - return parser.parse_args(args) -def make_profit_array(data, filter_pair): - xmin = 0 - xmax = 0 - - # pair profit-% time duration - # ['BTC_XMR', 0.00537847, 5057, 1] - for trade in data: - pair = trade[0] - profit = trade[1] - x = trade[2] - dur = trade[3] - xmax = max(xmax, x + dur) - - pg = np.zeros(xmax) - +# data:: [ pair, profit-%, time, duration] +# data:: ['BTC_XMR', 0.00537847, 5057, 1] +def make_profit_array(data, px, filter_pairs=[]): + pg = np.zeros(px) # Go through the trades # and make an total profit # array for trade in data: pair = trade[0] - if filter_pair and pair != filter_pair: + if filter_pairs and pair not in filter_pairs: continue profit = trade[1] tim = trade[2] @@ -78,13 +65,14 @@ def plot_profit(args) -> None: # and same timeperiod as used in backtesting # to match the tickerdata against the profits-results - filter_pair = args.pair + filter_pairs = args.pair config = misc.load_config(args.config) pairs = config['exchange']['pair_whitelist'] - if filter_pair: - print('Filtering out pair %s' % filter_pair) - pairs = list(filter(lambda pair: pair == filter_pair, pairs)) + if filter_pairs: + filter_pairs = filter_pairs.split(',') + pairs = list(set(pairs) & set(filter_pairs)) + print('Filter, keep pairs %s' % pairs) tickers = optimize.load_data(args.datadir, pairs=pairs, ticker_interval=args.ticker_interval, @@ -99,23 +87,28 @@ def plot_profit(args) -> None: # But we dont have the date information in the # backtesting results, this is needed to match the dates # For now, assume the dataframes are aligned. + max_x = 0 + for pair, pair_data in dataframes.items(): + n = len(pair_data['close']) + max_x = max(max_x, n) + # if max_x != n: + # raise Exception('Please rerun script. Input data has different lengths %s' + # %('Different pair length: %s <=> %s' %(max_x, n))) + print('max_x: %s' %(max_x)) # We are essentially saying: # array <- sum dataframes[*]['close'] / num_items dataframes # FIX: there should be some onliner numpy/panda for this - - first = True - avgclose = None + avgclose = np.zeros(max_x) num = 0 for pair, pair_data in dataframes.items(): - close = pair_data['close'] - print('Pair %s has length %s' %(pair, len(close))) - num += 1 - if first: - first = False - avgclose = np.copy(close) - else: - avgclose += close + close = pair_data['close'] + maxprice = max(close) # Normalize price to [0,1] + print('Pair %s has length %s' %(pair, len(close))) + for x in range(0, len(close)): + avgclose[x] += close[x] / maxprice + # avgclose += close + num += 1 avgclose /= num # Load the profits results @@ -124,7 +117,7 @@ def plot_profit(args) -> None: filename = 'backtest-result.json' with open(filename) as file: data = json.load(file) - pg = make_profit_array(data, filter_pair) + pg = make_profit_array(data, max_x, filter_pairs) # # Plot the pairs average close prices, and total profit growth @@ -134,17 +127,19 @@ def plot_profit(args) -> None: fig.suptitle('total profit') ax1.plot(avgclose, label='avgclose') ax2.plot(pg, label='profit') - ax1.legend() - ax2.legend() + ax1.legend(loc='upper left') + ax2.legend(loc='upper left') # FIX if we have one line pair in paris # then skip the plotting of the third graph, # or change what we plot # In third graph, we plot each profit separately for pair in pairs: - pg = make_profit_array(data, pair) + pg = make_profit_array(data, max_x, pair) ax3.plot(pg, label=pair) - ax3.legend() + ax3.legend(loc='upper left') + # black background to easier see multiple colors + ax3.set_facecolor('black') # Fine-tune figure; make subplots close to each other and hide x ticks for # all but bottom plot. From 48432abff1cb7e9316e4e944c9f71a68611c4dc3 Mon Sep 17 00:00:00 2001 From: kryofly Date: Fri, 12 Jan 2018 19:48:52 +0100 Subject: [PATCH 11/27] remove two-letter options --- freqtrade/misc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/misc.py b/freqtrade/misc.py index 8dceee5c3..47b1705b3 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -127,7 +127,7 @@ def parse_args(args: List[str], description: str): dest='dry_run_db', ) parser.add_argument( - '-dd', '--datadir', + '--datadir', help='path to backtest data (default freqdata/tests/testdata', dest='datadir', default=os.path.join('freqtrade', 'tests', 'testdata'), @@ -186,7 +186,7 @@ def build_subcommands(parser: argparse.ArgumentParser) -> None: dest='refresh_pairs', ) backtesting_cmd.add_argument( - '-tp', '--timeperiod', + '--timeperiod', help='Use the last N ticks of data.', default=None, type=int, From d4008374f622dfa2c31ab131f25cb605d42f7182 Mon Sep 17 00:00:00 2001 From: kryofly Date: Fri, 12 Jan 2018 21:28:56 +0100 Subject: [PATCH 12/27] 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 524899ccbf8629b7db97920a5386201ebd0ad76c Mon Sep 17 00:00:00 2001 From: kryofly Date: Fri, 12 Jan 2018 22:15:50 +0100 Subject: [PATCH 13/27] plot profit: export format change --- scripts/plot_profit.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/scripts/plot_profit.py b/scripts/plot_profit.py index a61335318..e8bdbee5c 100755 --- a/scripts/plot_profit.py +++ b/scripts/plot_profit.py @@ -25,8 +25,10 @@ def plot_parse_args(args ): return parser.parse_args(args) -# data:: [ pair, profit-%, time, duration] -# data:: ['BTC_XMR', 0.00537847, 5057, 1] +# data:: [ pair, profit-%, enter, exit, time, duration] +# data:: ['BTC_XMR', 0.00537847, '1511176800', '1511178000', 5057, 1] +# FIX: make use of the enter/exit dates to insert the +# profit more precisely into the pg array def make_profit_array(data, px, filter_pairs=[]): pg = np.zeros(px) # Go through the trades @@ -37,8 +39,8 @@ def make_profit_array(data, px, filter_pairs=[]): if filter_pairs and pair not in filter_pairs: continue profit = trade[1] - tim = trade[2] - dur = trade[3] + tim = trade[4] + dur = trade[5] pg[tim+dur-1] += profit # rewrite the pg array to go from From 71bb348698e5f7de928ed6232fbae55fca789a73 Mon Sep 17 00:00:00 2001 From: kryofly Date: Mon, 15 Jan 2018 21:49:06 +0100 Subject: [PATCH 14/27] rename --timeperiod to --timerange --- docs/backtesting.md | 4 ++-- freqtrade/misc.py | 8 ++++---- freqtrade/optimize/__init__.py | 6 +++--- freqtrade/optimize/backtesting.py | 2 +- freqtrade/optimize/hyperopt.py | 2 +- freqtrade/tests/optimize/test_backtesting.py | 2 +- freqtrade/tests/optimize/test_optimize.py | 2 +- 7 files changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 800420ea6..7984e67bc 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -52,12 +52,12 @@ python3 ./freqtrade/main.py backtesting --datadir freqtrade/tests/testdata-20180 ``` **Running backtest with smaller testset** -Use the --timeperiod argument to change how much of the testset +Use the --timerange argument to change how much of the testset you want to use. The last N ticks/timeframes will be used. Example: ```bash -python3 ./freqtrade/main.py backtesting --timeperiod -200 +python3 ./freqtrade/main.py backtesting --timerange -200 ``` **Update testdata directory diff --git a/freqtrade/misc.py b/freqtrade/misc.py index 9c2bce84f..c019509d6 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -191,11 +191,11 @@ def build_subcommands(parser: argparse.ArgumentParser) -> None: dest='refresh_pairs', ) backtesting_cmd.add_argument( - '--timeperiod', + '--timerange', help='Use the last N ticks of data.', default=None, type=int, - dest='timeperiod', + dest='timerange', ) # Add hyperopt subcommand @@ -224,11 +224,11 @@ def build_subcommands(parser: argparse.ArgumentParser) -> None: metavar='INT', ) hyperopt_cmd.add_argument( - '-tp', '--timeperiod', + '-tp', '--timerange', help='Use the last N ticks of data.', default=None, type=int, - dest='timeperiod', + dest='timerange', ) diff --git a/freqtrade/optimize/__init__.py b/freqtrade/optimize/__init__.py index 3f6a98ac1..54d339b73 100644 --- a/freqtrade/optimize/__init__.py +++ b/freqtrade/optimize/__init__.py @@ -68,9 +68,9 @@ def load_data(datadir: str, ticker_interval: int = 5, pairs: Optional[List[str]] return result -def tickerdata_to_dataframe(data, timeperiod=None): - if timeperiod: - data = trim_tickerlist(data, timeperiod) +def tickerdata_to_dataframe(data, timerange=None): + if timerange: + data = trim_tickerlist(data, timerange) preprocessed = preprocess(data) return preprocessed diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index afcd0c539..5b8afc267 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -175,7 +175,7 @@ def start(args): from freqtrade import main main._CONF = config - preprocessed = optimize.tickerdata_to_dataframe(data, timeperiod=args.timeperiod) + preprocessed = optimize.tickerdata_to_dataframe(data, timerange=args.timerange) # Print timeframe min_date, max_date = get_timeframe(preprocessed) logger.info('Measuring data from %s up to %s ...', min_date.isoformat(), max_date.isoformat()) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 15019e16b..abba7c35f 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -260,7 +260,7 @@ def start(args): config = load_config(args.config) pairs = config['exchange']['pair_whitelist'] data = optimize.load_data(args.datadir, pairs=pairs, ticker_interval=args.ticker_interval) - PROCESSED = optimize.tickerdata_to_dataframe(data, timeperiod=args.timeperiod) + PROCESSED = optimize.tickerdata_to_dataframe(data, timerange=args.timerange) if args.mongodb: logger.info('Using mongodb ...') diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index d4172d983..c6bcdd713 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -159,7 +159,7 @@ def test_backtest_start(default_conf, mocker, caplog): args.level = 10 args.live = False args.datadir = None - args.timeperiod = None # needed due to MagicMock malleability + args.timerange = None # needed due to MagicMock malleability backtesting.start(args) # check the logs, that will contain the backtest result exists = ['Using max_open_trades: 1 ...', diff --git a/freqtrade/tests/optimize/test_optimize.py b/freqtrade/tests/optimize/test_optimize.py index 6decb1414..bfbc99745 100644 --- a/freqtrade/tests/optimize/test_optimize.py +++ b/freqtrade/tests/optimize/test_optimize.py @@ -179,5 +179,5 @@ def test_load_tickerdata_file(): def test_tickerdata_to_dataframe(): tick = load_tickerdata_file(None, 'BTC_UNITEST', 1) tickerlist = {'BTC_UNITEST': tick} - data = optimize.tickerdata_to_dataframe(tickerlist, timeperiod=-100) + data = optimize.tickerdata_to_dataframe(tickerlist, timerange=-100) assert 100 == len(data['BTC_UNITEST']) From 0e58ab7e012a684d9d2aefa4e15aefe6e27153b2 Mon Sep 17 00:00:00 2001 From: kryofly Date: Mon, 15 Jan 2018 22:25:02 +0100 Subject: [PATCH 15/27] more advanced use of --timerange --- docs/backtesting.md | 15 ++++++- freqtrade/misc.py | 41 +++++++++++++++++--- freqtrade/optimize/__init__.py | 34 +++++++++------- freqtrade/optimize/backtesting.py | 7 ++-- freqtrade/optimize/hyperopt.py | 9 +++-- freqtrade/tests/optimize/test_backtesting.py | 14 +++---- freqtrade/tests/optimize/test_hyperopt.py | 15 ++++--- freqtrade/tests/optimize/test_optimize.py | 5 ++- freqtrade/tests/test_misc.py | 9 ++++- 9 files changed, 109 insertions(+), 40 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 7984e67bc..9829a10c8 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -57,9 +57,22 @@ you want to use. The last N ticks/timeframes will be used. Example: ```bash -python3 ./freqtrade/main.py backtesting --timerange -200 +python3 ./freqtrade/main.py backtesting --timerange=-200 ``` +***Advanced use of timerange*** + Doing --timerange=-200 will get the last 200 timeframes + from your inputdata. You can also specify specific dates, + or a range span indexed by start and stop. + The full timerange specification: + Not implemented yet! --timerange=-20180131 + Not implemented yet! --timerange=20180101- + Not implemented yet! --timerange=20180101-20181231 + Last 123 tickframes of data: --timerange=-123 + First 123 tickframes of data: --timerange=123- + Tickframes from line 123 through 456: --timerange=123-456 + + **Update testdata directory To update your testdata directory, or download into another testdata directory: ```bash diff --git a/freqtrade/misc.py b/freqtrade/misc.py index c019509d6..a9aeee80e 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -4,6 +4,7 @@ import json import logging import time import os +import re from typing import Any, Callable, Dict, List from jsonschema import Draft4Validator, validate @@ -192,9 +193,9 @@ def build_subcommands(parser: argparse.ArgumentParser) -> None: ) backtesting_cmd.add_argument( '--timerange', - help='Use the last N ticks of data.', + help='Specify what timerange of data to use.', default=None, - type=int, + type=str, dest='timerange', ) @@ -224,14 +225,44 @@ def build_subcommands(parser: argparse.ArgumentParser) -> None: metavar='INT', ) hyperopt_cmd.add_argument( - '-tp', '--timerange', - help='Use the last N ticks of data.', + '--timerange', + help='Specify what timerange of data to use.', default=None, - type=int, + type=str, dest='timerange', ) +def parse_timerange(text): + if text is None: + return None + syntax = [('^-(\d{8})$', (None, 'date')), + ('^(\d{8})-$', ('date', None)), + ('^(\d{8})-(\d{8})$', ('date', 'date')), + ('^(-\d+)$', (None, 'line')), + ('^(\d+)-$', ('line', None)), + ('^(\d+)-(\d+)$', ('index', 'index'))] + for rex, stype in syntax: + # Apply the regular expression to text + m = re.match(rex, text) + if m: # Regex has matched + rvals = m.groups() + n = 0 + start = None + stop = None + if stype[0]: + start = rvals[n] + if stype[0] != 'date': + start = int(start) + n += 1 + if stype[1]: + stop = rvals[n] + if stype[1] != 'date': + stop = int(stop) + return (stype, start, stop) + raise Exception('Incorrect syntax for timerange "%s"' % text) + + # Required json-schema for user specified config CONF_SCHEMA = { 'type': 'object', diff --git a/freqtrade/optimize/__init__.py b/freqtrade/optimize/__init__.py index 54d339b73..40269db46 100644 --- a/freqtrade/optimize/__init__.py +++ b/freqtrade/optimize/__init__.py @@ -12,14 +12,20 @@ from freqtrade.analyze import populate_indicators, parse_ticker_dataframe logger = logging.getLogger(__name__) -def trim_tickerlist(dl, num): - new = {} - for pair, pair_data in dl.items(): - new[pair] = pair_data[num:] - return new +def trim_tickerlist(tickerlist, timerange): + (stype, start, stop) = timerange + if stype == (None, 'line'): + return tickerlist[stop:] + elif stype == ('line', None): + return tickerlist[0:start] + elif stype == ('index', 'index'): + return tickerlist[start:stop] + else: + return tickerlist -def load_tickerdata_file(datadir, pair, ticker_interval): +def load_tickerdata_file(datadir, pair, ticker_interval, + timerange=None): """ Load a pair from file, :return dict OR empty if unsuccesful @@ -37,11 +43,15 @@ def load_tickerdata_file(datadir, pair, ticker_interval): # Read the file, load the json with open(file) as tickerdata: pairdata = json.load(tickerdata) + if timerange: + pairdata = trim_tickerlist(pairdata, timerange) return pairdata -def load_data(datadir: str, ticker_interval: int = 5, pairs: Optional[List[str]] = None, - refresh_pairs: Optional[bool] = False) -> Dict[str, List]: +def load_data(datadir: str, ticker_interval: int = 5, + pairs: Optional[List[str]] = None, + refresh_pairs: Optional[bool] = False, + timerange=None) -> Dict[str, List]: """ Loads ticker history data for the given parameters :param ticker_interval: ticker interval in minutes @@ -58,19 +68,17 @@ def load_data(datadir: str, ticker_interval: int = 5, pairs: Optional[List[str]] download_pairs(datadir, _pairs) for pair in _pairs: - pairdata = load_tickerdata_file(datadir, pair, ticker_interval) + pairdata = load_tickerdata_file(datadir, pair, ticker_interval, timerange=timerange) if not pairdata: # download the tickerdata from exchange download_backtesting_testdata(datadir, pair=pair, interval=ticker_interval) # and retry reading the pair - pairdata = load_tickerdata_file(datadir, pair, ticker_interval) + pairdata = load_tickerdata_file(datadir, pair, ticker_interval, timerange=timerange) result[pair] = pairdata return result -def tickerdata_to_dataframe(data, timerange=None): - if timerange: - data = trim_tickerlist(data, timerange) +def tickerdata_to_dataframe(data): preprocessed = preprocess(data) return preprocessed diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 5b8afc267..4f3d4bb24 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -163,9 +163,10 @@ def start(args): logger.info('Using stake_currency: %s ...', config['stake_currency']) logger.info('Using stake_amount: %s ...', config['stake_amount']) + timerange = misc.parse_timerange(args.timerange) data = optimize.load_data(args.datadir, pairs=pairs, ticker_interval=args.ticker_interval, - refresh_pairs=args.refresh_pairs) - + refresh_pairs=args.refresh_pairs, + timerange=timerange) max_open_trades = 0 if args.realistic_simulation: logger.info('Using max_open_trades: %s ...', config['max_open_trades']) @@ -175,7 +176,7 @@ def start(args): from freqtrade import main main._CONF = config - preprocessed = optimize.tickerdata_to_dataframe(data, timerange=args.timerange) + preprocessed = optimize.tickerdata_to_dataframe(data) # Print timeframe min_date, max_date = get_timeframe(preprocessed) logger.info('Measuring data from %s up to %s ...', min_date.isoformat(), max_date.isoformat()) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index abba7c35f..7a1744dc0 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -15,7 +15,7 @@ from hyperopt import STATUS_FAIL, STATUS_OK, Trials, fmin, hp, space_eval, tpe from hyperopt.mongoexp import MongoTrials from pandas import DataFrame -from freqtrade import main # noqa +from freqtrade import main, misc # noqa from freqtrade import exchange, optimize from freqtrade.exchange import Bittrex from freqtrade.misc import load_config @@ -259,8 +259,11 @@ def start(args): logger.info('Using config: %s ...', args.config) config = load_config(args.config) pairs = config['exchange']['pair_whitelist'] - data = optimize.load_data(args.datadir, pairs=pairs, ticker_interval=args.ticker_interval) - PROCESSED = optimize.tickerdata_to_dataframe(data, timerange=args.timerange) + timerange = misc.parse_timerange(args.timerange) + data = optimize.load_data(args.datadir, pairs=pairs, + ticker_interval=args.ticker_interval, + timerange=timerange) + PROCESSED = optimize.tickerdata_to_dataframe(data) if args.mongodb: logger.info('Using mongodb ...') diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index c6bcdd713..92170a184 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -6,7 +6,7 @@ import pandas as pd from unittest.mock import MagicMock from freqtrade import exchange, optimize from freqtrade.exchange import Bittrex -from freqtrade.optimize import preprocess, trim_tickerlist +from freqtrade.optimize import preprocess from freqtrade.optimize.backtesting import backtest, generate_text_table, get_timeframe import freqtrade.optimize.backtesting as backtesting @@ -60,8 +60,8 @@ def test_backtest_1min_ticker_interval(default_conf, mocker): def load_data_test(what): - data = optimize.load_data(None, ticker_interval=1, pairs=['BTC_UNITEST']) - data = trim_tickerlist(data, -100) + timerange = ((None, 'line'), None, -100) + data = optimize.load_data(None, ticker_interval=1, pairs=['BTC_UNITEST'], timerange=timerange) pair = data['BTC_UNITEST'] datalen = len(pair) # Depending on the what parameter we now adjust the @@ -142,10 +142,10 @@ def test_backtest_pricecontours(default_conf, mocker): simple_backtest(default_conf, contour, numres) -def mocked_load_data(datadir, pairs=[], ticker_interval=0, refresh_pairs=False): - tickerdata = optimize.load_tickerdata_file(datadir, 'BTC_UNITEST', 1) +def mocked_load_data(datadir, pairs=[], ticker_interval=0, refresh_pairs=False, timerange=None): + tickerdata = optimize.load_tickerdata_file(datadir, 'BTC_UNITEST', 1, timerange=timerange) pairdata = {'BTC_UNITEST': tickerdata} - return trim_tickerlist(pairdata, -100) + return pairdata def test_backtest_start(default_conf, mocker, caplog): @@ -159,7 +159,7 @@ def test_backtest_start(default_conf, mocker, caplog): args.level = 10 args.live = False args.datadir = None - args.timerange = None # needed due to MagicMock malleability + args.timerange = '-100' # needed due to MagicMock malleability backtesting.start(args) # check the logs, that will contain the backtest result exists = ['Using max_open_trades: 1 ...', diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index ac7b198e4..1c4cceee5 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -62,7 +62,8 @@ def test_start_calls_fmin(mocker): mocker.patch('freqtrade.optimize.load_data') mock_fmin = mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value={}) - args = mocker.Mock(epochs=1, config='config.json.example', mongodb=False) + args = mocker.Mock(epochs=1, config='config.json.example', mongodb=False, + timerange=None) start(args) mock_fmin.assert_called_once() @@ -75,7 +76,8 @@ def test_start_uses_mongotrials(mocker): mocker.patch('freqtrade.optimize.load_data') mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value={}) - args = mocker.Mock(epochs=1, config='config.json.example', mongodb=True) + args = mocker.Mock(epochs=1, config='config.json.example', mongodb=True, + timerange=None) start(args) mock_mongotrials.assert_called_once() @@ -129,7 +131,8 @@ def test_fmin_best_results(mocker, caplog): mocker.patch('freqtrade.optimize.load_data') mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value=fmin_result) - args = mocker.Mock(epochs=1, config='config.json.example') + args = mocker.Mock(epochs=1, config='config.json.example', + timerange=None) start(args) exists = [ @@ -151,7 +154,8 @@ def test_fmin_throw_value_error(mocker, caplog): mocker.patch('freqtrade.optimize.load_data') mocker.patch('freqtrade.optimize.hyperopt.fmin', side_effect=ValueError()) - args = mocker.Mock(epochs=1, config='config.json.example') + args = mocker.Mock(epochs=1, config='config.json.example', + timerange=None) start(args) exists = [ @@ -185,7 +189,8 @@ def test_resuming_previous_hyperopt_results_succeeds(mocker): return_value={}) args = mocker.Mock(epochs=1, config='config.json.example', - mongodb=False) + mongodb=False, + timerange=None) start(args) diff --git a/freqtrade/tests/optimize/test_optimize.py b/freqtrade/tests/optimize/test_optimize.py index bfbc99745..61325797b 100644 --- a/freqtrade/tests/optimize/test_optimize.py +++ b/freqtrade/tests/optimize/test_optimize.py @@ -177,7 +177,8 @@ def test_load_tickerdata_file(): def test_tickerdata_to_dataframe(): - tick = load_tickerdata_file(None, 'BTC_UNITEST', 1) + timerange = ((None, 'line'), None, -100) + tick = load_tickerdata_file(None, 'BTC_UNITEST', 1, timerange=timerange) tickerlist = {'BTC_UNITEST': tick} - data = optimize.tickerdata_to_dataframe(tickerlist, timerange=-100) + data = optimize.tickerdata_to_dataframe(tickerlist) assert 100 == len(data['BTC_UNITEST']) diff --git a/freqtrade/tests/test_misc.py b/freqtrade/tests/test_misc.py index 63cfba627..74f611f5f 100644 --- a/freqtrade/tests/test_misc.py +++ b/freqtrade/tests/test_misc.py @@ -8,7 +8,7 @@ import pytest from jsonschema import ValidationError from freqtrade.misc import (common_args_parser, load_config, parse_args, - throttle) + throttle, parse_timerange) def test_throttle(): @@ -133,6 +133,13 @@ def test_parse_args_hyperopt_custom(mocker): assert call_args.func is not None +def test_parse_timerange_incorrect(): + assert ((None, 'line'), None, -200) == parse_timerange('-200') + assert (('line', None), 200, None) == parse_timerange('200-') + with pytest.raises(Exception, match=r'Incorrect syntax.*'): + parse_timerange('-') + + def test_load_config(default_conf, mocker): file_mock = mocker.patch('freqtrade.misc.open', mocker.mock_open( read_data=json.dumps(default_conf) From 01e10014bb07d53d4c19b1406cb9f3bce6508909 Mon Sep 17 00:00:00 2001 From: Stephen Dade Date: Tue, 16 Jan 2018 21:55:15 +1100 Subject: [PATCH 16/27] Order timeouts - added exception catching and rpc messaging --- freqtrade/main.py | 15 ++++++++++++++- freqtrade/tests/test_main.py | 12 +++++++++--- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/freqtrade/main.py b/freqtrade/main.py index c404d6c11..796fa8515 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -129,9 +129,16 @@ def check_handle_timedout(timeoutvalue: int) -> None: timeoutthreashold = arrow.utcnow().shift(minutes=-timeoutvalue).datetime for trade in Trade.query.filter(Trade.open_order_id.isnot(None)).all(): - order = exchange.get_order(trade.open_order_id) + try: + order = exchange.get_order(trade.open_order_id) + except OperationalException: + continue ordertime = arrow.get(order['opened']) + # Check if trade is still actually open + if int(order['remaining']) == 0: + continue + if order['type'] == "LIMIT_BUY" and ordertime < timeoutthreashold: # Buy timeout - cancel order exchange.cancel_order(trade.open_order_id) @@ -140,6 +147,8 @@ def check_handle_timedout(timeoutvalue: int) -> None: Trade.session.delete(trade) Trade.session.flush() logger.info('Buy order timeout for %s.', trade) + rpc.send_msg('*Timeout:* Unfilled buy order for {} cancelled'.format( + trade.pair.replace('_', '/'))) else: # if trade is partially complete, edit the stake details for the trade # and close the order @@ -147,6 +156,8 @@ def check_handle_timedout(timeoutvalue: int) -> None: trade.stake_amount = trade.amount * trade.open_rate trade.open_order_id = None logger.info('Partial buy order timeout for %s.', trade) + rpc.send_msg('*Timeout:* Remaining buy order for {} cancelled'.format( + trade.pair.replace('_', '/'))) elif order['type'] == "LIMIT_SELL" and ordertime < timeoutthreashold: # Sell timeout - cancel order and update trade if order['remaining'] == order['amount']: @@ -157,6 +168,8 @@ def check_handle_timedout(timeoutvalue: int) -> None: trade.close_date = None trade.is_open = True trade.open_order_id = None + rpc.send_msg('*Timeout:* Unfilled sell order for {} cancelled'.format( + trade.pair.replace('_', '/'))) logger.info('Sell order timeout for %s.', trade) return True else: diff --git a/freqtrade/tests/test_main.py b/freqtrade/tests/test_main.py index 97bef2257..95eb0ad82 100644 --- a/freqtrade/tests/test_main.py +++ b/freqtrade/tests/test_main.py @@ -355,7 +355,8 @@ def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, mo def test_check_handle_timedout_buy(default_conf, ticker, limit_buy_order_old, mocker): mocker.patch.dict('freqtrade.main._CONF', default_conf) cancel_order_mock = MagicMock() - mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock()) + mocker.patch('freqtrade.rpc.init', MagicMock()) + rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock()) mocker.patch.multiple('freqtrade.main.exchange', validate_pairs=MagicMock(), get_ticker=ticker, @@ -380,6 +381,7 @@ def test_check_handle_timedout_buy(default_conf, ticker, limit_buy_order_old, mo # check it does cancel buy orders over the time limit check_handle_timedout(600) assert cancel_order_mock.call_count == 1 + assert rpc_mock.call_count == 1 trades = Trade.query.filter(Trade.open_order_id.is_(trade_buy.open_order_id)).all() assert len(trades) == 0 @@ -387,7 +389,8 @@ def test_check_handle_timedout_buy(default_conf, ticker, limit_buy_order_old, mo def test_check_handle_timedout_sell(default_conf, ticker, limit_sell_order_old, mocker): mocker.patch.dict('freqtrade.main._CONF', default_conf) cancel_order_mock = MagicMock() - mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock()) + mocker.patch('freqtrade.rpc.init', MagicMock()) + rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock()) mocker.patch.multiple('freqtrade.main.exchange', validate_pairs=MagicMock(), get_ticker=ticker, @@ -413,6 +416,7 @@ def test_check_handle_timedout_sell(default_conf, ticker, limit_sell_order_old, # check it does cancel sell orders over the time limit check_handle_timedout(600) assert cancel_order_mock.call_count == 1 + assert rpc_mock.call_count == 1 assert trade_sell.is_open is True @@ -420,7 +424,8 @@ def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old mocker): mocker.patch.dict('freqtrade.main._CONF', default_conf) cancel_order_mock = MagicMock() - mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock()) + mocker.patch('freqtrade.rpc.init', MagicMock()) + rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock()) mocker.patch.multiple('freqtrade.main.exchange', validate_pairs=MagicMock(), get_ticker=ticker, @@ -446,6 +451,7 @@ def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old # note this is for a partially-complete buy order check_handle_timedout(600) assert cancel_order_mock.call_count == 1 + assert rpc_mock.call_count == 1 trades = Trade.query.filter(Trade.open_order_id.is_(trade_buy.open_order_id)).all() assert len(trades) == 1 assert trades[0].amount == 23.0 From 12ffbf5047ffbdb00a7c94e7b1b4e9ee44902567 Mon Sep 17 00:00:00 2001 From: toto Date: Tue, 16 Jan 2018 20:22:15 +0100 Subject: [PATCH 17/27] - get_signal to return both SELL and BUY signal - _process modified so that we do not sell if we would buy afterwards - execute_sell modified so that that min_roi_reached is not executed if we would buy afterwards Veuillez saisir le message de validation pour vos modifications. Les lignes --- freqtrade/analyze.py | 20 ++++----- freqtrade/main.py | 35 ++++++++------- freqtrade/tests/rpc/test_rpc_telegram.py | 22 ++++----- freqtrade/tests/test_main.py | 57 +++++++++++++----------- 4 files changed, 71 insertions(+), 63 deletions(-) diff --git a/freqtrade/analyze.py b/freqtrade/analyze.py index f85c46248..936af1e29 100644 --- a/freqtrade/analyze.py +++ b/freqtrade/analyze.py @@ -280,36 +280,36 @@ def analyze_ticker(ticker_history: List[Dict]) -> DataFrame: return dataframe -def get_signal(pair: str, signal: SignalType) -> bool: +def get_signal(pair: str) -> (bool, bool): """ Calculates current signal based several technical analysis indicators :param pair: pair in format BTC_ANT or BTC-ANT - :return: True if pair is good for buying, False otherwise + :return: (True, False) if pair is good for buying and not for selling """ ticker_hist = get_ticker_history(pair) if not ticker_hist: logger.warning('Empty ticker history for pair %s', pair) - return False + return (False, False) try: dataframe = analyze_ticker(ticker_hist) except ValueError as ex: logger.warning('Unable to analyze ticker for pair %s: %s', pair, str(ex)) - return False + return (False, False) except Exception as ex: logger.exception('Unexpected error when analyzing ticker for pair %s: %s', pair, str(ex)) - return False + return (False, False) if dataframe.empty: - return False + return (False, False) latest = dataframe.iloc[-1] # Check if dataframe is out of date signal_date = arrow.get(latest['date']) if signal_date < arrow.now() - timedelta(minutes=10): - return False + return (False, False) - result = latest[signal.value] == 1 - logger.debug('%s_trigger: %s (pair=%s, signal=%s)', signal.value, latest['date'], pair, result) - return result + (buy, sell) = latest[SignalType.BUY] == 1, latest[SignalType.SELL] == 1 + logger.debug('%trigger: %s (pair=%s, buy=%s sell=%s)', latest['date'], pair, buy, sell) + return (buy, sell) diff --git a/freqtrade/main.py b/freqtrade/main.py index c404d6c11..1640c7836 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -14,7 +14,7 @@ from cachetools import cached, TTLCache from freqtrade import (DependencyException, OperationalException, __version__, exchange, persistence, rpc) -from freqtrade.analyze import SignalType, get_signal +from freqtrade.analyze import get_signal from freqtrade.fiat_convert import CryptoToFiatConverter from freqtrade.misc import (State, get_state, load_config, parse_args, throttle, update_state) @@ -247,24 +247,28 @@ def handle_trade(trade: Trade) -> bool: logger.debug('Handling %s ...', trade) current_rate = exchange.get_ticker(trade.pair)['bid'] - # Check if minimal roi has been reached - if min_roi_reached(trade, current_rate, datetime.utcnow()): + (buy, sell) = (False, False) + + if _CONF.get('experimental', {}).get('use_sell_signal'): + (buy, sell) = get_signal(trade.pair) + + # Check if minimal roi has been reached and no longer in buy conditions (avoiding a fee) + if not buy and min_roi_reached(trade, current_rate, datetime.utcnow()): logger.debug('Executing sell due to ROI ...') execute_sell(trade, current_rate) return True + # Experimental: Check if the trade is profitable before selling it (avoid selling at loss) + if _CONF.get('experimental', {}).get('sell_profit_only'): + logger.debug('Checking if trade is profitable ...') + if not buy and trade.calc_profit(rate=current_rate) <= 0: + return False + # Experimental: Check if sell signal has been enabled and triggered - if _CONF.get('experimental', {}).get('use_sell_signal'): - # Experimental: Check if the trade is profitable before selling it (avoid selling at loss) - if _CONF.get('experimental', {}).get('sell_profit_only'): - logger.debug('Checking if trade is profitable ...') - if trade.calc_profit(rate=current_rate) <= 0: - return False - logger.debug('Checking sell_signal ...') - if get_signal(trade.pair, SignalType.SELL): - logger.debug('Executing sell due to sell signal ...') - execute_sell(trade, current_rate) - return True + if sell and not buy: + logger.debug('Executing sell due to sell signal ...') + execute_sell(trade, current_rate) + return True return False @@ -305,7 +309,8 @@ def create_trade(stake_amount: float) -> bool: # Pick pair based on StochRSI buy signals for _pair in whitelist: - if get_signal(_pair, SignalType.BUY): + (buy, sell) = get_signal(_pair) + if buy and not sell: pair = _pair break else: diff --git a/freqtrade/tests/rpc/test_rpc_telegram.py b/freqtrade/tests/rpc/test_rpc_telegram.py index 58bf0154b..992693248 100644 --- a/freqtrade/tests/rpc/test_rpc_telegram.py +++ b/freqtrade/tests/rpc/test_rpc_telegram.py @@ -77,7 +77,7 @@ def test_authorized_only_exception(default_conf, mocker): def test_status_handle(default_conf, update, ticker, mocker): mocker.patch.dict('freqtrade.main._CONF', default_conf) - mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True) + mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (True, False)) msg_mock = MagicMock() mocker.patch('freqtrade.main.rpc.send_msg', MagicMock()) mocker.patch.multiple('freqtrade.rpc.telegram', @@ -112,7 +112,7 @@ def test_status_handle(default_conf, update, ticker, mocker): def test_status_table_handle(default_conf, update, ticker, mocker): mocker.patch.dict('freqtrade.main._CONF', default_conf) - mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True) + mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (True, False)) msg_mock = MagicMock() mocker.patch('freqtrade.main.rpc.send_msg', MagicMock()) mocker.patch.multiple( @@ -154,7 +154,7 @@ def test_status_table_handle(default_conf, update, ticker, mocker): def test_profit_handle( default_conf, update, ticker, ticker_sell_up, limit_buy_order, limit_sell_order, mocker): mocker.patch.dict('freqtrade.main._CONF', default_conf) - mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True) + mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (True, False)) msg_mock = MagicMock() mocker.patch('freqtrade.main.rpc.send_msg', MagicMock()) mocker.patch.multiple('freqtrade.rpc.telegram', @@ -210,7 +210,7 @@ def test_profit_handle( def test_forcesell_handle(default_conf, update, ticker, ticker_sell_up, mocker): mocker.patch.dict('freqtrade.main._CONF', default_conf) - mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True) + mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (True, False)) rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock()) mocker.patch.multiple('freqtrade.rpc.telegram', _CONF=default_conf, @@ -247,7 +247,7 @@ def test_forcesell_handle(default_conf, update, ticker, ticker_sell_up, mocker): def test_forcesell_down_handle(default_conf, update, ticker, ticker_sell_down, mocker): mocker.patch.dict('freqtrade.main._CONF', default_conf) - mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True) + mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (True, False)) rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock()) mocker.patch.multiple('freqtrade.rpc.telegram', _CONF=default_conf, @@ -308,7 +308,7 @@ def test_exec_forcesell_open_orders(default_conf, ticker, mocker): def test_forcesell_all_handle(default_conf, update, ticker, mocker): mocker.patch.dict('freqtrade.main._CONF', default_conf) - mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True) + mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (True, False)) rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock()) mocker.patch.multiple('freqtrade.rpc.telegram', _CONF=default_conf, @@ -339,7 +339,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, mocker): def test_forcesell_handle_invalid(default_conf, update, mocker): mocker.patch.dict('freqtrade.main._CONF', default_conf) - mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True) + mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (True, True)) msg_mock = MagicMock() mocker.patch.multiple('freqtrade.rpc.telegram', _CONF=default_conf, @@ -376,7 +376,7 @@ def test_forcesell_handle_invalid(default_conf, update, mocker): def test_performance_handle( default_conf, update, ticker, limit_buy_order, limit_sell_order, mocker): mocker.patch.dict('freqtrade.main._CONF', default_conf) - mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True) + mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (True, False)) msg_mock = MagicMock() mocker.patch('freqtrade.main.rpc.send_msg', MagicMock()) mocker.patch.multiple('freqtrade.rpc.telegram', @@ -410,7 +410,7 @@ def test_performance_handle( def test_daily_handle( default_conf, update, ticker, limit_buy_order, limit_sell_order, mocker): mocker.patch.dict('freqtrade.main._CONF', default_conf) - mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True) + mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (True, False)) msg_mock = MagicMock() mocker.patch('freqtrade.main.rpc.send_msg', MagicMock()) mocker.patch.multiple('freqtrade.rpc.telegram', @@ -460,7 +460,7 @@ def test_daily_handle( def test_count_handle(default_conf, update, ticker, mocker): mocker.patch.dict('freqtrade.main._CONF', default_conf) - mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True) + mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (True, False)) msg_mock = MagicMock() mocker.patch.multiple( 'freqtrade.rpc.telegram', @@ -492,7 +492,7 @@ def test_count_handle(default_conf, update, ticker, mocker): def test_performance_handle_invalid(default_conf, update, mocker): mocker.patch.dict('freqtrade.main._CONF', default_conf) - mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True) + mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (True, True)) msg_mock = MagicMock() mocker.patch.multiple('freqtrade.rpc.telegram', _CONF=default_conf, diff --git a/freqtrade/tests/test_main.py b/freqtrade/tests/test_main.py index 97bef2257..c99727135 100644 --- a/freqtrade/tests/test_main.py +++ b/freqtrade/tests/test_main.py @@ -10,7 +10,6 @@ from sqlalchemy import create_engine import freqtrade.main as main from freqtrade import DependencyException, OperationalException -from freqtrade.analyze import SignalType from freqtrade.exchange import Exchanges from freqtrade.main import (_process, check_handle_timedout, create_trade, execute_sell, get_target_bid, handle_trade, init) @@ -52,7 +51,7 @@ def test_main_start_hyperopt(mocker): def test_process_trade_creation(default_conf, ticker, limit_buy_order, health, mocker): mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock()) - mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True) + mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (True, False)) mocker.patch.multiple('freqtrade.main.exchange', validate_pairs=MagicMock(), get_ticker=ticker, @@ -82,7 +81,7 @@ def test_process_trade_creation(default_conf, ticker, limit_buy_order, health, m def test_process_exchange_failures(default_conf, ticker, health, mocker): mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock()) - mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True) + mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (True, False)) sleep_mock = mocker.patch('time.sleep', side_effect=lambda _: None) mocker.patch.multiple('freqtrade.main.exchange', validate_pairs=MagicMock(), @@ -99,7 +98,7 @@ def test_process_operational_exception(default_conf, ticker, health, mocker): msg_mock = MagicMock() mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=msg_mock) - mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True) + mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (True, False)) mocker.patch.multiple('freqtrade.main.exchange', validate_pairs=MagicMock(), get_ticker=ticker, @@ -117,8 +116,7 @@ def test_process_operational_exception(default_conf, ticker, health, mocker): def test_process_trade_handling(default_conf, ticker, limit_buy_order, health, mocker): mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock()) - mocker.patch('freqtrade.main.get_signal', - side_effect=lambda *args: False if args[1] == SignalType.SELL else True) + mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (True, False)) mocker.patch.multiple('freqtrade.main.exchange', validate_pairs=MagicMock(), get_ticker=ticker, @@ -140,7 +138,7 @@ def test_process_trade_handling(default_conf, ticker, limit_buy_order, health, m def test_create_trade(default_conf, ticker, limit_buy_order, mocker): mocker.patch.dict('freqtrade.main._CONF', default_conf) - mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True) + mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (True, False)) mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock()) mocker.patch.multiple('freqtrade.main.exchange', validate_pairs=MagicMock(), @@ -171,7 +169,7 @@ def test_create_trade(default_conf, ticker, limit_buy_order, mocker): def test_create_trade_minimal_amount(default_conf, ticker, mocker): mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock()) - mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True) + mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (True, False)) buy_mock = mocker.patch( 'freqtrade.main.exchange.buy', MagicMock(return_value='mocked_limit_buy') ) @@ -187,7 +185,7 @@ def test_create_trade_minimal_amount(default_conf, ticker, mocker): def test_create_trade_no_stake_amount(default_conf, ticker, mocker): mocker.patch.dict('freqtrade.main._CONF', default_conf) - mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True) + mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (True, False)) mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock()) mocker.patch.multiple('freqtrade.main.exchange', validate_pairs=MagicMock(), @@ -200,7 +198,7 @@ def test_create_trade_no_stake_amount(default_conf, ticker, mocker): def test_create_trade_no_pairs(default_conf, ticker, mocker): mocker.patch.dict('freqtrade.main._CONF', default_conf) - mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True) + mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (True, False)) mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock()) mocker.patch.multiple('freqtrade.main.exchange', validate_pairs=MagicMock(), @@ -216,7 +214,7 @@ def test_create_trade_no_pairs(default_conf, ticker, mocker): def test_create_trade_no_pairs_after_blacklist(default_conf, ticker, mocker): mocker.patch.dict('freqtrade.main._CONF', default_conf) - mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True) + mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (True, False)) mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock()) mocker.patch.multiple('freqtrade.main.exchange', validate_pairs=MagicMock(), @@ -233,7 +231,7 @@ def test_create_trade_no_pairs_after_blacklist(default_conf, ticker, mocker): def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, mocker): mocker.patch.dict('freqtrade.main._CONF', default_conf) - mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True) + mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (True, False)) mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock()) mocker.patch.multiple('freqtrade.main.exchange', validate_pairs=MagicMock(), @@ -256,6 +254,7 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, mocker): trade.update(limit_buy_order) assert trade.is_open is True + mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (False, True)) handle_trade(trade) assert trade.open_order_id == 'mocked_limit_sell' @@ -272,7 +271,7 @@ def test_handle_trade_roi(default_conf, ticker, mocker, caplog): default_conf.update({'experimental': {'use_sell_signal': True}}) mocker.patch.dict('freqtrade.main._CONF', default_conf) - mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True) + mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (True, False)) mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock()) mocker.patch.multiple('freqtrade.main.exchange', validate_pairs=MagicMock(), @@ -291,11 +290,11 @@ def test_handle_trade_roi(default_conf, ticker, mocker, caplog): # we might just want to check if we are in a sell condition without # executing # if ROI is reached we must sell - mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: False) + mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (False, True)) assert handle_trade(trade) assert ('freqtrade', logging.DEBUG, 'Executing sell due to ROI ...') in caplog.record_tuples # if ROI is reached we must sell even if sell-signal is not signalled - mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True) + mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (False, True)) assert handle_trade(trade) assert ('freqtrade', logging.DEBUG, 'Executing sell due to ROI ...') in caplog.record_tuples @@ -304,7 +303,7 @@ def test_handle_trade_experimental(default_conf, ticker, mocker, caplog): default_conf.update({'experimental': {'use_sell_signal': True}}) mocker.patch.dict('freqtrade.main._CONF', default_conf) - mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True) + mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (True, False)) mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock()) mocker.patch.multiple('freqtrade.main.exchange', validate_pairs=MagicMock(), @@ -318,11 +317,10 @@ def test_handle_trade_experimental(default_conf, ticker, mocker, caplog): trade = Trade.query.first() trade.is_open = True - mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: False) + mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (False, False)) value_returned = handle_trade(trade) - assert ('freqtrade', logging.DEBUG, 'Checking sell_signal ...') in caplog.record_tuples assert value_returned is False - mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True) + mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (False, True)) assert handle_trade(trade) s = 'Executing sell due to sell signal ...' assert ('freqtrade', logging.DEBUG, s) in caplog.record_tuples @@ -330,7 +328,7 @@ def test_handle_trade_experimental(default_conf, ticker, mocker, caplog): def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, mocker): mocker.patch.dict('freqtrade.main._CONF', default_conf) - mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True) + mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (True, False)) mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock()) mocker.patch.multiple('freqtrade.main.exchange', validate_pairs=MagicMock(), @@ -469,7 +467,7 @@ def test_balance_bigger_last_ask(mocker): def test_execute_sell_up(default_conf, ticker, ticker_sell_up, mocker): mocker.patch.dict('freqtrade.main._CONF', default_conf) - mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True) + mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (True, False)) mocker.patch('freqtrade.rpc.init', MagicMock()) rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock()) mocker.patch.multiple('freqtrade.main.exchange', @@ -502,7 +500,7 @@ def test_execute_sell_up(default_conf, ticker, ticker_sell_up, mocker): def test_execute_sell_down(default_conf, ticker, ticker_sell_down, mocker): mocker.patch.dict('freqtrade.main._CONF', default_conf) - mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True) + mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (True, False)) mocker.patch('freqtrade.rpc.init', MagicMock()) rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock()) mocker.patch.multiple('freqtrade.rpc.telegram', @@ -539,7 +537,7 @@ def test_execute_sell_down(default_conf, ticker, ticker_sell_down, mocker): def test_execute_sell_without_conf(default_conf, ticker, ticker_sell_up, mocker): mocker.patch.dict('freqtrade.main._CONF', default_conf) - mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True) + mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (True, False)) mocker.patch('freqtrade.rpc.init', MagicMock()) rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock()) mocker.patch.multiple('freqtrade.main.exchange', @@ -576,7 +574,7 @@ def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, mocker): mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch('freqtrade.main.min_roi_reached', return_value=False) - mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True) + mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (True, False)) mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock()) mocker.patch.multiple('freqtrade.main.exchange', validate_pairs=MagicMock(), @@ -592,6 +590,7 @@ def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, mocker): trade = Trade.query.first() trade.update(limit_buy_order) + mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (False, True)) assert handle_trade(trade) is True @@ -603,7 +602,7 @@ def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, mocker): mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch('freqtrade.main.min_roi_reached', return_value=False) - mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True) + mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (True, False)) mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock()) mocker.patch.multiple('freqtrade.main.exchange', validate_pairs=MagicMock(), @@ -619,6 +618,7 @@ def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, mocker): trade = Trade.query.first() trade.update(limit_buy_order) + mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (False, True)) assert handle_trade(trade) is True @@ -630,7 +630,7 @@ def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, mocker): mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch('freqtrade.main.min_roi_reached', return_value=False) - mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True) + mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (True, False)) mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock()) mocker.patch.multiple('freqtrade.main.exchange', validate_pairs=MagicMock(), @@ -646,6 +646,7 @@ def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, mocker): trade = Trade.query.first() trade.update(limit_buy_order) + mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (False, True)) assert handle_trade(trade) is False @@ -657,7 +658,7 @@ def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, mocker): mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch('freqtrade.main.min_roi_reached', return_value=False) - mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True) + mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (True, False)) mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock()) mocker.patch.multiple('freqtrade.main.exchange', validate_pairs=MagicMock(), @@ -673,4 +674,6 @@ def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, mocker): trade = Trade.query.first() trade.update(limit_buy_order) + + mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (False, True)) assert handle_trade(trade) is True From 6dd48fb820c1f1cdfa2773bc75493b1717c6baa1 Mon Sep 17 00:00:00 2001 From: toto Date: Tue, 16 Jan 2018 21:18:43 +0100 Subject: [PATCH 18/27] Adding unitest --- freqtrade/analyze.py | 4 +-- freqtrade/tests/test_analyze.py | 18 ++++++------- freqtrade/tests/test_main.py | 46 +++++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 11 deletions(-) diff --git a/freqtrade/analyze.py b/freqtrade/analyze.py index 936af1e29..80bc50445 100644 --- a/freqtrade/analyze.py +++ b/freqtrade/analyze.py @@ -310,6 +310,6 @@ def get_signal(pair: str) -> (bool, bool): if signal_date < arrow.now() - timedelta(minutes=10): return (False, False) - (buy, sell) = latest[SignalType.BUY] == 1, latest[SignalType.SELL] == 1 - logger.debug('%trigger: %s (pair=%s, buy=%s sell=%s)', latest['date'], pair, buy, sell) + (buy, sell) = latest[SignalType.BUY.value] == 1, latest[SignalType.SELL.value] == 1 + logger.debug('trigger: %s (pair=%s) buy=%s sell=%s', latest['date'], pair, str(buy), str(sell)) return (buy, sell) diff --git a/freqtrade/tests/test_analyze.py b/freqtrade/tests/test_analyze.py index d1afc4200..c913606fd 100644 --- a/freqtrade/tests/test_analyze.py +++ b/freqtrade/tests/test_analyze.py @@ -40,30 +40,30 @@ def test_returns_latest_buy_signal(mocker): mocker.patch('freqtrade.analyze.get_ticker_history', return_value=MagicMock()) mocker.patch( 'freqtrade.analyze.analyze_ticker', - return_value=DataFrame([{'buy': 1, 'date': arrow.utcnow()}]) + return_value=DataFrame([{'buy': 1, 'sell': 0, 'date': arrow.utcnow()}]) ) - assert get_signal('BTC-ETH', SignalType.BUY) + assert get_signal('BTC-ETH') == (True, False) mocker.patch( 'freqtrade.analyze.analyze_ticker', - return_value=DataFrame([{'buy': 0, 'date': arrow.utcnow()}]) + return_value=DataFrame([{'buy': 0, 'sell':1, 'date': arrow.utcnow()}]) ) - assert not get_signal('BTC-ETH', SignalType.BUY) + assert get_signal('BTC-ETH') == (False, True) def test_returns_latest_sell_signal(mocker): mocker.patch('freqtrade.analyze.get_ticker_history', return_value=MagicMock()) mocker.patch( 'freqtrade.analyze.analyze_ticker', - return_value=DataFrame([{'sell': 1, 'date': arrow.utcnow()}]) + return_value=DataFrame([{'sell': 1, 'buy': 0, 'date': arrow.utcnow()}]) ) - assert get_signal('BTC-ETH', SignalType.SELL) + assert get_signal('BTC-ETH') == (False, True) mocker.patch( 'freqtrade.analyze.analyze_ticker', - return_value=DataFrame([{'sell': 0, 'date': arrow.utcnow()}]) + return_value=DataFrame([{'sell': 0, 'buy': 1, 'date': arrow.utcnow()}]) ) - assert not get_signal('BTC-ETH', SignalType.SELL) + assert get_signal('BTC-ETH') == (True,False) def test_get_signal_handles_exceptions(mocker): @@ -71,4 +71,4 @@ def test_get_signal_handles_exceptions(mocker): mocker.patch('freqtrade.analyze.analyze_ticker', side_effect=Exception('invalid ticker history ')) - assert not get_signal('BTC-ETH', SignalType.BUY) + assert get_signal('BTC-ETH') == (False,False) diff --git a/freqtrade/tests/test_main.py b/freqtrade/tests/test_main.py index c99727135..103ebb70b 100644 --- a/freqtrade/tests/test_main.py +++ b/freqtrade/tests/test_main.py @@ -267,6 +267,52 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, mocker): assert trade.close_date is not None +def test_handle_overlpapping_signals(default_conf, ticker, mocker, caplog): + default_conf.update({'experimental': {'use_sell_signal': True}}) + mocker.patch.dict('freqtrade.main._CONF', default_conf) + + mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (True, True)) + mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock()) + mocker.patch.multiple('freqtrade.main.exchange', + validate_pairs=MagicMock(), + get_ticker=ticker, + buy=MagicMock(return_value='mocked_limit_buy')) + mocker.patch('freqtrade.main.min_roi_reached', return_value=False) + + init(default_conf, create_engine('sqlite://')) + create_trade(0.001) + + # Buy and Sell triggering, so doing nothing ... + trades = Trade.query.all() + assert len(trades) == 0 + + # Buy is triggering, so buying ... + mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (True, False)) + create_trade(0.001) + trades = Trade.query.all() + assert len(trades) == 1 + assert trades[0].is_open is True + + # Buy and Sell are not triggering, so doing nothing ... + mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (False, False)) + assert handle_trade(trades[0]) is False + trades = Trade.query.all() + assert len(trades) == 1 + assert trades[0].is_open is True + + # Buy and Sell are triggering, so doing nothing ... + mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (True, True)) + assert handle_trade(trades[0]) is False + trades = Trade.query.all() + assert len(trades) == 1 + assert trades[0].is_open is True + + # Sell is triggering, guess what : we are Selling! + mocker.patch('freqtrade.main.get_signal', side_effect=lambda s: (False, True)) + trades = Trade.query.all() + assert handle_trade(trades[0]) is True + + def test_handle_trade_roi(default_conf, ticker, mocker, caplog): default_conf.update({'experimental': {'use_sell_signal': True}}) mocker.patch.dict('freqtrade.main._CONF', default_conf) From 572303963733d4a6d63aef25b587791f0ab9a03c Mon Sep 17 00:00:00 2001 From: toto Date: Tue, 16 Jan 2018 21:21:43 +0100 Subject: [PATCH 19/27] fXXXXXXk8 --- freqtrade/tests/test_analyze.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/tests/test_analyze.py b/freqtrade/tests/test_analyze.py index c913606fd..b8cd2f6e3 100644 --- a/freqtrade/tests/test_analyze.py +++ b/freqtrade/tests/test_analyze.py @@ -6,7 +6,7 @@ import arrow import pytest from pandas import DataFrame -from freqtrade.analyze import (SignalType, get_signal, parse_ticker_dataframe, +from freqtrade.analyze import (get_signal, parse_ticker_dataframe, populate_buy_trend, populate_indicators, populate_sell_trend) @@ -46,7 +46,7 @@ def test_returns_latest_buy_signal(mocker): mocker.patch( 'freqtrade.analyze.analyze_ticker', - return_value=DataFrame([{'buy': 0, 'sell':1, 'date': arrow.utcnow()}]) + return_value=DataFrame([{'buy': 0, 'sell': 1, 'date': arrow.utcnow()}]) ) assert get_signal('BTC-ETH') == (False, True) @@ -63,7 +63,7 @@ def test_returns_latest_sell_signal(mocker): 'freqtrade.analyze.analyze_ticker', return_value=DataFrame([{'sell': 0, 'buy': 1, 'date': arrow.utcnow()}]) ) - assert get_signal('BTC-ETH') == (True,False) + assert get_signal('BTC-ETH') == (True, False) def test_get_signal_handles_exceptions(mocker): @@ -71,4 +71,4 @@ def test_get_signal_handles_exceptions(mocker): mocker.patch('freqtrade.analyze.analyze_ticker', side_effect=Exception('invalid ticker history ')) - assert get_signal('BTC-ETH') == (False,False) + assert get_signal('BTC-ETH') == (False, False) From 04be438b352232b21377fe3c244fd66b0f2b7999 Mon Sep 17 00:00:00 2001 From: Stephen Dade Date: Wed, 17 Jan 2018 19:51:27 +1100 Subject: [PATCH 20/27] Better exception handling for check_handle_timedout --- freqtrade/main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/main.py b/freqtrade/main.py index 796fa8515..01f3d6e16 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -131,7 +131,8 @@ def check_handle_timedout(timeoutvalue: int) -> None: for trade in Trade.query.filter(Trade.open_order_id.isnot(None)).all(): try: order = exchange.get_order(trade.open_order_id) - except OperationalException: + except requests.exceptions.RequestException: + logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc()) continue ordertime = arrow.get(order['opened']) From 0d709847ee5babd35439e812a46c8aa093714288 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste LE STANG Date: Wed, 17 Jan 2018 11:31:26 +0100 Subject: [PATCH 21/27] Fixing the doc and and the default value of sell_profit_only to False --- docs/configuration.md | 1 + freqtrade/main.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index 384775765..7aade1e59 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -29,6 +29,7 @@ The table below will list all configuration parameters. | `exchange.pair_whitelist` | [] | No | List of currency to use by the bot. Can be overrided with `--dynamic-whitelist` param. | `exchange.pair_blacklist` | [] | No | List of currency the bot must avoid. Useful when using `--dynamic-whitelist` param. | `experimental.use_sell_signal` | false | No | Use your sell strategy in addition of the `minimal_roi`. +| `experimental.sell_profit_only` | false | No | waits until you have made a positive profit before taking a sell decision. | `telegram.enabled` | true | Yes | Enable or not the usage of Telegram. | `telegram.token` | token | No | Your Telegram bot token. Only required is `enable` is `true`. | `telegram.chat_id` | chat_id | No | Your personal Telegram account id. Only required is `enable` is `true`. diff --git a/freqtrade/main.py b/freqtrade/main.py index 1640c7836..e692b7c6a 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -259,7 +259,7 @@ def handle_trade(trade: Trade) -> bool: return True # Experimental: Check if the trade is profitable before selling it (avoid selling at loss) - if _CONF.get('experimental', {}).get('sell_profit_only'): + if _CONF.get('experimental', {}).get('sell_profit_only', False): logger.debug('Checking if trade is profitable ...') if not buy and trade.calc_profit(rate=current_rate) <= 0: return False From 423b25146737abb1ae37aa1c6fab9499f64ec2ca Mon Sep 17 00:00:00 2001 From: kryofly Date: Wed, 17 Jan 2018 18:19:39 +0100 Subject: [PATCH 22/27] tests: speed up backtests --- freqtrade/tests/optimize/test_backtesting.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index 5f899a48a..4ef4b1606 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -11,6 +11,13 @@ from freqtrade.optimize.backtesting import backtest, generate_text_table, get_ti import freqtrade.optimize.backtesting as backtesting +def trim_dictlist(dl, num): + new = {} + for pair, pair_data in dl.items(): + new[pair] = pair_data[num:] + return new + + def test_generate_text_table(): results = pd.DataFrame( { @@ -43,6 +50,7 @@ def test_backtest(default_conf, mocker): exchange._API = Bittrex({'key': '', 'secret': ''}) 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) assert not results.empty @@ -54,18 +62,12 @@ 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) assert not results.empty -def trim_dictlist(dl, num): - new = {} - for pair, pair_data in dl.items(): - new[pair] = pair_data[num:] - return new - - def load_data_test(what): data = optimize.load_data(None, ticker_interval=1, pairs=['BTC_UNITEST']) data = trim_dictlist(data, -100) @@ -125,6 +127,7 @@ 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']) + data = trim_dictlist(data, -200) results = backtest(default_conf['stake_amount'], optimize.preprocess(data), 10, True) assert not results.empty From fb34fe8c9a3c174d13916b367cd1c6bd83f68d9f Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Wed, 17 Jan 2018 23:08:30 +0100 Subject: [PATCH 23/27] Update ta-lib from 0.4.15 to 0.4.16 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index d650efea8..cb24b327a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ scikit-learn==0.19.1 scipy==1.0.0 jsonschema==2.6.0 numpy==1.14.0 -TA-Lib==0.4.15 +TA-Lib==0.4.16 pytest==3.3.2 pytest-mock==1.6.3 pytest-cov==2.5.1 From 9a48e3b8670448384affa816205a7d882e2c469d Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Fri, 19 Jan 2018 01:33:33 +0100 Subject: [PATCH 24/27] Update arrow from 0.12.0 to 0.12.1 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index cb24b327a..d37312268 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ python-bittrex==0.2.2 SQLAlchemy==1.2.1 python-telegram-bot==9.0.0 -arrow==0.12.0 +arrow==0.12.1 cachetools==2.0.1 requests==2.18.4 urllib3==1.22 From 861e065d083b0f2134941bcfc33545883f44d477 Mon Sep 17 00:00:00 2001 From: Gerald Lonlas Date: Thu, 18 Jan 2018 21:07:55 -0800 Subject: [PATCH 25/27] Fix markdown mistakes in backtesting doc --- docs/backtesting.md | 46 +++++++++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 9829a10c8..7fc0366e6 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -14,7 +14,7 @@ real data. This is what we call Backtesting will use the crypto-currencies (pair) from your config file and load static tickers located in -[/freqtrade/tests/testdata](https://github.com/gcarq/freqtrade/tree/develop/freqtrade/tests/testdata). +[/freqtrade/tests/testdata](https://github.com/gcarq/freqtrade/tree/develop/freqtrade/tests/testdata). If the 5 min and 1 min ticker for the crypto-currencies to test is not already in the `testdata` folder, backtesting will download them automatically. Testdata files will not be updated until you specify it. @@ -51,38 +51,44 @@ python3 ./freqtrade/main.py backtesting --realistic-simulation --live python3 ./freqtrade/main.py backtesting --datadir freqtrade/tests/testdata-20180101 ``` -**Running backtest with smaller testset** -Use the --timerange argument to change how much of the testset +**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. -Example: +Example: ```bash python3 ./freqtrade/main.py backtesting --timerange=-200 ``` -***Advanced use of timerange*** - Doing --timerange=-200 will get the last 200 timeframes - from your inputdata. You can also specify specific dates, - or a range span indexed by start and stop. - The full timerange specification: - Not implemented yet! --timerange=-20180131 - Not implemented yet! --timerange=20180101- - Not implemented yet! --timerange=20180101-20181231 - Last 123 tickframes of data: --timerange=-123 - First 123 tickframes of data: --timerange=123- - Tickframes from line 123 through 456: --timerange=123-456 +***Advanced use of timerange*** +Doing `--timerange=-200` will get the last 200 timeframes +from your inputdata. You can also specify specific dates, +or a range span indexed by start and stop. + +The full timerange specification: +- Use last 123 tickframes of data: `--timerange=-123` +- Use first 123 tickframes of data: `--timerange=123-` +- Use tickframes from line 123 through 456: `--timerange=123-456` -**Update testdata directory +Incoming feature, not implemented yet: +- `--timerange=-20180131` +- `--timerange=20180101-` +- `--timerange=20180101-20181231` + + +**Update testdata directory** To update your testdata directory, or download into another testdata directory: ```bash -mkdir freqtrade/tests/testdata-20180113 -cp freqtrade/tests/testdata/pairs.json freqtrade/tests/testdata-20180113 -cd freqtrade/tests/testdata-20180113 +mkdir -p user_data/data/testdata-20180113 +cp freqtrade/tests/testdata/pairs.json user_data/data-20180113 +cd user_data/data-20180113 +``` Possibly edit pairs.json file to include/exclude pairs -python download_backtest_data.py -p pairs.json +```bash +python freqtrade/tests/testdata/download_backtest_data.py -p pairs.json ``` The script will read your pairs.json file, and download ticker data From ddc1b7cd495e0cbee8d2725be9b62002fe1466fd Mon Sep 17 00:00:00 2001 From: Gerald Lonlas Date: Thu, 18 Jan 2018 21:15:20 -0800 Subject: [PATCH 26/27] Update bot commands in README.md --- README.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 045d6b624..d9e537382 100644 --- a/README.md +++ b/README.md @@ -136,8 +136,8 @@ to understand the requirements before sending your pull-requests. ### Bot commands ```bash -usage: main.py [-h] [-c PATH] [-v] [--version] [--dynamic-whitelist [INT]] - [--dry-run-db] +usage: main.py [-h] [-v] [--version] [-c PATH] [--dry-run-db] [--datadir PATH] + [--dynamic-whitelist [INT]] {backtesting,hyperopt} ... Simple High Frequency Trading Bot for crypto currencies @@ -149,16 +149,17 @@ positional arguments: optional arguments: -h, --help show this help message and exit - -c PATH, --config PATH - specify configuration file (default: config.json) -v, --verbose be verbose --version show program's version number and exit - --dynamic-whitelist [INT] - dynamically generate and update whitelist based on 24h - BaseVolume (Default 20 currencies) + -c PATH, --config PATH + specify configuration file (default: config.json) --dry-run-db Force dry run to use a local DB "tradesv3.dry_run.sqlite" instead of memory DB. Work only if dry_run is enabled. + --datadir PATH path to backtest data (default freqdata/tests/testdata + --dynamic-whitelist [INT] + dynamically generate and update whitelist based on 24h + BaseVolume (Default 20 currencies) ``` More details on: - [How to run the bot](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-usage.md#bot-commands) From fb110ccfd2fd0d03d0e49ba2d5f1d5d5d80e4690 Mon Sep 17 00:00:00 2001 From: Gerald Lonlas Date: Fri, 19 Jan 2018 22:09:28 -0800 Subject: [PATCH 27/27] 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()