From 85979c317679a58172fd15c2432d3c94cb1d9c4f Mon Sep 17 00:00:00 2001 From: Cryptomeister Nox Date: Thu, 17 Jun 2021 20:35:02 +0200 Subject: [PATCH 01/72] * Adding command for Filtering * Read latest Backtest file and print trades --- freqtrade/commands/__init__.py | 2 +- freqtrade/commands/arguments.py | 16 ++++++++++++-- freqtrade/commands/cli_options.py | 5 +++++ freqtrade/commands/optimize_commands.py | 28 +++++++++++++++++++++++++ freqtrade/optimize/optimize_reports.py | 15 +++++++++++++ 5 files changed, 63 insertions(+), 3 deletions(-) diff --git a/freqtrade/commands/__init__.py b/freqtrade/commands/__init__.py index 784b99bed..ecce709e5 100644 --- a/freqtrade/commands/__init__.py +++ b/freqtrade/commands/__init__.py @@ -16,7 +16,7 @@ from freqtrade.commands.hyperopt_commands import start_hyperopt_list, start_hype from freqtrade.commands.list_commands import (start_list_exchanges, start_list_hyperopts, start_list_markets, start_list_strategies, start_list_timeframes, start_show_trades) -from freqtrade.commands.optimize_commands import start_backtesting, start_edge, start_hyperopt +from freqtrade.commands.optimize_commands import start_backtest_filter, start_backtesting, start_edge, start_hyperopt from freqtrade.commands.pairlist_commands import start_test_pairlist from freqtrade.commands.plot_commands import start_plot_dataframe, start_plot_profit from freqtrade.commands.trade_commands import start_trading diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index 7f4f7edd6..f5b9f9cc2 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -2,6 +2,7 @@ This module contains the argument manager class """ import argparse +from freqtrade.commands.optimize_commands import start_backtest_filter from functools import partial from pathlib import Path from typing import Any, Dict, List, Optional @@ -37,6 +38,8 @@ ARGS_LIST_STRATEGIES = ["strategy_path", "print_one_column", "print_colorized"] ARGS_LIST_HYPEROPTS = ["hyperopt_path", "print_one_column", "print_colorized"] +ARGS_BACKTEST_FILTER = ["backtest_path"] + ARGS_LIST_EXCHANGES = ["print_one_column", "list_exchanges_all"] ARGS_LIST_TIMEFRAMES = ["exchange", "print_one_column"] @@ -89,7 +92,7 @@ ARGS_HYPEROPT_SHOW = ["hyperopt_list_best", "hyperopt_list_profitable", "hyperop NO_CONF_REQURIED = ["convert-data", "convert-trade-data", "download-data", "list-timeframes", "list-markets", "list-pairs", "list-strategies", "list-data", - "list-hyperopts", "hyperopt-list", "hyperopt-show", + "list-hyperopts", "hyperopt-list", "backtest-filter", "hyperopt-show", "plot-dataframe", "plot-profit", "show-trades"] NO_CONF_ALLOWED = ["create-userdir", "list-exchanges", "new-hyperopt", "new-strategy"] @@ -168,7 +171,7 @@ class Arguments: self.parser = argparse.ArgumentParser(description='Free, open source crypto trading bot') self._build_args(optionlist=['version'], parser=self.parser) - from freqtrade.commands import (start_backtesting, start_convert_data, start_create_userdir, + from freqtrade.commands import (start_backtesting, start_backtest_filter, start_convert_data, start_create_userdir, start_download_data, start_edge, start_hyperopt, start_hyperopt_list, start_hyperopt_show, start_install_ui, start_list_data, start_list_exchanges, start_list_hyperopts, @@ -256,6 +259,15 @@ class Arguments: backtesting_cmd.set_defaults(func=start_backtesting) self._build_args(optionlist=ARGS_BACKTEST, parser=backtesting_cmd) + # Add backtest-filter subcommand + backtest_filter_cmd = subparsers.add_parser( + 'backtest-filter', + help='Filter Backtest results', + parents=[_common_parser], + ) + backtest_filter_cmd.set_defaults(func=start_backtest_filter) + self._build_args(optionlist=ARGS_BACKTEST_FILTER, parser=backtest_filter_cmd) + # Add edge subcommand edge_cmd = subparsers.add_parser('edge', help='Edge module.', parents=[_common_parser, _strategy_parser]) diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index d832693ee..8ba66b32d 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -202,6 +202,11 @@ AVAILABLE_CLI_OPTIONS = { help='Specify additional lookup path for Hyperopt and Hyperopt Loss functions.', metavar='PATH', ), + "backtest_path": Arg( + '--backtest-path', + help='Specify lookup file path for backtest filter.', + metavar='PATH', + ), "epochs": Arg( '-e', '--epochs', help='Specify number of epochs (default: %(default)d).', diff --git a/freqtrade/commands/optimize_commands.py b/freqtrade/commands/optimize_commands.py index a84b3b3bd..3165852fa 100644 --- a/freqtrade/commands/optimize_commands.py +++ b/freqtrade/commands/optimize_commands.py @@ -1,3 +1,7 @@ +from freqtrade.data.btanalysis import get_latest_backtest_filename +import pandas +from pandas.io import json +from freqtrade.optimize import backtesting import logging from typing import Any, Dict @@ -52,6 +56,30 @@ def start_backtesting(args: Dict[str, Any]) -> None: backtesting = Backtesting(config) backtesting.start() +def start_backtest_filter(args: Dict[str, Any]) -> None: + """ + List backtest pairs previously filtered + """ + + config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) + + no_header = config.get('backtest_show_pair_list', False) + results_file = get_latest_backtest_filename( + config['user_data_dir'] / 'backtest_results/') + + logger.info("Using Backtesting result {results_file}") + + # load data using Python JSON module + with open(config['user_data_dir'] / 'backtest_results/' / results_file,'r') as f: + data = json.loads(f.read()) + strategy = list(data["strategy"])[0] + trades = data["strategy"][strategy] + + print(trades) + + + logger.info("Backtest filtering complete. ") + def start_hyperopt(args: Dict[str, Any]) -> None: """ diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 84e052ac4..f401614c5 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -668,3 +668,18 @@ def show_backtest_results(config: Dict, backtest_stats: Dict): print(table) print('=' * len(table.splitlines()[0])) print('\nFor more details, please look at the detail tables above') + +def show_backtest_results_filtered(config: Dict, backtest_stats: Dict): + stake_currency = config['stake_currency'] + + for strategy, results in backtest_stats['strategy'].items(): + show_backtest_result(strategy, results, stake_currency) + + if len(backtest_stats['strategy']) > 1: + # Print Strategy summary table + + table = text_table_strategy(backtest_stats['strategy_comparison'], stake_currency) + print(' STRATEGY SUMMARY '.center(len(table.splitlines()[0]), '=')) + print(table) + print('=' * len(table.splitlines()[0])) + print('\nFor more details, please look at the detail tables above') From 3845d5518641263e8bb3a6864d8043d34f29c9b8 Mon Sep 17 00:00:00 2001 From: Robert Roman Date: Tue, 21 Sep 2021 20:04:23 -0500 Subject: [PATCH 02/72] a new hyperopt loss created that uses calmar ratio This is a new hyperopt loss file that uses the Calmar Ratio. Calmar Ratio = average annual rate of return / maximum drawdown --- freqtrade/optimize/hyperopt_loss_calmar.py | 52 ++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 freqtrade/optimize/hyperopt_loss_calmar.py diff --git a/freqtrade/optimize/hyperopt_loss_calmar.py b/freqtrade/optimize/hyperopt_loss_calmar.py new file mode 100644 index 000000000..c6211cb2b --- /dev/null +++ b/freqtrade/optimize/hyperopt_loss_calmar.py @@ -0,0 +1,52 @@ +""" +CalmarHyperOptLoss + +This module defines the alternative HyperOptLoss class which can be used for +Hyperoptimization. +""" +from datetime import datetime + +import numpy as np +from pandas import DataFrame + +from freqtrade.optimize.hyperopt import IHyperOptLoss +from freqtrade.data.btanalysis import calculate_max_drawdown + + +class CalmarHyperOptLoss(IHyperOptLoss): + """ + Defines the loss function for hyperopt. + + This implementation uses the Calmar Ratio calculation. + """ + + @staticmethod + def hyperopt_loss_function(results: DataFrame, trade_count: int, + min_date: datetime, max_date: datetime, + *args, **kwargs) -> float: + """ + Objective function, returns smaller number for more optimal results. + + Uses Calmar Ratio calculation. + """ + total_profit = results["profit_ratio"] + days_period = (max_date - min_date).days + + # adding slippage of 0.1% per trade + total_profit = total_profit - 0.0005 + expected_returns_mean = total_profit.sum() / days_period + + # calculate max drawdown + try: + _, _, _, high_val, low_val = calculate_max_drawdown(results) + max_drawdown = -(high_val - low_val) / high_val + except ValueError: + max_drawdown = 0 + + if max_drawdown > 0: + calmar_ratio = expected_returns_mean / max_drawdown * np.sqrt(365) + else: + calmar_ratio = -20. + + # print(calmar_ratio) + return -calmar_ratio From 3834bb86ff73794086ed188d6910bf7527c04cbf Mon Sep 17 00:00:00 2001 From: Robert Roman Date: Tue, 21 Sep 2021 20:25:17 -0500 Subject: [PATCH 03/72] updated line 42 I removed the minus sign on max drawdown. --- freqtrade/optimize/hyperopt_loss_calmar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt_loss_calmar.py b/freqtrade/optimize/hyperopt_loss_calmar.py index c6211cb2b..8ee1a5c27 100644 --- a/freqtrade/optimize/hyperopt_loss_calmar.py +++ b/freqtrade/optimize/hyperopt_loss_calmar.py @@ -39,7 +39,7 @@ class CalmarHyperOptLoss(IHyperOptLoss): # calculate max drawdown try: _, _, _, high_val, low_val = calculate_max_drawdown(results) - max_drawdown = -(high_val - low_val) / high_val + max_drawdown = (high_val - low_val) / high_val except ValueError: max_drawdown = 0 From b946f8e7f19fb5674b77385d44c6cf266e0e37e0 Mon Sep 17 00:00:00 2001 From: Robert Roman Date: Wed, 22 Sep 2021 09:18:17 -0500 Subject: [PATCH 04/72] I sorted imports with isort --- freqtrade/optimize/hyperopt_loss_calmar.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/freqtrade/optimize/hyperopt_loss_calmar.py b/freqtrade/optimize/hyperopt_loss_calmar.py index 8ee1a5c27..866c0aa5f 100644 --- a/freqtrade/optimize/hyperopt_loss_calmar.py +++ b/freqtrade/optimize/hyperopt_loss_calmar.py @@ -7,10 +7,9 @@ Hyperoptimization. from datetime import datetime import numpy as np -from pandas import DataFrame - -from freqtrade.optimize.hyperopt import IHyperOptLoss from freqtrade.data.btanalysis import calculate_max_drawdown +from freqtrade.optimize.hyperopt import IHyperOptLoss +from pandas import DataFrame class CalmarHyperOptLoss(IHyperOptLoss): @@ -38,15 +37,14 @@ class CalmarHyperOptLoss(IHyperOptLoss): # calculate max drawdown try: - _, _, _, high_val, low_val = calculate_max_drawdown(results) + _,_,_,high_val,low_val = calculate_max_drawdown(results) max_drawdown = (high_val - low_val) / high_val except ValueError: max_drawdown = 0 - if max_drawdown > 0: + if max_drawdown != 0 and trade_count > 1000: calmar_ratio = expected_returns_mean / max_drawdown * np.sqrt(365) else: calmar_ratio = -20. - # print(calmar_ratio) return -calmar_ratio From c6b684603caffb106b9dc7f550d5caf5b980d638 Mon Sep 17 00:00:00 2001 From: Robert Roman Date: Wed, 22 Sep 2021 09:21:43 -0500 Subject: [PATCH 05/72] removed trade_count inside if statement i removed trade_count inside if statement. Even though it helps overfitting, It is not useful when running hyperopt on small datasets. --- freqtrade/optimize/hyperopt_loss_calmar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt_loss_calmar.py b/freqtrade/optimize/hyperopt_loss_calmar.py index 866c0aa5f..b2a819444 100644 --- a/freqtrade/optimize/hyperopt_loss_calmar.py +++ b/freqtrade/optimize/hyperopt_loss_calmar.py @@ -42,7 +42,7 @@ class CalmarHyperOptLoss(IHyperOptLoss): except ValueError: max_drawdown = 0 - if max_drawdown != 0 and trade_count > 1000: + if max_drawdown != 0: calmar_ratio = expected_returns_mean / max_drawdown * np.sqrt(365) else: calmar_ratio = -20. From 3b99c84b0a879f9fbf5dae40a35a6a196c5b95d4 Mon Sep 17 00:00:00 2001 From: Robert Roman Date: Thu, 23 Sep 2021 21:31:33 -0500 Subject: [PATCH 06/72] resolved the total profit issue I resolved the total profit issue and locally ran flak8 and isort --- freqtrade/optimize/hyperopt_loss_calmar.py | 32 +++++++++++++++------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/freqtrade/optimize/hyperopt_loss_calmar.py b/freqtrade/optimize/hyperopt_loss_calmar.py index b2a819444..45a7cd7db 100644 --- a/freqtrade/optimize/hyperopt_loss_calmar.py +++ b/freqtrade/optimize/hyperopt_loss_calmar.py @@ -5,11 +5,13 @@ This module defines the alternative HyperOptLoss class which can be used for Hyperoptimization. """ from datetime import datetime +from math import sqrt as msqrt +from typing import Any, Dict + +from pandas import DataFrame -import numpy as np from freqtrade.data.btanalysis import calculate_max_drawdown from freqtrade.optimize.hyperopt import IHyperOptLoss -from pandas import DataFrame class CalmarHyperOptLoss(IHyperOptLoss): @@ -20,31 +22,41 @@ class CalmarHyperOptLoss(IHyperOptLoss): """ @staticmethod - def hyperopt_loss_function(results: DataFrame, trade_count: int, - min_date: datetime, max_date: datetime, - *args, **kwargs) -> float: + def hyperopt_loss_function( + results: DataFrame, + trade_count: int, + min_date: datetime, + max_date: datetime, + backtest_stats: Dict[str, Any], + *args, + **kwargs + ) -> float: """ Objective function, returns smaller number for more optimal results. Uses Calmar Ratio calculation. """ - total_profit = results["profit_ratio"] + total_profit = backtest_stats["profit_total"] days_period = (max_date - min_date).days # adding slippage of 0.1% per trade total_profit = total_profit - 0.0005 - expected_returns_mean = total_profit.sum() / days_period + expected_returns_mean = total_profit.sum() / days_period * 100 # calculate max drawdown try: - _,_,_,high_val,low_val = calculate_max_drawdown(results) + _, _, _, high_val, low_val = calculate_max_drawdown( + results, value_col="profit_abs" + ) max_drawdown = (high_val - low_val) / high_val except ValueError: max_drawdown = 0 if max_drawdown != 0: - calmar_ratio = expected_returns_mean / max_drawdown * np.sqrt(365) + calmar_ratio = expected_returns_mean / max_drawdown * msqrt(365) else: - calmar_ratio = -20. + # Define high (negative) calmar ratio to be clear that this is NOT optimal. + calmar_ratio = -20.0 + # print(expected_returns_mean, max_drawdown, calmar_ratio) return -calmar_ratio From 0f29cbc882806f04ba34956918cd238b73871ee1 Mon Sep 17 00:00:00 2001 From: Robert Roman Date: Thu, 23 Sep 2021 21:37:28 -0500 Subject: [PATCH 07/72] added CalmarHyperOptLoss I added CalmarHyperOptLoss to HYPEROPT_LOSS_BUILTIN variable inside constants.py file --- freqtrade/constants.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 9ca43d459..94ab5b6dd 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -24,7 +24,8 @@ ORDERTYPE_POSSIBILITIES = ['limit', 'market'] ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc'] HYPEROPT_LOSS_BUILTIN = ['ShortTradeDurHyperOptLoss', 'OnlyProfitHyperOptLoss', 'SharpeHyperOptLoss', 'SharpeHyperOptLossDaily', - 'SortinoHyperOptLoss', 'SortinoHyperOptLossDaily'] + 'SortinoHyperOptLoss', 'SortinoHyperOptLossDaily', + 'CalmarHyperOptLoss'] AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'AgeFilter', 'OffsetFilter', 'PerformanceFilter', 'PrecisionFilter', 'PriceFilter', 'RangeStabilityFilter', @@ -69,7 +70,9 @@ DUST_PER_COIN = { # Source files with destination directories within user-directory USER_DATA_FILES = { 'sample_strategy.py': USERPATH_STRATEGIES, + 'sample_hyperopt_advanced.py': USERPATH_HYPEROPTS, 'sample_hyperopt_loss.py': USERPATH_HYPEROPTS, + 'sample_hyperopt.py': USERPATH_HYPEROPTS, 'strategy_analysis_example.ipynb': USERPATH_NOTEBOOKS, } From b2ac039d5cbc9eec7ce15fc1b31f3a61bd968866 Mon Sep 17 00:00:00 2001 From: Robert Roman Date: Thu, 23 Sep 2021 21:46:07 -0500 Subject: [PATCH 08/72] added CalmarHyperOptLoss to HYPEROPT_LOSS_BUILTIN I added CalmarHyperOptLoss to HYPEROPT_LOSS_BUILTIN variable inside constants.py file From ca20e17d404c5aab902e765504aaa59aa42bc4a6 Mon Sep 17 00:00:00 2001 From: Robert Roman Date: Thu, 23 Sep 2021 21:48:08 -0500 Subject: [PATCH 09/72] added CalmarHyperOpt to hyperopt.md i added CalmarHyperOpt to hyperopt.md and gave a brief description inside the docs --- docs/hyperopt.md | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 09d43939a..aaad0634f 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -44,8 +44,9 @@ usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--data-format-ohlcv {json,jsongz,hdf5}] [--max-open-trades INT] [--stake-amount STAKE_AMOUNT] [--fee FLOAT] - [-p PAIRS [PAIRS ...]] [--hyperopt-path PATH] - [--eps] [--dmmp] [--enable-protections] + [-p PAIRS [PAIRS ...]] [--hyperopt NAME] + [--hyperopt-path PATH] [--eps] [--dmmp] + [--enable-protections] [--dry-run-wallet DRY_RUN_WALLET] [-e INT] [--spaces {all,buy,sell,roi,stoploss,trailing,protection,default} [{all,buy,sell,roi,stoploss,trailing,protection,default} ...]] [--print-all] [--no-color] [--print-json] [-j JOBS] @@ -72,8 +73,10 @@ optional arguments: -p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...] Limit command to these pairs. Pairs are space- separated. - --hyperopt-path PATH Specify additional lookup path for Hyperopt Loss - functions. + --hyperopt NAME Specify hyperopt class name which will be used by the + bot. + --hyperopt-path PATH Specify additional lookup path for Hyperopt and + Hyperopt Loss functions. --eps, --enable-position-stacking Allow buying the same pair multiple times (position stacking). @@ -114,7 +117,8 @@ optional arguments: Hyperopt-loss-functions are: ShortTradeDurHyperOptLoss, OnlyProfitHyperOptLoss, SharpeHyperOptLoss, SharpeHyperOptLossDaily, - SortinoHyperOptLoss, SortinoHyperOptLossDaily + SortinoHyperOptLoss, SortinoHyperOptLossDaily, + CalmarHyperOptLoss --disable-param-export Disable automatic hyperopt parameter export. @@ -518,6 +522,7 @@ Currently, the following loss functions are builtin: * `SharpeHyperOptLossDaily` (optimizes Sharpe Ratio calculated on **daily** trade returns relative to standard deviation) * `SortinoHyperOptLoss` (optimizes Sortino Ratio calculated on trade returns relative to **downside** standard deviation) * `SortinoHyperOptLossDaily` (optimizes Sortino Ratio calculated on **daily** trade returns relative to **downside** standard deviation) +* `CalmarHyperOptLoss` (optimizes Calmar Ratio calculated on trade returns relative to max drawdown) Creation of a custom loss function is covered in the [Advanced Hyperopt](advanced-hyperopt.md) part of the documentation. @@ -555,7 +560,7 @@ For example, to use one month of data, pass `--timerange 20210101-20210201` (fro Full command: ```bash -freqtrade hyperopt --strategy --timerange 20210101-20210201 +freqtrade hyperopt --hyperopt --strategy --timerange 20210101-20210201 ``` ### Running Hyperopt with Smaller Search Space @@ -677,11 +682,11 @@ If you are optimizing ROI, Freqtrade creates the 'roi' optimization hyperspace f These ranges should be sufficient in most cases. The minutes in the steps (ROI dict keys) are scaled linearly depending on the timeframe used. The ROI values in the steps (ROI dict values) are scaled logarithmically depending on the timeframe used. -If you have the `generate_roi_table()` and `roi_space()` methods in your custom hyperopt, remove them in order to utilize these adaptive ROI tables and the ROI hyperoptimization space generated by Freqtrade by default. +If you have the `generate_roi_table()` and `roi_space()` methods in your custom hyperopt file, remove them in order to utilize these adaptive ROI tables and the ROI hyperoptimization space generated by Freqtrade by default. Override the `roi_space()` method if you need components of the ROI tables to vary in other ranges. Override the `generate_roi_table()` and `roi_space()` methods and implement your own custom approach for generation of the ROI tables during hyperoptimization if you need a different structure of the ROI tables or other amount of rows (steps). -A sample for these methods can be found in the [overriding pre-defined spaces section](advanced-hyperopt.md#overriding-pre-defined-spaces). +A sample for these methods can be found in [sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py). !!! Note "Reduced search space" To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is usually sufficient, every value more precise than this will usually result in overfitted results. You can however [overriding pre-defined spaces](advanced-hyperopt.md#pverriding-pre-defined-spaces) to change this to your needs. @@ -723,7 +728,7 @@ If you are optimizing stoploss values, Freqtrade creates the 'stoploss' optimiza If you have the `stoploss_space()` method in your custom hyperopt file, remove it in order to utilize Stoploss hyperoptimization space generated by Freqtrade by default. -Override the `stoploss_space()` method and define the desired range in it if you need stoploss values to vary in other range during hyperoptimization. A sample for this method can be found in the [overriding pre-defined spaces section](advanced-hyperopt.md#overriding-pre-defined-spaces). +Override the `stoploss_space()` method and define the desired range in it if you need stoploss values to vary in other range during hyperoptimization. A sample for this method can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py). !!! Note "Reduced search space" To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is usually sufficient, every value more precise than this will usually result in overfitted results. You can however [overriding pre-defined spaces](advanced-hyperopt.md#pverriding-pre-defined-spaces) to change this to your needs. @@ -761,10 +766,10 @@ As stated in the comment, you can also use it as the values of the corresponding If you are optimizing trailing stop values, Freqtrade creates the 'trailing' optimization hyperspace for you. By default, the `trailing_stop` parameter is always set to True in that hyperspace, the value of the `trailing_only_offset_is_reached` vary between True and False, the values of the `trailing_stop_positive` and `trailing_stop_positive_offset` parameters vary in the ranges 0.02...0.35 and 0.01...0.1 correspondingly, which is sufficient in most cases. -Override the `trailing_space()` method and define the desired range in it if you need values of the trailing stop parameters to vary in other ranges during hyperoptimization. A sample for this method can be found in the [overriding pre-defined spaces section](advanced-hyperopt.md#overriding-pre-defined-spaces). +Override the `trailing_space()` method and define the desired range in it if you need values of the trailing stop parameters to vary in other ranges during hyperoptimization. A sample for this method can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py). !!! Note "Reduced search space" - To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is usually sufficient, every value more precise than this will usually result in overfitted results. You can however [overriding pre-defined spaces](advanced-hyperopt.md#overriding-pre-defined-spaces) to change this to your needs. + To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is usually sufficient, every value more precise than this will usually result in overfitted results. You can however [overriding pre-defined spaces](advanced-hyperopt.md#pverriding-pre-defined-spaces) to change this to your needs. ### Reproducible results From 24baad7884b2f076f5ec0b4b12f98e20beb7b0f5 Mon Sep 17 00:00:00 2001 From: Robert Roman Date: Sat, 25 Sep 2021 16:28:36 -0500 Subject: [PATCH 10/72] Add Calmar Ratio Daily This hyper opt loss calculates the daily Calmar ratio. --- .../optimize/hyperopt_loss_calmar_daily.py | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 freqtrade/optimize/hyperopt_loss_calmar_daily.py diff --git a/freqtrade/optimize/hyperopt_loss_calmar_daily.py b/freqtrade/optimize/hyperopt_loss_calmar_daily.py new file mode 100644 index 000000000..c7651a72a --- /dev/null +++ b/freqtrade/optimize/hyperopt_loss_calmar_daily.py @@ -0,0 +1,79 @@ +""" +CalmarHyperOptLossDaily + +This module defines the alternative HyperOptLoss class which can be used for +Hyperoptimization. +""" +from datetime import datetime +from math import sqrt as msqrt +from typing import Any, Dict + +from pandas import DataFrame, date_range + +from freqtrade.optimize.hyperopt import IHyperOptLoss + + +class CalmarHyperOptLossDaily(IHyperOptLoss): + """ + Defines the loss function for hyperopt. + + This implementation uses the Calmar Ratio calculation. + """ + + @staticmethod + def hyperopt_loss_function( + results: DataFrame, + trade_count: int, + min_date: datetime, + max_date: datetime, + backtest_stats: Dict[str, Any], + *args, + **kwargs + ) -> float: + """ + Objective function, returns smaller number for more optimal results. + + Uses Calmar Ratio calculation. + """ + resample_freq = "1D" + slippage_per_trade_ratio = 0.0005 + days_in_year = 365 + + # create the index within the min_date and end max_date + t_index = date_range( + start=min_date, end=max_date, freq=resample_freq, normalize=True + ) + + # apply slippage per trade to profit_total + results.loc[:, "profit_ratio_after_slippage"] = ( + results["profit_ratio"] - slippage_per_trade_ratio + ) + + sum_daily = ( + results.resample(resample_freq, on="close_date") + .agg({"profit_ratio_after_slippage": sum}) + .reindex(t_index) + .fillna(0) + ) + + total_profit = sum_daily["profit_ratio_after_slippage"] + expected_returns_mean = total_profit.mean() * 100 + + # calculate max drawdown + try: + high_val = total_profit.max() + low_val = total_profit.min() + max_drawdown = (high_val - low_val) / high_val + + except (ValueError, ZeroDivisionError): + max_drawdown = 0 + + if max_drawdown != 0: + calmar_ratio = expected_returns_mean / max_drawdown * msqrt(days_in_year) + else: + # Define high (negative) calmar ratio to be clear that this is NOT optimal. + calmar_ratio = -20.0 + + # print(t_index, sum_daily, total_profit) + # print(expected_returns_mean, max_drawdown, calmar_ratio) + return -calmar_ratio From 89b7dfda0e4a1a6d6192a3f72e01a7224b98737d Mon Sep 17 00:00:00 2001 From: Robert Roman Date: Sat, 25 Sep 2021 16:34:41 -0500 Subject: [PATCH 11/72] Added Calmar Ratio Daily --- freqtrade/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 94ab5b6dd..42b6fccc2 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -25,7 +25,7 @@ ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc'] HYPEROPT_LOSS_BUILTIN = ['ShortTradeDurHyperOptLoss', 'OnlyProfitHyperOptLoss', 'SharpeHyperOptLoss', 'SharpeHyperOptLossDaily', 'SortinoHyperOptLoss', 'SortinoHyperOptLossDaily', - 'CalmarHyperOptLoss'] + 'CalmarHyperOptLoss', 'CalmarHyperOptLossDaily'] AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'AgeFilter', 'OffsetFilter', 'PerformanceFilter', 'PrecisionFilter', 'PriceFilter', 'RangeStabilityFilter', From e1036d6f58c37ec1c2d4f13626ea7b01987b97a8 Mon Sep 17 00:00:00 2001 From: Robert Roman Date: Sat, 25 Sep 2021 16:40:02 -0500 Subject: [PATCH 12/72] Added Calmar Ratio Daily to hyperopt.md file --- docs/hyperopt.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index aaad0634f..1077ff503 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -118,7 +118,7 @@ optional arguments: ShortTradeDurHyperOptLoss, OnlyProfitHyperOptLoss, SharpeHyperOptLoss, SharpeHyperOptLossDaily, SortinoHyperOptLoss, SortinoHyperOptLossDaily, - CalmarHyperOptLoss + CalmarHyperOptLoss, CalmarHyperOptLossDaily --disable-param-export Disable automatic hyperopt parameter export. @@ -523,6 +523,7 @@ Currently, the following loss functions are builtin: * `SortinoHyperOptLoss` (optimizes Sortino Ratio calculated on trade returns relative to **downside** standard deviation) * `SortinoHyperOptLossDaily` (optimizes Sortino Ratio calculated on **daily** trade returns relative to **downside** standard deviation) * `CalmarHyperOptLoss` (optimizes Calmar Ratio calculated on trade returns relative to max drawdown) +* `CalmarHyperOptLossDaily` (optimizes Calmar Ratio calculated on **daily** trade returns relative to max drawdown) Creation of a custom loss function is covered in the [Advanced Hyperopt](advanced-hyperopt.md) part of the documentation. From bc86cb3280df1b63648f72a8cbc361d136c339ab Mon Sep 17 00:00:00 2001 From: Robert Roman Date: Mon, 27 Sep 2021 11:41:38 -0500 Subject: [PATCH 13/72] updated to correct hyperopt.md file --- docs/hyperopt.md | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 1077ff503..16d7ca742 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -44,9 +44,8 @@ usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--data-format-ohlcv {json,jsongz,hdf5}] [--max-open-trades INT] [--stake-amount STAKE_AMOUNT] [--fee FLOAT] - [-p PAIRS [PAIRS ...]] [--hyperopt NAME] - [--hyperopt-path PATH] [--eps] [--dmmp] - [--enable-protections] + [-p PAIRS [PAIRS ...]] [--hyperopt-path PATH] + [--eps] [--dmmp] [--enable-protections] [--dry-run-wallet DRY_RUN_WALLET] [-e INT] [--spaces {all,buy,sell,roi,stoploss,trailing,protection,default} [{all,buy,sell,roi,stoploss,trailing,protection,default} ...]] [--print-all] [--no-color] [--print-json] [-j JOBS] @@ -73,10 +72,8 @@ optional arguments: -p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...] Limit command to these pairs. Pairs are space- separated. - --hyperopt NAME Specify hyperopt class name which will be used by the - bot. - --hyperopt-path PATH Specify additional lookup path for Hyperopt and - Hyperopt Loss functions. + --hyperopt-path PATH Specify additional lookup path for Hyperopt Loss + functions. --eps, --enable-position-stacking Allow buying the same pair multiple times (position stacking). @@ -561,7 +558,7 @@ For example, to use one month of data, pass `--timerange 20210101-20210201` (fro Full command: ```bash -freqtrade hyperopt --hyperopt --strategy --timerange 20210101-20210201 +freqtrade hyperopt --strategy --timerange 20210101-20210201 ``` ### Running Hyperopt with Smaller Search Space @@ -683,11 +680,11 @@ If you are optimizing ROI, Freqtrade creates the 'roi' optimization hyperspace f These ranges should be sufficient in most cases. The minutes in the steps (ROI dict keys) are scaled linearly depending on the timeframe used. The ROI values in the steps (ROI dict values) are scaled logarithmically depending on the timeframe used. -If you have the `generate_roi_table()` and `roi_space()` methods in your custom hyperopt file, remove them in order to utilize these adaptive ROI tables and the ROI hyperoptimization space generated by Freqtrade by default. +If you have the `generate_roi_table()` and `roi_space()` methods in your custom hyperopt, remove them in order to utilize these adaptive ROI tables and the ROI hyperoptimization space generated by Freqtrade by default. Override the `roi_space()` method if you need components of the ROI tables to vary in other ranges. Override the `generate_roi_table()` and `roi_space()` methods and implement your own custom approach for generation of the ROI tables during hyperoptimization if you need a different structure of the ROI tables or other amount of rows (steps). -A sample for these methods can be found in [sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py). +A sample for these methods can be found in the [overriding pre-defined spaces section](advanced-hyperopt.md#overriding-pre-defined-spaces). !!! Note "Reduced search space" To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is usually sufficient, every value more precise than this will usually result in overfitted results. You can however [overriding pre-defined spaces](advanced-hyperopt.md#pverriding-pre-defined-spaces) to change this to your needs. @@ -729,7 +726,7 @@ If you are optimizing stoploss values, Freqtrade creates the 'stoploss' optimiza If you have the `stoploss_space()` method in your custom hyperopt file, remove it in order to utilize Stoploss hyperoptimization space generated by Freqtrade by default. -Override the `stoploss_space()` method and define the desired range in it if you need stoploss values to vary in other range during hyperoptimization. A sample for this method can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py). +Override the `stoploss_space()` method and define the desired range in it if you need stoploss values to vary in other range during hyperoptimization. A sample for this method can be found in the [overriding pre-defined spaces section](advanced-hyperopt.md#overriding-pre-defined-spaces). !!! Note "Reduced search space" To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is usually sufficient, every value more precise than this will usually result in overfitted results. You can however [overriding pre-defined spaces](advanced-hyperopt.md#pverriding-pre-defined-spaces) to change this to your needs. @@ -767,10 +764,10 @@ As stated in the comment, you can also use it as the values of the corresponding If you are optimizing trailing stop values, Freqtrade creates the 'trailing' optimization hyperspace for you. By default, the `trailing_stop` parameter is always set to True in that hyperspace, the value of the `trailing_only_offset_is_reached` vary between True and False, the values of the `trailing_stop_positive` and `trailing_stop_positive_offset` parameters vary in the ranges 0.02...0.35 and 0.01...0.1 correspondingly, which is sufficient in most cases. -Override the `trailing_space()` method and define the desired range in it if you need values of the trailing stop parameters to vary in other ranges during hyperoptimization. A sample for this method can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py). +Override the `trailing_space()` method and define the desired range in it if you need values of the trailing stop parameters to vary in other ranges during hyperoptimization. A sample for this method can be found in the [overriding pre-defined spaces section](advanced-hyperopt.md#overriding-pre-defined-spaces). !!! Note "Reduced search space" - To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is usually sufficient, every value more precise than this will usually result in overfitted results. You can however [overriding pre-defined spaces](advanced-hyperopt.md#pverriding-pre-defined-spaces) to change this to your needs. + To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is usually sufficient, every value more precise than this will usually result in overfitted results. You can however [overriding pre-defined spaces](advanced-hyperopt.md#overriding-pre-defined-spaces) to change this to your needs. ### Reproducible results From a1566fe5d70b1f526c493d007e1cbee137064685 Mon Sep 17 00:00:00 2001 From: Robert Roman Date: Mon, 27 Sep 2021 11:47:03 -0500 Subject: [PATCH 14/72] updated to latest constant.py file --- freqtrade/constants.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index bb50d385b..996e39499 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -5,7 +5,6 @@ bot constants """ from typing import List, Tuple - DEFAULT_CONFIG = 'config.json' DEFAULT_EXCHANGE = 'bittrex' PROCESS_THROTTLE_SECS = 5 # sec @@ -52,7 +51,6 @@ ENV_VAR_PREFIX = 'FREQTRADE__' NON_OPEN_EXCHANGE_STATES = ('cancelled', 'canceled', 'closed', 'expired') - # Define decimals per coin for outputs # Only used for outputs. DECIMAL_PER_COIN_FALLBACK = 3 # Should be low to avoid listing all possible FIAT's @@ -66,13 +64,10 @@ DUST_PER_COIN = { 'ETH': 0.01 } - # Source files with destination directories within user-directory USER_DATA_FILES = { 'sample_strategy.py': USERPATH_STRATEGIES, - 'sample_hyperopt_advanced.py': USERPATH_HYPEROPTS, 'sample_hyperopt_loss.py': USERPATH_HYPEROPTS, - 'sample_hyperopt.py': USERPATH_HYPEROPTS, 'strategy_analysis_example.ipynb': USERPATH_NOTEBOOKS, } @@ -195,7 +190,7 @@ CONF_SCHEMA = { 'required': ['price_side'] }, 'custom_price_max_distance_ratio': { - 'type': 'number', 'minimum': 0.0 + 'type': 'number', 'minimum': 0.0 }, 'order_types': { 'type': 'object', @@ -348,13 +343,13 @@ CONF_SCHEMA = { }, 'dataformat_ohlcv': { 'type': 'string', - 'enum': AVAILABLE_DATAHANDLERS, - 'default': 'json' + 'enum': AVAILABLE_DATAHANDLERS, + 'default': 'json' }, 'dataformat_trades': { 'type': 'string', - 'enum': AVAILABLE_DATAHANDLERS, - 'default': 'jsongz' + 'enum': AVAILABLE_DATAHANDLERS, + 'default': 'jsongz' } }, 'definitions': { From 67e9626da1a814d665307d0100864f8460e5a98a Mon Sep 17 00:00:00 2001 From: Robert Roman Date: Mon, 27 Sep 2021 12:16:57 -0500 Subject: [PATCH 15/72] fixed isort issue --- freqtrade/constants.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 996e39499..4a28fe90e 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -5,6 +5,7 @@ bot constants """ from typing import List, Tuple + DEFAULT_CONFIG = 'config.json' DEFAULT_EXCHANGE = 'bittrex' PROCESS_THROTTLE_SECS = 5 # sec From c3414c3b78eeddf2f1af4877a64c61643fa2a52e Mon Sep 17 00:00:00 2001 From: Robert Roman Date: Mon, 27 Sep 2021 17:32:49 -0500 Subject: [PATCH 16/72] resolved mypy error error: Signature of "hyperopt_loss_function" incompatible with supertype "IHyperOptLoss" --- freqtrade/optimize/hyperopt_loss_calmar.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt_loss_calmar.py b/freqtrade/optimize/hyperopt_loss_calmar.py index 45a7cd7db..802aa949b 100644 --- a/freqtrade/optimize/hyperopt_loss_calmar.py +++ b/freqtrade/optimize/hyperopt_loss_calmar.py @@ -27,6 +27,8 @@ class CalmarHyperOptLoss(IHyperOptLoss): trade_count: int, min_date: datetime, max_date: datetime, + config: Dict, + processed: Dict[str, DataFrame], backtest_stats: Dict[str, Any], *args, **kwargs @@ -52,7 +54,7 @@ class CalmarHyperOptLoss(IHyperOptLoss): except ValueError: max_drawdown = 0 - if max_drawdown != 0: + if max_drawdown != 0 and trade_count > 2000: calmar_ratio = expected_returns_mean / max_drawdown * msqrt(365) else: # Define high (negative) calmar ratio to be clear that this is NOT optimal. From 626a40252d445d7f2108175a931820e41db4f7bf Mon Sep 17 00:00:00 2001 From: Robert Roman Date: Mon, 27 Sep 2021 17:33:29 -0500 Subject: [PATCH 17/72] resolved mypy error error: Signature of "hyperopt_loss_function" incompatible with supertype "IHyperOptLoss" --- freqtrade/optimize/hyperopt_loss_calmar_daily.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/freqtrade/optimize/hyperopt_loss_calmar_daily.py b/freqtrade/optimize/hyperopt_loss_calmar_daily.py index c7651a72a..e99bc2c99 100644 --- a/freqtrade/optimize/hyperopt_loss_calmar_daily.py +++ b/freqtrade/optimize/hyperopt_loss_calmar_daily.py @@ -26,6 +26,8 @@ class CalmarHyperOptLossDaily(IHyperOptLoss): trade_count: int, min_date: datetime, max_date: datetime, + config: Dict, + processed: Dict[str, DataFrame], backtest_stats: Dict[str, Any], *args, **kwargs From fde10f5395993179be0833ff6c26ac4af74eae98 Mon Sep 17 00:00:00 2001 From: Simon Ebner Date: Sat, 23 Oct 2021 12:25:09 +0200 Subject: [PATCH 18/72] Use pathlib.stem instead of str(x).ends_with --- freqtrade/resolvers/iresolver.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/resolvers/iresolver.py b/freqtrade/resolvers/iresolver.py index 2cccec70a..c6f97c976 100644 --- a/freqtrade/resolvers/iresolver.py +++ b/freqtrade/resolvers/iresolver.py @@ -91,7 +91,7 @@ class IResolver: logger.debug(f"Searching for {cls.object_type.__name__} {object_name} in '{directory}'") for entry in directory.iterdir(): # Only consider python files - if not str(entry).endswith('.py'): + if entry.suffix != '.py': logger.debug('Ignoring %s', entry) continue if entry.is_symlink() and not entry.is_file(): @@ -169,7 +169,7 @@ class IResolver: objects = [] for entry in directory.iterdir(): # Only consider python files - if not str(entry).endswith('.py'): + if entry.suffix != '.py': logger.debug('Ignoring %s', entry) continue module_path = entry.resolve() From 5f309627eac656b65bfee9ce5761a138c3f809cd Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 24 Oct 2021 09:01:13 +0200 Subject: [PATCH 19/72] Update tests for Calmar ratio --- tests/optimize/test_hyperoptloss.py | 37 ++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/tests/optimize/test_hyperoptloss.py b/tests/optimize/test_hyperoptloss.py index a39190934..fd835c678 100644 --- a/tests/optimize/test_hyperoptloss.py +++ b/tests/optimize/test_hyperoptloss.py @@ -5,6 +5,7 @@ import pytest from freqtrade.exceptions import OperationalException from freqtrade.optimize.hyperopt_loss_short_trade_dur import ShortTradeDurHyperOptLoss +from freqtrade.optimize.optimize_reports import generate_strategy_stats from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver @@ -85,6 +86,9 @@ def test_loss_calculation_has_limited_profit(hyperopt_conf, hyperopt_results) -> "SharpeHyperOptLoss", "SharpeHyperOptLossDaily", "MaxDrawDownHyperOptLoss", + "CalmarHyperOptLossDaily", + "CalmarHyperOptLoss", + ]) def test_loss_functions_better_profits(default_conf, hyperopt_results, lossfunction) -> None: results_over = hyperopt_results.copy() @@ -96,11 +100,32 @@ def test_loss_functions_better_profits(default_conf, hyperopt_results, lossfunct default_conf.update({'hyperopt_loss': lossfunction}) hl = HyperOptLossResolver.load_hyperoptloss(default_conf) - correct = hl.hyperopt_loss_function(hyperopt_results, len(hyperopt_results), - datetime(2019, 1, 1), datetime(2019, 5, 1)) - over = hl.hyperopt_loss_function(results_over, len(results_over), - datetime(2019, 1, 1), datetime(2019, 5, 1)) - under = hl.hyperopt_loss_function(results_under, len(results_under), - datetime(2019, 1, 1), datetime(2019, 5, 1)) + correct = hl.hyperopt_loss_function( + hyperopt_results, + trade_count=len(hyperopt_results), + min_date=datetime(2019, 1, 1), + max_date=datetime(2019, 5, 1), + config=default_conf, + processed=None, + backtest_stats={'profit_total': hyperopt_results['profit_abs'].sum()} + ) + over = hl.hyperopt_loss_function( + results_over, + trade_count=len(results_over), + min_date=datetime(2019, 1, 1), + max_date=datetime(2019, 5, 1), + config=default_conf, + processed=None, + backtest_stats={'profit_total': results_over['profit_abs'].sum()} + ) + under = hl.hyperopt_loss_function( + results_under, + trade_count=len(results_under), + min_date=datetime(2019, 1, 1), + max_date=datetime(2019, 5, 1), + config=default_conf, + processed=None, + backtest_stats={'profit_total': results_under['profit_abs'].sum()} + ) assert over < correct assert under > correct From df033d92ef4743093fec3b85f5d4b8f9e09d7606 Mon Sep 17 00:00:00 2001 From: Simon Ebner Date: Sat, 23 Oct 2021 09:20:00 +0200 Subject: [PATCH 20/72] Improve performance of decimalspace.py decimalspace.py is heavily used in the hyperoptimization. The following benchmark code runs an optimization which is taken from optimizing a real strategy (wtc). The optimized version takes on my machine approx. 11/12s compared to the original 32s. Results are equivalent in both cases. ``` import freqtrade.optimize.space import numpy as np import skopt import timeit def init(): Decimal = freqtrade.optimize.space.decimalspace.SKDecimal Integer = skopt.space.space.Integer dimensions = [Decimal(low=-1.0, high=1.0, decimals=4, prior='uniform', transform='identity')] * 20 return skopt.Optimizer( dimensions, base_estimator="ET", acq_optimizer="auto", n_initial_points=5, acq_optimizer_kwargs={'n_jobs': 96}, random_state=0, model_queue_size=10, ) def test(): opt = init() actual = opt.ask(n_points=2) expected = [[ 0.7515, -0.4723, -0.6941, -0.7988, 0.0448, 0.8605, -0.108, 0.5399, 0.763, -0.2948, 0.8345, -0.7683, 0.7077, -0.2478, -0.333, 0.8575, 0.6108, 0.4514, 0.5982, 0.3506 ], [ 0.5563, 0.7386, -0.6407, 0.9073, -0.5211, -0.8167, -0.3771, -0.0318, 0.2861, 0.1176, 0.0943, -0.6077, -0.9317, -0.5372, -0.4934, -0.3637, -0.8035, -0.8627, -0.5399, 0.6036 ]] absdiff = np.max(np.abs(np.asarray(expected) - np.asarray(actual))) assert absdiff < 1e-5 def time(): opt = init() print('dt', timeit.timeit("opt.ask(n_points=20)", globals=locals())) if __name__ == "__main__": test() time() ``` --- freqtrade/optimize/space/decimalspace.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/freqtrade/optimize/space/decimalspace.py b/freqtrade/optimize/space/decimalspace.py index 643999cc1..220502e69 100644 --- a/freqtrade/optimize/space/decimalspace.py +++ b/freqtrade/optimize/space/decimalspace.py @@ -7,11 +7,15 @@ class SKDecimal(Integer): def __init__(self, low, high, decimals=3, prior="uniform", base=10, transform=None, name=None, dtype=np.int64): self.decimals = decimals - _low = int(low * pow(10, self.decimals)) - _high = int(high * pow(10, self.decimals)) + + self.pow_dot_one = pow(0.1, self.decimals) + self.pow_ten = pow(10, self.decimals) + + _low = int(low * self.pow_ten) + _high = int(high * self.pow_ten) # trunc to precision to avoid points out of space - self.low_orig = round(_low * pow(0.1, self.decimals), self.decimals) - self.high_orig = round(_high * pow(0.1, self.decimals), self.decimals) + self.low_orig = round(_low * self.pow_dot_one, self.decimals) + self.high_orig = round(_high * self.pow_dot_one, self.decimals) super().__init__(_low, _high, prior, base, transform, name, dtype) @@ -25,9 +29,9 @@ class SKDecimal(Integer): return self.low_orig <= point <= self.high_orig def transform(self, Xt): - aa = [int(x * pow(10, self.decimals)) for x in Xt] - return super().transform(aa) + return super().transform([int(v * self.pow_ten) for v in Xt]) def inverse_transform(self, Xt): res = super().inverse_transform(Xt) - return [round(x * pow(0.1, self.decimals), self.decimals) for x in res] + # equivalent to [round(x * pow(0.1, self.decimals), self.decimals) for x in res] + return [int(v) / self.pow_ten for v in res] From f7926083ca9a1eaa16c401bd18bfce4a209a2d74 Mon Sep 17 00:00:00 2001 From: Simon Ebner Date: Sun, 24 Oct 2021 22:59:28 +0200 Subject: [PATCH 21/72] Clean up unclosed file handles Close all file handles that are left dangling to avoid warnings such as ``` ResourceWarning: unclosed file <_io.TextIOWrapper name='...' mode='r' encoding='UTF-8'> params = json_load(filename.open('r')) ``` --- freqtrade/optimize/hyperopt_tools.py | 10 +++++----- freqtrade/strategy/hyper.py | 3 ++- tests/optimize/test_hyperopt_tools.py | 3 ++- tests/strategy/test_strategy_loading.py | 4 ++-- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index cfbc2757e..0b2efa5c2 100755 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -1,4 +1,3 @@ - import io import logging from copy import deepcopy @@ -64,10 +63,11 @@ class HyperoptTools(): 'export_time': datetime.now(timezone.utc), } logger.info(f"Dumping parameters to {filename}") - rapidjson.dump(final_params, filename.open('w'), indent=2, - default=hyperopt_serializer, - number_mode=rapidjson.NM_NATIVE | rapidjson.NM_NAN - ) + with filename.open('w') as f: + rapidjson.dump(final_params, f, indent=2, + default=hyperopt_serializer, + number_mode=rapidjson.NM_NATIVE | rapidjson.NM_NAN + ) @staticmethod def try_export_params(config: Dict[str, Any], strategy_name: str, params: Dict): diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index dad282d7e..eaf41263a 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -381,7 +381,8 @@ class HyperStrategyMixin(object): if filename.is_file(): logger.info(f"Loading parameters from file {filename}") try: - params = json_load(filename.open('r')) + with filename.open('r') as f: + params = json_load(f) if params.get('strategy_name') != self.__class__.__name__: raise OperationalException('Invalid parameter file provided.') return params diff --git a/tests/optimize/test_hyperopt_tools.py b/tests/optimize/test_hyperopt_tools.py index 9c2b2e8fc..d9a52db39 100644 --- a/tests/optimize/test_hyperopt_tools.py +++ b/tests/optimize/test_hyperopt_tools.py @@ -209,7 +209,8 @@ def test_export_params(tmpdir): assert filename.is_file() - content = rapidjson.load(filename.open('r')) + with filename.open('r') as f: + content = rapidjson.load(f) assert content['strategy_name'] == 'StrategyTestV2' assert 'params' in content assert "buy" in content["params"] diff --git a/tests/strategy/test_strategy_loading.py b/tests/strategy/test_strategy_loading.py index 3a30a824a..3590c3e01 100644 --- a/tests/strategy/test_strategy_loading.py +++ b/tests/strategy/test_strategy_loading.py @@ -62,8 +62,8 @@ def test_load_strategy(default_conf, result): def test_load_strategy_base64(result, caplog, default_conf): - with (Path(__file__).parents[2] / 'freqtrade/templates/sample_strategy.py').open("rb") as file: - encoded_string = urlsafe_b64encode(file.read()).decode("utf-8") + filepath = Path(__file__).parents[2] / 'freqtrade/templates/sample_strategy.py' + encoded_string = urlsafe_b64encode(filepath.read_bytes()).decode("utf-8") default_conf.update({'strategy': 'SampleStrategy:{}'.format(encoded_string)}) strategy = StrategyResolver.load_strategy(default_conf) From 520c5687aa56ea60d0d021d2d944e98913913669 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Oct 2021 03:01:05 +0000 Subject: [PATCH 22/72] Bump prompt-toolkit from 3.0.20 to 3.0.21 Bumps [prompt-toolkit](https://github.com/prompt-toolkit/python-prompt-toolkit) from 3.0.20 to 3.0.21. - [Release notes](https://github.com/prompt-toolkit/python-prompt-toolkit/releases) - [Changelog](https://github.com/prompt-toolkit/python-prompt-toolkit/blob/master/CHANGELOG) - [Commits](https://github.com/prompt-toolkit/python-prompt-toolkit/commits) --- updated-dependencies: - dependency-name: prompt-toolkit dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index b10bbabf6..b12697040 100644 --- a/requirements.txt +++ b/requirements.txt @@ -41,4 +41,4 @@ psutil==5.8.0 colorama==0.4.4 # Building config files interactively questionary==1.10.0 -prompt-toolkit==3.0.20 +prompt-toolkit==3.0.21 From b50b38f049374634903c2911636dd0150dd34aa3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Oct 2021 03:01:10 +0000 Subject: [PATCH 23/72] Bump sqlalchemy from 1.4.25 to 1.4.26 Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 1.4.25 to 1.4.26. - [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases) - [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/main/CHANGES) - [Commits](https://github.com/sqlalchemy/sqlalchemy/commits) --- updated-dependencies: - dependency-name: sqlalchemy dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index b10bbabf6..27649eb7a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ ccxt==1.58.47 # Pin cryptography for now due to rust build errors with piwheels cryptography==35.0.0 aiohttp==3.7.4.post0 -SQLAlchemy==1.4.25 +SQLAlchemy==1.4.26 python-telegram-bot==13.7 arrow==1.2.0 cachetools==4.2.2 From 3d90305f8e4a130dfda821c7008ab9fdd69d8511 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Oct 2021 03:01:29 +0000 Subject: [PATCH 24/72] Bump ccxt from 1.58.47 to 1.59.2 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.58.47 to 1.59.2. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/exchanges.cfg) - [Commits](https://github.com/ccxt/ccxt/compare/1.58.47...1.59.2) --- updated-dependencies: - dependency-name: ccxt dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index b10bbabf6..b3e5b1cc7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.21.2 pandas==1.3.4 pandas-ta==0.3.14b -ccxt==1.58.47 +ccxt==1.59.2 # Pin cryptography for now due to rust build errors with piwheels cryptography==35.0.0 aiohttp==3.7.4.post0 From 826d4eb2f42bea1656c6a3bf8f8e5519fbfc5479 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Oct 2021 03:01:33 +0000 Subject: [PATCH 25/72] Bump jsonschema from 4.1.0 to 4.1.2 Bumps [jsonschema](https://github.com/Julian/jsonschema) from 4.1.0 to 4.1.2. - [Release notes](https://github.com/Julian/jsonschema/releases) - [Changelog](https://github.com/Julian/jsonschema/blob/main/CHANGELOG.rst) - [Commits](https://github.com/Julian/jsonschema/compare/v4.1.0...v4.1.2) --- updated-dependencies: - dependency-name: jsonschema dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index b10bbabf6..504123583 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ arrow==1.2.0 cachetools==4.2.2 requests==2.26.0 urllib3==1.26.7 -jsonschema==4.1.0 +jsonschema==4.1.2 TA-Lib==0.4.21 technical==1.3.0 tabulate==0.8.9 From 538d9e8b375d863105855901d4cccde8d98770bf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Oct 2021 04:40:24 +0000 Subject: [PATCH 26/72] Bump arrow from 1.2.0 to 1.2.1 Bumps [arrow](https://github.com/arrow-py/arrow) from 1.2.0 to 1.2.1. - [Release notes](https://github.com/arrow-py/arrow/releases) - [Changelog](https://github.com/arrow-py/arrow/blob/master/CHANGELOG.rst) - [Commits](https://github.com/arrow-py/arrow/compare/1.2.0...1.2.1) --- updated-dependencies: - dependency-name: arrow dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 84fd2b771..10abecd0d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ cryptography==35.0.0 aiohttp==3.7.4.post0 SQLAlchemy==1.4.26 python-telegram-bot==13.7 -arrow==1.2.0 +arrow==1.2.1 cachetools==4.2.2 requests==2.26.0 urllib3==1.26.7 From 4e88bd07fa0073584f1bead80a2ded02f14606ff Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Oct 2021 04:40:30 +0000 Subject: [PATCH 27/72] Bump numpy from 1.21.2 to 1.21.3 Bumps [numpy](https://github.com/numpy/numpy) from 1.21.2 to 1.21.3. - [Release notes](https://github.com/numpy/numpy/releases) - [Changelog](https://github.com/numpy/numpy/blob/main/doc/HOWTO_RELEASE.rst.txt) - [Commits](https://github.com/numpy/numpy/compare/v1.21.2...v1.21.3) --- updated-dependencies: - dependency-name: numpy dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 84fd2b771..04683b97a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -numpy==1.21.2 +numpy==1.21.3 pandas==1.3.4 pandas-ta==0.3.14b From cea251c83c771ae2d3a220d4103569bb46cd6040 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 25 Oct 2021 06:46:02 +0200 Subject: [PATCH 28/72] Clarify documentation for /forcebuy closes #5783 --- docs/telegram-usage.md | 2 +- freqtrade/rpc/telegram.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index b9d01a236..0c45fbbf1 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -171,7 +171,7 @@ official commands. You can ask at any moment for help with `/help`. | `/profit []` | Display a summary of your profit/loss from close trades and some stats about your performance, over the last n days (all trades by default) | `/forcesell ` | Instantly sells the given trade (Ignoring `minimum_roi`). | `/forcesell all` | Instantly sells all open trades (Ignoring `minimum_roi`). -| `/forcebuy [rate]` | Instantly buys the given pair. Rate is optional. (`forcebuy_enable` must be set to True) +| `/forcebuy [rate]` | Instantly buys the given pair. Rate is optional and only applies to limit orders. (`forcebuy_enable` must be set to True) | `/performance` | Show performance of each finished trade grouped by pair | `/balance` | Show account balance per currency | `/daily ` | Shows profit or loss per day, over the last n days (n defaults to 7) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 846747f40..771d8bfa4 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -1033,7 +1033,7 @@ class Telegram(RPCHandler): :return: None """ forcebuy_text = ("*/forcebuy []:* `Instantly buys the given pair. " - "Optionally takes a rate at which to buy.` \n") + "Optionally takes a rate at which to buy (only applies to limit orders).` \n") message = ("*/start:* `Starts the trader`\n" "*/stop:* `Stops the trader`\n" "*/status |[table]:* `Lists all open trades`\n" From 262f186a37cf58ec5720f872fe94ad25c3992052 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 25 Oct 2021 07:19:47 +0200 Subject: [PATCH 29/72] . --- freqtrade/rpc/telegram.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 771d8bfa4..073583940 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -1033,7 +1033,8 @@ class Telegram(RPCHandler): :return: None """ forcebuy_text = ("*/forcebuy []:* `Instantly buys the given pair. " - "Optionally takes a rate at which to buy (only applies to limit orders).` \n") + "Optionally takes a rate at which to buy " + "(only applies to limit orders).` \n") message = ("*/start:* `Starts the trader`\n" "*/stop:* `Stops the trader`\n" "*/status |[table]:* `Lists all open trades`\n" From 88b96d5d1b1aea451331bd92a664e7a92b00dfed Mon Sep 17 00:00:00 2001 From: Robert Roman Date: Mon, 25 Oct 2021 00:45:10 -0500 Subject: [PATCH 30/72] Update hyperopt_loss_calmar.py --- freqtrade/optimize/hyperopt_loss_calmar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt_loss_calmar.py b/freqtrade/optimize/hyperopt_loss_calmar.py index 802aa949b..ace08794a 100644 --- a/freqtrade/optimize/hyperopt_loss_calmar.py +++ b/freqtrade/optimize/hyperopt_loss_calmar.py @@ -54,7 +54,7 @@ class CalmarHyperOptLoss(IHyperOptLoss): except ValueError: max_drawdown = 0 - if max_drawdown != 0 and trade_count > 2000: + if max_drawdown != 0: calmar_ratio = expected_returns_mean / max_drawdown * msqrt(365) else: # Define high (negative) calmar ratio to be clear that this is NOT optimal. From c3f3bdaa2ae03ebffba27ba71cf1b7276fa77e76 Mon Sep 17 00:00:00 2001 From: incrementby1 Date: Tue, 26 Oct 2021 00:04:40 +0200 Subject: [PATCH 31/72] Add "allow_position_stacking" value to config, which allows rebuys of a pair Add function unlock_reason(str: pair) which removes all PairLocks with reason Provide demo strategy that allows buying the same pair multiple times --- StackingConfig.json | 89 +++ StackingDemo.py | 593 +++++++++++++++++++ freqtrade/configuration/configuration.py | 6 + freqtrade/freqtradebot.py | 17 +- freqtrade/persistence/pairlock_middleware.py | 18 + freqtrade/strategy/interface.py | 9 + 6 files changed, 727 insertions(+), 5 deletions(-) create mode 100644 StackingConfig.json create mode 100644 StackingDemo.py diff --git a/StackingConfig.json b/StackingConfig.json new file mode 100644 index 000000000..750ef92c6 --- /dev/null +++ b/StackingConfig.json @@ -0,0 +1,89 @@ + +{ + "max_open_trades": 12, + "stake_currency": "USDT", + "stake_amount": 100, + "tradable_balance_ratio": 0.99, + "fiat_display_currency": "USD", + "timeframe": "5m", + "dry_run": true, + "cancel_open_orders_on_exit": false, + "allow_position_stacking": true, + "unfilledtimeout": { + "buy": 10, + "sell": 30, + "unit": "minutes" + }, + "bid_strategy": { + "price_side": "ask", + "ask_last_balance": 0.0, + "use_order_book": true, + "order_book_top": 1, + "check_depth_of_market": { + "enabled": false, + "bids_to_ask_delta": 1 + } + }, + "ask_strategy": { + "price_side": "bid", + "use_order_book": true, + "order_book_top": 1 + }, + "exchange": { + "name": "binance", + "key": "", + "secret": "", + "ccxt_config": {}, + "ccxt_async_config": {}, + "pair_whitelist": [ + ], + "pair_blacklist": [ + "BNB/.*" + ] + }, + "pairlists": [ + { + "method": "VolumePairList", + "number_assets": 80, + "sort_key": "quoteVolume", + "min_value": 0, + "refresh_period": 1800 + } + ], + "edge": { + "enabled": false, + "process_throttle_secs": 3600, + "calculate_since_number_of_days": 7, + "allowed_risk": 0.01, + "stoploss_range_min": -0.01, + "stoploss_range_max": -0.1, + "stoploss_range_step": -0.01, + "minimum_winrate": 0.60, + "minimum_expectancy": 0.20, + "min_trade_number": 10, + "max_trade_duration_minute": 1440, + "remove_pumps": false + }, + "telegram": { + "enabled": false, + "token": "", + "chat_id": "" + }, + "api_server": { + "enabled": true, + "listen_ip_address": "127.0.0.1", + "listen_port": 8080, + "verbosity": "error", + "enable_openapi": false, + "jwt_secret_key": "908cd4469c824f3838bfe56e4120d3a3dbda5294ef583ffc62c82f54d2c1bf58", + "CORS_origins": [], + "username": "user", + "password": "pass" + }, + "bot_name": "freqtrade", + "initial_state": "running", + "forcebuy_enable": false, + "internals": { + "process_throttle_secs": 5 + } +} diff --git a/StackingDemo.py b/StackingDemo.py new file mode 100644 index 000000000..739e847b7 --- /dev/null +++ b/StackingDemo.py @@ -0,0 +1,593 @@ +# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement +# flake8: noqa: F401 + +# --- Do not remove these libs --- +import numpy as np # noqa +import pandas as pd # noqa +from pandas import DataFrame + +from freqtrade.strategy import (BooleanParameter, CategoricalParameter, DecimalParameter, + IStrategy, IntParameter) + +# -------------------------------- +# Add your lib to import here +import talib.abstract as ta +import freqtrade.vendor.qtpylib.indicators as qtpylib + +from freqtrade.persistence import Trade +from datetime import datetime,timezone,timedelta + +""" + Warning: +This is still work in progress, so there is no warranty that everything works as intended, +it is possible that this strategy results in huge losses or doesn't even work at all. +Make sure to only run this in dry_mode so you don't lose any money. + +""" + +class StackingDemo(IStrategy): + """ + This is the default strategy template with added functions for trade stacking / buying the same positions multiple times. + It should function like this: + Find good buys using indicators. + When a new buy occurs the strategy will enable rebuys of the pair like this: + self.custom_info[metadata["pair"]]["rebuy"] = 1 + Then, if the price should drop after the last buy within the timerange of rebuy_time_limit_hours, + the same pair will be purchased again. This is intended to help with reducing possible losses. + If the price only goes up after the first buy, the strategy won't buy this pair again, and after the time limit is over, + look for other pairs to buy. + For selling there is this flag: + self.custom_info[metadata["pair"]]["resell"] = 1 + which should simply sell all trades of this pair until none are left. + + You can set how many pairs you want to trade and how many trades you want to allow for a pair, + but you must make sure to set max_open_trades to the produce of max_open_pairs and max_open_trades in your configuration file. + Also allow_position_stacking has to be set to true in the configuration file. + + For backtesting make sure to provide --enable-position-stacking as an argument in the command line. + Backtesting will be slow. + Hyperopt was not tested. + + # run the bot: + freqtrade trade -c StackingConfig.json -s StackingDemo --db-url sqlite:///tradesv3_StackingDemo_dry-run.sqlite --dry-run + """ + # Strategy interface version - allow new iterations of the strategy interface. + # Check the documentation or the Sample strategy to get the latest version. + INTERFACE_VERSION = 2 + + # how many pairs to trade / trades per pair if allow_position_stacking is enabled + max_open_pairs, max_trades_per_pair = 4, 3 + # make sure to have this value in your config file + max_open_trades = max_open_pairs * max_trades_per_pair + + # debugging + print_trades = True + + # specify for how long to want to allow rebuys of this pair + rebuy_time_limit_hours = 2 + + # store additional information needed for this strategy: + custom_info = {} + custom_num_open_pairs = {} + + # Minimal ROI designed for the strategy. + # This attribute will be overridden if the config file contains "minimal_roi". + minimal_roi = { +# "60": 0.01, +# "30": 0.02, + "0": 0.001 + } + + # Optimal stoploss designed for the strategy. + # This attribute will be overridden if the config file contains "stoploss". + stoploss = -0.10 + + # Trailing stoploss + trailing_stop = False + # trailing_only_offset_is_reached = False + # trailing_stop_positive = 0.01 + # trailing_stop_positive_offset = 0.0 # Disabled / not configured + + # Optimal timeframe for the strategy. + timeframe = '5m' + + # Run "populate_indicators()" only for new candle. + process_only_new_candles = False + + # These values can be overridden in the "ask_strategy" section in the config. + use_sell_signal = True + sell_profit_only = False + ignore_roi_if_buy_signal = False + + # Number of candles the strategy requires before producing valid signals + startup_candle_count: int = 30 + + # Optional order type mapping. + order_types = { + 'buy': 'market', + 'sell': 'market', + 'stoploss': 'market', + 'stoploss_on_exchange': False + } + + # Optional order time in force. + order_time_in_force = { + 'buy': 'gtc', + 'sell': 'gtc' + } + + plot_config = { + # Main plot indicators (Moving averages, ...) + 'main_plot': { + 'tema': {}, + 'sar': {'color': 'white'}, + }, + 'subplots': { + # Subplots - each dict defines one additional plot + "MACD": { + 'macd': {'color': 'blue'}, + 'macdsignal': {'color': 'orange'}, + }, + "RSI": { + 'rsi': {'color': 'red'}, + } + } + } + def informative_pairs(self): + """ + Define additional, informative pair/interval combinations to be cached from the exchange. + These pair/interval combinations are non-tradeable, unless they are part + of the whitelist as well. + For more information, please consult the documentation + :return: List of tuples in the format (pair, interval) + Sample: return [("ETH/USDT", "5m"), + ("BTC/USDT", "15m"), + ] + """ + return [] + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Adds several different TA indicators to the given DataFrame + + Performance Note: For the best performance be frugal on the number of indicators + you are using. Let uncomment only the indicator you are using in your strategies + or your hyperopt configuration, otherwise you will waste your memory and CPU usage. + :param dataframe: Dataframe with data from the exchange + :param metadata: Additional information, like the currently traded pair + :return: a Dataframe with all mandatory indicators for the strategies + """ + + # STACKING STUFF + + # confirm config + self.max_trades_per_pair = self.config['max_open_trades'] / self.max_open_pairs + if not self.config["allow_position_stacking"]: + self.max_trades_per_pair = 1 + + # store number of open pairs + self.custom_num_open_pairs = {"num_open_pairs": 0} + + # Store custom information for this pair: + if not metadata["pair"] in self.custom_info: + self.custom_info[metadata["pair"]] = {} + + if not "rebuy" in self.custom_info[metadata["pair"]]: + # number of trades for this pair + self.custom_info[metadata["pair"]]["num_trades"] = 0 + # use rebuy/resell as buy-/sell- indicators + self.custom_info[metadata["pair"]]["rebuy"] = 0 + self.custom_info[metadata["pair"]]["resell"] = 0 + # store latest open_date for this pair + self.custom_info[metadata["pair"]]["last_open_date"] = datetime.now(timezone.utc) - timedelta(days=100) + # stare the value of the latest open price for this pair + self.custom_info[metadata["pair"]]["latest_open_rate"] = 0 + + # INDICATORS + + # Momentum Indicators + # ------------------------------------ + + # ADX + dataframe['adx'] = ta.ADX(dataframe) + + # # Plus Directional Indicator / Movement + # dataframe['plus_dm'] = ta.PLUS_DM(dataframe) + # dataframe['plus_di'] = ta.PLUS_DI(dataframe) + + # # Minus Directional Indicator / Movement + # dataframe['minus_dm'] = ta.MINUS_DM(dataframe) + # dataframe['minus_di'] = ta.MINUS_DI(dataframe) + + # # Aroon, Aroon Oscillator + # aroon = ta.AROON(dataframe) + # dataframe['aroonup'] = aroon['aroonup'] + # dataframe['aroondown'] = aroon['aroondown'] + # dataframe['aroonosc'] = ta.AROONOSC(dataframe) + + # # Awesome Oscillator + # dataframe['ao'] = qtpylib.awesome_oscillator(dataframe) + + # # Keltner Channel + # keltner = qtpylib.keltner_channel(dataframe) + # dataframe["kc_upperband"] = keltner["upper"] + # dataframe["kc_lowerband"] = keltner["lower"] + # dataframe["kc_middleband"] = keltner["mid"] + # dataframe["kc_percent"] = ( + # (dataframe["close"] - dataframe["kc_lowerband"]) / + # (dataframe["kc_upperband"] - dataframe["kc_lowerband"]) + # ) + # dataframe["kc_width"] = ( + # (dataframe["kc_upperband"] - dataframe["kc_lowerband"]) / dataframe["kc_middleband"] + # ) + + # # Ultimate Oscillator + # dataframe['uo'] = ta.ULTOSC(dataframe) + + # # Commodity Channel Index: values [Oversold:-100, Overbought:100] + # dataframe['cci'] = ta.CCI(dataframe) + + # RSI + dataframe['rsi'] = ta.RSI(dataframe) + + # # Inverse Fisher transform on RSI: values [-1.0, 1.0] (https://goo.gl/2JGGoy) + # rsi = 0.1 * (dataframe['rsi'] - 50) + # dataframe['fisher_rsi'] = (np.exp(2 * rsi) - 1) / (np.exp(2 * rsi) + 1) + + # # Inverse Fisher transform on RSI normalized: values [0.0, 100.0] (https://goo.gl/2JGGoy) + # dataframe['fisher_rsi_norma'] = 50 * (dataframe['fisher_rsi'] + 1) + + # # Stochastic Slow + # stoch = ta.STOCH(dataframe) + # dataframe['slowd'] = stoch['slowd'] + # dataframe['slowk'] = stoch['slowk'] + + # Stochastic Fast + stoch_fast = ta.STOCHF(dataframe) + dataframe['fastd'] = stoch_fast['fastd'] + dataframe['fastk'] = stoch_fast['fastk'] + + # # Stochastic RSI + # Please read https://github.com/freqtrade/freqtrade/issues/2961 before using this. + # STOCHRSI is NOT aligned with tradingview, which may result in non-expected results. + # stoch_rsi = ta.STOCHRSI(dataframe) + # dataframe['fastd_rsi'] = stoch_rsi['fastd'] + # dataframe['fastk_rsi'] = stoch_rsi['fastk'] + + # MACD + macd = ta.MACD(dataframe) + dataframe['macd'] = macd['macd'] + dataframe['macdsignal'] = macd['macdsignal'] + dataframe['macdhist'] = macd['macdhist'] + + # MFI + dataframe['mfi'] = ta.MFI(dataframe) + + # # ROC + # dataframe['roc'] = ta.ROC(dataframe) + + # Overlap Studies + # ------------------------------------ + + # Bollinger Bands + bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2) + dataframe['bb_lowerband'] = bollinger['lower'] + dataframe['bb_middleband'] = bollinger['mid'] + dataframe['bb_upperband'] = bollinger['upper'] + dataframe["bb_percent"] = ( + (dataframe["close"] - dataframe["bb_lowerband"]) / + (dataframe["bb_upperband"] - dataframe["bb_lowerband"]) + ) + dataframe["bb_width"] = ( + (dataframe["bb_upperband"] - dataframe["bb_lowerband"]) / dataframe["bb_middleband"] + ) + + # Bollinger Bands - Weighted (EMA based instead of SMA) + # weighted_bollinger = qtpylib.weighted_bollinger_bands( + # qtpylib.typical_price(dataframe), window=20, stds=2 + # ) + # dataframe["wbb_upperband"] = weighted_bollinger["upper"] + # dataframe["wbb_lowerband"] = weighted_bollinger["lower"] + # dataframe["wbb_middleband"] = weighted_bollinger["mid"] + # dataframe["wbb_percent"] = ( + # (dataframe["close"] - dataframe["wbb_lowerband"]) / + # (dataframe["wbb_upperband"] - dataframe["wbb_lowerband"]) + # ) + # dataframe["wbb_width"] = ( + # (dataframe["wbb_upperband"] - dataframe["wbb_lowerband"]) / dataframe["wbb_middleband"] + # ) + + # # EMA - Exponential Moving Average + # dataframe['ema3'] = ta.EMA(dataframe, timeperiod=3) + # dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5) + # dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10) + # dataframe['ema21'] = ta.EMA(dataframe, timeperiod=21) + # dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50) + # dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100) + + # # SMA - Simple Moving Average + # dataframe['sma3'] = ta.SMA(dataframe, timeperiod=3) + # dataframe['sma5'] = ta.SMA(dataframe, timeperiod=5) + # dataframe['sma10'] = ta.SMA(dataframe, timeperiod=10) + # dataframe['sma21'] = ta.SMA(dataframe, timeperiod=21) + # dataframe['sma50'] = ta.SMA(dataframe, timeperiod=50) + # dataframe['sma100'] = ta.SMA(dataframe, timeperiod=100) + + # Parabolic SAR + dataframe['sar'] = ta.SAR(dataframe) + + # TEMA - Triple Exponential Moving Average + dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9) + + # Cycle Indicator + # ------------------------------------ + # Hilbert Transform Indicator - SineWave + hilbert = ta.HT_SINE(dataframe) + dataframe['htsine'] = hilbert['sine'] + dataframe['htleadsine'] = hilbert['leadsine'] + + # Pattern Recognition - Bullish candlestick patterns + # ------------------------------------ + # # Hammer: values [0, 100] + # dataframe['CDLHAMMER'] = ta.CDLHAMMER(dataframe) + # # Inverted Hammer: values [0, 100] + # dataframe['CDLINVERTEDHAMMER'] = ta.CDLINVERTEDHAMMER(dataframe) + # # Dragonfly Doji: values [0, 100] + # dataframe['CDLDRAGONFLYDOJI'] = ta.CDLDRAGONFLYDOJI(dataframe) + # # Piercing Line: values [0, 100] + # dataframe['CDLPIERCING'] = ta.CDLPIERCING(dataframe) # values [0, 100] + # # Morningstar: values [0, 100] + # dataframe['CDLMORNINGSTAR'] = ta.CDLMORNINGSTAR(dataframe) # values [0, 100] + # # Three White Soldiers: values [0, 100] + # dataframe['CDL3WHITESOLDIERS'] = ta.CDL3WHITESOLDIERS(dataframe) # values [0, 100] + + # Pattern Recognition - Bearish candlestick patterns + # ------------------------------------ + # # Hanging Man: values [0, 100] + # dataframe['CDLHANGINGMAN'] = ta.CDLHANGINGMAN(dataframe) + # # Shooting Star: values [0, 100] + # dataframe['CDLSHOOTINGSTAR'] = ta.CDLSHOOTINGSTAR(dataframe) + # # Gravestone Doji: values [0, 100] + # dataframe['CDLGRAVESTONEDOJI'] = ta.CDLGRAVESTONEDOJI(dataframe) + # # Dark Cloud Cover: values [0, 100] + # dataframe['CDLDARKCLOUDCOVER'] = ta.CDLDARKCLOUDCOVER(dataframe) + # # Evening Doji Star: values [0, 100] + # dataframe['CDLEVENINGDOJISTAR'] = ta.CDLEVENINGDOJISTAR(dataframe) + # # Evening Star: values [0, 100] + # dataframe['CDLEVENINGSTAR'] = ta.CDLEVENINGSTAR(dataframe) + + # Pattern Recognition - Bullish/Bearish candlestick patterns + # ------------------------------------ + # # Three Line Strike: values [0, -100, 100] + # dataframe['CDL3LINESTRIKE'] = ta.CDL3LINESTRIKE(dataframe) + # # Spinning Top: values [0, -100, 100] + # dataframe['CDLSPINNINGTOP'] = ta.CDLSPINNINGTOP(dataframe) # values [0, -100, 100] + # # Engulfing: values [0, -100, 100] + # dataframe['CDLENGULFING'] = ta.CDLENGULFING(dataframe) # values [0, -100, 100] + # # Harami: values [0, -100, 100] + # dataframe['CDLHARAMI'] = ta.CDLHARAMI(dataframe) # values [0, -100, 100] + # # Three Outside Up/Down: values [0, -100, 100] + # dataframe['CDL3OUTSIDE'] = ta.CDL3OUTSIDE(dataframe) # values [0, -100, 100] + # # Three Inside Up/Down: values [0, -100, 100] + # dataframe['CDL3INSIDE'] = ta.CDL3INSIDE(dataframe) # values [0, -100, 100] + + # # Chart type + # # ------------------------------------ + # # Heikin Ashi Strategy + # heikinashi = qtpylib.heikinashi(dataframe) + # dataframe['ha_open'] = heikinashi['open'] + # dataframe['ha_close'] = heikinashi['close'] + # dataframe['ha_high'] = heikinashi['high'] + # dataframe['ha_low'] = heikinashi['low'] + + # Retrieve best bid and best ask from the orderbook + # ------------------------------------ + """ + # first check if dataprovider is available + if self.dp: + if self.dp.runmode.value in ('live', 'dry_run'): + ob = self.dp.orderbook(metadata['pair'], 1) + dataframe['best_bid'] = ob['bids'][0][0] + dataframe['best_ask'] = ob['asks'][0][0] + """ + + return dataframe + + def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Based on TA indicators, populates the buy signal for the given dataframe + :param dataframe: DataFrame populated with indicators + :param metadata: Additional information, like the currently traded pair + :return: DataFrame with buy column + """ + dataframe.loc[ + ( + ( +# (qtpylib.crossed_above(dataframe['rsi'], 30)) & # Signal: RSI crosses above 30 +# (dataframe['tema'] <= dataframe['bb_middleband']) & # Guard: tema below BB middle +# (dataframe['tema'] > dataframe['tema'].shift(1)) | # Guard: tema is raising + (dataframe['close'] < dataframe['close'].shift(1)) | + # use either buy signal or rebuy flag to trigger a buy + (self.custom_info[metadata["pair"]]["rebuy"] == 1) + ) & + (dataframe['volume'] > 0) # Make sure Volume is not 0 + ), + 'buy'] = 1 + + return dataframe + + def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Based on TA indicators, populates the sell signal for the given dataframe + :param dataframe: DataFrame populated with indicators + :param metadata: Additional information, like the currently traded pair + :return: DataFrame with buy column + """ + dataframe.loc[ + ( + ( +# (qtpylib.crossed_above(dataframe['rsi'], 70)) & # Signal: RSI crosses above 70 +# (dataframe['tema'] > dataframe['bb_middleband']) & # Guard: tema above BB middle +# (dataframe['tema'] < dataframe['tema'].shift(1)) | # Guard: tema is falling + # use either sell signal or resell flag to trigger a sell + (dataframe['close'] > dataframe['close'].shift(1)) | + (self.custom_info[metadata["pair"]]["resell"] == 1) + ) & + (dataframe['volume'] > 0) # Make sure Volume is not 0 + ), + 'sell'] = 1 + return dataframe + + # use_custom_sell = True + + def custom_sell(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float, + current_profit: float, **kwargs) -> 'Optional[Union[str, bool]]': + """ + Custom sell signal logic indicating that specified position should be sold. Returning a + string or True from this method is equal to setting sell signal on a candle at specified + time. This method is not called when sell signal is set. + + This method should be overridden to create sell signals that depend on trade parameters. For + example you could implement a sell relative to the candle when the trade was opened, + or a custom 1:2 risk-reward ROI. + + Custom sell reason max length is 64. Exceeding characters will be removed. + + :param pair: Pair that's currently analyzed + :param trade: trade object. + :param current_time: datetime object, containing the current datetime + :param current_rate: Rate, calculated based on pricing settings in ask_strategy. + :param current_profit: Current profit (as ratio), calculated based on current_rate. + :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. + :return: To execute sell, return a string with custom sell reason or True. Otherwise return + None or False. + """ + # if self.custom_info[pair]["resell"] == 1: + # return 'resell' + return None + + def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, + time_in_force: str, current_time: 'datetime', **kwargs) -> bool: + return_statement = True + + if self.config['allow_position_stacking']: + return_statement = self.check_open_trades(pair, rate, current_time) + + # debugging + if return_statement and self.print_trades: + # use str.join() for speed + out = (current_time.strftime("%c"), " Bought: ", pair, ", rate: ", str(rate), ", rebuy: ", str(self.custom_info[pair]["rebuy"]), ", trades: ", str(self.custom_info[pair]["num_trades"])) + print("".join(out)) + + return return_statement + + def confirm_trade_exit(self, pair: str, trade: 'Trade', order_type: str, amount: float, + rate: float, time_in_force: str, sell_reason: str, + current_time: 'datetime', **kwargs) -> bool: + + if self.config["allow_position_stacking"]: + + # unlock open pairs limit after every sell + self.unlock_reason('Open pairs limit') + + # unlock open pairs limit after last item is sold + if self.custom_info[pair]["num_trades"] == 1: + # decrement open_pairs_count by 1 if last item is sold + self.custom_num_open_pairs["num_open_pairs"]-=1 + self.custom_info[pair]["resell"] = 0 + # reset rate + self.custom_info[pair]["latest_open_rate"] = 0.0 + self.unlock_reason('Trades per pair limit') + + # change dataframe to produce sell signal after a sell + if self.custom_info[pair]["num_trades"] >= self.max_trades_per_pair: + self.custom_info[pair]["resell"] = 1 + + # decrement number of trades by 1: + self.custom_info[pair]["num_trades"]-=1 + + # debugging stuff + if self.print_trades: + # use str.join() for speed + out = (current_time.strftime("%c"), " Sold: ", pair, ", rate: ", str(rate),", profit: ", str(trade.calc_profit_ratio(rate)), ", resell: ", str(self.custom_info[pair]["resell"]), ", trades: ", str(self.custom_info[pair]["num_trades"])) + print("".join(out)) + + return True + + def check_open_trades(self, pair: str, rate: float, current_time: datetime): + + # retrieve information about current open pairs + tr_info = self.get_trade_information(pair) + + # update number of open trades for the pair + self.custom_info[pair]["num_trades"] = tr_info[1] + self.custom_num_open_pairs["num_open_pairs"] = len(tr_info[0]) + # update value of the last open price + self.custom_info[pair]["latest_open_rate"] = tr_info[2] + + # don't buy if we have enough trades for this pair + if self.custom_info[pair]["num_trades"] >= self.max_trades_per_pair: + # lock if we already have enough pairs open, will be unlocked after last item of a pair is sold + self.lock_pair(pair, until=datetime.now(timezone.utc) + timedelta(days=100), reason='Trades per pair limit') + self.custom_info[pair]["rebuy"] = 0 + return False + + # don't buy if we have enough pairs + if self.custom_num_open_pairs["num_open_pairs"] >= self.max_open_pairs: + if not pair in tr_info[0]: + # lock if this pair is not in our list, will be unlocked after the next sell + self.lock_pair(pair, until=datetime.now(timezone.utc) + timedelta(days=100), reason='Open pairs limit') + self.custom_info[pair]["rebuy"] = 0 + return False + + # don't buy at a higher price, try until time limit is exceeded; skips if it's the first trade' + if rate > self.custom_info[pair]["latest_open_rate"] and self.custom_info[pair]["latest_open_rate"] != 0.0: + # how long do we want to try buying cheaper before we look for other pairs? + if (current_time - self.custom_info[pair]['last_open_date']).seconds/3600 > self.rebuy_time_limit_hours: + self.custom_info[pair]["rebuy"] = 0 + self.unlock_reason('Open pairs limit') + return False + + # set rebuy flag if num_trades < limit-1 + if self.custom_info[pair]["num_trades"] < self.max_trades_per_pair-1: + self.custom_info[pair]["rebuy"] = 1 + else: + self.custom_info[pair]["rebuy"] = 0 + + # update rate + self.custom_info[pair]["latest_open_rate"] = rate + + #update date open + self.custom_info[pair]["last_open_date"] = current_time + + # increment trade count by 1 + self.custom_info[pair]["num_trades"]+=1 + + return True + + # custom function to help with the strategy + def get_trade_information(self, pair:str): + + latest_open_rate, trade_count = 0, 0.0 + # store all open pairs + open_pairs = [] + + ### start nested function + def compare_trade(trade: Trade): + nonlocal trade_count, latest_open_rate, pair + if trade.pair == pair: + # update latest_rate + latest_open_rate = trade.open_rate + trade_count+=1 + return trade.pair + ### end nested function + + # replaced for loop with map for speed + open_pairs = map(compare_trade, Trade.get_open_trades()) + # remove duplicates + open_pairs = (list(dict.fromkeys(open_pairs))) + + #print(*open_pairs, sep="\n") + + # put this all together to reduce the amount of loops + return open_pairs, trade_count, latest_open_rate diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 822577916..d4cf09821 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -137,6 +137,12 @@ class Configuration: setup_logging(config) def _process_trading_options(self, config: Dict[str, Any]) -> None: + + # Allow_position_stacking defaults to False + if not config.get('allow_position_stacking'): + config['allow_position_stacking'] = False + logger.info('Allow_position_stacking is set to ' + str(config['allow_position_stacking'])) + if config['runmode'] not in TRADING_MODES: return diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index bf4742fdc..850cd1700 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -4,7 +4,7 @@ Freqtrade is the main module of this bot. It contains the class Freqtrade() import copy import logging import traceback -from datetime import datetime, timezone +from datetime import datetime, timedelta, timezone from math import isclose from threading import Lock from typing import Any, Dict, List, Optional @@ -359,10 +359,12 @@ class FreqtradeBot(LoggingMixin): logger.info("Active pair whitelist is empty.") return trades_created # Remove pairs for currently opened trades from the whitelist - for trade in Trade.get_open_trades(): - if trade.pair in whitelist: - whitelist.remove(trade.pair) - logger.debug('Ignoring %s in pair whitelist', trade.pair) + # Allow rebuying of the same pair if allow_position_stacking is set to True + if not self.config['allow_position_stacking']: + for trade in Trade.get_open_trades(): + if trade.pair in whitelist: + whitelist.remove(trade.pair) + logger.debug('Ignoring %s in pair whitelist', trade.pair) if not whitelist: logger.info("No currency pair in active pair whitelist, " @@ -592,6 +594,11 @@ class FreqtradeBot(LoggingMixin): self._notify_enter(trade, order_type) + # Lock pair for 1 timeframe duration to prevent immediate rebuys + if self.config['allow_position_stacking']: + self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc) + timedelta(minutes=timeframe_to_minutes(self.config['timeframe'])), + reason='Prevent immediate rebuys') + return True def _notify_enter(self, trade: Trade, order_type: str) -> None: diff --git a/freqtrade/persistence/pairlock_middleware.py b/freqtrade/persistence/pairlock_middleware.py index 8662fc36d..6e0164182 100644 --- a/freqtrade/persistence/pairlock_middleware.py +++ b/freqtrade/persistence/pairlock_middleware.py @@ -103,6 +103,24 @@ class PairLocks(): if PairLocks.use_db: PairLock.query.session.commit() + @staticmethod + def unlock_reason(reason: str, now: Optional[datetime] = None) -> None: + """ + Release all locks for this reason. + :param reason: Which reason to unlock + :param now: Datetime object (generated via datetime.now(timezone.utc)). + defaults to datetime.now(timezone.utc) + """ + if not now: + now = datetime.now(timezone.utc) + logger.info(f"Releasing all locks with reason \'{reason}\'.") + locks = PairLocks.get_all_locks() + for lock in locks: + if lock.reason == reason: + lock.active = False + if PairLocks.use_db: + PairLock.query.session.commit() + @staticmethod def is_global_lock(now: Optional[datetime] = None) -> bool: """ diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 7420bd9fd..547d9313f 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -443,6 +443,15 @@ class IStrategy(ABC, HyperStrategyMixin): """ PairLocks.unlock_pair(pair, datetime.now(timezone.utc)) + def unlock_reason(self, reason: str) -> None: + """ + Unlocks all pairs previously locked using lock_pair with specified reason. + Not used by freqtrade itself, but intended to be used if users lock pairs + manually from within the strategy, to allow an easy way to unlock pairs. + :param reason: Unlock pairs to allow trading again + """ + PairLocks.unlock_reason(reason, datetime.now(timezone.utc)) + def is_pair_locked(self, pair: str, candle_date: datetime = None) -> bool: """ Checks if a pair is currently locked From ae068996941bf10f95786da39942402f77944116 Mon Sep 17 00:00:00 2001 From: incrementby1 Date: Tue, 26 Oct 2021 00:29:11 +0200 Subject: [PATCH 32/72] removed commenting --- StackingDemo.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/StackingDemo.py b/StackingDemo.py index 739e847b7..2500f86f0 100644 --- a/StackingDemo.py +++ b/StackingDemo.py @@ -403,10 +403,9 @@ class StackingDemo(IStrategy): dataframe.loc[ ( ( -# (qtpylib.crossed_above(dataframe['rsi'], 30)) & # Signal: RSI crosses above 30 -# (dataframe['tema'] <= dataframe['bb_middleband']) & # Guard: tema below BB middle -# (dataframe['tema'] > dataframe['tema'].shift(1)) | # Guard: tema is raising - (dataframe['close'] < dataframe['close'].shift(1)) | + (qtpylib.crossed_above(dataframe['rsi'], 30)) & # Signal: RSI crosses above 30 + (dataframe['tema'] <= dataframe['bb_middleband']) & # Guard: tema below BB middle + (dataframe['tema'] > dataframe['tema'].shift(1)) | # Guard: tema is raising # use either buy signal or rebuy flag to trigger a buy (self.custom_info[metadata["pair"]]["rebuy"] == 1) ) & @@ -426,11 +425,10 @@ class StackingDemo(IStrategy): dataframe.loc[ ( ( -# (qtpylib.crossed_above(dataframe['rsi'], 70)) & # Signal: RSI crosses above 70 -# (dataframe['tema'] > dataframe['bb_middleband']) & # Guard: tema above BB middle -# (dataframe['tema'] < dataframe['tema'].shift(1)) | # Guard: tema is falling + (qtpylib.crossed_above(dataframe['rsi'], 70)) & # Signal: RSI crosses above 70 + (dataframe['tema'] > dataframe['bb_middleband']) & # Guard: tema above BB middle + (dataframe['tema'] < dataframe['tema'].shift(1)) | # Guard: tema is falling # use either sell signal or resell flag to trigger a sell - (dataframe['close'] > dataframe['close'].shift(1)) | (self.custom_info[metadata["pair"]]["resell"] == 1) ) & (dataframe['volume'] > 0) # Make sure Volume is not 0 From 9f6e4c6c0e3c6e3fc4d2ad80cc4bfb3385b13152 Mon Sep 17 00:00:00 2001 From: incrementby1 Date: Tue, 26 Oct 2021 00:31:17 +0200 Subject: [PATCH 33/72] uncomment --- StackingDemo.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/StackingDemo.py b/StackingDemo.py index 2500f86f0..b88248fac 100644 --- a/StackingDemo.py +++ b/StackingDemo.py @@ -73,8 +73,8 @@ class StackingDemo(IStrategy): # Minimal ROI designed for the strategy. # This attribute will be overridden if the config file contains "minimal_roi". minimal_roi = { -# "60": 0.01, -# "30": 0.02, + "60": 0.01, + "30": 0.02, "0": 0.001 } From 9c6cbc025aa6886bf551080869d1368e7d480591 Mon Sep 17 00:00:00 2001 From: incrementby1 <91958753+incrementby1@users.noreply.github.com> Date: Tue, 26 Oct 2021 00:34:01 +0200 Subject: [PATCH 34/72] Update StackingDemo.py --- StackingDemo.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/StackingDemo.py b/StackingDemo.py index 739e847b7..b88248fac 100644 --- a/StackingDemo.py +++ b/StackingDemo.py @@ -73,8 +73,8 @@ class StackingDemo(IStrategy): # Minimal ROI designed for the strategy. # This attribute will be overridden if the config file contains "minimal_roi". minimal_roi = { -# "60": 0.01, -# "30": 0.02, + "60": 0.01, + "30": 0.02, "0": 0.001 } @@ -403,10 +403,9 @@ class StackingDemo(IStrategy): dataframe.loc[ ( ( -# (qtpylib.crossed_above(dataframe['rsi'], 30)) & # Signal: RSI crosses above 30 -# (dataframe['tema'] <= dataframe['bb_middleband']) & # Guard: tema below BB middle -# (dataframe['tema'] > dataframe['tema'].shift(1)) | # Guard: tema is raising - (dataframe['close'] < dataframe['close'].shift(1)) | + (qtpylib.crossed_above(dataframe['rsi'], 30)) & # Signal: RSI crosses above 30 + (dataframe['tema'] <= dataframe['bb_middleband']) & # Guard: tema below BB middle + (dataframe['tema'] > dataframe['tema'].shift(1)) | # Guard: tema is raising # use either buy signal or rebuy flag to trigger a buy (self.custom_info[metadata["pair"]]["rebuy"] == 1) ) & @@ -426,11 +425,10 @@ class StackingDemo(IStrategy): dataframe.loc[ ( ( -# (qtpylib.crossed_above(dataframe['rsi'], 70)) & # Signal: RSI crosses above 70 -# (dataframe['tema'] > dataframe['bb_middleband']) & # Guard: tema above BB middle -# (dataframe['tema'] < dataframe['tema'].shift(1)) | # Guard: tema is falling + (qtpylib.crossed_above(dataframe['rsi'], 70)) & # Signal: RSI crosses above 70 + (dataframe['tema'] > dataframe['bb_middleband']) & # Guard: tema above BB middle + (dataframe['tema'] < dataframe['tema'].shift(1)) | # Guard: tema is falling # use either sell signal or resell flag to trigger a sell - (dataframe['close'] > dataframe['close'].shift(1)) | (self.custom_info[metadata["pair"]]["resell"] == 1) ) & (dataframe['volume'] > 0) # Make sure Volume is not 0 From f80d3d48e46fee629b3e748c9363e5a89a488393 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 27 Oct 2021 06:29:35 +0200 Subject: [PATCH 35/72] Add default to minimal_roi to avoid failures closes #5796 --- freqtrade/resolvers/strategy_resolver.py | 18 +++++++++++------- freqtrade/strategy/interface.py | 4 ++-- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index e7c077e84..a7b95a3c5 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -56,17 +56,21 @@ class StrategyResolver(IResolver): if strategy._ft_params_from_file: # Set parameters from Hyperopt results file params = strategy._ft_params_from_file - strategy.minimal_roi = params.get('roi', strategy.minimal_roi) + strategy.minimal_roi = params.get('roi', getattr(strategy, 'minimal_roi', {})) - strategy.stoploss = params.get('stoploss', {}).get('stoploss', strategy.stoploss) + strategy.stoploss = params.get('stoploss', {}).get( + 'stoploss', getattr(strategy, 'stoploss', -0.1)) trailing = params.get('trailing', {}) - strategy.trailing_stop = trailing.get('trailing_stop', strategy.trailing_stop) - strategy.trailing_stop_positive = trailing.get('trailing_stop_positive', - strategy.trailing_stop_positive) + strategy.trailing_stop = trailing.get( + 'trailing_stop', getattr(strategy, 'trailing_stop', False)) + strategy.trailing_stop_positive = trailing.get( + 'trailing_stop_positive', getattr(strategy, 'trailing_stop_positive', None)) strategy.trailing_stop_positive_offset = trailing.get( - 'trailing_stop_positive_offset', strategy.trailing_stop_positive_offset) + 'trailing_stop_positive_offset', + getattr(strategy, 'trailing_stop_positive_offset', 0)) strategy.trailing_only_offset_is_reached = trailing.get( - 'trailing_only_offset_is_reached', strategy.trailing_only_offset_is_reached) + 'trailing_only_offset_is_reached', + getattr(strategy, 'trailing_only_offset_is_reached', 0.0)) # Set attributes # Check if we need to override configuration diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 7420bd9fd..834ba5975 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -65,9 +65,9 @@ class IStrategy(ABC, HyperStrategyMixin): _populate_fun_len: int = 0 _buy_fun_len: int = 0 _sell_fun_len: int = 0 - _ft_params_from_file: Dict = {} + _ft_params_from_file: Dict # associated minimal roi - minimal_roi: Dict + minimal_roi: Dict = {} # associated stoploss stoploss: float From 51c925f9f3737e59bcd61b7ee698afce9491784e Mon Sep 17 00:00:00 2001 From: incrementby1 <91958753+incrementby1@users.noreply.github.com> Date: Wed, 27 Oct 2021 12:40:26 +0200 Subject: [PATCH 36/72] Delete StackingConfig.json --- StackingConfig.json | 89 --------------------------------------------- 1 file changed, 89 deletions(-) delete mode 100644 StackingConfig.json diff --git a/StackingConfig.json b/StackingConfig.json deleted file mode 100644 index 750ef92c6..000000000 --- a/StackingConfig.json +++ /dev/null @@ -1,89 +0,0 @@ - -{ - "max_open_trades": 12, - "stake_currency": "USDT", - "stake_amount": 100, - "tradable_balance_ratio": 0.99, - "fiat_display_currency": "USD", - "timeframe": "5m", - "dry_run": true, - "cancel_open_orders_on_exit": false, - "allow_position_stacking": true, - "unfilledtimeout": { - "buy": 10, - "sell": 30, - "unit": "minutes" - }, - "bid_strategy": { - "price_side": "ask", - "ask_last_balance": 0.0, - "use_order_book": true, - "order_book_top": 1, - "check_depth_of_market": { - "enabled": false, - "bids_to_ask_delta": 1 - } - }, - "ask_strategy": { - "price_side": "bid", - "use_order_book": true, - "order_book_top": 1 - }, - "exchange": { - "name": "binance", - "key": "", - "secret": "", - "ccxt_config": {}, - "ccxt_async_config": {}, - "pair_whitelist": [ - ], - "pair_blacklist": [ - "BNB/.*" - ] - }, - "pairlists": [ - { - "method": "VolumePairList", - "number_assets": 80, - "sort_key": "quoteVolume", - "min_value": 0, - "refresh_period": 1800 - } - ], - "edge": { - "enabled": false, - "process_throttle_secs": 3600, - "calculate_since_number_of_days": 7, - "allowed_risk": 0.01, - "stoploss_range_min": -0.01, - "stoploss_range_max": -0.1, - "stoploss_range_step": -0.01, - "minimum_winrate": 0.60, - "minimum_expectancy": 0.20, - "min_trade_number": 10, - "max_trade_duration_minute": 1440, - "remove_pumps": false - }, - "telegram": { - "enabled": false, - "token": "", - "chat_id": "" - }, - "api_server": { - "enabled": true, - "listen_ip_address": "127.0.0.1", - "listen_port": 8080, - "verbosity": "error", - "enable_openapi": false, - "jwt_secret_key": "908cd4469c824f3838bfe56e4120d3a3dbda5294ef583ffc62c82f54d2c1bf58", - "CORS_origins": [], - "username": "user", - "password": "pass" - }, - "bot_name": "freqtrade", - "initial_state": "running", - "forcebuy_enable": false, - "internals": { - "process_throttle_secs": 5 - } -} From 6b17094c6ffee7ee81cc975a72859ffad7460163 Mon Sep 17 00:00:00 2001 From: incrementby1 <91958753+incrementby1@users.noreply.github.com> Date: Wed, 27 Oct 2021 12:41:49 +0200 Subject: [PATCH 37/72] Delete configuration.py --- freqtrade/configuration/configuration.py | 506 ----------------------- 1 file changed, 506 deletions(-) delete mode 100644 freqtrade/configuration/configuration.py diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py deleted file mode 100644 index d4cf09821..000000000 --- a/freqtrade/configuration/configuration.py +++ /dev/null @@ -1,506 +0,0 @@ -""" -This module contains the configuration class -""" -import logging -import warnings -from copy import deepcopy -from pathlib import Path -from typing import Any, Callable, Dict, List, Optional - -from freqtrade import constants -from freqtrade.configuration.check_exchange import check_exchange -from freqtrade.configuration.deprecated_settings import process_temporary_deprecated_settings -from freqtrade.configuration.directory_operations import create_datadir, create_userdata_dir -from freqtrade.configuration.environment_vars import enironment_vars_to_dict -from freqtrade.configuration.load_config import load_config_file, load_file -from freqtrade.enums import NON_UTIL_MODES, TRADING_MODES, RunMode -from freqtrade.exceptions import OperationalException -from freqtrade.loggers import setup_logging -from freqtrade.misc import deep_merge_dicts, parse_db_uri_for_logging - - -logger = logging.getLogger(__name__) - - -class Configuration: - """ - Class to read and init the bot configuration - Reuse this class for the bot, backtesting, hyperopt and every script that required configuration - """ - - def __init__(self, args: Dict[str, Any], runmode: RunMode = None) -> None: - self.args = args - self.config: Optional[Dict[str, Any]] = None - self.runmode = runmode - - def get_config(self) -> Dict[str, Any]: - """ - Return the config. Use this method to get the bot config - :return: Dict: Bot config - """ - if self.config is None: - self.config = self.load_config() - - return self.config - - @staticmethod - def from_files(files: List[str]) -> Dict[str, Any]: - """ - Iterate through the config files passed in, loading all of them - and merging their contents. - Files are loaded in sequence, parameters in later configuration files - override the same parameter from an earlier file (last definition wins). - Runs through the whole Configuration initialization, so all expected config entries - are available to interactive environments. - :param files: List of file paths - :return: configuration dictionary - """ - c = Configuration({'config': files}, RunMode.OTHER) - return c.get_config() - - def load_from_files(self, files: List[str]) -> Dict[str, Any]: - - # Keep this method as staticmethod, so it can be used from interactive environments - config: Dict[str, Any] = {} - - if not files: - return deepcopy(constants.MINIMAL_CONFIG) - - # We expect here a list of config filenames - for path in files: - logger.info(f'Using config: {path} ...') - - # Merge config options, overwriting old values - config = deep_merge_dicts(load_config_file(path), config) - - # Load environment variables - env_data = enironment_vars_to_dict() - config = deep_merge_dicts(env_data, config) - - config['config_files'] = files - # Normalize config - if 'internals' not in config: - config['internals'] = {} - if 'ask_strategy' not in config: - config['ask_strategy'] = {} - - if 'pairlists' not in config: - config['pairlists'] = [] - - return config - - def load_config(self) -> Dict[str, Any]: - """ - Extract information for sys.argv and load the bot configuration - :return: Configuration dictionary - """ - # Load all configs - config: Dict[str, Any] = self.load_from_files(self.args.get("config", [])) - - # Keep a copy of the original configuration file - config['original_config'] = deepcopy(config) - - self._process_logging_options(config) - - self._process_runmode(config) - - self._process_common_options(config) - - self._process_trading_options(config) - - self._process_optimize_options(config) - - self._process_plot_options(config) - - self._process_data_options(config) - - # Check if the exchange set by the user is supported - check_exchange(config, config.get('experimental', {}).get('block_bad_exchanges', True)) - - self._resolve_pairs_list(config) - - process_temporary_deprecated_settings(config) - - return config - - def _process_logging_options(self, config: Dict[str, Any]) -> None: - """ - Extract information for sys.argv and load logging configuration: - the -v/--verbose, --logfile options - """ - # Log level - config.update({'verbosity': self.args.get('verbosity', 0)}) - - if 'logfile' in self.args and self.args['logfile']: - config.update({'logfile': self.args['logfile']}) - - setup_logging(config) - - def _process_trading_options(self, config: Dict[str, Any]) -> None: - - # Allow_position_stacking defaults to False - if not config.get('allow_position_stacking'): - config['allow_position_stacking'] = False - logger.info('Allow_position_stacking is set to ' + str(config['allow_position_stacking'])) - - if config['runmode'] not in TRADING_MODES: - return - - if config.get('dry_run', False): - logger.info('Dry run is enabled') - if config.get('db_url') in [None, constants.DEFAULT_DB_PROD_URL]: - # Default to in-memory db for dry_run if not specified - config['db_url'] = constants.DEFAULT_DB_DRYRUN_URL - else: - if not config.get('db_url', None): - config['db_url'] = constants.DEFAULT_DB_PROD_URL - logger.info('Dry run is disabled') - - logger.info(f'Using DB: "{parse_db_uri_for_logging(config["db_url"])}"') - - def _process_common_options(self, config: Dict[str, Any]) -> None: - - # Set strategy if not specified in config and or if it's non default - if self.args.get('strategy') or not config.get('strategy'): - config.update({'strategy': self.args.get('strategy')}) - - self._args_to_config(config, argname='strategy_path', - logstring='Using additional Strategy lookup path: {}') - - if ('db_url' in self.args and self.args['db_url'] and - self.args['db_url'] != constants.DEFAULT_DB_PROD_URL): - config.update({'db_url': self.args['db_url']}) - logger.info('Parameter --db-url detected ...') - - if config.get('forcebuy_enable', False): - logger.warning('`forcebuy` RPC message enabled.') - - # Support for sd_notify - if 'sd_notify' in self.args and self.args['sd_notify']: - config['internals'].update({'sd_notify': True}) - - def _process_datadir_options(self, config: Dict[str, Any]) -> None: - """ - Extract information for sys.argv and load directory configurations - --user-data, --datadir - """ - # Check exchange parameter here - otherwise `datadir` might be wrong. - if 'exchange' in self.args and self.args['exchange']: - config['exchange']['name'] = self.args['exchange'] - logger.info(f"Using exchange {config['exchange']['name']}") - - if 'pair_whitelist' not in config['exchange']: - config['exchange']['pair_whitelist'] = [] - - if 'user_data_dir' in self.args and self.args['user_data_dir']: - config.update({'user_data_dir': self.args['user_data_dir']}) - elif 'user_data_dir' not in config: - # Default to cwd/user_data (legacy option ...) - config.update({'user_data_dir': str(Path.cwd() / 'user_data')}) - - # reset to user_data_dir so this contains the absolute path. - config['user_data_dir'] = create_userdata_dir(config['user_data_dir'], create_dir=False) - logger.info('Using user-data directory: %s ...', config['user_data_dir']) - - config.update({'datadir': create_datadir(config, self.args.get('datadir', None))}) - logger.info('Using data directory: %s ...', config.get('datadir')) - - if self.args.get('exportfilename'): - self._args_to_config(config, argname='exportfilename', - logstring='Storing backtest results to {} ...') - config['exportfilename'] = Path(config['exportfilename']) - else: - config['exportfilename'] = (config['user_data_dir'] - / 'backtest_results') - - def _process_optimize_options(self, config: Dict[str, Any]) -> None: - - # This will override the strategy configuration - self._args_to_config(config, argname='timeframe', - logstring='Parameter -i/--timeframe detected ... ' - 'Using timeframe: {} ...') - - self._args_to_config(config, argname='position_stacking', - logstring='Parameter --enable-position-stacking detected ...') - - self._args_to_config( - config, argname='enable_protections', - logstring='Parameter --enable-protections detected, enabling Protections. ...') - - if 'use_max_market_positions' in self.args and not self.args["use_max_market_positions"]: - config.update({'use_max_market_positions': False}) - logger.info('Parameter --disable-max-market-positions detected ...') - logger.info('max_open_trades set to unlimited ...') - elif 'max_open_trades' in self.args and self.args['max_open_trades']: - config.update({'max_open_trades': self.args['max_open_trades']}) - logger.info('Parameter --max-open-trades detected, ' - 'overriding max_open_trades to: %s ...', config.get('max_open_trades')) - elif config['runmode'] in NON_UTIL_MODES: - logger.info('Using max_open_trades: %s ...', config.get('max_open_trades')) - # Setting max_open_trades to infinite if -1 - if config.get('max_open_trades') == -1: - config['max_open_trades'] = float('inf') - - if self.args.get('stake_amount', None): - # Convert explicitly to float to support CLI argument for both unlimited and value - try: - self.args['stake_amount'] = float(self.args['stake_amount']) - except ValueError: - pass - - self._args_to_config(config, argname='timeframe_detail', - logstring='Parameter --timeframe-detail detected, ' - 'using {} for intra-candle backtesting ...') - self._args_to_config(config, argname='stake_amount', - logstring='Parameter --stake-amount detected, ' - 'overriding stake_amount to: {} ...') - self._args_to_config(config, argname='dry_run_wallet', - logstring='Parameter --dry-run-wallet detected, ' - 'overriding dry_run_wallet to: {} ...') - self._args_to_config(config, argname='fee', - logstring='Parameter --fee detected, ' - 'setting fee to: {} ...') - - self._args_to_config(config, argname='timerange', - logstring='Parameter --timerange detected: {} ...') - - self._process_datadir_options(config) - - self._args_to_config(config, argname='strategy_list', - logstring='Using strategy list of {} strategies', logfun=len) - - self._args_to_config(config, argname='timeframe', - logstring='Overriding timeframe with Command line argument') - - self._args_to_config(config, argname='export', - logstring='Parameter --export detected: {} ...') - - self._args_to_config(config, argname='backtest_breakdown', - logstring='Parameter --breakdown detected ...') - - self._args_to_config(config, argname='disableparamexport', - logstring='Parameter --disableparamexport detected: {} ...') - - # Edge section: - if 'stoploss_range' in self.args and self.args["stoploss_range"]: - txt_range = eval(self.args["stoploss_range"]) - config['edge'].update({'stoploss_range_min': txt_range[0]}) - config['edge'].update({'stoploss_range_max': txt_range[1]}) - config['edge'].update({'stoploss_range_step': txt_range[2]}) - logger.info('Parameter --stoplosses detected: %s ...', self.args["stoploss_range"]) - - # Hyperopt section - self._args_to_config(config, argname='hyperopt', - logstring='Using Hyperopt class name: {}') - - self._args_to_config(config, argname='hyperopt_path', - logstring='Using additional Hyperopt lookup path: {}') - - self._args_to_config(config, argname='hyperoptexportfilename', - logstring='Using hyperopt file: {}') - - self._args_to_config(config, argname='epochs', - logstring='Parameter --epochs detected ... ' - 'Will run Hyperopt with for {} epochs ...' - ) - - self._args_to_config(config, argname='spaces', - logstring='Parameter -s/--spaces detected: {}') - - self._args_to_config(config, argname='print_all', - logstring='Parameter --print-all detected ...') - - if 'print_colorized' in self.args and not self.args["print_colorized"]: - logger.info('Parameter --no-color detected ...') - config.update({'print_colorized': False}) - else: - config.update({'print_colorized': True}) - - self._args_to_config(config, argname='print_json', - logstring='Parameter --print-json detected ...') - - self._args_to_config(config, argname='export_csv', - logstring='Parameter --export-csv detected: {}') - - self._args_to_config(config, argname='hyperopt_jobs', - logstring='Parameter -j/--job-workers detected: {}') - - self._args_to_config(config, argname='hyperopt_random_state', - logstring='Parameter --random-state detected: {}') - - self._args_to_config(config, argname='hyperopt_min_trades', - logstring='Parameter --min-trades detected: {}') - - self._args_to_config(config, argname='hyperopt_loss', - logstring='Using Hyperopt loss class name: {}') - - self._args_to_config(config, argname='hyperopt_show_index', - logstring='Parameter -n/--index detected: {}') - - self._args_to_config(config, argname='hyperopt_list_best', - logstring='Parameter --best detected: {}') - - self._args_to_config(config, argname='hyperopt_list_profitable', - logstring='Parameter --profitable detected: {}') - - self._args_to_config(config, argname='hyperopt_list_min_trades', - logstring='Parameter --min-trades detected: {}') - - self._args_to_config(config, argname='hyperopt_list_max_trades', - logstring='Parameter --max-trades detected: {}') - - self._args_to_config(config, argname='hyperopt_list_min_avg_time', - logstring='Parameter --min-avg-time detected: {}') - - self._args_to_config(config, argname='hyperopt_list_max_avg_time', - logstring='Parameter --max-avg-time detected: {}') - - self._args_to_config(config, argname='hyperopt_list_min_avg_profit', - logstring='Parameter --min-avg-profit detected: {}') - - self._args_to_config(config, argname='hyperopt_list_max_avg_profit', - logstring='Parameter --max-avg-profit detected: {}') - - self._args_to_config(config, argname='hyperopt_list_min_total_profit', - logstring='Parameter --min-total-profit detected: {}') - - self._args_to_config(config, argname='hyperopt_list_max_total_profit', - logstring='Parameter --max-total-profit detected: {}') - - self._args_to_config(config, argname='hyperopt_list_min_objective', - logstring='Parameter --min-objective detected: {}') - - self._args_to_config(config, argname='hyperopt_list_max_objective', - logstring='Parameter --max-objective detected: {}') - - self._args_to_config(config, argname='hyperopt_list_no_details', - logstring='Parameter --no-details detected: {}') - - self._args_to_config(config, argname='hyperopt_show_no_header', - logstring='Parameter --no-header detected: {}') - - self._args_to_config(config, argname="hyperopt_ignore_missing_space", - logstring="Paramter --ignore-missing-space detected: {}") - - def _process_plot_options(self, config: Dict[str, Any]) -> None: - - self._args_to_config(config, argname='pairs', - logstring='Using pairs {}') - - self._args_to_config(config, argname='indicators1', - logstring='Using indicators1: {}') - - self._args_to_config(config, argname='indicators2', - logstring='Using indicators2: {}') - - self._args_to_config(config, argname='trade_ids', - logstring='Filtering on trade_ids: {}') - - self._args_to_config(config, argname='plot_limit', - logstring='Limiting plot to: {}') - - self._args_to_config(config, argname='plot_auto_open', - logstring='Parameter --auto-open detected.') - - self._args_to_config(config, argname='trade_source', - logstring='Using trades from: {}') - - self._args_to_config(config, argname='erase', - logstring='Erase detected. Deleting existing data.') - - self._args_to_config(config, argname='no_trades', - logstring='Parameter --no-trades detected.') - - self._args_to_config(config, argname='timeframes', - logstring='timeframes --timeframes: {}') - - self._args_to_config(config, argname='days', - logstring='Detected --days: {}') - - self._args_to_config(config, argname='include_inactive', - logstring='Detected --include-inactive-pairs: {}') - - self._args_to_config(config, argname='download_trades', - logstring='Detected --dl-trades: {}') - - self._args_to_config(config, argname='dataformat_ohlcv', - logstring='Using "{}" to store OHLCV data.') - - self._args_to_config(config, argname='dataformat_trades', - logstring='Using "{}" to store trades data.') - - def _process_data_options(self, config: Dict[str, Any]) -> None: - - self._args_to_config(config, argname='new_pairs_days', - logstring='Detected --new-pairs-days: {}') - - def _process_runmode(self, config: Dict[str, Any]) -> None: - - self._args_to_config(config, argname='dry_run', - logstring='Parameter --dry-run detected, ' - 'overriding dry_run to: {} ...') - - if not self.runmode: - # Handle real mode, infer dry/live from config - self.runmode = RunMode.DRY_RUN if config.get('dry_run', True) else RunMode.LIVE - logger.info(f"Runmode set to {self.runmode.value}.") - - config.update({'runmode': self.runmode}) - - def _args_to_config(self, config: Dict[str, Any], argname: str, - logstring: str, logfun: Optional[Callable] = None, - deprecated_msg: Optional[str] = None) -> None: - """ - :param config: Configuration dictionary - :param argname: Argumentname in self.args - will be copied to config dict. - :param logstring: Logging String - :param logfun: logfun is applied to the configuration entry before passing - that entry to the log string using .format(). - sample: logfun=len (prints the length of the found - configuration instead of the content) - """ - if (argname in self.args and self.args[argname] is not None - and self.args[argname] is not False): - - config.update({argname: self.args[argname]}) - if logfun: - logger.info(logstring.format(logfun(config[argname]))) - else: - logger.info(logstring.format(config[argname])) - if deprecated_msg: - warnings.warn(f"DEPRECATED: {deprecated_msg}", DeprecationWarning) - - def _resolve_pairs_list(self, config: Dict[str, Any]) -> None: - """ - Helper for download script. - Takes first found: - * -p (pairs argument) - * --pairs-file - * whitelist from config - """ - - if "pairs" in config: - config['exchange']['pair_whitelist'] = config['pairs'] - return - - if "pairs_file" in self.args and self.args["pairs_file"]: - pairs_file = Path(self.args["pairs_file"]) - logger.info(f'Reading pairs file "{pairs_file}".') - # Download pairs from the pairs file if no config is specified - # or if pairs file is specified explicitly - if not pairs_file.exists(): - raise OperationalException(f'No pairs file found with path "{pairs_file}".') - config['pairs'] = load_file(pairs_file) - config['pairs'].sort() - return - - if 'config' in self.args and self.args['config']: - logger.info("Using pairlist from configuration.") - config['pairs'] = config.get('exchange', {}).get('pair_whitelist') - else: - # Fall back to /dl_path/pairs.json - pairs_file = config['datadir'] / 'pairs.json' - if pairs_file.exists(): - config['pairs'] = load_file(pairs_file) - if 'pairs' in config: - config['pairs'].sort() From c1b5dcd756eaace8c274dfdcbca701bb7197d6bd Mon Sep 17 00:00:00 2001 From: incrementby1 <91958753+incrementby1@users.noreply.github.com> Date: Wed, 27 Oct 2021 12:42:18 +0200 Subject: [PATCH 38/72] Delete freqtradebot.py --- freqtrade/freqtradebot.py | 1438 ------------------------------------- 1 file changed, 1438 deletions(-) delete mode 100644 freqtrade/freqtradebot.py diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py deleted file mode 100644 index 850cd1700..000000000 --- a/freqtrade/freqtradebot.py +++ /dev/null @@ -1,1438 +0,0 @@ -""" -Freqtrade is the main module of this bot. It contains the class Freqtrade() -""" -import copy -import logging -import traceback -from datetime import datetime, timedelta, timezone -from math import isclose -from threading import Lock -from typing import Any, Dict, List, Optional - -import arrow - -from freqtrade import __version__, constants -from freqtrade.configuration import validate_config_consistency -from freqtrade.data.converter import order_book_to_dataframe -from freqtrade.data.dataprovider import DataProvider -from freqtrade.edge import Edge -from freqtrade.enums import RPCMessageType, SellType, State -from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError, - InvalidOrderException, PricingError) -from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds -from freqtrade.misc import safe_value_fallback, safe_value_fallback2 -from freqtrade.mixins import LoggingMixin -from freqtrade.persistence import Order, PairLocks, Trade, cleanup_db, init_db -from freqtrade.plugins.pairlistmanager import PairListManager -from freqtrade.plugins.protectionmanager import ProtectionManager -from freqtrade.resolvers import ExchangeResolver, StrategyResolver -from freqtrade.rpc import RPCManager -from freqtrade.strategy.interface import IStrategy, SellCheckTuple -from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper -from freqtrade.wallets import Wallets - - -logger = logging.getLogger(__name__) - - -class FreqtradeBot(LoggingMixin): - """ - Freqtrade is the main class of the bot. - This is from here the bot start its logic. - """ - - def __init__(self, config: Dict[str, Any]) -> None: - """ - Init all variables and objects the bot needs to work - :param config: configuration dict, you can use Configuration.get_config() - to get the config dict. - """ - self.active_pair_whitelist: List[str] = [] - - logger.info('Starting freqtrade %s', __version__) - - # Init bot state - self.state = State.STOPPED - - # Init objects - self.config = config - - self.strategy: IStrategy = StrategyResolver.load_strategy(self.config) - - # Check config consistency here since strategies can set certain options - validate_config_consistency(config) - - self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config) - - init_db(self.config.get('db_url', None), clean_open_orders=self.config['dry_run']) - - self.wallets = Wallets(self.config, self.exchange) - - PairLocks.timeframe = self.config['timeframe'] - - self.protections = ProtectionManager(self.config, self.strategy.protections) - - # RPC runs in separate threads, can start handling external commands just after - # initialization, even before Freqtradebot has a chance to start its throttling, - # so anything in the Freqtradebot instance should be ready (initialized), including - # the initial state of the bot. - # Keep this at the end of this initialization method. - self.rpc: RPCManager = RPCManager(self) - - self.pairlists = PairListManager(self.exchange, self.config) - - self.dataprovider = DataProvider(self.config, self.exchange, self.pairlists) - - # Attach Dataprovider to strategy instance - self.strategy.dp = self.dataprovider - # Attach Wallets to strategy instance - self.strategy.wallets = self.wallets - - # Initializing Edge only if enabled - self.edge = Edge(self.config, self.exchange, self.strategy) if \ - self.config.get('edge', {}).get('enabled', False) else None - - self.active_pair_whitelist = self._refresh_active_whitelist() - - # Set initial bot state from config - initial_state = self.config.get('initial_state') - self.state = State[initial_state.upper()] if initial_state else State.STOPPED - - # Protect sell-logic from forcesell and vice versa - self._exit_lock = Lock() - LoggingMixin.__init__(self, logger, timeframe_to_seconds(self.strategy.timeframe)) - - def notify_status(self, msg: str) -> None: - """ - Public method for users of this class (worker, etc.) to send notifications - via RPC about changes in the bot status. - """ - self.rpc.send_msg({ - 'type': RPCMessageType.STATUS, - 'status': msg - }) - - def cleanup(self) -> None: - """ - Cleanup pending resources on an already stopped bot - :return: None - """ - logger.info('Cleaning up modules ...') - - if self.config['cancel_open_orders_on_exit']: - self.cancel_all_open_orders() - - self.check_for_open_trades() - - self.rpc.cleanup() - cleanup_db() - - def startup(self) -> None: - """ - Called on startup and after reloading the bot - triggers notifications and - performs startup tasks - """ - self.rpc.startup_messages(self.config, self.pairlists, self.protections) - if not self.edge: - # Adjust stoploss if it was changed - Trade.stoploss_reinitialization(self.strategy.stoploss) - - # Only update open orders on startup - # This will update the database after the initial migration - self.startup_update_open_orders() - - def process(self) -> None: - """ - Queries the persistence layer for open trades and handles them, - otherwise a new trade is created. - :return: True if one or more trades has been created or closed, False otherwise - """ - - # Check whether markets have to be reloaded and reload them when it's needed - self.exchange.reload_markets() - - self.update_closed_trades_without_assigned_fees() - - # Query trades from persistence layer - trades = Trade.get_open_trades() - - self.active_pair_whitelist = self._refresh_active_whitelist(trades) - - # Refreshing candles - self.dataprovider.refresh(self.pairlists.create_pair_list(self.active_pair_whitelist), - self.strategy.gather_informative_pairs()) - - strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)() - - self.strategy.analyze(self.active_pair_whitelist) - - with self._exit_lock: - # Check and handle any timed out open orders - self.check_handle_timedout() - - # Protect from collisions with forcesell. - # Without this, freqtrade my try to recreate stoploss_on_exchange orders - # while selling is in process, since telegram messages arrive in an different thread. - with self._exit_lock: - trades = Trade.get_open_trades() - # First process current opened trades (positions) - self.exit_positions(trades) - - # Then looking for buy opportunities - if self.get_free_open_trades(): - self.enter_positions() - - Trade.commit() - - def process_stopped(self) -> None: - """ - Close all orders that were left open - """ - if self.config['cancel_open_orders_on_exit']: - self.cancel_all_open_orders() - - def check_for_open_trades(self): - """ - Notify the user when the bot is stopped - and there are still open trades active. - """ - open_trades = Trade.get_trades([Trade.is_open.is_(True)]).all() - - if len(open_trades) != 0: - msg = { - 'type': RPCMessageType.WARNING, - 'status': f"{len(open_trades)} open trades active.\n\n" - f"Handle these trades manually on {self.exchange.name}, " - f"or '/start' the bot again and use '/stopbuy' " - f"to handle open trades gracefully. \n" - f"{'Trades are simulated.' if self.config['dry_run'] else ''}", - } - self.rpc.send_msg(msg) - - def _refresh_active_whitelist(self, trades: List[Trade] = []) -> List[str]: - """ - Refresh active whitelist from pairlist or edge and extend it with - pairs that have open trades. - """ - # Refresh whitelist - self.pairlists.refresh_pairlist() - _whitelist = self.pairlists.whitelist - - # Calculating Edge positioning - if self.edge: - self.edge.calculate(_whitelist) - _whitelist = self.edge.adjust(_whitelist) - - if trades: - # Extend active-pair whitelist with pairs of open trades - # It ensures that candle (OHLCV) data are downloaded for open trades as well - _whitelist.extend([trade.pair for trade in trades if trade.pair not in _whitelist]) - return _whitelist - - def get_free_open_trades(self) -> int: - """ - Return the number of free open trades slots or 0 if - max number of open trades reached - """ - open_trades = len(Trade.get_open_trades()) - return max(0, self.config['max_open_trades'] - open_trades) - - def startup_update_open_orders(self): - """ - Updates open orders based on order list kept in the database. - Mainly updates the state of orders - but may also close trades - """ - if self.config['dry_run'] or self.config['exchange'].get('skip_open_order_update', False): - # Updating open orders in dry-run does not make sense and will fail. - return - - orders = Order.get_open_orders() - logger.info(f"Updating {len(orders)} open orders.") - for order in orders: - try: - fo = self.exchange.fetch_order_or_stoploss_order(order.order_id, order.ft_pair, - order.ft_order_side == 'stoploss') - - self.update_trade_state(order.trade, order.order_id, fo) - - except ExchangeError as e: - - logger.warning(f"Error updating Order {order.order_id} due to {e}") - - def update_closed_trades_without_assigned_fees(self): - """ - Update closed trades without close fees assigned. - Only acts when Orders are in the database, otherwise the last order-id is unknown. - """ - if self.config['dry_run']: - # Updating open orders in dry-run does not make sense and will fail. - return - - trades: List[Trade] = Trade.get_sold_trades_without_assigned_fees() - for trade in trades: - - if not trade.is_open and not trade.fee_updated('sell'): - # Get sell fee - order = trade.select_order('sell', False) - if order: - logger.info(f"Updating sell-fee on trade {trade} for order {order.order_id}.") - self.update_trade_state(trade, order.order_id, - stoploss_order=order.ft_order_side == 'stoploss') - - trades: List[Trade] = Trade.get_open_trades_without_assigned_fees() - for trade in trades: - if trade.is_open and not trade.fee_updated('buy'): - order = trade.select_order('buy', False) - if order: - logger.info(f"Updating buy-fee on trade {trade} for order {order.order_id}.") - self.update_trade_state(trade, order.order_id) - - def handle_insufficient_funds(self, trade: Trade): - """ - Determine if we ever opened a sell order for this trade. - If not, try update buy fees - otherwise "refind" the open order we obviously lost. - """ - sell_order = trade.select_order('sell', None) - if sell_order: - self.refind_lost_order(trade) - else: - self.reupdate_enter_order_fees(trade) - - def reupdate_enter_order_fees(self, trade: Trade): - """ - Get buy order from database, and try to reupdate. - Handles trades where the initial fee-update did not work. - """ - logger.info(f"Trying to reupdate buy fees for {trade}") - order = trade.select_order('buy', False) - if order: - logger.info(f"Updating buy-fee on trade {trade} for order {order.order_id}.") - self.update_trade_state(trade, order.order_id) - - def refind_lost_order(self, trade): - """ - Try refinding a lost trade. - Only used when InsufficientFunds appears on sell orders (stoploss or sell). - Tries to walk the stored orders and sell them off eventually. - """ - logger.info(f"Trying to refind lost order for {trade}") - for order in trade.orders: - logger.info(f"Trying to refind {order}") - fo = None - if not order.ft_is_open: - logger.debug(f"Order {order} is no longer open.") - continue - if order.ft_order_side == 'buy': - # Skip buy side - this is handled by reupdate_buy_order_fees - continue - try: - fo = self.exchange.fetch_order_or_stoploss_order(order.order_id, order.ft_pair, - order.ft_order_side == 'stoploss') - if order.ft_order_side == 'stoploss': - if fo and fo['status'] == 'open': - # Assume this as the open stoploss order - trade.stoploss_order_id = order.order_id - elif order.ft_order_side == 'sell': - if fo and fo['status'] == 'open': - # Assume this as the open order - trade.open_order_id = order.order_id - if fo: - logger.info(f"Found {order} for trade {trade}.") - self.update_trade_state(trade, order.order_id, fo, - stoploss_order=order.ft_order_side == 'stoploss') - - except ExchangeError: - logger.warning(f"Error updating {order.order_id}.") - -# -# BUY / enter positions / open trades logic and methods -# - - def enter_positions(self) -> int: - """ - Tries to execute buy orders for new trades (positions) - """ - trades_created = 0 - - whitelist = copy.deepcopy(self.active_pair_whitelist) - if not whitelist: - logger.info("Active pair whitelist is empty.") - return trades_created - # Remove pairs for currently opened trades from the whitelist - # Allow rebuying of the same pair if allow_position_stacking is set to True - if not self.config['allow_position_stacking']: - for trade in Trade.get_open_trades(): - if trade.pair in whitelist: - whitelist.remove(trade.pair) - logger.debug('Ignoring %s in pair whitelist', trade.pair) - - if not whitelist: - logger.info("No currency pair in active pair whitelist, " - "but checking to sell open trades.") - return trades_created - if PairLocks.is_global_lock(): - lock = PairLocks.get_pair_longest_lock('*') - if lock: - self.log_once(f"Global pairlock active until " - f"{lock.lock_end_time.strftime(constants.DATETIME_PRINT_FORMAT)}. " - f"Not creating new trades, reason: {lock.reason}.", logger.info) - else: - self.log_once("Global pairlock active. Not creating new trades.", logger.info) - return trades_created - # Create entity and execute trade for each pair from whitelist - for pair in whitelist: - try: - trades_created += self.create_trade(pair) - except DependencyException as exception: - logger.warning('Unable to create trade for %s: %s', pair, exception) - - if not trades_created: - logger.debug("Found no buy signals for whitelisted currencies. Trying again...") - - return trades_created - - def create_trade(self, pair: str) -> bool: - """ - Check the implemented trading strategy for buy signals. - - If the pair triggers the buy signal a new trade record gets created - and the buy-order opening the trade gets issued towards the exchange. - - :return: True if a trade has been created. - """ - logger.debug(f"create_trade for pair {pair}") - - analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(pair, self.strategy.timeframe) - nowtime = analyzed_df.iloc[-1]['date'] if len(analyzed_df) > 0 else None - if self.strategy.is_pair_locked(pair, nowtime): - lock = PairLocks.get_pair_longest_lock(pair, nowtime) - if lock: - self.log_once(f"Pair {pair} is still locked until " - f"{lock.lock_end_time.strftime(constants.DATETIME_PRINT_FORMAT)} " - f"due to {lock.reason}.", - logger.info) - else: - self.log_once(f"Pair {pair} is still locked.", logger.info) - return False - - # get_free_open_trades is checked before create_trade is called - # but it is still used here to prevent opening too many trades within one iteration - if not self.get_free_open_trades(): - logger.debug(f"Can't open a new trade for {pair}: max number of trades is reached.") - return False - - # running get_signal on historical data fetched - (buy, sell, buy_tag) = self.strategy.get_signal( - pair, - self.strategy.timeframe, - analyzed_df - ) - - if buy and not sell: - stake_amount = self.wallets.get_trade_stake_amount(pair, self.edge) - - bid_check_dom = self.config.get('bid_strategy', {}).get('check_depth_of_market', {}) - if ((bid_check_dom.get('enabled', False)) and - (bid_check_dom.get('bids_to_ask_delta', 0) > 0)): - if self._check_depth_of_market_buy(pair, bid_check_dom): - return self.execute_entry(pair, stake_amount, buy_tag=buy_tag) - else: - return False - - return self.execute_entry(pair, stake_amount, buy_tag=buy_tag) - else: - return False - - def _check_depth_of_market_buy(self, pair: str, conf: Dict) -> bool: - """ - Checks depth of market before executing a buy - """ - conf_bids_to_ask_delta = conf.get('bids_to_ask_delta', 0) - logger.info(f"Checking depth of market for {pair} ...") - order_book = self.exchange.fetch_l2_order_book(pair, 1000) - order_book_data_frame = order_book_to_dataframe(order_book['bids'], order_book['asks']) - order_book_bids = order_book_data_frame['b_size'].sum() - order_book_asks = order_book_data_frame['a_size'].sum() - bids_ask_delta = order_book_bids / order_book_asks - logger.info( - f"Bids: {order_book_bids}, Asks: {order_book_asks}, Delta: {bids_ask_delta}, " - f"Bid Price: {order_book['bids'][0][0]}, Ask Price: {order_book['asks'][0][0]}, " - f"Immediate Bid Quantity: {order_book['bids'][0][1]}, " - f"Immediate Ask Quantity: {order_book['asks'][0][1]}." - ) - if bids_ask_delta >= conf_bids_to_ask_delta: - logger.info(f"Bids to asks delta for {pair} DOES satisfy condition.") - return True - else: - logger.info(f"Bids to asks delta for {pair} does not satisfy condition.") - return False - - def execute_entry(self, pair: str, stake_amount: float, price: Optional[float] = None, - forcebuy: bool = False, buy_tag: Optional[str] = None) -> bool: - """ - Executes a limit buy for the given pair - :param pair: pair for which we want to create a LIMIT_BUY - :param stake_amount: amount of stake-currency for the pair - :return: True if a buy order is created, false if it fails. - """ - time_in_force = self.strategy.order_time_in_force['buy'] - - if price: - enter_limit_requested = price - else: - # Calculate price - proposed_enter_rate = self.exchange.get_rate(pair, refresh=True, side="buy") - custom_entry_price = strategy_safe_wrapper(self.strategy.custom_entry_price, - default_retval=proposed_enter_rate)( - pair=pair, current_time=datetime.now(timezone.utc), - proposed_rate=proposed_enter_rate) - - enter_limit_requested = self.get_valid_price(custom_entry_price, proposed_enter_rate) - - if not enter_limit_requested: - raise PricingError('Could not determine buy price.') - - min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, enter_limit_requested, - self.strategy.stoploss) - - if not self.edge: - max_stake_amount = self.wallets.get_available_stake_amount() - stake_amount = strategy_safe_wrapper(self.strategy.custom_stake_amount, - default_retval=stake_amount)( - pair=pair, current_time=datetime.now(timezone.utc), - current_rate=enter_limit_requested, proposed_stake=stake_amount, - min_stake=min_stake_amount, max_stake=max_stake_amount) - stake_amount = self.wallets._validate_stake_amount(pair, stake_amount, min_stake_amount) - - if not stake_amount: - return False - - logger.info(f"Buy signal found: about create a new trade for {pair} with stake_amount: " - f"{stake_amount} ...") - - amount = stake_amount / enter_limit_requested - order_type = self.strategy.order_types['buy'] - if forcebuy: - # Forcebuy can define a different ordertype - order_type = self.strategy.order_types.get('forcebuy', order_type) - - if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)( - pair=pair, order_type=order_type, amount=amount, rate=enter_limit_requested, - time_in_force=time_in_force, current_time=datetime.now(timezone.utc)): - logger.info(f"User requested abortion of buying {pair}") - return False - amount = self.exchange.amount_to_precision(pair, amount) - order = self.exchange.create_order(pair=pair, ordertype=order_type, side="buy", - amount=amount, rate=enter_limit_requested, - time_in_force=time_in_force) - order_obj = Order.parse_from_ccxt_object(order, pair, 'buy') - order_id = order['id'] - order_status = order.get('status', None) - - # we assume the order is executed at the price requested - enter_limit_filled_price = enter_limit_requested - amount_requested = amount - - if order_status == 'expired' or order_status == 'rejected': - order_tif = self.strategy.order_time_in_force['buy'] - - # return false if the order is not filled - if float(order['filled']) == 0: - logger.warning('Buy %s order with time in force %s for %s is %s by %s.' - ' zero amount is fulfilled.', - order_tif, order_type, pair, order_status, self.exchange.name) - return False - else: - # the order is partially fulfilled - # in case of IOC orders we can check immediately - # if the order is fulfilled fully or partially - logger.warning('Buy %s order with time in force %s for %s is %s by %s.' - ' %s amount fulfilled out of %s (%s remaining which is canceled).', - order_tif, order_type, pair, order_status, self.exchange.name, - order['filled'], order['amount'], order['remaining'] - ) - stake_amount = order['cost'] - amount = safe_value_fallback(order, 'filled', 'amount') - enter_limit_filled_price = safe_value_fallback(order, 'average', 'price') - - # in case of FOK the order may be filled immediately and fully - elif order_status == 'closed': - stake_amount = order['cost'] - amount = safe_value_fallback(order, 'filled', 'amount') - enter_limit_filled_price = safe_value_fallback(order, 'average', 'price') - - # Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL - fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker') - trade = Trade( - pair=pair, - stake_amount=stake_amount, - amount=amount, - is_open=True, - amount_requested=amount_requested, - fee_open=fee, - fee_close=fee, - open_rate=enter_limit_filled_price, - open_rate_requested=enter_limit_requested, - open_date=datetime.utcnow(), - exchange=self.exchange.id, - open_order_id=order_id, - strategy=self.strategy.get_strategy_name(), - buy_tag=buy_tag, - timeframe=timeframe_to_minutes(self.config['timeframe']) - ) - trade.orders.append(order_obj) - - # Update fees if order is closed - if order_status == 'closed': - self.update_trade_state(trade, order_id, order) - - Trade.query.session.add(trade) - Trade.commit() - - # Updating wallets - self.wallets.update() - - self._notify_enter(trade, order_type) - - # Lock pair for 1 timeframe duration to prevent immediate rebuys - if self.config['allow_position_stacking']: - self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc) + timedelta(minutes=timeframe_to_minutes(self.config['timeframe'])), - reason='Prevent immediate rebuys') - - return True - - def _notify_enter(self, trade: Trade, order_type: str) -> None: - """ - Sends rpc notification when a buy occurred. - """ - msg = { - 'trade_id': trade.id, - 'type': RPCMessageType.BUY, - 'buy_tag': trade.buy_tag, - 'exchange': self.exchange.name.capitalize(), - 'pair': trade.pair, - 'limit': trade.open_rate, - 'order_type': order_type, - 'stake_amount': trade.stake_amount, - 'stake_currency': self.config['stake_currency'], - 'fiat_currency': self.config.get('fiat_display_currency', None), - 'amount': trade.amount, - 'open_date': trade.open_date or datetime.utcnow(), - 'current_rate': trade.open_rate_requested, - } - - # Send the message - self.rpc.send_msg(msg) - - def _notify_enter_cancel(self, trade: Trade, order_type: str, reason: str) -> None: - """ - Sends rpc notification when a buy cancel occurred. - """ - current_rate = self.exchange.get_rate(trade.pair, refresh=False, side="buy") - - msg = { - 'trade_id': trade.id, - 'type': RPCMessageType.BUY_CANCEL, - 'buy_tag': trade.buy_tag, - 'exchange': self.exchange.name.capitalize(), - 'pair': trade.pair, - 'limit': trade.open_rate, - 'order_type': order_type, - 'stake_amount': trade.stake_amount, - 'stake_currency': self.config['stake_currency'], - 'fiat_currency': self.config.get('fiat_display_currency', None), - 'amount': trade.amount, - 'open_date': trade.open_date, - 'current_rate': current_rate, - 'reason': reason, - } - - # Send the message - self.rpc.send_msg(msg) - - def _notify_enter_fill(self, trade: Trade) -> None: - msg = { - 'trade_id': trade.id, - 'type': RPCMessageType.BUY_FILL, - 'buy_tag': trade.buy_tag, - 'exchange': self.exchange.name.capitalize(), - 'pair': trade.pair, - 'open_rate': trade.open_rate, - 'stake_amount': trade.stake_amount, - 'stake_currency': self.config['stake_currency'], - 'fiat_currency': self.config.get('fiat_display_currency', None), - 'amount': trade.amount, - 'open_date': trade.open_date, - } - self.rpc.send_msg(msg) - -# -# SELL / exit positions / close trades logic and methods -# - - def exit_positions(self, trades: List[Any]) -> int: - """ - Tries to execute sell orders for open trades (positions) - """ - trades_closed = 0 - for trade in trades: - try: - - if (self.strategy.order_types.get('stoploss_on_exchange') and - self.handle_stoploss_on_exchange(trade)): - trades_closed += 1 - Trade.commit() - continue - # Check if we can sell our current pair - if trade.open_order_id is None and trade.is_open and self.handle_trade(trade): - trades_closed += 1 - - except DependencyException as exception: - logger.warning('Unable to sell trade %s: %s', trade.pair, exception) - - # Updating wallets if any trade occurred - if trades_closed: - self.wallets.update() - - return trades_closed - - def handle_trade(self, trade: Trade) -> bool: - """ - Sells the current pair if the threshold is reached and updates the trade record. - :return: True if trade has been sold, False otherwise - """ - if not trade.is_open: - raise DependencyException(f'Attempt to handle closed trade: {trade}') - - logger.debug('Handling %s ...', trade) - - (buy, sell) = (False, False) - - if (self.config.get('use_sell_signal', True) or - self.config.get('ignore_roi_if_buy_signal', False)): - analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair, - self.strategy.timeframe) - - (buy, sell, _) = self.strategy.get_signal( - trade.pair, - self.strategy.timeframe, - analyzed_df - ) - - logger.debug('checking sell') - exit_rate = self.exchange.get_rate(trade.pair, refresh=True, side="sell") - if self._check_and_execute_exit(trade, exit_rate, buy, sell): - return True - - logger.debug('Found no sell signal for %s.', trade) - return False - - def create_stoploss_order(self, trade: Trade, stop_price: float) -> bool: - """ - Abstracts creating stoploss orders from the logic. - Handles errors and updates the trade database object. - Force-sells the pair (using EmergencySell reason) in case of Problems creating the order. - :return: True if the order succeeded, and False in case of problems. - """ - try: - stoploss_order = self.exchange.stoploss(pair=trade.pair, amount=trade.amount, - stop_price=stop_price, - order_types=self.strategy.order_types) - - order_obj = Order.parse_from_ccxt_object(stoploss_order, trade.pair, 'stoploss') - trade.orders.append(order_obj) - trade.stoploss_order_id = str(stoploss_order['id']) - return True - except InsufficientFundsError as e: - logger.warning(f"Unable to place stoploss order {e}.") - # Try to figure out what went wrong - self.handle_insufficient_funds(trade) - - except InvalidOrderException as e: - trade.stoploss_order_id = None - logger.error(f'Unable to place a stoploss order on exchange. {e}') - logger.warning('Exiting the trade forcefully') - self.execute_trade_exit(trade, trade.stop_loss, sell_reason=SellCheckTuple( - sell_type=SellType.EMERGENCY_SELL)) - - except ExchangeError: - trade.stoploss_order_id = None - logger.exception('Unable to place a stoploss order on exchange.') - return False - - def handle_stoploss_on_exchange(self, trade: Trade) -> bool: - """ - Check if trade is fulfilled in which case the stoploss - on exchange should be added immediately if stoploss on exchange - is enabled. - """ - - logger.debug('Handling stoploss on exchange %s ...', trade) - - stoploss_order = None - - try: - # First we check if there is already a stoploss on exchange - stoploss_order = self.exchange.fetch_stoploss_order( - trade.stoploss_order_id, trade.pair) if trade.stoploss_order_id else None - except InvalidOrderException as exception: - logger.warning('Unable to fetch stoploss order: %s', exception) - - if stoploss_order: - trade.update_order(stoploss_order) - - # We check if stoploss order is fulfilled - if stoploss_order and stoploss_order['status'] in ('closed', 'triggered'): - trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value - self.update_trade_state(trade, trade.stoploss_order_id, stoploss_order, - stoploss_order=True) - # Lock pair for one candle to prevent immediate rebuys - self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc), - reason='Auto lock') - self._notify_exit(trade, "stoploss") - return True - - if trade.open_order_id or not trade.is_open: - # Trade has an open Buy or Sell order, Stoploss-handling can't happen in this case - # as the Amount on the exchange is tied up in another trade. - # The trade can be closed already (sell-order fill confirmation came in this iteration) - return False - - # If buy order is fulfilled but there is no stoploss, we add a stoploss on exchange - if not stoploss_order: - stoploss = self.edge.stoploss(pair=trade.pair) if self.edge else self.strategy.stoploss - stop_price = trade.open_rate * (1 + stoploss) - - if self.create_stoploss_order(trade=trade, stop_price=stop_price): - trade.stoploss_last_update = datetime.utcnow() - return False - - # If stoploss order is canceled for some reason we add it - if stoploss_order and stoploss_order['status'] in ('canceled', 'cancelled'): - if self.create_stoploss_order(trade=trade, stop_price=trade.stop_loss): - return False - else: - trade.stoploss_order_id = None - logger.warning('Stoploss order was cancelled, but unable to recreate one.') - - # Finally we check if stoploss on exchange should be moved up because of trailing. - # Triggered Orders are now real orders - so don't replace stoploss anymore - if ( - stoploss_order - and stoploss_order.get('status_stop') != 'triggered' - and (self.config.get('trailing_stop', False) - or self.config.get('use_custom_stoploss', False)) - ): - # if trailing stoploss is enabled we check if stoploss value has changed - # in which case we cancel stoploss order and put another one with new - # value immediately - self.handle_trailing_stoploss_on_exchange(trade, stoploss_order) - - return False - - def handle_trailing_stoploss_on_exchange(self, trade: Trade, order: dict) -> None: - """ - Check to see if stoploss on exchange should be updated - in case of trailing stoploss on exchange - :param trade: Corresponding Trade - :param order: Current on exchange stoploss order - :return: None - """ - if self.exchange.stoploss_adjust(trade.stop_loss, order): - # we check if the update is necessary - update_beat = self.strategy.order_types.get('stoploss_on_exchange_interval', 60) - if (datetime.utcnow() - trade.stoploss_last_update).total_seconds() >= update_beat: - # cancelling the current stoploss on exchange first - logger.info(f"Cancelling current stoploss on exchange for pair {trade.pair} " - f"(orderid:{order['id']}) in order to add another one ...") - try: - co = self.exchange.cancel_stoploss_order_with_result(order['id'], trade.pair, - trade.amount) - trade.update_order(co) - except InvalidOrderException: - logger.exception(f"Could not cancel stoploss order {order['id']} " - f"for pair {trade.pair}") - - # Create new stoploss order - if not self.create_stoploss_order(trade=trade, stop_price=trade.stop_loss): - logger.warning(f"Could not create trailing stoploss order " - f"for pair {trade.pair}.") - - def _check_and_execute_exit(self, trade: Trade, exit_rate: float, - buy: bool, sell: bool) -> bool: - """ - Check and execute exit - """ - should_sell = self.strategy.should_sell( - trade, exit_rate, datetime.now(timezone.utc), buy, sell, - force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0 - ) - - if should_sell.sell_flag: - logger.info(f'Executing Sell for {trade.pair}. Reason: {should_sell.sell_type}') - self.execute_trade_exit(trade, exit_rate, should_sell) - return True - return False - - def _check_timed_out(self, side: str, order: dict) -> bool: - """ - Check if timeout is active, and if the order is still open and timed out - """ - timeout = self.config.get('unfilledtimeout', {}).get(side) - ordertime = arrow.get(order['datetime']).datetime - if timeout is not None: - timeout_unit = self.config.get('unfilledtimeout', {}).get('unit', 'minutes') - timeout_kwargs = {timeout_unit: -timeout} - timeout_threshold = arrow.utcnow().shift(**timeout_kwargs).datetime - return (order['status'] == 'open' and order['side'] == side - and ordertime < timeout_threshold) - return False - - def check_handle_timedout(self) -> None: - """ - Check if any orders are timed out and cancel if necessary - :param timeoutvalue: Number of minutes until order is considered timed out - :return: None - """ - - for trade in Trade.get_open_order_trades(): - try: - if not trade.open_order_id: - continue - order = self.exchange.fetch_order(trade.open_order_id, trade.pair) - except (ExchangeError): - logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc()) - continue - - fully_cancelled = self.update_trade_state(trade, trade.open_order_id, order) - - if (order['side'] == 'buy' and (order['status'] == 'open' or fully_cancelled) and ( - fully_cancelled - or self._check_timed_out('buy', order) - or strategy_safe_wrapper(self.strategy.check_buy_timeout, - default_retval=False)(pair=trade.pair, - trade=trade, - order=order))): - self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['TIMEOUT']) - - elif (order['side'] == 'sell' and (order['status'] == 'open' or fully_cancelled) and ( - fully_cancelled - or self._check_timed_out('sell', order) - or strategy_safe_wrapper(self.strategy.check_sell_timeout, - default_retval=False)(pair=trade.pair, - trade=trade, - order=order))): - self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['TIMEOUT']) - - def cancel_all_open_orders(self) -> None: - """ - Cancel all orders that are currently open - :return: None - """ - - for trade in Trade.get_open_order_trades(): - try: - order = self.exchange.fetch_order(trade.open_order_id, trade.pair) - except (ExchangeError): - logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc()) - continue - - if order['side'] == 'buy': - self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['ALL_CANCELLED']) - - elif order['side'] == 'sell': - self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['ALL_CANCELLED']) - Trade.commit() - - def handle_cancel_enter(self, trade: Trade, order: Dict, reason: str) -> bool: - """ - Buy cancel - cancel order - :return: True if order was fully cancelled - """ - was_trade_fully_canceled = False - - # Cancelled orders may have the status of 'canceled' or 'closed' - if order['status'] not in constants.NON_OPEN_EXCHANGE_STATES: - filled_val = order.get('filled', 0.0) or 0.0 - filled_stake = filled_val * trade.open_rate - minstake = self.exchange.get_min_pair_stake_amount( - trade.pair, trade.open_rate, self.strategy.stoploss) - - if filled_val > 0 and filled_stake < minstake: - logger.warning( - f"Order {trade.open_order_id} for {trade.pair} not cancelled, " - f"as the filled amount of {filled_val} would result in an unsellable trade.") - return False - corder = self.exchange.cancel_order_with_result(trade.open_order_id, trade.pair, - trade.amount) - # Avoid race condition where the order could not be cancelled coz its already filled. - # Simply bailing here is the only safe way - as this order will then be - # handled in the next iteration. - if corder.get('status') not in constants.NON_OPEN_EXCHANGE_STATES: - logger.warning(f"Order {trade.open_order_id} for {trade.pair} not cancelled.") - return False - else: - # Order was cancelled already, so we can reuse the existing dict - corder = order - reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE'] - - logger.info('Buy order %s for %s.', reason, trade) - - # Using filled to determine the filled amount - filled_amount = safe_value_fallback2(corder, order, 'filled', 'filled') - if isclose(filled_amount, 0.0, abs_tol=constants.MATH_CLOSE_PREC): - logger.info('Buy order fully cancelled. Removing %s from database.', trade) - # if trade is not partially completed, just delete the trade - trade.delete() - was_trade_fully_canceled = True - reason += f", {constants.CANCEL_REASON['FULLY_CANCELLED']}" - else: - # if trade is partially complete, edit the stake details for the trade - # and close the order - # cancel_order may not contain the full order dict, so we need to fallback - # to the order dict acquired before cancelling. - # we need to fall back to the values from order if corder does not contain these keys. - trade.amount = filled_amount - trade.stake_amount = trade.amount * trade.open_rate - self.update_trade_state(trade, trade.open_order_id, corder) - - trade.open_order_id = None - logger.info('Partial buy order timeout for %s.', trade) - reason += f", {constants.CANCEL_REASON['PARTIALLY_FILLED']}" - - self.wallets.update() - self._notify_enter_cancel(trade, order_type=self.strategy.order_types['buy'], - reason=reason) - return was_trade_fully_canceled - - def handle_cancel_exit(self, trade: Trade, order: Dict, reason: str) -> str: - """ - Sell cancel - cancel order and update trade - :return: Reason for cancel - """ - # if trade is not partially completed, just cancel the order - if order['remaining'] == order['amount'] or order.get('filled') == 0.0: - if not self.exchange.check_order_canceled_empty(order): - try: - # if trade is not partially completed, just delete the order - co = self.exchange.cancel_order_with_result(trade.open_order_id, trade.pair, - trade.amount) - trade.update_order(co) - except InvalidOrderException: - logger.exception(f"Could not cancel sell order {trade.open_order_id}") - return 'error cancelling order' - logger.info('Sell order %s for %s.', reason, trade) - else: - reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE'] - logger.info('Sell order %s for %s.', reason, trade) - trade.update_order(order) - - trade.close_rate = None - trade.close_rate_requested = None - trade.close_profit = None - trade.close_profit_abs = None - trade.close_date = None - trade.is_open = True - trade.open_order_id = None - else: - # TODO: figure out how to handle partially complete sell orders - reason = constants.CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'] - - self.wallets.update() - self._notify_exit_cancel( - trade, - order_type=self.strategy.order_types['sell'], - reason=reason - ) - return reason - - def _safe_exit_amount(self, pair: str, amount: float) -> float: - """ - Get sellable amount. - Should be trade.amount - but will fall back to the available amount if necessary. - This should cover cases where get_real_amount() was not able to update the amount - for whatever reason. - :param pair: Pair we're trying to sell - :param amount: amount we expect to be available - :return: amount to sell - :raise: DependencyException: if available balance is not within 2% of the available amount. - """ - # Update wallets to ensure amounts tied up in a stoploss is now free! - self.wallets.update() - trade_base_currency = self.exchange.get_pair_base_currency(pair) - wallet_amount = self.wallets.get_free(trade_base_currency) - logger.debug(f"{pair} - Wallet: {wallet_amount} - Trade-amount: {amount}") - if wallet_amount >= amount: - return amount - elif wallet_amount > amount * 0.98: - logger.info(f"{pair} - Falling back to wallet-amount {wallet_amount} -> {amount}.") - return wallet_amount - else: - raise DependencyException( - f"Not enough amount to sell. Trade-amount: {amount}, Wallet: {wallet_amount}") - - def execute_trade_exit(self, trade: Trade, limit: float, sell_reason: SellCheckTuple) -> bool: - """ - Executes a trade exit for the given trade and limit - :param trade: Trade instance - :param limit: limit rate for the sell order - :param sell_reason: Reason the sell was triggered - :return: True if it succeeds (supported) False (not supported) - """ - sell_type = 'sell' - if sell_reason.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS): - sell_type = 'stoploss' - - # if stoploss is on exchange and we are on dry_run mode, - # we consider the sell price stop price - if self.config['dry_run'] and sell_type == 'stoploss' \ - and self.strategy.order_types['stoploss_on_exchange']: - limit = trade.stop_loss - - # set custom_exit_price if available - proposed_limit_rate = limit - current_profit = trade.calc_profit_ratio(limit) - custom_exit_price = strategy_safe_wrapper(self.strategy.custom_exit_price, - default_retval=proposed_limit_rate)( - pair=trade.pair, trade=trade, - current_time=datetime.now(timezone.utc), - proposed_rate=proposed_limit_rate, current_profit=current_profit) - - limit = self.get_valid_price(custom_exit_price, proposed_limit_rate) - - # First cancelling stoploss on exchange ... - if self.strategy.order_types.get('stoploss_on_exchange') and trade.stoploss_order_id: - try: - co = self.exchange.cancel_stoploss_order_with_result(trade.stoploss_order_id, - trade.pair, trade.amount) - trade.update_order(co) - except InvalidOrderException: - logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}") - - order_type = self.strategy.order_types[sell_type] - if sell_reason.sell_type == SellType.EMERGENCY_SELL: - # Emergency sells (default to market!) - order_type = self.strategy.order_types.get("emergencysell", "market") - if sell_reason.sell_type == SellType.FORCE_SELL: - # Force sells (default to the sell_type defined in the strategy, - # but we allow this value to be changed) - order_type = self.strategy.order_types.get("forcesell", order_type) - - amount = self._safe_exit_amount(trade.pair, trade.amount) - time_in_force = self.strategy.order_time_in_force['sell'] - - if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)( - pair=trade.pair, trade=trade, order_type=order_type, amount=amount, rate=limit, - time_in_force=time_in_force, sell_reason=sell_reason.sell_reason, - current_time=datetime.now(timezone.utc)): - logger.info(f"User requested abortion of selling {trade.pair}") - return False - - try: - # Execute sell and update trade record - order = self.exchange.create_order(pair=trade.pair, - ordertype=order_type, side="sell", - amount=amount, rate=limit, - time_in_force=time_in_force - ) - except InsufficientFundsError as e: - logger.warning(f"Unable to place order {e}.") - # Try to figure out what went wrong - self.handle_insufficient_funds(trade) - return False - - order_obj = Order.parse_from_ccxt_object(order, trade.pair, 'sell') - trade.orders.append(order_obj) - - trade.open_order_id = order['id'] - trade.sell_order_status = '' - trade.close_rate_requested = limit - trade.sell_reason = sell_reason.sell_reason - # In case of market sell orders the order can be closed immediately - if order.get('status', 'unknown') in ('closed', 'expired'): - self.update_trade_state(trade, trade.open_order_id, order) - Trade.commit() - - # Lock pair for one candle to prevent immediate re-buys - self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc), - reason='Auto lock') - - self._notify_exit(trade, order_type) - - return True - - def _notify_exit(self, trade: Trade, order_type: str, fill: bool = False) -> None: - """ - Sends rpc notification when a sell occurred. - """ - profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested - profit_trade = trade.calc_profit(rate=profit_rate) - # Use cached rates here - it was updated seconds ago. - current_rate = self.exchange.get_rate( - trade.pair, refresh=False, side="sell") if not fill else None - profit_ratio = trade.calc_profit_ratio(profit_rate) - gain = "profit" if profit_ratio > 0 else "loss" - - msg = { - 'type': (RPCMessageType.SELL_FILL if fill - else RPCMessageType.SELL), - 'trade_id': trade.id, - 'exchange': trade.exchange.capitalize(), - 'pair': trade.pair, - 'gain': gain, - 'limit': profit_rate, - 'order_type': order_type, - 'amount': trade.amount, - 'open_rate': trade.open_rate, - 'close_rate': trade.close_rate, - 'current_rate': current_rate, - 'profit_amount': profit_trade, - 'profit_ratio': profit_ratio, - 'sell_reason': trade.sell_reason, - 'open_date': trade.open_date, - 'close_date': trade.close_date or datetime.utcnow(), - 'stake_currency': self.config['stake_currency'], - 'fiat_currency': self.config.get('fiat_display_currency', None), - } - - if 'fiat_display_currency' in self.config: - msg.update({ - 'fiat_currency': self.config['fiat_display_currency'], - }) - - # Send the message - self.rpc.send_msg(msg) - - def _notify_exit_cancel(self, trade: Trade, order_type: str, reason: str) -> None: - """ - Sends rpc notification when a sell cancel occurred. - """ - if trade.sell_order_status == reason: - return - else: - trade.sell_order_status = reason - - profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested - profit_trade = trade.calc_profit(rate=profit_rate) - current_rate = self.exchange.get_rate(trade.pair, refresh=False, side="sell") - profit_ratio = trade.calc_profit_ratio(profit_rate) - gain = "profit" if profit_ratio > 0 else "loss" - - msg = { - 'type': RPCMessageType.SELL_CANCEL, - 'trade_id': trade.id, - 'exchange': trade.exchange.capitalize(), - 'pair': trade.pair, - 'gain': gain, - 'limit': profit_rate or 0, - 'order_type': order_type, - 'amount': trade.amount, - 'open_rate': trade.open_rate, - 'current_rate': current_rate, - 'profit_amount': profit_trade, - 'profit_ratio': profit_ratio, - 'sell_reason': trade.sell_reason, - 'open_date': trade.open_date, - 'close_date': trade.close_date or datetime.now(timezone.utc), - 'stake_currency': self.config['stake_currency'], - 'fiat_currency': self.config.get('fiat_display_currency', None), - 'reason': reason, - } - - if 'fiat_display_currency' in self.config: - msg.update({ - 'fiat_currency': self.config['fiat_display_currency'], - }) - - # Send the message - self.rpc.send_msg(msg) - -# -# Common update trade state methods -# - - def update_trade_state(self, trade: Trade, order_id: str, action_order: Dict[str, Any] = None, - stoploss_order: bool = False) -> bool: - """ - Checks trades with open orders and updates the amount if necessary - Handles closing both buy and sell orders. - :param trade: Trade object of the trade we're analyzing - :param order_id: Order-id of the order we're analyzing - :param action_order: Already acquired order object - :return: True if order has been cancelled without being filled partially, False otherwise - """ - if not order_id: - logger.warning(f'Orderid for trade {trade} is empty.') - return False - - # Update trade with order values - logger.info('Found open order for %s', trade) - try: - order = action_order or self.exchange.fetch_order_or_stoploss_order(order_id, - trade.pair, - stoploss_order) - except InvalidOrderException as exception: - logger.warning('Unable to fetch order %s: %s', order_id, exception) - return False - - trade.update_order(order) - - # Try update amount (binance-fix) - try: - new_amount = self.get_real_amount(trade, order) - if not isclose(safe_value_fallback(order, 'filled', 'amount'), new_amount, - abs_tol=constants.MATH_CLOSE_PREC): - order['amount'] = new_amount - order.pop('filled', None) - trade.recalc_open_trade_value() - except DependencyException as exception: - logger.warning("Could not update trade amount: %s", exception) - - if self.exchange.check_order_canceled_empty(order): - # Trade has been cancelled on exchange - # Handling of this will happen in check_handle_timeout. - return True - trade.update(order) - Trade.commit() - - # Updating wallets when order is closed - if not trade.is_open: - if not stoploss_order and not trade.open_order_id: - self._notify_exit(trade, '', True) - self.handle_protections(trade.pair) - self.wallets.update() - elif not trade.open_order_id: - # Buy fill - self._notify_enter_fill(trade) - - return False - - def handle_protections(self, pair: str) -> None: - prot_trig = self.protections.stop_per_pair(pair) - if prot_trig: - msg = {'type': RPCMessageType.PROTECTION_TRIGGER, } - msg.update(prot_trig.to_json()) - self.rpc.send_msg(msg) - - prot_trig_glb = self.protections.global_stop() - if prot_trig_glb: - msg = {'type': RPCMessageType.PROTECTION_TRIGGER_GLOBAL, } - msg.update(prot_trig_glb.to_json()) - self.rpc.send_msg(msg) - - def apply_fee_conditional(self, trade: Trade, trade_base_currency: str, - amount: float, fee_abs: float) -> float: - """ - Applies the fee to amount (either from Order or from Trades). - Can eat into dust if more than the required asset is available. - """ - self.wallets.update() - if fee_abs != 0 and self.wallets.get_free(trade_base_currency) >= amount: - # Eat into dust if we own more than base currency - logger.info(f"Fee amount for {trade} was in base currency - " - f"Eating Fee {fee_abs} into dust.") - elif fee_abs != 0: - real_amount = self.exchange.amount_to_precision(trade.pair, amount - fee_abs) - logger.info(f"Applying fee on amount for {trade} " - f"(from {amount} to {real_amount}).") - return real_amount - return amount - - def get_real_amount(self, trade: Trade, order: Dict) -> float: - """ - Detect and update trade fee. - Calls trade.update_fee() upon correct detection. - Returns modified amount if the fee was taken from the destination currency. - Necessary for exchanges which charge fees in base currency (e.g. binance) - :return: identical (or new) amount for the trade - """ - # Init variables - order_amount = safe_value_fallback(order, 'filled', 'amount') - # Only run for closed orders - if trade.fee_updated(order.get('side', '')) or order['status'] == 'open': - return order_amount - - trade_base_currency = self.exchange.get_pair_base_currency(trade.pair) - # use fee from order-dict if possible - if self.exchange.order_has_fee(order): - fee_cost, fee_currency, fee_rate = self.exchange.extract_cost_curr_rate(order) - logger.info(f"Fee for Trade {trade} [{order.get('side')}]: " - f"{fee_cost:.8g} {fee_currency} - rate: {fee_rate}") - if fee_rate is None or fee_rate < 0.02: - # Reject all fees that report as > 2%. - # These are most likely caused by a parsing bug in ccxt - # due to multiple trades (https://github.com/ccxt/ccxt/issues/8025) - trade.update_fee(fee_cost, fee_currency, fee_rate, order.get('side', '')) - if trade_base_currency == fee_currency: - # Apply fee to amount - return self.apply_fee_conditional(trade, trade_base_currency, - amount=order_amount, fee_abs=fee_cost) - return order_amount - return self.fee_detection_from_trades(trade, order, order_amount) - - def fee_detection_from_trades(self, trade: Trade, order: Dict, order_amount: float) -> float: - """ - fee-detection fallback to Trades. Parses result of fetch_my_trades to get correct fee. - """ - trades = self.exchange.get_trades_for_order(self.exchange.get_order_id_conditional(order), - trade.pair, trade.open_date) - - if len(trades) == 0: - logger.info("Applying fee on amount for %s failed: myTrade-Dict empty found", trade) - return order_amount - fee_currency = None - amount = 0 - fee_abs = 0.0 - fee_cost = 0.0 - trade_base_currency = self.exchange.get_pair_base_currency(trade.pair) - fee_rate_array: List[float] = [] - for exectrade in trades: - amount += exectrade['amount'] - if self.exchange.order_has_fee(exectrade): - fee_cost_, fee_currency, fee_rate_ = self.exchange.extract_cost_curr_rate(exectrade) - fee_cost += fee_cost_ - if fee_rate_ is not None: - fee_rate_array.append(fee_rate_) - # only applies if fee is in quote currency! - if trade_base_currency == fee_currency: - fee_abs += fee_cost_ - # Ensure at least one trade was found: - if fee_currency: - # fee_rate should use mean - fee_rate = sum(fee_rate_array) / float(len(fee_rate_array)) if fee_rate_array else None - if fee_rate is not None and fee_rate < 0.02: - # Only update if fee-rate is < 2% - trade.update_fee(fee_cost, fee_currency, fee_rate, order.get('side', '')) - - if not isclose(amount, order_amount, abs_tol=constants.MATH_CLOSE_PREC): - logger.warning(f"Amount {amount} does not match amount {trade.amount}") - raise DependencyException("Half bought? Amounts don't match") - - if fee_abs != 0: - return self.apply_fee_conditional(trade, trade_base_currency, - amount=amount, fee_abs=fee_abs) - else: - return amount - - def get_valid_price(self, custom_price: float, proposed_price: float) -> float: - """ - Return the valid price. - Check if the custom price is of the good type if not return proposed_price - :return: valid price for the order - """ - if custom_price: - try: - valid_custom_price = float(custom_price) - except ValueError: - valid_custom_price = proposed_price - else: - valid_custom_price = proposed_price - - cust_p_max_dist_r = self.config.get('custom_price_max_distance_ratio', 0.02) - min_custom_price_allowed = proposed_price - (proposed_price * cust_p_max_dist_r) - max_custom_price_allowed = proposed_price + (proposed_price * cust_p_max_dist_r) - - # Bracket between min_custom_price_allowed and max_custom_price_allowed - return max( - min(valid_custom_price, max_custom_price_allowed), - min_custom_price_allowed) From 91b9e5ce6872f9cf5f679ee51d7794cfbf9a9519 Mon Sep 17 00:00:00 2001 From: incrementby1 <91958753+incrementby1@users.noreply.github.com> Date: Wed, 27 Oct 2021 12:43:00 +0200 Subject: [PATCH 39/72] Delete StackingDemo.py --- StackingDemo.py | 591 ------------------------------------------------ 1 file changed, 591 deletions(-) delete mode 100644 StackingDemo.py diff --git a/StackingDemo.py b/StackingDemo.py deleted file mode 100644 index b88248fac..000000000 --- a/StackingDemo.py +++ /dev/null @@ -1,591 +0,0 @@ -# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement -# flake8: noqa: F401 - -# --- Do not remove these libs --- -import numpy as np # noqa -import pandas as pd # noqa -from pandas import DataFrame - -from freqtrade.strategy import (BooleanParameter, CategoricalParameter, DecimalParameter, - IStrategy, IntParameter) - -# -------------------------------- -# Add your lib to import here -import talib.abstract as ta -import freqtrade.vendor.qtpylib.indicators as qtpylib - -from freqtrade.persistence import Trade -from datetime import datetime,timezone,timedelta - -""" - Warning: -This is still work in progress, so there is no warranty that everything works as intended, -it is possible that this strategy results in huge losses or doesn't even work at all. -Make sure to only run this in dry_mode so you don't lose any money. - -""" - -class StackingDemo(IStrategy): - """ - This is the default strategy template with added functions for trade stacking / buying the same positions multiple times. - It should function like this: - Find good buys using indicators. - When a new buy occurs the strategy will enable rebuys of the pair like this: - self.custom_info[metadata["pair"]]["rebuy"] = 1 - Then, if the price should drop after the last buy within the timerange of rebuy_time_limit_hours, - the same pair will be purchased again. This is intended to help with reducing possible losses. - If the price only goes up after the first buy, the strategy won't buy this pair again, and after the time limit is over, - look for other pairs to buy. - For selling there is this flag: - self.custom_info[metadata["pair"]]["resell"] = 1 - which should simply sell all trades of this pair until none are left. - - You can set how many pairs you want to trade and how many trades you want to allow for a pair, - but you must make sure to set max_open_trades to the produce of max_open_pairs and max_open_trades in your configuration file. - Also allow_position_stacking has to be set to true in the configuration file. - - For backtesting make sure to provide --enable-position-stacking as an argument in the command line. - Backtesting will be slow. - Hyperopt was not tested. - - # run the bot: - freqtrade trade -c StackingConfig.json -s StackingDemo --db-url sqlite:///tradesv3_StackingDemo_dry-run.sqlite --dry-run - """ - # Strategy interface version - allow new iterations of the strategy interface. - # Check the documentation or the Sample strategy to get the latest version. - INTERFACE_VERSION = 2 - - # how many pairs to trade / trades per pair if allow_position_stacking is enabled - max_open_pairs, max_trades_per_pair = 4, 3 - # make sure to have this value in your config file - max_open_trades = max_open_pairs * max_trades_per_pair - - # debugging - print_trades = True - - # specify for how long to want to allow rebuys of this pair - rebuy_time_limit_hours = 2 - - # store additional information needed for this strategy: - custom_info = {} - custom_num_open_pairs = {} - - # Minimal ROI designed for the strategy. - # This attribute will be overridden if the config file contains "minimal_roi". - minimal_roi = { - "60": 0.01, - "30": 0.02, - "0": 0.001 - } - - # Optimal stoploss designed for the strategy. - # This attribute will be overridden if the config file contains "stoploss". - stoploss = -0.10 - - # Trailing stoploss - trailing_stop = False - # trailing_only_offset_is_reached = False - # trailing_stop_positive = 0.01 - # trailing_stop_positive_offset = 0.0 # Disabled / not configured - - # Optimal timeframe for the strategy. - timeframe = '5m' - - # Run "populate_indicators()" only for new candle. - process_only_new_candles = False - - # These values can be overridden in the "ask_strategy" section in the config. - use_sell_signal = True - sell_profit_only = False - ignore_roi_if_buy_signal = False - - # Number of candles the strategy requires before producing valid signals - startup_candle_count: int = 30 - - # Optional order type mapping. - order_types = { - 'buy': 'market', - 'sell': 'market', - 'stoploss': 'market', - 'stoploss_on_exchange': False - } - - # Optional order time in force. - order_time_in_force = { - 'buy': 'gtc', - 'sell': 'gtc' - } - - plot_config = { - # Main plot indicators (Moving averages, ...) - 'main_plot': { - 'tema': {}, - 'sar': {'color': 'white'}, - }, - 'subplots': { - # Subplots - each dict defines one additional plot - "MACD": { - 'macd': {'color': 'blue'}, - 'macdsignal': {'color': 'orange'}, - }, - "RSI": { - 'rsi': {'color': 'red'}, - } - } - } - def informative_pairs(self): - """ - Define additional, informative pair/interval combinations to be cached from the exchange. - These pair/interval combinations are non-tradeable, unless they are part - of the whitelist as well. - For more information, please consult the documentation - :return: List of tuples in the format (pair, interval) - Sample: return [("ETH/USDT", "5m"), - ("BTC/USDT", "15m"), - ] - """ - return [] - - def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - """ - Adds several different TA indicators to the given DataFrame - - Performance Note: For the best performance be frugal on the number of indicators - you are using. Let uncomment only the indicator you are using in your strategies - or your hyperopt configuration, otherwise you will waste your memory and CPU usage. - :param dataframe: Dataframe with data from the exchange - :param metadata: Additional information, like the currently traded pair - :return: a Dataframe with all mandatory indicators for the strategies - """ - - # STACKING STUFF - - # confirm config - self.max_trades_per_pair = self.config['max_open_trades'] / self.max_open_pairs - if not self.config["allow_position_stacking"]: - self.max_trades_per_pair = 1 - - # store number of open pairs - self.custom_num_open_pairs = {"num_open_pairs": 0} - - # Store custom information for this pair: - if not metadata["pair"] in self.custom_info: - self.custom_info[metadata["pair"]] = {} - - if not "rebuy" in self.custom_info[metadata["pair"]]: - # number of trades for this pair - self.custom_info[metadata["pair"]]["num_trades"] = 0 - # use rebuy/resell as buy-/sell- indicators - self.custom_info[metadata["pair"]]["rebuy"] = 0 - self.custom_info[metadata["pair"]]["resell"] = 0 - # store latest open_date for this pair - self.custom_info[metadata["pair"]]["last_open_date"] = datetime.now(timezone.utc) - timedelta(days=100) - # stare the value of the latest open price for this pair - self.custom_info[metadata["pair"]]["latest_open_rate"] = 0 - - # INDICATORS - - # Momentum Indicators - # ------------------------------------ - - # ADX - dataframe['adx'] = ta.ADX(dataframe) - - # # Plus Directional Indicator / Movement - # dataframe['plus_dm'] = ta.PLUS_DM(dataframe) - # dataframe['plus_di'] = ta.PLUS_DI(dataframe) - - # # Minus Directional Indicator / Movement - # dataframe['minus_dm'] = ta.MINUS_DM(dataframe) - # dataframe['minus_di'] = ta.MINUS_DI(dataframe) - - # # Aroon, Aroon Oscillator - # aroon = ta.AROON(dataframe) - # dataframe['aroonup'] = aroon['aroonup'] - # dataframe['aroondown'] = aroon['aroondown'] - # dataframe['aroonosc'] = ta.AROONOSC(dataframe) - - # # Awesome Oscillator - # dataframe['ao'] = qtpylib.awesome_oscillator(dataframe) - - # # Keltner Channel - # keltner = qtpylib.keltner_channel(dataframe) - # dataframe["kc_upperband"] = keltner["upper"] - # dataframe["kc_lowerband"] = keltner["lower"] - # dataframe["kc_middleband"] = keltner["mid"] - # dataframe["kc_percent"] = ( - # (dataframe["close"] - dataframe["kc_lowerband"]) / - # (dataframe["kc_upperband"] - dataframe["kc_lowerband"]) - # ) - # dataframe["kc_width"] = ( - # (dataframe["kc_upperband"] - dataframe["kc_lowerband"]) / dataframe["kc_middleband"] - # ) - - # # Ultimate Oscillator - # dataframe['uo'] = ta.ULTOSC(dataframe) - - # # Commodity Channel Index: values [Oversold:-100, Overbought:100] - # dataframe['cci'] = ta.CCI(dataframe) - - # RSI - dataframe['rsi'] = ta.RSI(dataframe) - - # # Inverse Fisher transform on RSI: values [-1.0, 1.0] (https://goo.gl/2JGGoy) - # rsi = 0.1 * (dataframe['rsi'] - 50) - # dataframe['fisher_rsi'] = (np.exp(2 * rsi) - 1) / (np.exp(2 * rsi) + 1) - - # # Inverse Fisher transform on RSI normalized: values [0.0, 100.0] (https://goo.gl/2JGGoy) - # dataframe['fisher_rsi_norma'] = 50 * (dataframe['fisher_rsi'] + 1) - - # # Stochastic Slow - # stoch = ta.STOCH(dataframe) - # dataframe['slowd'] = stoch['slowd'] - # dataframe['slowk'] = stoch['slowk'] - - # Stochastic Fast - stoch_fast = ta.STOCHF(dataframe) - dataframe['fastd'] = stoch_fast['fastd'] - dataframe['fastk'] = stoch_fast['fastk'] - - # # Stochastic RSI - # Please read https://github.com/freqtrade/freqtrade/issues/2961 before using this. - # STOCHRSI is NOT aligned with tradingview, which may result in non-expected results. - # stoch_rsi = ta.STOCHRSI(dataframe) - # dataframe['fastd_rsi'] = stoch_rsi['fastd'] - # dataframe['fastk_rsi'] = stoch_rsi['fastk'] - - # MACD - macd = ta.MACD(dataframe) - dataframe['macd'] = macd['macd'] - dataframe['macdsignal'] = macd['macdsignal'] - dataframe['macdhist'] = macd['macdhist'] - - # MFI - dataframe['mfi'] = ta.MFI(dataframe) - - # # ROC - # dataframe['roc'] = ta.ROC(dataframe) - - # Overlap Studies - # ------------------------------------ - - # Bollinger Bands - bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2) - dataframe['bb_lowerband'] = bollinger['lower'] - dataframe['bb_middleband'] = bollinger['mid'] - dataframe['bb_upperband'] = bollinger['upper'] - dataframe["bb_percent"] = ( - (dataframe["close"] - dataframe["bb_lowerband"]) / - (dataframe["bb_upperband"] - dataframe["bb_lowerband"]) - ) - dataframe["bb_width"] = ( - (dataframe["bb_upperband"] - dataframe["bb_lowerband"]) / dataframe["bb_middleband"] - ) - - # Bollinger Bands - Weighted (EMA based instead of SMA) - # weighted_bollinger = qtpylib.weighted_bollinger_bands( - # qtpylib.typical_price(dataframe), window=20, stds=2 - # ) - # dataframe["wbb_upperband"] = weighted_bollinger["upper"] - # dataframe["wbb_lowerband"] = weighted_bollinger["lower"] - # dataframe["wbb_middleband"] = weighted_bollinger["mid"] - # dataframe["wbb_percent"] = ( - # (dataframe["close"] - dataframe["wbb_lowerband"]) / - # (dataframe["wbb_upperband"] - dataframe["wbb_lowerband"]) - # ) - # dataframe["wbb_width"] = ( - # (dataframe["wbb_upperband"] - dataframe["wbb_lowerband"]) / dataframe["wbb_middleband"] - # ) - - # # EMA - Exponential Moving Average - # dataframe['ema3'] = ta.EMA(dataframe, timeperiod=3) - # dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5) - # dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10) - # dataframe['ema21'] = ta.EMA(dataframe, timeperiod=21) - # dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50) - # dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100) - - # # SMA - Simple Moving Average - # dataframe['sma3'] = ta.SMA(dataframe, timeperiod=3) - # dataframe['sma5'] = ta.SMA(dataframe, timeperiod=5) - # dataframe['sma10'] = ta.SMA(dataframe, timeperiod=10) - # dataframe['sma21'] = ta.SMA(dataframe, timeperiod=21) - # dataframe['sma50'] = ta.SMA(dataframe, timeperiod=50) - # dataframe['sma100'] = ta.SMA(dataframe, timeperiod=100) - - # Parabolic SAR - dataframe['sar'] = ta.SAR(dataframe) - - # TEMA - Triple Exponential Moving Average - dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9) - - # Cycle Indicator - # ------------------------------------ - # Hilbert Transform Indicator - SineWave - hilbert = ta.HT_SINE(dataframe) - dataframe['htsine'] = hilbert['sine'] - dataframe['htleadsine'] = hilbert['leadsine'] - - # Pattern Recognition - Bullish candlestick patterns - # ------------------------------------ - # # Hammer: values [0, 100] - # dataframe['CDLHAMMER'] = ta.CDLHAMMER(dataframe) - # # Inverted Hammer: values [0, 100] - # dataframe['CDLINVERTEDHAMMER'] = ta.CDLINVERTEDHAMMER(dataframe) - # # Dragonfly Doji: values [0, 100] - # dataframe['CDLDRAGONFLYDOJI'] = ta.CDLDRAGONFLYDOJI(dataframe) - # # Piercing Line: values [0, 100] - # dataframe['CDLPIERCING'] = ta.CDLPIERCING(dataframe) # values [0, 100] - # # Morningstar: values [0, 100] - # dataframe['CDLMORNINGSTAR'] = ta.CDLMORNINGSTAR(dataframe) # values [0, 100] - # # Three White Soldiers: values [0, 100] - # dataframe['CDL3WHITESOLDIERS'] = ta.CDL3WHITESOLDIERS(dataframe) # values [0, 100] - - # Pattern Recognition - Bearish candlestick patterns - # ------------------------------------ - # # Hanging Man: values [0, 100] - # dataframe['CDLHANGINGMAN'] = ta.CDLHANGINGMAN(dataframe) - # # Shooting Star: values [0, 100] - # dataframe['CDLSHOOTINGSTAR'] = ta.CDLSHOOTINGSTAR(dataframe) - # # Gravestone Doji: values [0, 100] - # dataframe['CDLGRAVESTONEDOJI'] = ta.CDLGRAVESTONEDOJI(dataframe) - # # Dark Cloud Cover: values [0, 100] - # dataframe['CDLDARKCLOUDCOVER'] = ta.CDLDARKCLOUDCOVER(dataframe) - # # Evening Doji Star: values [0, 100] - # dataframe['CDLEVENINGDOJISTAR'] = ta.CDLEVENINGDOJISTAR(dataframe) - # # Evening Star: values [0, 100] - # dataframe['CDLEVENINGSTAR'] = ta.CDLEVENINGSTAR(dataframe) - - # Pattern Recognition - Bullish/Bearish candlestick patterns - # ------------------------------------ - # # Three Line Strike: values [0, -100, 100] - # dataframe['CDL3LINESTRIKE'] = ta.CDL3LINESTRIKE(dataframe) - # # Spinning Top: values [0, -100, 100] - # dataframe['CDLSPINNINGTOP'] = ta.CDLSPINNINGTOP(dataframe) # values [0, -100, 100] - # # Engulfing: values [0, -100, 100] - # dataframe['CDLENGULFING'] = ta.CDLENGULFING(dataframe) # values [0, -100, 100] - # # Harami: values [0, -100, 100] - # dataframe['CDLHARAMI'] = ta.CDLHARAMI(dataframe) # values [0, -100, 100] - # # Three Outside Up/Down: values [0, -100, 100] - # dataframe['CDL3OUTSIDE'] = ta.CDL3OUTSIDE(dataframe) # values [0, -100, 100] - # # Three Inside Up/Down: values [0, -100, 100] - # dataframe['CDL3INSIDE'] = ta.CDL3INSIDE(dataframe) # values [0, -100, 100] - - # # Chart type - # # ------------------------------------ - # # Heikin Ashi Strategy - # heikinashi = qtpylib.heikinashi(dataframe) - # dataframe['ha_open'] = heikinashi['open'] - # dataframe['ha_close'] = heikinashi['close'] - # dataframe['ha_high'] = heikinashi['high'] - # dataframe['ha_low'] = heikinashi['low'] - - # Retrieve best bid and best ask from the orderbook - # ------------------------------------ - """ - # first check if dataprovider is available - if self.dp: - if self.dp.runmode.value in ('live', 'dry_run'): - ob = self.dp.orderbook(metadata['pair'], 1) - dataframe['best_bid'] = ob['bids'][0][0] - dataframe['best_ask'] = ob['asks'][0][0] - """ - - return dataframe - - def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - """ - Based on TA indicators, populates the buy signal for the given dataframe - :param dataframe: DataFrame populated with indicators - :param metadata: Additional information, like the currently traded pair - :return: DataFrame with buy column - """ - dataframe.loc[ - ( - ( - (qtpylib.crossed_above(dataframe['rsi'], 30)) & # Signal: RSI crosses above 30 - (dataframe['tema'] <= dataframe['bb_middleband']) & # Guard: tema below BB middle - (dataframe['tema'] > dataframe['tema'].shift(1)) | # Guard: tema is raising - # use either buy signal or rebuy flag to trigger a buy - (self.custom_info[metadata["pair"]]["rebuy"] == 1) - ) & - (dataframe['volume'] > 0) # Make sure Volume is not 0 - ), - 'buy'] = 1 - - return dataframe - - def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - """ - Based on TA indicators, populates the sell signal for the given dataframe - :param dataframe: DataFrame populated with indicators - :param metadata: Additional information, like the currently traded pair - :return: DataFrame with buy column - """ - dataframe.loc[ - ( - ( - (qtpylib.crossed_above(dataframe['rsi'], 70)) & # Signal: RSI crosses above 70 - (dataframe['tema'] > dataframe['bb_middleband']) & # Guard: tema above BB middle - (dataframe['tema'] < dataframe['tema'].shift(1)) | # Guard: tema is falling - # use either sell signal or resell flag to trigger a sell - (self.custom_info[metadata["pair"]]["resell"] == 1) - ) & - (dataframe['volume'] > 0) # Make sure Volume is not 0 - ), - 'sell'] = 1 - return dataframe - - # use_custom_sell = True - - def custom_sell(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float, - current_profit: float, **kwargs) -> 'Optional[Union[str, bool]]': - """ - Custom sell signal logic indicating that specified position should be sold. Returning a - string or True from this method is equal to setting sell signal on a candle at specified - time. This method is not called when sell signal is set. - - This method should be overridden to create sell signals that depend on trade parameters. For - example you could implement a sell relative to the candle when the trade was opened, - or a custom 1:2 risk-reward ROI. - - Custom sell reason max length is 64. Exceeding characters will be removed. - - :param pair: Pair that's currently analyzed - :param trade: trade object. - :param current_time: datetime object, containing the current datetime - :param current_rate: Rate, calculated based on pricing settings in ask_strategy. - :param current_profit: Current profit (as ratio), calculated based on current_rate. - :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. - :return: To execute sell, return a string with custom sell reason or True. Otherwise return - None or False. - """ - # if self.custom_info[pair]["resell"] == 1: - # return 'resell' - return None - - def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, - time_in_force: str, current_time: 'datetime', **kwargs) -> bool: - return_statement = True - - if self.config['allow_position_stacking']: - return_statement = self.check_open_trades(pair, rate, current_time) - - # debugging - if return_statement and self.print_trades: - # use str.join() for speed - out = (current_time.strftime("%c"), " Bought: ", pair, ", rate: ", str(rate), ", rebuy: ", str(self.custom_info[pair]["rebuy"]), ", trades: ", str(self.custom_info[pair]["num_trades"])) - print("".join(out)) - - return return_statement - - def confirm_trade_exit(self, pair: str, trade: 'Trade', order_type: str, amount: float, - rate: float, time_in_force: str, sell_reason: str, - current_time: 'datetime', **kwargs) -> bool: - - if self.config["allow_position_stacking"]: - - # unlock open pairs limit after every sell - self.unlock_reason('Open pairs limit') - - # unlock open pairs limit after last item is sold - if self.custom_info[pair]["num_trades"] == 1: - # decrement open_pairs_count by 1 if last item is sold - self.custom_num_open_pairs["num_open_pairs"]-=1 - self.custom_info[pair]["resell"] = 0 - # reset rate - self.custom_info[pair]["latest_open_rate"] = 0.0 - self.unlock_reason('Trades per pair limit') - - # change dataframe to produce sell signal after a sell - if self.custom_info[pair]["num_trades"] >= self.max_trades_per_pair: - self.custom_info[pair]["resell"] = 1 - - # decrement number of trades by 1: - self.custom_info[pair]["num_trades"]-=1 - - # debugging stuff - if self.print_trades: - # use str.join() for speed - out = (current_time.strftime("%c"), " Sold: ", pair, ", rate: ", str(rate),", profit: ", str(trade.calc_profit_ratio(rate)), ", resell: ", str(self.custom_info[pair]["resell"]), ", trades: ", str(self.custom_info[pair]["num_trades"])) - print("".join(out)) - - return True - - def check_open_trades(self, pair: str, rate: float, current_time: datetime): - - # retrieve information about current open pairs - tr_info = self.get_trade_information(pair) - - # update number of open trades for the pair - self.custom_info[pair]["num_trades"] = tr_info[1] - self.custom_num_open_pairs["num_open_pairs"] = len(tr_info[0]) - # update value of the last open price - self.custom_info[pair]["latest_open_rate"] = tr_info[2] - - # don't buy if we have enough trades for this pair - if self.custom_info[pair]["num_trades"] >= self.max_trades_per_pair: - # lock if we already have enough pairs open, will be unlocked after last item of a pair is sold - self.lock_pair(pair, until=datetime.now(timezone.utc) + timedelta(days=100), reason='Trades per pair limit') - self.custom_info[pair]["rebuy"] = 0 - return False - - # don't buy if we have enough pairs - if self.custom_num_open_pairs["num_open_pairs"] >= self.max_open_pairs: - if not pair in tr_info[0]: - # lock if this pair is not in our list, will be unlocked after the next sell - self.lock_pair(pair, until=datetime.now(timezone.utc) + timedelta(days=100), reason='Open pairs limit') - self.custom_info[pair]["rebuy"] = 0 - return False - - # don't buy at a higher price, try until time limit is exceeded; skips if it's the first trade' - if rate > self.custom_info[pair]["latest_open_rate"] and self.custom_info[pair]["latest_open_rate"] != 0.0: - # how long do we want to try buying cheaper before we look for other pairs? - if (current_time - self.custom_info[pair]['last_open_date']).seconds/3600 > self.rebuy_time_limit_hours: - self.custom_info[pair]["rebuy"] = 0 - self.unlock_reason('Open pairs limit') - return False - - # set rebuy flag if num_trades < limit-1 - if self.custom_info[pair]["num_trades"] < self.max_trades_per_pair-1: - self.custom_info[pair]["rebuy"] = 1 - else: - self.custom_info[pair]["rebuy"] = 0 - - # update rate - self.custom_info[pair]["latest_open_rate"] = rate - - #update date open - self.custom_info[pair]["last_open_date"] = current_time - - # increment trade count by 1 - self.custom_info[pair]["num_trades"]+=1 - - return True - - # custom function to help with the strategy - def get_trade_information(self, pair:str): - - latest_open_rate, trade_count = 0, 0.0 - # store all open pairs - open_pairs = [] - - ### start nested function - def compare_trade(trade: Trade): - nonlocal trade_count, latest_open_rate, pair - if trade.pair == pair: - # update latest_rate - latest_open_rate = trade.open_rate - trade_count+=1 - return trade.pair - ### end nested function - - # replaced for loop with map for speed - open_pairs = map(compare_trade, Trade.get_open_trades()) - # remove duplicates - open_pairs = (list(dict.fromkeys(open_pairs))) - - #print(*open_pairs, sep="\n") - - # put this all together to reduce the amount of loops - return open_pairs, trade_count, latest_open_rate From 2eb33707c93379a5a40bf7d2e6f6270d47563db8 Mon Sep 17 00:00:00 2001 From: incrementby1 Date: Wed, 27 Oct 2021 15:58:41 +0200 Subject: [PATCH 40/72] Undo changes --- freqtrade/configuration/configuration.py | 7 +------ freqtrade/freqtradebot.py | 18 ++++++------------ 2 files changed, 7 insertions(+), 18 deletions(-) diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index d4cf09821..9aa4b794e 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -137,12 +137,6 @@ class Configuration: setup_logging(config) def _process_trading_options(self, config: Dict[str, Any]) -> None: - - # Allow_position_stacking defaults to False - if not config.get('allow_position_stacking'): - config['allow_position_stacking'] = False - logger.info('Allow_position_stacking is set to ' + str(config['allow_position_stacking'])) - if config['runmode'] not in TRADING_MODES: return @@ -504,3 +498,4 @@ class Configuration: config['pairs'] = load_file(pairs_file) if 'pairs' in config: config['pairs'].sort() + diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 850cd1700..4def3747c 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -4,7 +4,7 @@ Freqtrade is the main module of this bot. It contains the class Freqtrade() import copy import logging import traceback -from datetime import datetime, timedelta, timezone +from datetime import datetime, timezone from math import isclose from threading import Lock from typing import Any, Dict, List, Optional @@ -359,12 +359,10 @@ class FreqtradeBot(LoggingMixin): logger.info("Active pair whitelist is empty.") return trades_created # Remove pairs for currently opened trades from the whitelist - # Allow rebuying of the same pair if allow_position_stacking is set to True - if not self.config['allow_position_stacking']: - for trade in Trade.get_open_trades(): - if trade.pair in whitelist: - whitelist.remove(trade.pair) - logger.debug('Ignoring %s in pair whitelist', trade.pair) + for trade in Trade.get_open_trades(): + if trade.pair in whitelist: + whitelist.remove(trade.pair) + logger.debug('Ignoring %s in pair whitelist', trade.pair) if not whitelist: logger.info("No currency pair in active pair whitelist, " @@ -594,11 +592,6 @@ class FreqtradeBot(LoggingMixin): self._notify_enter(trade, order_type) - # Lock pair for 1 timeframe duration to prevent immediate rebuys - if self.config['allow_position_stacking']: - self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc) + timedelta(minutes=timeframe_to_minutes(self.config['timeframe'])), - reason='Prevent immediate rebuys') - return True def _notify_enter(self, trade: Trade, order_type: str) -> None: @@ -1436,3 +1429,4 @@ class FreqtradeBot(LoggingMixin): return max( min(valid_custom_price, max_custom_price_allowed), min_custom_price_allowed) + From e2b64a750fcebbc5751fea13ffa14c33b73be1f6 Mon Sep 17 00:00:00 2001 From: JackBananas <81236318+JackBananas@users.noreply.github.com> Date: Wed, 27 Oct 2021 17:14:26 +0200 Subject: [PATCH 41/72] Update data-download.md Minor change due to a misleading sentence --- docs/data-download.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/data-download.md b/docs/data-download.md index 6c7d5312d..dbd7998c3 100644 --- a/docs/data-download.md +++ b/docs/data-download.md @@ -11,7 +11,7 @@ Otherwise `--exchange` becomes mandatory. You can use a relative timerange (`--days 20`) or an absolute starting point (`--timerange 20200101-`). For incremental downloads, the relative approach should be used. !!! Tip "Tip: Updating existing data" - If you already have backtesting data available in your data-directory and would like to refresh this data up to today, do not use `--days` or `--timerange` parameters. Freqtrade will keep the available data and only download the missing data. + If you already have backtesting data available in your data-directory and would like to refresh this data up to today, freqtrade will automatically calculate the data missing for the existing pairs and the download will occur from the latest available point until "now", neither --days or --timerange parameters are required. Freqtrade will keep the available data and only download the missing data. If you are updating existing data after inserting new pairs that you have no data for, use `--new-pairs-days xx` parameter. Specified number of days will be downloaded for new pairs while old pairs will be updated with missing data only. If you use `--days xx` parameter alone - data for specified number of days will be downloaded for _all_ pairs. Be careful, if specified number of days is smaller than gap between now and last downloaded candle - freqtrade will delete all existing data to avoid gaps in candle data. From 92130837a980cb9caeccd91c8fbc2d6dfe67da9b Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 27 Oct 2021 19:58:29 +0200 Subject: [PATCH 42/72] Improve and clarify informative pairs documentation --- docs/strategy-customization.md | 254 +++++++++++++++++---------------- 1 file changed, 128 insertions(+), 126 deletions(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 0bfc0a2f6..d0c9e5f4f 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -312,7 +312,7 @@ Currently this is `pair`, which can be accessed using `metadata['pair']` - and w The Metadata-dict should not be modified and does not persist information across multiple calls. Instead, have a look at the section [Storing information](strategy-advanced.md#Storing-information) -## Additional data (informative_pairs) +## Informative Pairs ### Get data for non-tradeable pairs @@ -341,6 +341,133 @@ A full sample can be found [in the DataProvider section](#complete-data-provider *** +### Informative pairs decorator (`@informative()`) + +In most common case it is possible to easily define informative pairs by using a decorator. All decorated `populate_indicators_*` methods run in isolation, +not having access to data from other informative pairs, in the end all informative dataframes are merged and passed to main `populate_indicators()` method. +When hyperopting, use of hyperoptable parameter `.value` attribute is not supported. Please use `.range` attribute. See [optimizing an indicator parameter](hyperopt.md#optimizing-an-indicator-parameter) +for more information. + +??? info "Full documentation" + ``` python + def informative(timeframe: str, asset: str = '', + fmt: Optional[Union[str, Callable[[KwArg(str)], str]]] = None, + ffill: bool = True) -> Callable[[PopulateIndicators], PopulateIndicators]: + """ + A decorator for populate_indicators_Nn(self, dataframe, metadata), allowing these functions to + define informative indicators. + + Example usage: + + @informative('1h') + def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) + return dataframe + + :param timeframe: Informative timeframe. Must always be equal or higher than strategy timeframe. + :param asset: Informative asset, for example BTC, BTC/USDT, ETH/BTC. Do not specify to use + current pair. + :param fmt: Column format (str) or column formatter (callable(name, asset, timeframe)). When not + specified, defaults to: + * {base}_{quote}_{column}_{timeframe} if asset is specified. + * {column}_{timeframe} if asset is not specified. + Format string supports these format variables: + * {asset} - full name of the asset, for example 'BTC/USDT'. + * {base} - base currency in lower case, for example 'eth'. + * {BASE} - same as {base}, except in upper case. + * {quote} - quote currency in lower case, for example 'usdt'. + * {QUOTE} - same as {quote}, except in upper case. + * {column} - name of dataframe column. + * {timeframe} - timeframe of informative dataframe. + :param ffill: ffill dataframe after merging informative pair. + """ + ``` + +??? Example "Fast and easy way to define informative pairs" + + Most of the time we do not need power and flexibility offered by `merge_informative_pair()`, therefore we can use a decorator to quickly define informative pairs. + + ``` python + + from datetime import datetime + from freqtrade.persistence import Trade + from freqtrade.strategy import IStrategy, informative + + class AwesomeStrategy(IStrategy): + + # This method is not required. + # def informative_pairs(self): ... + + # Define informative upper timeframe for each pair. Decorators can be stacked on same + # method. Available in populate_indicators as 'rsi_30m' and 'rsi_1h'. + @informative('30m') + @informative('1h') + def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) + return dataframe + + # Define BTC/STAKE informative pair. Available in populate_indicators and other methods as + # 'btc_rsi_1h'. Current stake currency should be specified as {stake} format variable + # instead of hardcoding actual stake currency. Available in populate_indicators and other + # methods as 'btc_usdt_rsi_1h' (when stake currency is USDT). + @informative('1h', 'BTC/{stake}') + def populate_indicators_btc_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) + return dataframe + + # Define BTC/ETH informative pair. You must specify quote currency if it is different from + # stake currency. Available in populate_indicators and other methods as 'eth_btc_rsi_1h'. + @informative('1h', 'ETH/BTC') + def populate_indicators_eth_btc_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) + return dataframe + + # Define BTC/STAKE informative pair. A custom formatter may be specified for formatting + # column names. A callable `fmt(**kwargs) -> str` may be specified, to implement custom + # formatting. Available in populate_indicators and other methods as 'rsi_upper'. + @informative('1h', 'BTC/{stake}', '{column}') + def populate_indicators_btc_1h_2(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['rsi_upper'] = ta.RSI(dataframe, timeperiod=14) + return dataframe + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + # Strategy timeframe indicators for current pair. + dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) + # Informative pairs are available in this method. + dataframe['rsi_less'] = dataframe['rsi'] < dataframe['rsi_1h'] + return dataframe + + ``` + +!!! Note + Do not use `@informative` decorator if you need to use data of one informative pair when generating another informative pair. Instead, define informative pairs + manually as described [in the DataProvider section](#complete-data-provider-sample). + +!!! Note + Use string formatting when accessing informative dataframes of other pairs. This will allow easily changing stake currency in config without having to adjust strategy code. + + ``` python + def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + stake = self.config['stake_currency'] + dataframe.loc[ + ( + (dataframe[f'btc_{stake}_rsi_1h'] < 35) + & + (dataframe['volume'] > 0) + ), + ['buy', 'buy_tag']] = (1, 'buy_signal_rsi') + + return dataframe + ``` + + Alternatively column renaming may be used to remove stake currency from column names: `@informative('1h', 'BTC/{stake}', fmt='{base}_{column}_{timeframe}')`. + +!!! Warning "Duplicate method names" + Methods tagged with `@informative()` decorator must always have unique names! Re-using same name (for example when copy-pasting already defined informative method) + will overwrite previously defined method and not produce any errors due to limitations of Python programming language. In such cases you will find that indicators + created in earlier-defined methods are not available in the dataframe. Carefully review method names and make sure they are unique! + + ## Additional data (DataProvider) The strategy provides access to the `DataProvider`. This allows you to get additional data to use in your strategy. @@ -686,131 +813,6 @@ In some situations it may be confusing to deal with stops relative to current ra ``` -### *@informative()* - -``` python -def informative(timeframe: str, asset: str = '', - fmt: Optional[Union[str, Callable[[KwArg(str)], str]]] = None, - ffill: bool = True) -> Callable[[PopulateIndicators], PopulateIndicators]: - """ - A decorator for populate_indicators_Nn(self, dataframe, metadata), allowing these functions to - define informative indicators. - - Example usage: - - @informative('1h') - def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) - return dataframe - - :param timeframe: Informative timeframe. Must always be equal or higher than strategy timeframe. - :param asset: Informative asset, for example BTC, BTC/USDT, ETH/BTC. Do not specify to use - current pair. - :param fmt: Column format (str) or column formatter (callable(name, asset, timeframe)). When not - specified, defaults to: - * {base}_{quote}_{column}_{timeframe} if asset is specified. - * {column}_{timeframe} if asset is not specified. - Format string supports these format variables: - * {asset} - full name of the asset, for example 'BTC/USDT'. - * {base} - base currency in lower case, for example 'eth'. - * {BASE} - same as {base}, except in upper case. - * {quote} - quote currency in lower case, for example 'usdt'. - * {QUOTE} - same as {quote}, except in upper case. - * {column} - name of dataframe column. - * {timeframe} - timeframe of informative dataframe. - :param ffill: ffill dataframe after merging informative pair. - """ -``` - -In most common case it is possible to easily define informative pairs by using a decorator. All decorated `populate_indicators_*` methods run in isolation, -not having access to data from other informative pairs, in the end all informative dataframes are merged and passed to main `populate_indicators()` method. -When hyperopting, use of hyperoptable parameter `.value` attribute is not supported. Please use `.range` attribute. See [optimizing an indicator parameter](hyperopt.md#optimizing-an-indicator-parameter) -for more information. - -??? Example "Fast and easy way to define informative pairs" - - Most of the time we do not need power and flexibility offered by `merge_informative_pair()`, therefore we can use a decorator to quickly define informative pairs. - - ``` python - - from datetime import datetime - from freqtrade.persistence import Trade - from freqtrade.strategy import IStrategy, informative - - class AwesomeStrategy(IStrategy): - - # This method is not required. - # def informative_pairs(self): ... - - # Define informative upper timeframe for each pair. Decorators can be stacked on same - # method. Available in populate_indicators as 'rsi_30m' and 'rsi_1h'. - @informative('30m') - @informative('1h') - def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) - return dataframe - - # Define BTC/STAKE informative pair. Available in populate_indicators and other methods as - # 'btc_rsi_1h'. Current stake currency should be specified as {stake} format variable - # instead of hardcoding actual stake currency. Available in populate_indicators and other - # methods as 'btc_usdt_rsi_1h' (when stake currency is USDT). - @informative('1h', 'BTC/{stake}') - def populate_indicators_btc_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) - return dataframe - - # Define BTC/ETH informative pair. You must specify quote currency if it is different from - # stake currency. Available in populate_indicators and other methods as 'eth_btc_rsi_1h'. - @informative('1h', 'ETH/BTC') - def populate_indicators_eth_btc_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) - return dataframe - - # Define BTC/STAKE informative pair. A custom formatter may be specified for formatting - # column names. A callable `fmt(**kwargs) -> str` may be specified, to implement custom - # formatting. Available in populate_indicators and other methods as 'rsi_upper'. - @informative('1h', 'BTC/{stake}', '{column}') - def populate_indicators_btc_1h_2(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - dataframe['rsi_upper'] = ta.RSI(dataframe, timeperiod=14) - return dataframe - - def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - # Strategy timeframe indicators for current pair. - dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) - # Informative pairs are available in this method. - dataframe['rsi_less'] = dataframe['rsi'] < dataframe['rsi_1h'] - return dataframe - - ``` - -!!! Note - Do not use `@informative` decorator if you need to use data of one informative pair when generating another informative pair. Instead, define informative pairs - manually as described [in the DataProvider section](#complete-data-provider-sample). - -!!! Note - Use string formatting when accessing informative dataframes of other pairs. This will allow easily changing stake currency in config without having to adjust strategy code. - - ``` python - def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - stake = self.config['stake_currency'] - dataframe.loc[ - ( - (dataframe[f'btc_{stake}_rsi_1h'] < 35) - & - (dataframe['volume'] > 0) - ), - ['buy', 'buy_tag']] = (1, 'buy_signal_rsi') - - return dataframe - ``` - - Alternatively column renaming may be used to remove stake currency from column names: `@informative('1h', 'BTC/{stake}', fmt='{base}_{column}_{timeframe}')`. - -!!! Warning "Duplicate method names" - Methods tagged with `@informative()` decorator must always have unique names! Re-using same name (for example when copy-pasting already defined informative method) - will overwrite previously defined method and not produce any errors due to limitations of Python programming language. In such cases you will find that indicators - created in earlier-defined methods are not available in the dataframe. Carefully review method names and make sure they are unique! - ## Additional data (Wallets) The strategy provides access to the `Wallets` object. This contains the current balances on the exchange. From dc605e29aa8ade2cff0d6e105b243df13b65c7fd Mon Sep 17 00:00:00 2001 From: incrementby1 Date: Wed, 27 Oct 2021 21:04:08 +0200 Subject: [PATCH 43/72] removed empty lines for flake8 --- freqtrade/configuration/configuration.py | 1 - freqtrade/freqtradebot.py | 1 - 2 files changed, 2 deletions(-) diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 9aa4b794e..822577916 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -498,4 +498,3 @@ class Configuration: config['pairs'] = load_file(pairs_file) if 'pairs' in config: config['pairs'].sort() - diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 4def3747c..bf4742fdc 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1429,4 +1429,3 @@ class FreqtradeBot(LoggingMixin): return max( min(valid_custom_price, max_custom_price_allowed), min_custom_price_allowed) - From f280397fd781b4c3527af07cd06107b313490985 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 28 Oct 2021 07:51:32 +0200 Subject: [PATCH 44/72] Add FAQ section about Fees closes #5807 --- docs/faq.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/faq.md b/docs/faq.md index d9777ddf1..dae8f71af 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -54,6 +54,20 @@ you can't say much from few trades. Yes. You can edit your config and use the `/reload_config` command to reload the configuration. The bot will stop, reload the configuration and strategy and will restart with the new configuration and strategy. +### Why does my bot not sell everything it bought? + +This is called "coin dust" and can happen on all exchanges. +It happens because many exchanges subtract fees from the "receiving currency" - so you buy 100 COIN - but you only get 99.9 COIN. +As COIN is trading in full lot sizes (1COIN steps), you cannot sell 0.9 COIN (or 99.9 COIN) - but you need to round down to 99 COIN. +This is not a bot-problem, but will happen while manual trading as well. + +While freqtrade can handle this (it'll sell 99 COIN), fees are often below the minimum tradable lot-size (you can only trade full COIN, not 0.9 COIN). +Leaving the dust (0.99 COIN) on the exchange makes usually sense, as the next time freqtrade buys COIN, it'll eat into the remaining small balance, this time selling everything it bought, and therefore slowly declining the dust balance (although it most likely will never reach exactly 0). + +Where possible (e.g. on binance), the use of the exchange's dedicated fee currency will fix this. +On binance, it's sufficient to have BNB in your account, and have "Pay fees in BNB" enabled in your profile. Your BNB balance will slowly decline (as it's used to pay fees) - but you'll no longer encounter dust (Freqtrade will include the fees in the profit calculations). + + ### I want to use incomplete candles Freqtrade will not provide incomplete candles to strategies. Using incomplete candles will lead to repainting and consequently to strategies with "ghost" buys, which are impossible to both backtest, and verify after they happened. From 335412a3a8f020903e42ed674ee1a99b6952b859 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 28 Oct 2021 07:59:28 +0200 Subject: [PATCH 45/72] Improve wording of FAQ entry --- docs/faq.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/faq.md b/docs/faq.md index dae8f71af..99c0c2c75 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -59,14 +59,15 @@ Yes. You can edit your config and use the `/reload_config` command to reload the This is called "coin dust" and can happen on all exchanges. It happens because many exchanges subtract fees from the "receiving currency" - so you buy 100 COIN - but you only get 99.9 COIN. As COIN is trading in full lot sizes (1COIN steps), you cannot sell 0.9 COIN (or 99.9 COIN) - but you need to round down to 99 COIN. -This is not a bot-problem, but will happen while manual trading as well. + +This is not a bot-problem, but will also happen while manual trading. While freqtrade can handle this (it'll sell 99 COIN), fees are often below the minimum tradable lot-size (you can only trade full COIN, not 0.9 COIN). -Leaving the dust (0.99 COIN) on the exchange makes usually sense, as the next time freqtrade buys COIN, it'll eat into the remaining small balance, this time selling everything it bought, and therefore slowly declining the dust balance (although it most likely will never reach exactly 0). +Leaving the dust (0.9 COIN) on the exchange makes usually sense, as the next time freqtrade buys COIN, it'll eat into the remaining small balance, this time selling everything it bought, and therefore slowly declining the dust balance (although it most likely will never reach exactly 0). -Where possible (e.g. on binance), the use of the exchange's dedicated fee currency will fix this. +Where possible (e.g. on binance), the use of the exchange's dedicated fee currency will fix this. On binance, it's sufficient to have BNB in your account, and have "Pay fees in BNB" enabled in your profile. Your BNB balance will slowly decline (as it's used to pay fees) - but you'll no longer encounter dust (Freqtrade will include the fees in the profit calculations). - +Other exchanges don't offer such possibilities, where it's simply something you'll have to accept or move to a different exchange. ### I want to use incomplete candles From 02e69e16676d44706afa5d79b116e4cf55249960 Mon Sep 17 00:00:00 2001 From: incrementby1 Date: Thu, 28 Oct 2021 15:16:07 +0200 Subject: [PATCH 46/72] Changes to unlock_reason: - introducing filter - replaced get_all_locks with a query for speed . removed logging in backtesting mode for speed . replaced for-loop with map-function for speed Changes to models.py: - changed string representation of Pairlock to also contain reason and active-state --- freqtrade/persistence/models.py | 3 +-- freqtrade/persistence/pairlock_middleware.py | 23 +++++++++++++++----- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index bc5ef961a..04f1d67b2 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -896,7 +896,7 @@ class PairLock(_DECL_BASE): lock_time = self.lock_time.strftime(DATETIME_PRINT_FORMAT) lock_end_time = self.lock_end_time.strftime(DATETIME_PRINT_FORMAT) return (f'PairLock(id={self.id}, pair={self.pair}, lock_time={lock_time}, ' - f'lock_end_time={lock_end_time})') + f'lock_end_time={lock_end_time}, reason={self.reason}, active={self.active})') @staticmethod def query_pair_locks(pair: Optional[str], now: datetime) -> Query: @@ -905,7 +905,6 @@ class PairLock(_DECL_BASE): :param pair: Pair to check for. Returns all current locks if pair is empty :param now: Datetime object (generated via datetime.now(timezone.utc)). """ - filters = [PairLock.lock_end_time > now, # Only active locks PairLock.active.is_(True), ] diff --git a/freqtrade/persistence/pairlock_middleware.py b/freqtrade/persistence/pairlock_middleware.py index 6e0164182..386c3d1d7 100644 --- a/freqtrade/persistence/pairlock_middleware.py +++ b/freqtrade/persistence/pairlock_middleware.py @@ -113,13 +113,26 @@ class PairLocks(): """ if not now: now = datetime.now(timezone.utc) - logger.info(f"Releasing all locks with reason \'{reason}\'.") - locks = PairLocks.get_all_locks() - for lock in locks: - if lock.reason == reason: - lock.active = False + + def local_unlock(lock): + lock.active = False + if PairLocks.use_db: + # used in live modes + logger.info(f"Releasing all locks with reason \'{reason}\':") + filters = [PairLock.lock_end_time > now, + PairLock.active.is_(True), + PairLock.reason == reason + ] + locks = PairLock.query.filter(*filters) + for lock in locks: + logger.info(f"Releasing lock for \'{lock.pair}\' with reason \'{reason}\'.") + lock.active = False PairLock.query.session.commit() + else: + # no logging in backtesting to increase speed + locks = filter(lambda reason: reason == reason, PairLocks.locks) + locks = map(local_unlock, locks) @staticmethod def is_global_lock(now: Optional[datetime] = None) -> bool: From 658006e7eedfd6a09fa7ee439e5fee1dbc81752b Mon Sep 17 00:00:00 2001 From: incrementby1 Date: Thu, 28 Oct 2021 23:29:26 +0200 Subject: [PATCH 47/72] removed wrong use of map and filter function --- freqtrade/persistence/pairlock_middleware.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/freqtrade/persistence/pairlock_middleware.py b/freqtrade/persistence/pairlock_middleware.py index 386c3d1d7..f1ed50ec7 100644 --- a/freqtrade/persistence/pairlock_middleware.py +++ b/freqtrade/persistence/pairlock_middleware.py @@ -114,9 +114,6 @@ class PairLocks(): if not now: now = datetime.now(timezone.utc) - def local_unlock(lock): - lock.active = False - if PairLocks.use_db: # used in live modes logger.info(f"Releasing all locks with reason \'{reason}\':") @@ -126,13 +123,15 @@ class PairLocks(): ] locks = PairLock.query.filter(*filters) for lock in locks: - logger.info(f"Releasing lock for \'{lock.pair}\' with reason \'{reason}\'.") + logger.info(f"Releasing lock for {lock.pair} with reason \'{reason}\'.") lock.active = False PairLock.query.session.commit() else: - # no logging in backtesting to increase speed - locks = filter(lambda reason: reason == reason, PairLocks.locks) - locks = map(local_unlock, locks) + # used in backtesting mode; don't show log messages for speed + locks = PairLocks.get_locks(None) + for lock in locks: + if lock.reason == reason: + lock.active = False @staticmethod def is_global_lock(now: Optional[datetime] = None) -> bool: @@ -159,7 +158,9 @@ class PairLocks(): @staticmethod def get_all_locks() -> List[PairLock]: - + """ + Return all locks, also locks with expired end date + """ if PairLocks.use_db: return PairLock.query.all() else: From e9d71f26b3f28ac6ef0cb0dcd265b6f1eb66d7fe Mon Sep 17 00:00:00 2001 From: incrementby1 Date: Fri, 29 Oct 2021 00:03:20 +0200 Subject: [PATCH 48/72] small changes --- freqtrade/persistence/pairlock_middleware.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/persistence/pairlock_middleware.py b/freqtrade/persistence/pairlock_middleware.py index f1ed50ec7..e74948813 100644 --- a/freqtrade/persistence/pairlock_middleware.py +++ b/freqtrade/persistence/pairlock_middleware.py @@ -128,7 +128,7 @@ class PairLocks(): PairLock.query.session.commit() else: # used in backtesting mode; don't show log messages for speed - locks = PairLocks.get_locks(None) + locks = PairLocks.get_pair_locks(None) for lock in locks: if lock.reason == reason: lock.active = False From 5cdae2ce3f79866d49a7b4aa2caf2b2924b8cffd Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 29 Oct 2021 06:42:17 +0200 Subject: [PATCH 49/72] Remove CalmarDaily hyperopt loss --- docs/hyperopt.md | 4 +- freqtrade/constants.py | 2 +- .../optimize/hyperopt_loss_calmar_daily.py | 81 ------------------- tests/optimize/test_hyperoptloss.py | 2 - 4 files changed, 2 insertions(+), 87 deletions(-) delete mode 100644 freqtrade/optimize/hyperopt_loss_calmar_daily.py diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 5c98da5e2..b7b6cb772 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -116,8 +116,7 @@ optional arguments: ShortTradeDurHyperOptLoss, OnlyProfitHyperOptLoss, SharpeHyperOptLoss, SharpeHyperOptLossDaily, SortinoHyperOptLoss, SortinoHyperOptLossDaily, - CalmarHyperOptLoss, CalmarHyperOptLossDaily, - MaxDrawDownHyperOptLoss + CalmarHyperOptLoss, MaxDrawDownHyperOptLoss --disable-param-export Disable automatic hyperopt parameter export. --ignore-missing-spaces, --ignore-unparameterized-spaces @@ -526,7 +525,6 @@ Currently, the following loss functions are builtin: * `SortinoHyperOptLossDaily` - optimizes Sortino Ratio calculated on **daily** trade returns relative to **downside** standard deviation. * `MaxDrawDownHyperOptLoss` - Optimizes Maximum drawdown. * `CalmarHyperOptLoss` - Optimizes Calmar Ratio calculated on trade returns relative to max drawdown. -* `CalmarHyperOptLossDaily` Optimizes Calmar Ratio calculated on **daily** trade returns relative to max drawdown. Creation of a custom loss function is covered in the [Advanced Hyperopt](advanced-hyperopt.md) part of the documentation. diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 6b3652609..656893999 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -25,7 +25,7 @@ ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc'] HYPEROPT_LOSS_BUILTIN = ['ShortTradeDurHyperOptLoss', 'OnlyProfitHyperOptLoss', 'SharpeHyperOptLoss', 'SharpeHyperOptLossDaily', 'SortinoHyperOptLoss', 'SortinoHyperOptLossDaily', - 'CalmarHyperOptLoss', 'CalmarHyperOptLossDaily', + 'CalmarHyperOptLoss', 'MaxDrawDownHyperOptLoss'] AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'AgeFilter', 'OffsetFilter', 'PerformanceFilter', diff --git a/freqtrade/optimize/hyperopt_loss_calmar_daily.py b/freqtrade/optimize/hyperopt_loss_calmar_daily.py deleted file mode 100644 index e99bc2c99..000000000 --- a/freqtrade/optimize/hyperopt_loss_calmar_daily.py +++ /dev/null @@ -1,81 +0,0 @@ -""" -CalmarHyperOptLossDaily - -This module defines the alternative HyperOptLoss class which can be used for -Hyperoptimization. -""" -from datetime import datetime -from math import sqrt as msqrt -from typing import Any, Dict - -from pandas import DataFrame, date_range - -from freqtrade.optimize.hyperopt import IHyperOptLoss - - -class CalmarHyperOptLossDaily(IHyperOptLoss): - """ - Defines the loss function for hyperopt. - - This implementation uses the Calmar Ratio calculation. - """ - - @staticmethod - def hyperopt_loss_function( - results: DataFrame, - trade_count: int, - min_date: datetime, - max_date: datetime, - config: Dict, - processed: Dict[str, DataFrame], - backtest_stats: Dict[str, Any], - *args, - **kwargs - ) -> float: - """ - Objective function, returns smaller number for more optimal results. - - Uses Calmar Ratio calculation. - """ - resample_freq = "1D" - slippage_per_trade_ratio = 0.0005 - days_in_year = 365 - - # create the index within the min_date and end max_date - t_index = date_range( - start=min_date, end=max_date, freq=resample_freq, normalize=True - ) - - # apply slippage per trade to profit_total - results.loc[:, "profit_ratio_after_slippage"] = ( - results["profit_ratio"] - slippage_per_trade_ratio - ) - - sum_daily = ( - results.resample(resample_freq, on="close_date") - .agg({"profit_ratio_after_slippage": sum}) - .reindex(t_index) - .fillna(0) - ) - - total_profit = sum_daily["profit_ratio_after_slippage"] - expected_returns_mean = total_profit.mean() * 100 - - # calculate max drawdown - try: - high_val = total_profit.max() - low_val = total_profit.min() - max_drawdown = (high_val - low_val) / high_val - - except (ValueError, ZeroDivisionError): - max_drawdown = 0 - - if max_drawdown != 0: - calmar_ratio = expected_returns_mean / max_drawdown * msqrt(days_in_year) - else: - # Define high (negative) calmar ratio to be clear that this is NOT optimal. - calmar_ratio = -20.0 - - # print(t_index, sum_daily, total_profit) - # print(expected_returns_mean, max_drawdown, calmar_ratio) - return -calmar_ratio diff --git a/tests/optimize/test_hyperoptloss.py b/tests/optimize/test_hyperoptloss.py index fd835c678..e4a2eec2e 100644 --- a/tests/optimize/test_hyperoptloss.py +++ b/tests/optimize/test_hyperoptloss.py @@ -5,7 +5,6 @@ import pytest from freqtrade.exceptions import OperationalException from freqtrade.optimize.hyperopt_loss_short_trade_dur import ShortTradeDurHyperOptLoss -from freqtrade.optimize.optimize_reports import generate_strategy_stats from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver @@ -86,7 +85,6 @@ def test_loss_calculation_has_limited_profit(hyperopt_conf, hyperopt_results) -> "SharpeHyperOptLoss", "SharpeHyperOptLossDaily", "MaxDrawDownHyperOptLoss", - "CalmarHyperOptLossDaily", "CalmarHyperOptLoss", ]) From c579fcfc19ca91e5567eca1ad8b8d9f16a577f3d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 30 Oct 2021 09:39:40 +0200 Subject: [PATCH 50/72] Add tests and documentation for unlock_reason --- docs/strategy-customization.md | 3 ++- freqtrade/persistence/pairlock_middleware.py | 4 ++-- tests/plugins/test_pairlocks.py | 25 ++++++++++++++++++++ tests/strategy/test_interface.py | 7 ++++++ 4 files changed, 36 insertions(+), 3 deletions(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 0bfc0a2f6..84d6b2320 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -894,7 +894,8 @@ Sometimes it may be desired to lock a pair after certain events happen (e.g. mul Freqtrade has an easy method to do this from within the strategy, by calling `self.lock_pair(pair, until, [reason])`. `until` must be a datetime object in the future, after which trading will be re-enabled for that pair, while `reason` is an optional string detailing why the pair was locked. -Locks can also be lifted manually, by calling `self.unlock_pair(pair)`. +Locks can also be lifted manually, by calling `self.unlock_pair(pair)` or `self.unlock_reason()` - providing reason the pair was locked with. +`self.unlock_reason()` will unlock all pairs currently locked with the provided reason. To verify if a pair is currently locked, use `self.is_pair_locked(pair)`. diff --git a/freqtrade/persistence/pairlock_middleware.py b/freqtrade/persistence/pairlock_middleware.py index e74948813..afbd9781b 100644 --- a/freqtrade/persistence/pairlock_middleware.py +++ b/freqtrade/persistence/pairlock_middleware.py @@ -116,14 +116,14 @@ class PairLocks(): if PairLocks.use_db: # used in live modes - logger.info(f"Releasing all locks with reason \'{reason}\':") + logger.info(f"Releasing all locks with reason '{reason}':") filters = [PairLock.lock_end_time > now, PairLock.active.is_(True), PairLock.reason == reason ] locks = PairLock.query.filter(*filters) for lock in locks: - logger.info(f"Releasing lock for {lock.pair} with reason \'{reason}\'.") + logger.info(f"Releasing lock for {lock.pair} with reason '{reason}'.") lock.active = False PairLock.query.session.commit() else: diff --git a/tests/plugins/test_pairlocks.py b/tests/plugins/test_pairlocks.py index c694fd7c1..f9e5583ed 100644 --- a/tests/plugins/test_pairlocks.py +++ b/tests/plugins/test_pairlocks.py @@ -116,3 +116,28 @@ def test_PairLocks_getlongestlock(use_db): PairLocks.reset_locks() PairLocks.use_db = True + + +@pytest.mark.parametrize('use_db', (False, True)) +@pytest.mark.usefixtures("init_persistence") +def test_PairLocks_reason(use_db): + PairLocks.timeframe = '5m' + PairLocks.use_db = use_db + # No lock should be present + if use_db: + assert len(PairLock.query.all()) == 0 + + assert PairLocks.use_db == use_db + + PairLocks.lock_pair('XRP/USDT', arrow.utcnow().shift(minutes=4).datetime, 'TestLock1') + PairLocks.lock_pair('ETH/USDT', arrow.utcnow().shift(minutes=4).datetime, 'TestLock2') + + assert PairLocks.is_pair_locked('XRP/USDT') + assert PairLocks.is_pair_locked('ETH/USDT') + + PairLocks.unlock_reason('TestLock1') + assert not PairLocks.is_pair_locked('XRP/USDT') + assert PairLocks.is_pair_locked('ETH/USDT') + + PairLocks.reset_locks() + PairLocks.use_db = True diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index dcb9e3e64..ebd950fd6 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -575,6 +575,13 @@ def test_is_pair_locked(default_conf): strategy.unlock_pair(pair) assert not strategy.is_pair_locked(pair) + # Lock with reason + reason = "TestLockR" + strategy.lock_pair(pair, arrow.now(timezone.utc).shift(minutes=4).datetime, reason) + assert strategy.is_pair_locked(pair) + strategy.unlock_reason(reason) + assert not strategy.is_pair_locked(pair) + pair = 'BTC/USDT' # Lock until 14:30 lock_time = datetime(2020, 5, 1, 14, 30, 0, tzinfo=timezone.utc) From 0f3809345a737ab3e8279cd02ba78fd244472395 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 30 Oct 2021 10:28:12 +0200 Subject: [PATCH 51/72] Remove backtest-path parameter --- freqtrade/commands/__init__.py | 6 +++--- freqtrade/commands/arguments.py | 3 +-- freqtrade/commands/cli_options.py | 5 ----- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/freqtrade/commands/__init__.py b/freqtrade/commands/__init__.py index 323d85825..89b6fdc3a 100644 --- a/freqtrade/commands/__init__.py +++ b/freqtrade/commands/__init__.py @@ -13,9 +13,9 @@ from freqtrade.commands.data_commands import (start_convert_data, start_convert_ from freqtrade.commands.deploy_commands import (start_create_userdir, start_install_ui, start_new_strategy) from freqtrade.commands.hyperopt_commands import start_hyperopt_list, start_hyperopt_show -from freqtrade.commands.list_commands import (start_list_exchanges, start_list_hyperopts, - start_list_markets, start_list_strategies, - start_list_timeframes, start_show_trades) +from freqtrade.commands.list_commands import (start_list_exchanges, start_list_markets, + start_list_strategies, start_list_timeframes, + start_show_trades) from freqtrade.commands.optimize_commands import (start_backtest_filter, start_backtesting, start_edge, start_hyperopt) from freqtrade.commands.pairlist_commands import start_test_pairlist diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index 080c5a9f6..df4c35cc0 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -2,7 +2,6 @@ This module contains the argument manager class """ import argparse -from freqtrade.commands.optimize_commands import start_backtest_filter from functools import partial from pathlib import Path from typing import Any, Dict, List, Optional @@ -42,7 +41,7 @@ ARGS_LIST_STRATEGIES = ["strategy_path", "print_one_column", "print_colorized"] ARGS_LIST_HYPEROPTS = ["hyperopt_path", "print_one_column", "print_colorized"] -ARGS_BACKTEST_FILTER = ["backtest_path"] +ARGS_BACKTEST_FILTER = [] ARGS_LIST_EXCHANGES = ["print_one_column", "list_exchanges_all"] diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index 60c888756..8d9b28c40 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -218,11 +218,6 @@ AVAILABLE_CLI_OPTIONS = { help='Specify additional lookup path for Hyperopt Loss functions.', metavar='PATH', ), - "backtest_path": Arg( - '--backtest-path', - help='Specify lookup file path for backtest filter.', - metavar='PATH', - ), "epochs": Arg( '-e', '--epochs', help='Specify number of epochs (default: %(default)d).', From f47270943817d5ad3a863256d953289c59485eb7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 30 Oct 2021 10:50:40 +0200 Subject: [PATCH 52/72] Add option to show sorted pairlist Allows easy copy/pasting of the pairlist to a configuration --- freqtrade/commands/arguments.py | 21 +++++++++---------- freqtrade/commands/cli_options.py | 6 ++++++ freqtrade/commands/optimize_commands.py | 26 ++++++++---------------- freqtrade/configuration/configuration.py | 4 ++++ freqtrade/optimize/optimize_reports.py | 21 ++++++++----------- 5 files changed, 36 insertions(+), 42 deletions(-) diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index df4c35cc0..8f68e6521 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -41,7 +41,7 @@ ARGS_LIST_STRATEGIES = ["strategy_path", "print_one_column", "print_colorized"] ARGS_LIST_HYPEROPTS = ["hyperopt_path", "print_one_column", "print_colorized"] -ARGS_BACKTEST_FILTER = [] +ARGS_BACKTEST_FILTER = ["exportfilename", "backtest_show_pair_list"] ARGS_LIST_EXCHANGES = ["print_one_column", "list_exchanges_all"] @@ -175,16 +175,15 @@ class Arguments: self.parser = argparse.ArgumentParser(description='Free, open source crypto trading bot') self._build_args(optionlist=['version'], parser=self.parser) - from freqtrade.commands import ( - start_backtesting, start_backtest_filter, start_convert_data, start_convert_trades, - start_create_userdir, start_download_data, start_edge, - start_hyperopt, start_hyperopt_list, start_hyperopt_show, - start_install_ui, start_list_data, start_list_exchanges, - start_list_markets, start_list_strategies, - start_list_timeframes, start_new_config, start_new_strategy, - start_plot_dataframe, start_plot_profit, start_show_trades, - start_test_pairlist, start_trading, start_webserver - ) + from freqtrade.commands import (start_backtest_filter, start_backtesting, + start_convert_data, start_convert_trades, + start_create_userdir, start_download_data, start_edge, + start_hyperopt, start_hyperopt_list, start_hyperopt_show, + start_install_ui, start_list_data, start_list_exchanges, + start_list_markets, start_list_strategies, + start_list_timeframes, start_new_config, start_new_strategy, + start_plot_dataframe, start_plot_profit, start_show_trades, + start_test_pairlist, start_trading, start_webserver) subparsers = self.parser.add_subparsers(dest='command', # Use custom message when no subhandler is added diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index 8d9b28c40..6aa4ed363 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -152,6 +152,12 @@ AVAILABLE_CLI_OPTIONS = { action='store_false', default=True, ), + "backtest_show_pair_list": Arg( + '--show-pair-list', + help='Show backtesting pairlist sorted by profit.', + action='store_true', + default=False, + ), "enable_protections": Arg( '--enable-protections', '--enableprotections', help='Enable protections for backtesting.' diff --git a/freqtrade/commands/optimize_commands.py b/freqtrade/commands/optimize_commands.py index 75392d897..0d1f304db 100644 --- a/freqtrade/commands/optimize_commands.py +++ b/freqtrade/commands/optimize_commands.py @@ -1,15 +1,13 @@ -from freqtrade.data.btanalysis import get_latest_backtest_filename -import pandas -from pandas.io import json -from freqtrade.optimize import backtesting import logging from typing import Any, Dict from freqtrade import constants from freqtrade.configuration import setup_utils_configuration +from freqtrade.data.btanalysis import load_backtest_stats from freqtrade.enums import RunMode from freqtrade.exceptions import OperationalException from freqtrade.misc import round_coin_value +from freqtrade.optimize.optimize_reports import show_backtest_results, show_filtered_pairlist logger = logging.getLogger(__name__) @@ -57,27 +55,19 @@ def start_backtesting(args: Dict[str, Any]) -> None: backtesting = Backtesting(config) backtesting.start() + def start_backtest_filter(args: Dict[str, Any]) -> None: """ - List backtest pairs previously filtered + Show previous backtest result """ config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) - no_header = config.get('backtest_show_pair_list', False) - results_file = get_latest_backtest_filename( - config['user_data_dir'] / 'backtest_results/') - - logger.info("Using Backtesting result {results_file}") - - # load data using Python JSON module - with open(config['user_data_dir'] / 'backtest_results/' / results_file,'r') as f: - data = json.loads(f.read()) - strategy = list(data["strategy"])[0] - trades = data["strategy"][strategy] - - print(trades) + results = load_backtest_stats(config['exportfilename']) + # print(results) + show_backtest_results(config, results) + show_filtered_pairlist(config, results) logger.info("Backtest filtering complete. ") diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 822577916..f5a674878 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -245,6 +245,10 @@ class Configuration: self._args_to_config(config, argname='timeframe_detail', logstring='Parameter --timeframe-detail detected, ' 'using {} for intra-candle backtesting ...') + + self._args_to_config(config, argname='backtest_show_pair_list', + logstring='Parameter --show-pair-list detected.') + self._args_to_config(config, argname='stake_amount', logstring='Parameter --stake-amount detected, ' 'overriding stake_amount to: {} ...') diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 712cce028..5adad39c1 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -736,17 +736,12 @@ def show_backtest_results(config: Dict, backtest_stats: Dict): print('=' * len(table.splitlines()[0])) print('\nFor more details, please look at the detail tables above') -def show_backtest_results_filtered(config: Dict, backtest_stats: Dict): - stake_currency = config['stake_currency'] - for strategy, results in backtest_stats['strategy'].items(): - show_backtest_result(strategy, results, stake_currency) - - if len(backtest_stats['strategy']) > 1: - # Print Strategy summary table - - table = text_table_strategy(backtest_stats['strategy_comparison'], stake_currency) - print(' STRATEGY SUMMARY '.center(len(table.splitlines()[0]), '=')) - print(table) - print('=' * len(table.splitlines()[0])) - print('\nFor more details, please look at the detail tables above') +def show_filtered_pairlist(config: Dict, backtest_stats: Dict): + if config.get('backtest_show_pair_list', False): + for strategy, results in backtest_stats['strategy'].items(): + print("Pairs for Strategy: \n[") + for result in results['results_per_pair']: + if result["key"] != 'TOTAL': + print(f'"{result["key"]}", // {round(result["profit_mean_pct"], 2)}%') + print("]") From 851062ca46463139276544dbb9b8289ec705af86 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 30 Oct 2021 10:53:18 +0200 Subject: [PATCH 53/72] Rename backtest-filter to backtest_show --- freqtrade/commands/__init__.py | 2 +- freqtrade/commands/arguments.py | 16 ++++++++-------- freqtrade/commands/optimize_commands.py | 4 +--- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/freqtrade/commands/__init__.py b/freqtrade/commands/__init__.py index 89b6fdc3a..ba977b6bd 100644 --- a/freqtrade/commands/__init__.py +++ b/freqtrade/commands/__init__.py @@ -16,7 +16,7 @@ from freqtrade.commands.hyperopt_commands import start_hyperopt_list, start_hype from freqtrade.commands.list_commands import (start_list_exchanges, start_list_markets, start_list_strategies, start_list_timeframes, start_show_trades) -from freqtrade.commands.optimize_commands import (start_backtest_filter, start_backtesting, +from freqtrade.commands.optimize_commands import (start_backtest_show, start_backtesting, start_edge, start_hyperopt) from freqtrade.commands.pairlist_commands import start_test_pairlist from freqtrade.commands.plot_commands import start_plot_dataframe, start_plot_profit diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index 8f68e6521..be4f9188f 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -41,7 +41,7 @@ ARGS_LIST_STRATEGIES = ["strategy_path", "print_one_column", "print_colorized"] ARGS_LIST_HYPEROPTS = ["hyperopt_path", "print_one_column", "print_colorized"] -ARGS_BACKTEST_FILTER = ["exportfilename", "backtest_show_pair_list"] +ARGS_BACKTEST_SHOW = ["exportfilename", "backtest_show_pair_list"] ARGS_LIST_EXCHANGES = ["print_one_column", "list_exchanges_all"] @@ -175,7 +175,7 @@ class Arguments: self.parser = argparse.ArgumentParser(description='Free, open source crypto trading bot') self._build_args(optionlist=['version'], parser=self.parser) - from freqtrade.commands import (start_backtest_filter, start_backtesting, + from freqtrade.commands import (start_backtest_show, start_backtesting, start_convert_data, start_convert_trades, start_create_userdir, start_download_data, start_edge, start_hyperopt, start_hyperopt_list, start_hyperopt_show, @@ -267,14 +267,14 @@ class Arguments: backtesting_cmd.set_defaults(func=start_backtesting) self._build_args(optionlist=ARGS_BACKTEST, parser=backtesting_cmd) - # Add backtest-filter subcommand - backtest_filter_cmd = subparsers.add_parser( - 'backtest-filter', - help='Filter Backtest results', + # Add backtest-show subcommand + backtest_show_cmd = subparsers.add_parser( + 'backtest-show', + help='Show past Backtest results', parents=[_common_parser], ) - backtest_filter_cmd.set_defaults(func=start_backtest_filter) - self._build_args(optionlist=ARGS_BACKTEST_FILTER, parser=backtest_filter_cmd) + backtest_show_cmd.set_defaults(func=start_backtest_show) + self._build_args(optionlist=ARGS_BACKTEST_SHOW, parser=backtest_show_cmd) # Add edge subcommand edge_cmd = subparsers.add_parser('edge', help='Edge module.', diff --git a/freqtrade/commands/optimize_commands.py b/freqtrade/commands/optimize_commands.py index 0d1f304db..c35e78153 100644 --- a/freqtrade/commands/optimize_commands.py +++ b/freqtrade/commands/optimize_commands.py @@ -56,7 +56,7 @@ def start_backtesting(args: Dict[str, Any]) -> None: backtesting.start() -def start_backtest_filter(args: Dict[str, Any]) -> None: +def start_backtest_show(args: Dict[str, Any]) -> None: """ Show previous backtest result """ @@ -69,8 +69,6 @@ def start_backtest_filter(args: Dict[str, Any]) -> None: show_backtest_results(config, results) show_filtered_pairlist(config, results) - logger.info("Backtest filtering complete. ") - def start_hyperopt(args: Dict[str, Any]) -> None: """ From 6cf140f8fb95c70a35cb2dfc56fe2fdd55ac7d8e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 30 Oct 2021 15:58:14 +0200 Subject: [PATCH 54/72] FIx testcases --- tests/optimize/test_backtest_detail.py | 60 +++++++++++++------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index e5c037f3e..f64855baf 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -17,10 +17,10 @@ tc0 = BTContainer(data=[ # D O H L C V B S [0, 5000, 5025, 4975, 4987, 6172, 1, 0], [1, 5000, 5025, 4975, 4987, 6172, 0, 0], # enter trade (signal on last candle) - [2, 4987, 5012, 4986, 4600, 6172, 0, 0], # exit with stoploss hit + [2, 4987, 5012, 4986, 4986, 6172, 0, 0], # exit with stoploss hit [3, 5010, 5000, 4980, 5010, 6172, 0, 1], [4, 5010, 4987, 4977, 4995, 6172, 0, 0], - [5, 4995, 4995, 4995, 4950, 6172, 0, 0]], + [5, 4995, 4995, 4950, 4950, 6172, 0, 0]], stop_loss=-0.01, roi={"0": 1}, profit_perc=0.002, use_sell_signal=True, trades=[BTrade(sell_reason=SellType.SELL_SIGNAL, open_tick=1, close_tick=4)] ) @@ -32,9 +32,9 @@ tc1 = BTContainer(data=[ [0, 5000, 5025, 4975, 4987, 6172, 1, 0], [1, 5000, 5025, 4975, 4987, 6172, 0, 0], # enter trade (signal on last candle) [2, 4987, 5012, 4600, 4600, 6172, 0, 0], # exit with stoploss hit - [3, 4975, 5000, 4980, 4977, 6172, 0, 0], + [3, 4975, 5000, 4977, 4977, 6172, 0, 0], [4, 4977, 4987, 4977, 4995, 6172, 0, 0], - [5, 4995, 4995, 4995, 4950, 6172, 0, 0]], + [5, 4995, 4995, 4950, 4950, 6172, 0, 0]], stop_loss=-0.01, roi={"0": 1}, profit_perc=-0.01, trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=2)] ) @@ -69,7 +69,7 @@ tc3 = BTContainer(data=[ [3, 4975, 5000, 4950, 4962, 6172, 1, 0], [4, 4975, 5000, 4950, 4962, 6172, 0, 0], # enter trade 2 (signal on last candle) [5, 4962, 4987, 4000, 4000, 6172, 0, 0], # exit with stoploss hit - [6, 4950, 4975, 4975, 4950, 6172, 0, 0]], + [6, 4950, 4975, 4950, 4950, 6172, 0, 0]], stop_loss=-0.02, roi={"0": 1}, profit_perc=-0.04, trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=2), BTrade(sell_reason=SellType.STOP_LOSS, open_tick=4, close_tick=5)] @@ -99,7 +99,7 @@ tc5 = BTContainer(data=[ [1, 5000, 5025, 4980, 4987, 6172, 0, 0], # enter trade (signal on last candle) [2, 4987, 5025, 4975, 4987, 6172, 0, 0], [3, 4975, 6000, 4975, 6000, 6172, 0, 0], # ROI - [4, 4962, 4987, 4972, 4950, 6172, 0, 0], + [4, 4962, 4987, 4972, 4972, 6172, 0, 0], [5, 4950, 4975, 4925, 4950, 6172, 0, 0]], stop_loss=-0.01, roi={"0": 0.03}, profit_perc=0.03, trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)] @@ -113,7 +113,7 @@ tc6 = BTContainer(data=[ [1, 5000, 5025, 4975, 4987, 6172, 0, 0], # enter trade (signal on last candle) [2, 4987, 5300, 4850, 5050, 6172, 0, 0], # Exit with stoploss [3, 4975, 5000, 4950, 4962, 6172, 0, 0], - [4, 4962, 4987, 4972, 4950, 6172, 0, 0], + [4, 4962, 4987, 4950, 4950, 6172, 0, 0], [5, 4950, 4975, 4925, 4950, 6172, 0, 0]], stop_loss=-0.02, roi={"0": 0.05}, profit_perc=-0.02, trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=2)] @@ -127,7 +127,7 @@ tc7 = BTContainer(data=[ [1, 5000, 5025, 4975, 4987, 6172, 0, 0], [2, 4987, 5300, 4950, 5050, 6172, 0, 0], [3, 4975, 5000, 4950, 4962, 6172, 0, 0], - [4, 4962, 4987, 4972, 4950, 6172, 0, 0], + [4, 4962, 4987, 4950, 4950, 6172, 0, 0], [5, 4950, 4975, 4925, 4950, 6172, 0, 0]], stop_loss=-0.02, roi={"0": 0.03}, profit_perc=0.03, trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=2)] @@ -216,8 +216,8 @@ tc13 = BTContainer(data=[ [0, 5000, 5050, 4950, 5000, 6172, 1, 0], [1, 5000, 5100, 4950, 5100, 6172, 0, 0], [2, 5100, 5251, 4850, 5100, 6172, 0, 0], - [3, 4850, 5050, 4850, 4750, 6172, 0, 0], - [4, 4750, 4950, 4850, 4750, 6172, 0, 0]], + [3, 4850, 5050, 4750, 4750, 6172, 0, 0], + [4, 4750, 4950, 4750, 4750, 6172, 0, 0]], stop_loss=-0.10, roi={"0": 0.01}, profit_perc=0.01, trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=1)] ) @@ -229,7 +229,7 @@ tc14 = BTContainer(data=[ [0, 5000, 5050, 4950, 5000, 6172, 1, 0], [1, 5000, 5100, 4600, 5100, 6172, 0, 0], [2, 5100, 5251, 4850, 5100, 6172, 0, 0], - [3, 4850, 5050, 4850, 4750, 6172, 0, 0], + [3, 4850, 5050, 4750, 4750, 6172, 0, 0], [4, 4750, 4950, 4350, 4750, 6172, 0, 0]], stop_loss=-0.05, roi={"0": 0.10}, profit_perc=-0.05, trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=1)] @@ -243,7 +243,7 @@ tc15 = BTContainer(data=[ [0, 5000, 5050, 4950, 5000, 6172, 1, 0], [1, 5000, 5100, 4900, 5100, 6172, 1, 0], [2, 5100, 5251, 4650, 5100, 6172, 0, 0], - [3, 4850, 5050, 4850, 4750, 6172, 0, 0], + [3, 4850, 5050, 4750, 4750, 6172, 0, 0], [4, 4750, 4950, 4350, 4750, 6172, 0, 0]], stop_loss=-0.05, roi={"0": 0.01}, profit_perc=-0.04, trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=1), @@ -259,7 +259,7 @@ tc16 = BTContainer(data=[ [1, 5000, 5025, 4975, 4987, 6172, 0, 0], [2, 4987, 5300, 4950, 5050, 6172, 0, 0], [3, 4975, 5000, 4940, 4962, 6172, 0, 0], # ForceSell on ROI (roi=-1) - [4, 4962, 4987, 4972, 4950, 6172, 0, 0], + [4, 4962, 4987, 4950, 4950, 6172, 0, 0], [5, 4950, 4975, 4925, 4950, 6172, 0, 0]], stop_loss=-0.10, roi={"0": 0.10, "65": -1}, profit_perc=-0.012, trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)] @@ -275,7 +275,7 @@ tc17 = BTContainer(data=[ [1, 5000, 5025, 4975, 4987, 6172, 0, 0], [2, 4987, 5300, 4950, 5050, 6172, 0, 0], [3, 4980, 5000, 4940, 4962, 6172, 0, 0], # ForceSell on ROI (roi=-1) - [4, 4962, 4987, 4972, 4950, 6172, 0, 0], + [4, 4962, 4987, 4950, 4950, 6172, 0, 0], [5, 4950, 4975, 4925, 4950, 6172, 0, 0]], stop_loss=-0.10, roi={"0": 0.10, "120": -1}, profit_perc=-0.004, trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)] @@ -291,7 +291,7 @@ tc18 = BTContainer(data=[ [1, 5000, 5025, 4975, 4987, 6172, 0, 0], [2, 4987, 5300, 4950, 5200, 6172, 0, 0], [3, 5200, 5220, 4940, 4962, 6172, 0, 0], # Sell on ROI (sells on open) - [4, 4962, 4987, 4972, 4950, 6172, 0, 0], + [4, 4962, 4987, 4950, 4950, 6172, 0, 0], [5, 4950, 4975, 4925, 4950, 6172, 0, 0]], stop_loss=-0.10, roi={"0": 0.10, "120": 0.01}, profit_perc=0.04, trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)] @@ -306,7 +306,7 @@ tc19 = BTContainer(data=[ [1, 5000, 5025, 4975, 4987, 6172, 0, 0], [2, 4987, 5300, 4950, 5200, 6172, 0, 0], [3, 5000, 5300, 4940, 4962, 6172, 0, 0], # Sell on ROI - [4, 4962, 4987, 4972, 4950, 6172, 0, 0], + [4, 4962, 4987, 4950, 4950, 6172, 0, 0], [5, 4550, 4975, 4925, 4950, 6172, 0, 0]], stop_loss=-0.10, roi={"0": 0.10, "120": 0.01}, profit_perc=0.01, trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)] @@ -321,7 +321,7 @@ tc20 = BTContainer(data=[ [1, 5000, 5025, 4975, 4987, 6172, 0, 0], [2, 4987, 5300, 4950, 5200, 6172, 0, 0], [3, 5200, 5300, 4940, 4962, 6172, 0, 0], # Sell on ROI - [4, 4962, 4987, 4972, 4950, 6172, 0, 0], + [4, 4962, 4987, 4950, 4950, 6172, 0, 0], [5, 4550, 4975, 4925, 4950, 6172, 0, 0]], stop_loss=-0.10, roi={"0": 0.10, "119": 0.01}, profit_perc=0.01, trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)] @@ -386,10 +386,10 @@ tc24 = BTContainer(data=[ # D O H L C V B S [0, 5000, 5025, 4975, 4987, 6172, 1, 0], [1, 5000, 5025, 4975, 4987, 6172, 0, 0], # enter trade (signal on last candle) - [2, 4987, 5012, 4986, 4600, 6172, 0, 0], + [2, 4987, 5012, 4986, 4986, 6172, 0, 0], [3, 5010, 5000, 4855, 5010, 6172, 0, 1], # Triggers stoploss + sellsignal [4, 5010, 4987, 4977, 4995, 6172, 0, 0], - [5, 4995, 4995, 4995, 4950, 6172, 0, 0]], + [5, 4995, 4995, 4950, 4950, 6172, 0, 0]], stop_loss=-0.01, roi={"0": 1}, profit_perc=-0.01, use_sell_signal=True, trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=3)] ) @@ -401,10 +401,10 @@ tc25 = BTContainer(data=[ # D O H L C V B S [0, 5000, 5025, 4975, 4987, 6172, 1, 0], [1, 5000, 5025, 4975, 4987, 6172, 0, 0], # enter trade (signal on last candle) - [2, 4987, 5012, 4986, 4600, 6172, 0, 0], + [2, 4987, 5012, 4986, 4986, 6172, 0, 0], [3, 5010, 5000, 4986, 5010, 6172, 0, 1], [4, 5010, 4987, 4855, 4995, 6172, 0, 0], # Triggers stoploss + sellsignal acted on - [5, 4995, 4995, 4995, 4950, 6172, 0, 0]], + [5, 4995, 4995, 4950, 4950, 6172, 0, 0]], stop_loss=-0.01, roi={"0": 1}, profit_perc=0.002, use_sell_signal=True, trades=[BTrade(sell_reason=SellType.SELL_SIGNAL, open_tick=1, close_tick=4)] ) @@ -416,10 +416,10 @@ tc26 = BTContainer(data=[ # D O H L C V B S [0, 5000, 5025, 4975, 4987, 6172, 1, 0], [1, 5000, 5025, 4975, 4987, 6172, 0, 0], # enter trade (signal on last candle) - [2, 4987, 5012, 4986, 4600, 6172, 0, 0], + [2, 4987, 5012, 4986, 4986, 6172, 0, 0], [3, 5010, 5251, 4986, 5010, 6172, 0, 1], # Triggers ROI, sell-signal [4, 5010, 4987, 4855, 4995, 6172, 0, 0], - [5, 4995, 4995, 4995, 4950, 6172, 0, 0]], + [5, 4995, 4995, 4950, 4950, 6172, 0, 0]], stop_loss=-0.10, roi={"0": 0.05}, profit_perc=0.05, use_sell_signal=True, trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)] ) @@ -432,10 +432,10 @@ tc27 = BTContainer(data=[ # D O H L C V B S [0, 5000, 5025, 4975, 4987, 6172, 1, 0], [1, 5000, 5025, 4975, 4987, 6172, 0, 0], # enter trade (signal on last candle) - [2, 4987, 5012, 4986, 4600, 6172, 0, 0], + [2, 4987, 5012, 4986, 4986, 6172, 0, 0], [3, 5010, 5012, 4986, 5010, 6172, 0, 1], # sell-signal [4, 5010, 5251, 4855, 4995, 6172, 0, 0], # Triggers ROI, sell-signal acted on - [5, 4995, 4995, 4995, 4950, 6172, 0, 0]], + [5, 4995, 4995, 4950, 4950, 6172, 0, 0]], stop_loss=-0.10, roi={"0": 0.05}, profit_perc=0.05, use_sell_signal=True, trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=4)] ) @@ -463,7 +463,7 @@ tc28 = BTContainer(data=[ tc29 = BTContainer(data=[ # D O H L C V B S [0, 5000, 5050, 4950, 5000, 6172, 1, 0], - [1, 5000, 5050, 5000, 4900, 6172, 0, 0], # enter trade (signal on last candle) + [1, 5000, 5050, 5000, 5000, 6172, 0, 0], # enter trade (signal on last candle) [2, 4900, 5250, 4500, 5100, 6172, 0, 0], # Triggers trailing-stoploss [3, 5100, 5100, 4650, 4750, 6172, 0, 0], [4, 4750, 4950, 4350, 4750, 6172, 0, 0]], @@ -477,7 +477,7 @@ tc29 = BTContainer(data=[ tc30 = BTContainer(data=[ # D O H L C V B S [0, 5000, 5050, 4950, 5000, 6172, 1, 0], - [1, 5000, 5500, 5000, 4900, 6172, 0, 0], # enter trade (signal on last candle) and stop + [1, 5000, 5500, 4900, 4900, 6172, 0, 0], # enter trade (signal on last candle) and stop [2, 4900, 5250, 4500, 5100, 6172, 0, 0], [3, 5100, 5100, 4650, 4750, 6172, 0, 0], [4, 4750, 4950, 4350, 4750, 6172, 0, 0]], @@ -491,7 +491,7 @@ tc30 = BTContainer(data=[ tc31 = BTContainer(data=[ # D O H L C V B S [0, 5000, 5050, 4950, 5000, 6172, 1, 0], - [1, 5000, 5500, 5000, 4900, 6172, 0, 0], # enter trade (signal on last candle) and stop + [1, 5000, 5500, 4900, 4900, 6172, 0, 0], # enter trade (signal on last candle) and stop [2, 4900, 5250, 4500, 5100, 6172, 0, 0], [3, 5100, 5100, 4650, 4750, 6172, 0, 0], [4, 4750, 4950, 4350, 4750, 6172, 0, 0]], @@ -506,7 +506,7 @@ tc31 = BTContainer(data=[ tc32 = BTContainer(data=[ # D O H L C V B S [0, 5000, 5050, 4950, 5000, 6172, 1, 0], - [1, 5000, 5500, 5000, 4900, 6172, 0, 0], # enter trade (signal on last candle) and stop + [1, 5000, 5500, 4951, 5000, 6172, 0, 0], # enter trade (signal on last candle) and stop [2, 4900, 5250, 4500, 5100, 6172, 0, 0], [3, 5100, 5100, 4650, 4750, 6172, 0, 0], [4, 4750, 4950, 4350, 4750, 6172, 0, 0]], @@ -521,7 +521,7 @@ tc32 = BTContainer(data=[ tc33 = BTContainer(data=[ # D O H L C V B S BT [0, 5000, 5050, 4950, 5000, 6172, 1, 0, 'buy_signal_01'], - [1, 5000, 5500, 5000, 4900, 6172, 0, 0, None], # enter trade (signal on last candle) and stop + [1, 5000, 5500, 4951, 5000, 6172, 0, 0, None], # enter trade (signal on last candle) and stop [2, 4900, 5250, 4500, 5100, 6172, 0, 0, None], [3, 5100, 5100, 4650, 4750, 6172, 0, 0, None], [4, 4750, 4950, 4350, 4750, 6172, 0, 0, None]], From 459a2239ce4837fedab427bbc492cb0352381613 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 30 Oct 2021 16:10:28 +0200 Subject: [PATCH 55/72] Fix candle ranges in backtesting test --- tests/optimize/__init__.py | 6 ++++ tests/optimize/test_backtest_detail.py | 38 +++++++++++++------------- 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/tests/optimize/__init__.py b/tests/optimize/__init__.py index 6ad2d300b..8a2be39a1 100644 --- a/tests/optimize/__init__.py +++ b/tests/optimize/__init__.py @@ -54,4 +54,10 @@ def _build_backtest_dataframe(data): frame[column] = frame[column].astype('float64') if 'buy_tag' not in columns: frame['buy_tag'] = None + + # Ensure all candles make kindof sense + assert all(frame['low'] <= frame['close']) + assert all(frame['low'] <= frame['open']) + assert all(frame['high'] >= frame['close']) + assert all(frame['high'] >= frame['open']) return frame diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index f64855baf..775f15b87 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -18,8 +18,8 @@ tc0 = BTContainer(data=[ [0, 5000, 5025, 4975, 4987, 6172, 1, 0], [1, 5000, 5025, 4975, 4987, 6172, 0, 0], # enter trade (signal on last candle) [2, 4987, 5012, 4986, 4986, 6172, 0, 0], # exit with stoploss hit - [3, 5010, 5000, 4980, 5010, 6172, 0, 1], - [4, 5010, 4987, 4977, 4995, 6172, 0, 0], + [3, 5010, 5010, 4980, 5010, 6172, 0, 1], + [4, 5010, 5011, 4977, 4995, 6172, 0, 0], [5, 4995, 4995, 4950, 4950, 6172, 0, 0]], stop_loss=-0.01, roi={"0": 1}, profit_perc=0.002, use_sell_signal=True, trades=[BTrade(sell_reason=SellType.SELL_SIGNAL, open_tick=1, close_tick=4)] @@ -32,8 +32,8 @@ tc1 = BTContainer(data=[ [0, 5000, 5025, 4975, 4987, 6172, 1, 0], [1, 5000, 5025, 4975, 4987, 6172, 0, 0], # enter trade (signal on last candle) [2, 4987, 5012, 4600, 4600, 6172, 0, 0], # exit with stoploss hit - [3, 4975, 5000, 4977, 4977, 6172, 0, 0], - [4, 4977, 4987, 4977, 4995, 6172, 0, 0], + [3, 4975, 5000, 4975, 4977, 6172, 0, 0], + [4, 4977, 4995, 4977, 4995, 6172, 0, 0], [5, 4995, 4995, 4950, 4950, 6172, 0, 0]], stop_loss=-0.01, roi={"0": 1}, profit_perc=-0.01, trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=2)] @@ -99,7 +99,7 @@ tc5 = BTContainer(data=[ [1, 5000, 5025, 4980, 4987, 6172, 0, 0], # enter trade (signal on last candle) [2, 4987, 5025, 4975, 4987, 6172, 0, 0], [3, 4975, 6000, 4975, 6000, 6172, 0, 0], # ROI - [4, 4962, 4987, 4972, 4972, 6172, 0, 0], + [4, 4962, 4987, 4962, 4972, 6172, 0, 0], [5, 4950, 4975, 4925, 4950, 6172, 0, 0]], stop_loss=-0.01, roi={"0": 0.03}, profit_perc=0.03, trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)] @@ -167,7 +167,7 @@ tc9 = BTContainer(data=[ tc10 = BTContainer(data=[ # D O H L C V B S [0, 5000, 5050, 4950, 5000, 6172, 1, 0], - [1, 5000, 5050, 4950, 5100, 6172, 0, 0], + [1, 5000, 5100, 4950, 5100, 6172, 0, 0], [2, 5100, 5251, 5100, 5100, 6172, 0, 0], [3, 4850, 5050, 4650, 4750, 6172, 0, 0], [4, 4750, 4950, 4350, 4750, 6172, 0, 0]], @@ -183,7 +183,7 @@ tc10 = BTContainer(data=[ tc11 = BTContainer(data=[ # D O H L C V B S [0, 5000, 5050, 4950, 5000, 6172, 1, 0], - [1, 5000, 5050, 4950, 5100, 6172, 0, 0], + [1, 5000, 5100, 4950, 5100, 6172, 0, 0], [2, 5100, 5251, 5100, 5100, 6172, 0, 0], [3, 5000, 5150, 4650, 4750, 6172, 0, 0], [4, 4750, 4950, 4350, 4750, 6172, 0, 0]], @@ -199,7 +199,7 @@ tc11 = BTContainer(data=[ tc12 = BTContainer(data=[ # D O H L C V B S [0, 5000, 5050, 4950, 5000, 6172, 1, 0], - [1, 5000, 5050, 4950, 5100, 6172, 0, 0], + [1, 5000, 5100, 4950, 5100, 6172, 0, 0], [2, 5100, 5251, 4650, 5100, 6172, 0, 0], [3, 4850, 5050, 4650, 4750, 6172, 0, 0], [4, 4750, 4950, 4350, 4750, 6172, 0, 0]], @@ -307,7 +307,7 @@ tc19 = BTContainer(data=[ [2, 4987, 5300, 4950, 5200, 6172, 0, 0], [3, 5000, 5300, 4940, 4962, 6172, 0, 0], # Sell on ROI [4, 4962, 4987, 4950, 4950, 6172, 0, 0], - [5, 4550, 4975, 4925, 4950, 6172, 0, 0]], + [5, 4550, 4975, 4550, 4950, 6172, 0, 0]], stop_loss=-0.10, roi={"0": 0.10, "120": 0.01}, profit_perc=0.01, trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)] ) @@ -322,7 +322,7 @@ tc20 = BTContainer(data=[ [2, 4987, 5300, 4950, 5200, 6172, 0, 0], [3, 5200, 5300, 4940, 4962, 6172, 0, 0], # Sell on ROI [4, 4962, 4987, 4950, 4950, 6172, 0, 0], - [5, 4550, 4975, 4925, 4950, 6172, 0, 0]], + [5, 4925, 4975, 4925, 4950, 6172, 0, 0]], stop_loss=-0.10, roi={"0": 0.10, "119": 0.01}, profit_perc=0.01, trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)] ) @@ -334,7 +334,7 @@ tc20 = BTContainer(data=[ tc21 = BTContainer(data=[ # D O H L C V B S [0, 5000, 5050, 4950, 5000, 6172, 1, 0], - [1, 5000, 5050, 4950, 5100, 6172, 0, 0], + [1, 5000, 5100, 4950, 5100, 6172, 0, 0], [2, 5100, 5251, 4650, 5100, 6172, 0, 0], [3, 4850, 5050, 4650, 4750, 6172, 0, 0], [4, 4750, 4950, 4350, 4750, 6172, 0, 0]], @@ -350,7 +350,7 @@ tc21 = BTContainer(data=[ tc22 = BTContainer(data=[ # D O H L C V B S [0, 5000, 5050, 4950, 5000, 6172, 1, 0], - [1, 5000, 5050, 4950, 5100, 6172, 0, 0], + [1, 5000, 5100, 4950, 5100, 6172, 0, 0], [2, 5100, 5251, 5100, 5100, 6172, 0, 0], [3, 4850, 5050, 4650, 4750, 6172, 0, 0], [4, 4750, 4950, 4350, 4750, 6172, 0, 0]], @@ -369,7 +369,7 @@ tc22 = BTContainer(data=[ tc23 = BTContainer(data=[ # D O H L C V B S [0, 5000, 5050, 4950, 5000, 6172, 1, 0], - [1, 5000, 5050, 4950, 5100, 6172, 0, 0], + [1, 5000, 5100, 4950, 5100, 6172, 0, 0], [2, 5100, 5251, 5100, 5100, 6172, 0, 0], [3, 4850, 5251, 4650, 4750, 6172, 0, 0], [4, 4750, 4950, 4350, 4750, 6172, 0, 0]], @@ -387,8 +387,8 @@ tc24 = BTContainer(data=[ [0, 5000, 5025, 4975, 4987, 6172, 1, 0], [1, 5000, 5025, 4975, 4987, 6172, 0, 0], # enter trade (signal on last candle) [2, 4987, 5012, 4986, 4986, 6172, 0, 0], - [3, 5010, 5000, 4855, 5010, 6172, 0, 1], # Triggers stoploss + sellsignal - [4, 5010, 4987, 4977, 4995, 6172, 0, 0], + [3, 5010, 5010, 4855, 5010, 6172, 0, 1], # Triggers stoploss + sellsignal + [4, 5010, 5010, 4977, 4995, 6172, 0, 0], [5, 4995, 4995, 4950, 4950, 6172, 0, 0]], stop_loss=-0.01, roi={"0": 1}, profit_perc=-0.01, use_sell_signal=True, trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=3)] @@ -402,8 +402,8 @@ tc25 = BTContainer(data=[ [0, 5000, 5025, 4975, 4987, 6172, 1, 0], [1, 5000, 5025, 4975, 4987, 6172, 0, 0], # enter trade (signal on last candle) [2, 4987, 5012, 4986, 4986, 6172, 0, 0], - [3, 5010, 5000, 4986, 5010, 6172, 0, 1], - [4, 5010, 4987, 4855, 4995, 6172, 0, 0], # Triggers stoploss + sellsignal acted on + [3, 5010, 5010, 4986, 5010, 6172, 0, 1], + [4, 5010, 5010, 4855, 4995, 6172, 0, 0], # Triggers stoploss + sellsignal acted on [5, 4995, 4995, 4950, 4950, 6172, 0, 0]], stop_loss=-0.01, roi={"0": 1}, profit_perc=0.002, use_sell_signal=True, trades=[BTrade(sell_reason=SellType.SELL_SIGNAL, open_tick=1, close_tick=4)] @@ -418,7 +418,7 @@ tc26 = BTContainer(data=[ [1, 5000, 5025, 4975, 4987, 6172, 0, 0], # enter trade (signal on last candle) [2, 4987, 5012, 4986, 4986, 6172, 0, 0], [3, 5010, 5251, 4986, 5010, 6172, 0, 1], # Triggers ROI, sell-signal - [4, 5010, 4987, 4855, 4995, 6172, 0, 0], + [4, 5010, 5010, 4855, 4995, 6172, 0, 0], [5, 4995, 4995, 4950, 4950, 6172, 0, 0]], stop_loss=-0.10, roi={"0": 0.05}, profit_perc=0.05, use_sell_signal=True, trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)] @@ -447,7 +447,7 @@ tc27 = BTContainer(data=[ tc28 = BTContainer(data=[ # D O H L C V B S [0, 5000, 5050, 4950, 5000, 6172, 1, 0], - [1, 5000, 5050, 4950, 5100, 6172, 0, 0], + [1, 5000, 5100, 4950, 5100, 6172, 0, 0], [2, 5100, 5251, 5100, 5100, 6172, 0, 0], [3, 4850, 5050, 4650, 4750, 6172, 0, 0], [4, 4750, 4950, 4350, 4750, 6172, 0, 0]], From d60001e886caf91344697af41554857186774cb1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 30 Oct 2021 16:14:13 +0200 Subject: [PATCH 56/72] Stoploss cannot be below candle low fix #5816 --- freqtrade/optimize/backtesting.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 8328d61d3..82c072867 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -312,7 +312,9 @@ class Backtesting: # Worst case: price ticks tiny bit above open and dives down. stop_rate = sell_row[OPEN_IDX] * (1 - abs(trade.stop_loss_pct)) assert stop_rate < sell_row[HIGH_IDX] - return stop_rate + # Limit lower-end to candle low to avoid sells below the low. + # This still remains "worst case" - but "worst realistic case". + return max(sell_row[LOW_IDX], stop_rate) # Set close_rate to stoploss return trade.stop_loss From 650d6c276a3bd54686217fbcc48b7f34f58f13b8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 30 Oct 2021 10:58:03 +0200 Subject: [PATCH 57/72] Add documentation --- docs/utils.md | 40 +++++++++++++++++++++++++++++++++ freqtrade/commands/arguments.py | 18 +++++++-------- 2 files changed, 49 insertions(+), 9 deletions(-) diff --git a/docs/utils.md b/docs/utils.md index a65ba5db4..6934e0a5c 100644 --- a/docs/utils.md +++ b/docs/utils.md @@ -577,6 +577,46 @@ Common arguments: ``` +## Show previous Backtest results + +Allows you to show previous backtest results. +Adding `--show-pair-list` outputs a sorted pair list you can easily copy/paste into your configuration (omitting bad pairs). + +??? Warning "Strategy overfitting" + Only using winning pairs can lead to an overfitted strategy, which will not work well on future data. Make sure to extensively test your strategy in dry-run before risking real money. + +``` +usage: freqtrade backtest-show [-h] [-v] [--logfile FILE] [-V] [-c PATH] + [-d PATH] [--userdir PATH] + [--export-filename PATH] [--show-pair-list] + +optional arguments: + -h, --help show this help message and exit + --export-filename PATH + Save backtest results to the file with this filename. + Requires `--export` to be set as well. Example: + `--export-filename=user_data/backtest_results/backtest + _today.json` + --show-pair-list Show backtesting pairlist sorted by profit. + +Common arguments: + -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). + --logfile FILE Log to the file specified. Special values are: + 'syslog', 'journald'. See the documentation for more + details. + -V, --version show program's version number and exit + -c PATH, --config PATH + Specify configuration file (default: + `userdir/config.json` or `config.json` whichever + exists). Multiple --config options may be used. Can be + set to `-` to read config from stdin. + -d PATH, --datadir PATH + Path to directory with historical backtesting data. + --userdir PATH, --user-data-dir PATH + Path to userdata directory. + +``` + ## List Hyperopt results You can list the hyperoptimization epochs the Hyperopt module evaluated previously with the `hyperopt-list` sub-command. diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index be4f9188f..9d14bb38d 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -175,15 +175,15 @@ class Arguments: self.parser = argparse.ArgumentParser(description='Free, open source crypto trading bot') self._build_args(optionlist=['version'], parser=self.parser) - from freqtrade.commands import (start_backtest_show, start_backtesting, - start_convert_data, start_convert_trades, - start_create_userdir, start_download_data, start_edge, - start_hyperopt, start_hyperopt_list, start_hyperopt_show, - start_install_ui, start_list_data, start_list_exchanges, - start_list_markets, start_list_strategies, - start_list_timeframes, start_new_config, start_new_strategy, - start_plot_dataframe, start_plot_profit, start_show_trades, - start_test_pairlist, start_trading, start_webserver) + from freqtrade.commands import (start_backtest_show, start_backtesting, start_convert_data, + start_convert_trades, start_create_userdir, + start_download_data, start_edge, start_hyperopt, + start_hyperopt_list, start_hyperopt_show, start_install_ui, + start_list_data, start_list_exchanges, start_list_markets, + start_list_strategies, start_list_timeframes, + start_new_config, start_new_strategy, start_plot_dataframe, + start_plot_profit, start_show_trades, start_test_pairlist, + start_trading, start_webserver) subparsers = self.parser.add_subparsers(dest='command', # Use custom message when no subhandler is added From 72ecb45d868dbf2dbcb10a2a2597a795ae451436 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 30 Oct 2021 16:53:48 +0200 Subject: [PATCH 58/72] Add test for backtest_show logic --- freqtrade/optimize/optimize_reports.py | 2 +- tests/optimize/test_optimize_reports.py | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 5adad39c1..d6fcf8a04 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -740,7 +740,7 @@ def show_backtest_results(config: Dict, backtest_stats: Dict): def show_filtered_pairlist(config: Dict, backtest_stats: Dict): if config.get('backtest_show_pair_list', False): for strategy, results in backtest_stats['strategy'].items(): - print("Pairs for Strategy: \n[") + print(f"Pairs for Strategy {strategy}: \n[") for result in results['results_per_pair']: if result["key"] != 'TOTAL': print(f'"{result["key"]}", // {round(result["profit_mean_pct"], 2)}%') diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index b5eb09923..7f5f5e8a9 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -10,7 +10,7 @@ from arrow import Arrow from freqtrade.configuration import TimeRange from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN from freqtrade.data import history -from freqtrade.data.btanalysis import get_latest_backtest_filename, load_backtest_data +from freqtrade.data.btanalysis import get_latest_backtest_filename, load_backtest_data, load_backtest_stats from freqtrade.edge import PairInfo from freqtrade.enums import SellType from freqtrade.optimize.optimize_reports import (_get_resample_from_period, generate_backtest_stats, @@ -19,7 +19,7 @@ from freqtrade.optimize.optimize_reports import (_get_resample_from_period, gene generate_periodic_breakdown_stats, generate_sell_reason_stats, generate_strategy_comparison, - generate_trading_stats, store_backtest_stats, + generate_trading_stats, show_filtered_pairlist, store_backtest_stats, text_table_bt_results, text_table_sell_reason, text_table_strategy) from freqtrade.resolvers.strategy_resolver import StrategyResolver @@ -407,3 +407,16 @@ def test__get_resample_from_period(): assert _get_resample_from_period('month') == '1M' with pytest.raises(ValueError, match=r"Period noooo is not supported."): _get_resample_from_period('noooo') + + +def test_show_filtered_pairlist(testdatadir, default_conf, capsys): + filename = testdatadir / "backtest-result_new.json" + bt_data = load_backtest_stats(filename) + default_conf['backtest_show_pair_list'] = True + + show_filtered_pairlist(default_conf, bt_data) + + out, err = capsys.readouterr() + assert 'Pairs for Strategy StrategyTestV2: \n[' in out + assert 'TOTAL' not in out + assert '"ETH/BTC", // ' in out From 20904f1ca49d3d59d65eaf35b8699d1a33c025ee Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 30 Oct 2021 17:05:12 +0200 Subject: [PATCH 59/72] Add tests for new command --- freqtrade/commands/optimize_commands.py | 8 +++---- freqtrade/optimize/optimize_reports.py | 2 +- tests/commands/test_commands.py | 28 +++++++++++++++++++------ tests/optimize/test_optimize_reports.py | 13 ++++++------ 4 files changed, 34 insertions(+), 17 deletions(-) diff --git a/freqtrade/commands/optimize_commands.py b/freqtrade/commands/optimize_commands.py index c35e78153..4acc0d939 100644 --- a/freqtrade/commands/optimize_commands.py +++ b/freqtrade/commands/optimize_commands.py @@ -3,11 +3,9 @@ from typing import Any, Dict from freqtrade import constants from freqtrade.configuration import setup_utils_configuration -from freqtrade.data.btanalysis import load_backtest_stats from freqtrade.enums import RunMode from freqtrade.exceptions import OperationalException from freqtrade.misc import round_coin_value -from freqtrade.optimize.optimize_reports import show_backtest_results, show_filtered_pairlist logger = logging.getLogger(__name__) @@ -63,11 +61,13 @@ def start_backtest_show(args: Dict[str, Any]) -> None: config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) + from freqtrade.optimize.optimize_reports import show_backtest_results, show_sorted_pairlist + from freqtrade.data.btanalysis import load_backtest_stats + results = load_backtest_stats(config['exportfilename']) - # print(results) show_backtest_results(config, results) - show_filtered_pairlist(config, results) + show_sorted_pairlist(config, results) def start_hyperopt(args: Dict[str, Any]) -> None: diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index d6fcf8a04..09de655ef 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -737,7 +737,7 @@ def show_backtest_results(config: Dict, backtest_stats: Dict): print('\nFor more details, please look at the detail tables above') -def show_filtered_pairlist(config: Dict, backtest_stats: Dict): +def show_sorted_pairlist(config: Dict, backtest_stats: Dict): if config.get('backtest_show_pair_list', False): for strategy, results in backtest_stats['strategy'].items(): print(f"Pairs for Strategy {strategy}: \n[") diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 6e717afdf..fcccca539 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -8,12 +8,12 @@ from zipfile import ZipFile import arrow import pytest -from freqtrade.commands import (start_convert_data, start_convert_trades, start_create_userdir, - start_download_data, start_hyperopt_list, start_hyperopt_show, - start_install_ui, start_list_data, start_list_exchanges, - start_list_markets, start_list_strategies, start_list_timeframes, - start_new_strategy, start_show_trades, start_test_pairlist, - start_trading, start_webserver) +from freqtrade.commands import (start_backtest_show, start_convert_data, start_convert_trades, + start_create_userdir, start_download_data, start_hyperopt_list, + start_hyperopt_show, start_install_ui, start_list_data, + start_list_exchanges, start_list_markets, start_list_strategies, + start_list_timeframes, start_new_strategy, start_show_trades, + start_test_pairlist, start_trading, start_webserver) from freqtrade.commands.deploy_commands import (clean_ui_subdir, download_and_install_ui, get_ui_download_url, read_ui_version) from freqtrade.configuration import setup_utils_configuration @@ -1389,3 +1389,19 @@ def test_show_trades(mocker, fee, capsys, caplog): with pytest.raises(OperationalException, match=r"--db-url is required for this command."): start_show_trades(pargs) + + +def test_backtest_show(mocker, testdatadir, capsys): + sbr = mocker.patch('freqtrade.optimize.optimize_reports.show_backtest_results') + args = [ + "backtest-show", + "--export-filename", + f"{testdatadir / 'backtest-result_new.json'}", + "--show-pair-list" + ] + pargs = get_args(args) + pargs['config'] = None + start_backtest_show(pargs) + assert sbr.call_count == 1 + out, err = capsys.readouterr() + assert "Pairs for Strategy" in out diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index 7f5f5e8a9..e56572522 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -10,7 +10,8 @@ from arrow import Arrow from freqtrade.configuration import TimeRange from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN from freqtrade.data import history -from freqtrade.data.btanalysis import get_latest_backtest_filename, load_backtest_data, load_backtest_stats +from freqtrade.data.btanalysis import (get_latest_backtest_filename, load_backtest_data, + load_backtest_stats) from freqtrade.edge import PairInfo from freqtrade.enums import SellType from freqtrade.optimize.optimize_reports import (_get_resample_from_period, generate_backtest_stats, @@ -19,9 +20,9 @@ from freqtrade.optimize.optimize_reports import (_get_resample_from_period, gene generate_periodic_breakdown_stats, generate_sell_reason_stats, generate_strategy_comparison, - generate_trading_stats, show_filtered_pairlist, store_backtest_stats, - text_table_bt_results, text_table_sell_reason, - text_table_strategy) + generate_trading_stats, show_sorted_pairlist, + store_backtest_stats, text_table_bt_results, + text_table_sell_reason, text_table_strategy) from freqtrade.resolvers.strategy_resolver import StrategyResolver from tests.data.test_history import _backup_file, _clean_test_file @@ -409,12 +410,12 @@ def test__get_resample_from_period(): _get_resample_from_period('noooo') -def test_show_filtered_pairlist(testdatadir, default_conf, capsys): +def test_show_sorted_pairlist(testdatadir, default_conf, capsys): filename = testdatadir / "backtest-result_new.json" bt_data = load_backtest_stats(filename) default_conf['backtest_show_pair_list'] = True - show_filtered_pairlist(default_conf, bt_data) + show_sorted_pairlist(default_conf, bt_data) out, err = capsys.readouterr() assert 'Pairs for Strategy StrategyTestV2: \n[' in out From c15f73aa1f35aaea7db1d701ed6d745fd80ecbb6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 31 Oct 2021 09:55:19 +0100 Subject: [PATCH 60/72] Rename command to backtesting-show --- docs/utils.md | 6 +++--- freqtrade/commands/__init__.py | 2 +- freqtrade/commands/arguments.py | 28 ++++++++++++------------- freqtrade/commands/optimize_commands.py | 4 ++-- tests/commands/test_commands.py | 8 +++---- 5 files changed, 24 insertions(+), 24 deletions(-) diff --git a/docs/utils.md b/docs/utils.md index 6934e0a5c..4a032db26 100644 --- a/docs/utils.md +++ b/docs/utils.md @@ -586,9 +586,9 @@ Adding `--show-pair-list` outputs a sorted pair list you can easily copy/paste i Only using winning pairs can lead to an overfitted strategy, which will not work well on future data. Make sure to extensively test your strategy in dry-run before risking real money. ``` -usage: freqtrade backtest-show [-h] [-v] [--logfile FILE] [-V] [-c PATH] - [-d PATH] [--userdir PATH] - [--export-filename PATH] [--show-pair-list] +usage: freqtrade backtesting-show [-h] [-v] [--logfile FILE] [-V] [-c PATH] + [-d PATH] [--userdir PATH] + [--export-filename PATH] [--show-pair-list] optional arguments: -h, --help show this help message and exit diff --git a/freqtrade/commands/__init__.py b/freqtrade/commands/__init__.py index ba977b6bd..129836000 100644 --- a/freqtrade/commands/__init__.py +++ b/freqtrade/commands/__init__.py @@ -16,7 +16,7 @@ from freqtrade.commands.hyperopt_commands import start_hyperopt_list, start_hype from freqtrade.commands.list_commands import (start_list_exchanges, start_list_markets, start_list_strategies, start_list_timeframes, start_show_trades) -from freqtrade.commands.optimize_commands import (start_backtest_show, start_backtesting, +from freqtrade.commands.optimize_commands import (start_backtesting, start_backtesting_show, start_edge, start_hyperopt) from freqtrade.commands.pairlist_commands import start_test_pairlist from freqtrade.commands.plot_commands import start_plot_dataframe, start_plot_profit diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index 9d14bb38d..032f7dd51 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -175,15 +175,15 @@ class Arguments: self.parser = argparse.ArgumentParser(description='Free, open source crypto trading bot') self._build_args(optionlist=['version'], parser=self.parser) - from freqtrade.commands import (start_backtest_show, start_backtesting, start_convert_data, - start_convert_trades, start_create_userdir, - start_download_data, start_edge, start_hyperopt, - start_hyperopt_list, start_hyperopt_show, start_install_ui, - start_list_data, start_list_exchanges, start_list_markets, - start_list_strategies, start_list_timeframes, - start_new_config, start_new_strategy, start_plot_dataframe, - start_plot_profit, start_show_trades, start_test_pairlist, - start_trading, start_webserver) + from freqtrade.commands import (start_backtesting, start_backtesting_show, + start_convert_data, start_convert_trades, + start_create_userdir, start_download_data, start_edge, + start_hyperopt, start_hyperopt_list, start_hyperopt_show, + start_install_ui, start_list_data, start_list_exchanges, + start_list_markets, start_list_strategies, + start_list_timeframes, start_new_config, start_new_strategy, + start_plot_dataframe, start_plot_profit, start_show_trades, + start_test_pairlist, start_trading, start_webserver) subparsers = self.parser.add_subparsers(dest='command', # Use custom message when no subhandler is added @@ -267,14 +267,14 @@ class Arguments: backtesting_cmd.set_defaults(func=start_backtesting) self._build_args(optionlist=ARGS_BACKTEST, parser=backtesting_cmd) - # Add backtest-show subcommand - backtest_show_cmd = subparsers.add_parser( - 'backtest-show', + # Add backtesting-show subcommand + backtesting_show_cmd = subparsers.add_parser( + 'backtesting-show', help='Show past Backtest results', parents=[_common_parser], ) - backtest_show_cmd.set_defaults(func=start_backtest_show) - self._build_args(optionlist=ARGS_BACKTEST_SHOW, parser=backtest_show_cmd) + backtesting_show_cmd.set_defaults(func=start_backtesting_show) + self._build_args(optionlist=ARGS_BACKTEST_SHOW, parser=backtesting_show_cmd) # Add edge subcommand edge_cmd = subparsers.add_parser('edge', help='Edge module.', diff --git a/freqtrade/commands/optimize_commands.py b/freqtrade/commands/optimize_commands.py index 4acc0d939..f230b696c 100644 --- a/freqtrade/commands/optimize_commands.py +++ b/freqtrade/commands/optimize_commands.py @@ -54,15 +54,15 @@ def start_backtesting(args: Dict[str, Any]) -> None: backtesting.start() -def start_backtest_show(args: Dict[str, Any]) -> None: +def start_backtesting_show(args: Dict[str, Any]) -> None: """ Show previous backtest result """ config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) - from freqtrade.optimize.optimize_reports import show_backtest_results, show_sorted_pairlist from freqtrade.data.btanalysis import load_backtest_stats + from freqtrade.optimize.optimize_reports import show_backtest_results, show_sorted_pairlist results = load_backtest_stats(config['exportfilename']) diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index fcccca539..e0d0cc38d 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -8,7 +8,7 @@ from zipfile import ZipFile import arrow import pytest -from freqtrade.commands import (start_backtest_show, start_convert_data, start_convert_trades, +from freqtrade.commands import (start_backtesting_show, start_convert_data, start_convert_trades, start_create_userdir, start_download_data, start_hyperopt_list, start_hyperopt_show, start_install_ui, start_list_data, start_list_exchanges, start_list_markets, start_list_strategies, @@ -1391,17 +1391,17 @@ def test_show_trades(mocker, fee, capsys, caplog): start_show_trades(pargs) -def test_backtest_show(mocker, testdatadir, capsys): +def test_backtesting_show(mocker, testdatadir, capsys): sbr = mocker.patch('freqtrade.optimize.optimize_reports.show_backtest_results') args = [ - "backtest-show", + "backtesting-show", "--export-filename", f"{testdatadir / 'backtest-result_new.json'}", "--show-pair-list" ] pargs = get_args(args) pargs['config'] = None - start_backtest_show(pargs) + start_backtesting_show(pargs) assert sbr.call_count == 1 out, err = capsys.readouterr() assert "Pairs for Strategy" in out From 3d59289b0929deaea54bcaa9c872a3365dc93fe7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Nov 2021 03:01:03 +0000 Subject: [PATCH 61/72] Bump filelock from 3.3.1 to 3.3.2 Bumps [filelock](https://github.com/tox-dev/py-filelock) from 3.3.1 to 3.3.2. - [Release notes](https://github.com/tox-dev/py-filelock/releases) - [Changelog](https://github.com/tox-dev/py-filelock/blob/main/docs/changelog.rst) - [Commits](https://github.com/tox-dev/py-filelock/compare/3.3.1...3.3.2) --- updated-dependencies: - dependency-name: filelock dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-hyperopt.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index 288d3efad..9beb17b4a 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -5,7 +5,7 @@ scipy==1.7.1 scikit-learn==1.0 scikit-optimize==0.9.0 -filelock==3.3.1 +filelock==3.3.2 joblib==1.1.0 psutil==5.8.0 progressbar2==3.55.0 From e2041ddb70a4bef7f31b9e4a19e43c96904e4a37 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Nov 2021 03:01:10 +0000 Subject: [PATCH 62/72] Bump ccxt from 1.59.2 to 1.59.77 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.59.2 to 1.59.77. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/exchanges.cfg) - [Commits](https://github.com/ccxt/ccxt/compare/1.59.2...1.59.77) --- updated-dependencies: - dependency-name: ccxt dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f242fb9b1..ef43797bc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.21.3 pandas==1.3.4 pandas-ta==0.3.14b -ccxt==1.59.2 +ccxt==1.59.77 # Pin cryptography for now due to rust build errors with piwheels cryptography==35.0.0 aiohttp==3.7.4.post0 From 45f7093e520e4ad7a9e8b69d5ee841043d1ec5d0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Nov 2021 03:01:14 +0000 Subject: [PATCH 63/72] Bump mkdocs-material from 7.3.4 to 7.3.6 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 7.3.4 to 7.3.6. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/7.3.4...7.3.6) --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- docs/requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 72d1d0494..40269b109 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,4 +1,4 @@ mkdocs==1.2.3 -mkdocs-material==7.3.4 +mkdocs-material==7.3.6 mdx_truly_sane_lists==1.2 pymdown-extensions==9.0 From 46d4418e85abc58b640e895a7c482d0929227b85 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Nov 2021 03:01:23 +0000 Subject: [PATCH 64/72] Bump scikit-learn from 1.0 to 1.0.1 Bumps [scikit-learn](https://github.com/scikit-learn/scikit-learn) from 1.0 to 1.0.1. - [Release notes](https://github.com/scikit-learn/scikit-learn/releases) - [Commits](https://github.com/scikit-learn/scikit-learn/compare/1.0...1.0.1) --- updated-dependencies: - dependency-name: scikit-learn dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-hyperopt.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index 288d3efad..99d5576bd 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -3,7 +3,7 @@ # Required for hyperopt scipy==1.7.1 -scikit-learn==1.0 +scikit-learn==1.0.1 scikit-optimize==0.9.0 filelock==3.3.1 joblib==1.1.0 From 6623dfe7da88d80f43c405a58718b7e7db3b2b95 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 1 Nov 2021 11:07:06 +0100 Subject: [PATCH 65/72] Improve CORS documentation --- docs/assets/frequi_url.png | Bin 0 -> 11391 bytes docs/rest-api.md | 27 ++++++++++++++++++++++----- 2 files changed, 22 insertions(+), 5 deletions(-) create mode 100644 docs/assets/frequi_url.png diff --git a/docs/assets/frequi_url.png b/docs/assets/frequi_url.png new file mode 100644 index 0000000000000000000000000000000000000000..bd6ef52b62d3e0cd8013922e22bea795828fe802 GIT binary patch literal 11391 zcma)Cg;!Kxv>rkbke0>)r9nViLP5H_JES`tIuwxZ&LM~HZV(uRA*8#z8M^23d+YrH z@2*?xuJ!G+@4ow-`|Su*R+PfVB*z2*0N66p;;H}u(#FfZ=4<4ab7m1T&C3PDNm|Dh z0Kn<{??6go!Jz;Er~xwK-_$(}kC%P*)n?a)&y-xmT*M}5QGWsm2!Mg6-|B;HSVD>8 zrimAI=R1!NAdB_h#mg3l3yxZCzBgGJ%bkr66~~RB$ws{&4TQFqtc{qBjP+jwIJZ?q z@m^EM_602kEp6QlVB>9|DCPEhtyFd1W9}|xW-QTyy-!+p4L#+ws*s%3EaObpaIQh(fO;pNw;{hHk%SdEUl=hm@&SvMN442|H?ONm8REwi z`T|4bY#faE!@kge)5j&dY>x8)gk06a_SZa>?i3OSHYu3hcXpg{(e&PVZyFUwZxx>| zEIK}4I$_-K;R1mk4j#=azGJk)Z+|tTy-1Hof}XetJjxA(p?g$@dOARVp|7A)ZT@^D z8d>U=^o0p!Ba$~gg@C~JyRWxhD!T7O;WQk+y~bIJmR%!V+)hGkp4~q%Jl6s<9-MuR zN~FoRkG+-vF(LRSqhEsMZ>S-pVf!Tw*1ZBT0~deFsvfi=69zVG7R#Rt3IA+Io-PRj z9U1fQmXMcf9<}4|EqXScQ7Aey`Y7hb46JG!LQ6PGQj<~95huG!NZMU@{D@Y7PCGSW zcM&*?(&^Q?PYUhl+vja}_I6ep!+f-V1$^q@GGDW?uX_H%0SXi|_TJk(h~3Cl2=?K5 zEs0AEL)sKdr0>VwH`Z}`*Pdj9Very3Cjhjvvh_D)OflP(G9NqX#Rpt>6JB09W$IC7 z0LcL;bS5N(Shqxs@_dtz3LCpz;}kH=2MWNsCAMnyPTGA9V~th4-x_(&yLVoN*stnq z{59te@QAv9{?*>}T+Pb+ghbPg?7zbe`oN*5Lugj!)N0h$c>#>b5VQ=~R!_H!Nj& zRUST@aKKr>z$TkDoE7{C!W?|M#_i-$+A)&H3#3o=6(d`onz&EcZMxS&AAPY|`^B}V zzAAcwQdWxZE(F^aDKRk#aVR3A7J($~dAH|8n zHW3Q?;A2y=t zOCG$i-Od>&nInpVU2keXif_xrJE6cNL}fpr3V^9rEi^8BUcJUYG$ zag5s*a~;I4`@{Vs&**kbRo1{J+@nwIP)(AN{`}#6s580yfo~ky$a=g0weJBs=6ON< zT72z!e67+`DX+b@`(gR_u0I%OFssT~xdV)wlI8r4)nQLEQ{BsjQQv2xz!j#Io8Hq3 zSZ0a}=&0F03)fLAuB}ixws8%9TCNvM$>avA&t7;Mg0+p!0jQ<*s@DZ$wrFjG_BgV= zj_-_!nnb-0Q4k#^Q!oxolaB$^m0hUqjCgIP3UF~5HlA#uC+SwT;8WWXX32!NWrawcLYYN6vdTR0BV!s_e@QURi6E~CpZI#*>~NP zgbbRN*jmZUMt1qhKPZ2^-%n;xvuSj^Q;`63|JZJ2Xc7t>0Iw{UYS-`IG5jDg^k3f( zU&;APDkH}N_*`L7w2FWoTC!bA?+9IALZf2T)!Uu6bAmqcIC{cZ7P~G)QkaTdf=m(S_j>?C}&!B-DwHsr+ZIU8rz<*t@N@V#g4G6C&Y%?v$3hC|j2NwvthM?lMG7 zlKOUrh^5wC;bCQAawE2%eds~Axx1|mnF!o;ZaW0IdG-@IjW67;@URNr9BUg(?`Y^# zu<_w|YTM-<7=QvoW1^Uz52>EY(0TT;2{RsQJibBnK@tA@()y}Y{F?hl9B4}sROE-G zwZ}=b^ui5hjE9?MW|gfdbyqj8?8faf$7HM!4SbU;nt`kqeB9OQlcNJgMR`)gjeAi} zDqTEv_#armr5zxWy(81(5UWfJa^lP7aP=!E+TO;_UR= z;=k&XUO^-NBi|)|2L#Y-J8;$4u5*yi*hX&U^V9}_8h@NE#NMhjqIbvXl^pHVZHnd3 z*m|AZKq4&y=@H9kud}1Gt?Lzt+LGp;BmcaWOoV#4u^Z|@QGbrZ>A-HQP7Y!?H+(+q z6?aoR;~nVTb6r_^C5Z^aa{DSxqAuRtQkY@|LBN*}@4wW3W%2eLjj&1u{IcF9$0cy> z9uU;mJQvP#4c+x}kUyXbn#k+ZvJubXyc+FC=sQ(Lr!j@4k%dJN^==!M-r)*0dRf~< ziobqc_&$$5clCv}SxOxS_e|Q4!W2V;zKH@joM{#>E3quE+x5?f6A%x&*LUH|Z8t|> z#z7J{OSnYoUY3vvRo}}@m9)$fz2<@r_f`LUAE~#J;%Uw>XhpHD6s|qw> zX>H$oGUt@z0C=Iazd)FPEfdVk8SLws^}R#xw)R!DLYveA3OPkm9qq+$DLAN=wP!V6~8p~8rqlPWEj{FG|}Z{m#(Y(;wl?v(^{Zn z{`j@E7cyXIW$kOGRO7#FH6wiG3J3u`jdX2b3g1Zt>_SNe+i3puKFUdjzsBVPfgVz# z^z0-DkNTDHIKcKKb+P4Ym0=FA@&-I;|WK2R3 zEzUljGH6}@3t`$_0kf2O+s>{#5P#M)NO(BT`!yWhMODX7`g3@Z0*yL86cwi~FxcgJ zE`^U@8*rfG9N(D#wd``&)K}Dy_a+y;PJi1Q(K=vdhF^1=v5Iq}b6$#z)ZEJAek{yi zpR3PC-!06!0p;rCnifkS-rzY{eR^b)ajHfTsP(evGw^=GUYpx30xTJ=bxi(C0(xIk zU^&7*~xax6#rLelw=AOS6fc=H zYPTb4YAYVk1nkwRRm#4>-yVnH~kdMyrLpy{DTX+V|i#tCr8G$(2bjfn#QN#MR z$hJ)EiOWRDC=`)mZBO(Ih&+3nyY|1A8G}HeT2Ac`SQ$wfM5pi4(C!aorUl$uU?j9b zgXXpW{@zB`Yu_2uIZu+W=Ac_zO(3UCsd3zM&<2oUch9UM&PV<27OFv$zZ;q$w5LyN zczpM9ymIb0dBIC*9rji2EAzgCh%_VSuXN4NnL^NYMt*&-)APquq5CsI?^Q0Hk75kC z|B=I_o60$r8S%NB9U9Kpx%jcA7P&O7H0s zi=wdE?(_X37x=SM`_p2)0!33hoM*0FLCVOl!_+L=-*wi!%m?05R?foq{p8@(PB|oW z?Z{f7;U>6?@*UulGJIz=f9%VB#%#)GVT9Z`7sJmU$C~Y4a3OlK#5qbK-dcIsbAR75I1|=k!N38M7iG`2N7ty&;-zGKDhjj1Cf6oIuNx7W92_jD%LnHp zMiac>r@bJBX`bB!@^sE!b6Psuy?6q^%L*EkOjSh*t4IkAqekF;fJ>x$$fH!h{HZ4? zAfrEa{N#VXFz=`@{c0R&zDIrguP%1)Q|=j=SZ>(Kv2oj}(}ASD%PFKG`A2hb4x?UC zygqDWRSpIH{T33#b9PMB+|P5{#?Y^D&p*FgL-KD{97VrJ;2Hf^f#BgyqQ(pm%1E_* zb&YTKUh!T-654Dg*s<8~jv)AT`{%!~{PnLOQTSU$|E=>7^zPn~SM=YGdpF54Nx#%a zQbU0MMbWSOdw@;e-95Kj@;L5NLNVy1?O%I0*PnTq;>CKU`Om)K0wjEO-( z;k2q>c|2@GIVdOtcRGkutf&x^7ywYXmjT6CVf_u5V%OyM1c-{SOxT4PmrB3nz$duo zR>#&9VXg!Ehyd^l0nfTG0Kk_pK6_vUrTYR$HO7+kx@-ze32VFd;2U2X6vvHe^CA=i zf+=PZ%=Y-E>C_<|d_rHrV3XWg5imrO#lfl$+8Z*Uu*8I+R8sPJTSZrA=z zmo4!2Z4AK~Bj)QRVQLvXnz+bCS&5(t*MAMaCt$q^84vc1{;$@kedos{I1NsM5;?=9i5qU;$v zjl%~Z0O;a;d+WTbi{3U)TN=hh>!CuvX(YKMBm{Ia5i|8__YitB_DCJdB#!pQ!9fTp z1e{39eqB2r%8nVfp8?!|L(q3ASt&~U`d8|Vv^*i|H+frH2*xjJ2oJ#|>NmVe@-XXP z)S=cYyDDPUj6i;Y_07pE zI4&$PbS*vX_2M!xI(&0%@@M__0@50~+qpu@5;zHmd$w&F9xC@!{GjX8e=^eppxkCs z`(2iXHXXvk?DnI~y0oU0(XIEtZ)9<%r?alMDg!u&5lsJ$^8^8oKvicsX8157#b)=U zFj5o}`x%P^zS%lZBi$y_#k?L)7cd*{6K>K`@4>XY-gG8;o!9*qSX4^+XS75 ztMC1HG^I}!IIEbrND74ZdESUS|I6nJk`+byv_Ogo zmw7EN?@sE#e_1)aoeBmWMk;TEVbPFb#fs9(=i9$=G=+t(6BAb>BMyAtvrCt9r(I)h zQwt_~p^0t?NlCKO@3DCMG7% z&dvv^sh2u+1+v>fu}M_Z^nIHJpJv~B_V2No?8_s4FV2;t_Tr*sWs9oV+9*g=fI!NHGoK7!Cu7! z7Lw3Zw3m%EBrvadZ?AZ_$&TOs#7AF$=}3o3C9d?~fQ!pgl7Wsk@iilK9q%*cCON$M ztEXE4KsjBnC24z_B5u0DcXnQ6#{n!!rDNto8P$!l(Fi zjr(TCReG?sgiVXA@k*!JDq&;j014!6KO+x8P5AFYJ2v~`d3{LLMVMn8r-iM9WSnj( za@i@T-IKPkJ|E);dh^3S1&>bq4diL`lp{qhE9FQ4WgRRMM_j(jRU@@+WWe087#VBc zS}HO7ilOt2azFCh!kog^ztw-#TPjvJnU@%YNX8j0|$PtfD-3 zW0Sw8^J883CLk{_&!VUxRQyBCfa}gM*>Gt0JI`m<$XBd0!4L917Uk0`%U_e~teiC+ zFJ|J?bv28PGjlA2uC5C8^5?=YqVyE!NM)o=!a)zEpOyvDdFx%2;(Q&qYqd6u7k;;n zpiyk$+ny)yd=6SkerKP7xVqbEU&Nf>xuQCZ0%%*>$21WrI~aOqbk-nQU0dyNjsZl3p#l!AX_o7Sv>Aj@Op6Eo}nTlK*&|(O{+lfN|M0 z-LWh8TCTv>tWLVvpc^}_ERTy!H8R*V*QmBNJA>$S6uyr&lm zK5^W7MMLosNJTYe70vUeJ3~w|AXB$i{bGQ8WJBJ(^1gi(m@~Gm<>7>prbO()r)LM`iV_s>NBsW)pjP zIY*=Ie*{{U%O5&+8pGvvsm$4r^Co)xF_HQ|?Sw-cht|c2W$}SYg!$^@#^p2Nl7UJZ z#^ON(TLXVJxL>6#&rSPQw>X~JN*2+H?*@ev8At7$V1{rSSuCq8uV9?yYwgpD>UMKR zPR=>Z&Er>dgtKl>lc&*2RZRRv;llDH;5a4+fRVv5XVTs)f!9jHln3;)Q&W%6H(gym zcbp27F+%5mH~J&K0C5kmQvw#Kn1pO4ugBXe)3KGGdjc1ftXE1okN}0% zJQf|UtzOlq$&HM*a&Pq>SAHfntWf*FzaK8fuo)&~wLev)3y5ULHqt2;3qPpLpdVWR zJ|k={ewAv7J&5UBr<2x!Qwxe8As6@|ciGYL*YZ~?}Og@{4Rz5H5;$6fdae+fm> zJ@^WF&60hXF@7-u*QdD_iUyee(dL3ByZg{rFI~r@Rd&FiaWS(mTbY%x3vy#E`>VJZ z|L^t+3h*x~iWO zHu$`(Z7GKlGN8S5#NCn;8?tTN5Pa0KY~Y~${w_?`jSMukK&Fh@qRufDAd(8Csj}iZ zauOjT%c|0#7!k|wO=OnsCtUz>3tG1@8)=d_y^cZgN+f^EY$`k+|9Hw*$`94M&@4a|0%3*sBmM-%c;5nTLzK=xGh zaI_Z7k0XNEPV-|hKmDV4gBqu*mdMCU_C~PvC^l%Dp)`W=dRj69a2I1?H{EvIAjqi9AY*jaTT=q3cNK?X~mvl6x zEV(kGnK-|wi#PF0PWF!*7VJL6?PU|O+#g>PGALR#EFipYO>{q=oSvR?=?QUt$WcI! zi9upO{pM6xTnPy9biZ%%f0eMl1WoT;@3LQ(44`;lQCdPZWJO)Ts3Mbb*vPjiF}22% z9O+r^`QaQaPHy?g}&wM5kNM&U&W~%{` z)YMNccHTOG@a*=Shi4g}0ETgLt{w?1R>E6X&PNVHK>K;yJPH=lGX+wUB+N7FjUeQ` z3T-N&CI?tgSv4_D1T3toJ(v|z{b<+4edt|cJ5@B4UX{UQW?7e!qaGRrK!FwFqATg9 zx9Q56pNkgiPfhRU1Y=f1fwo1x*l$wXNLRUx?={7AswhM(U7Ogrb!m3BaI$#0DLQ2*HqNckEV=uFX3@CRPUHN>}uMLuEPXPhyhbzi4Y_v*Iebi=Z`1)ytP+lX2ZvA zHjhg0Cje{Q3qJR66ZIL;Q3nfS7;s6v=7|Wp-%tGwHXd&1UB~R!HE}kT=dh=`69>3m zw@0$>kMFzG{IPWP3IwP|p<11mogcs9+|i|QK|@Rg$r0taB!B7Fuc4FA>&Z=}_yyBl zJmf;A4)b8?MPf##Pp~^ zr|a}b!=W3TDyal#HvP2IpKC?dP>5P^*V+9JCH08Im5UGqDiixwedVl6h=}fu_bN~c3s55~nzj(a7cp3&7S8~HUN=L><#Nu8a%Fk)p}RYLlNsm&N`^uW=GPgkP& zn7K~3J~{LyBU2X}qfNia^wizdY@jpZi&^Kb(%0EOGcM)!X^e zY^m{eDd{DMhTfr%85*`eYJ{DKbU8)zZiYfC8Krzh*n}Yl++wxg^ReYwbUArQ&MPCq zx#qRfas%yxf6pPFa=?Z0{CLH&7UkTcSJnNobF=o&ft210AGiDHDj{bSyKb*2-0A}- zZZUaUIo|+9q;0e7J&VB4-r4Jb>5x`&RRR{mpO~b4qcf4^K4=E;2xJ0;SyeJBUolzC zD3xb$cj%8h|6;$@M~*rQ1L>5|ZBj~s-QTB7h-=>&zbyM#Z-BEnt>?D zjW@DxvMrKqV=^k+sn~)x56D!wNSx29U!OVH$a3WZJCW80_x6J(F*LX{-*~Z?(EQO_ zR*QANnfUilxoL4^7hb`;ShG2R==641Sf#tJeWgk^1-EiGgAh*>g(`?Y=@gjXSDF9O zwyhw$BX~c#;@L1*0m$^KBUEw8qI_uY%>@+b)MEUQFRtccl5O3!8_!-p#GpyE{<^AL zwu1IoY=Y@cOyz`27AonZ>Rwy)?o}uxUEyZ1*{K~RCK0@N%il0uo zzC4#E4OZQVHGwn_=ai?gVVI<7+AxUgC7n-HP28l5HpUx4gq+$Qbyq`S|4+J4pM)qt zpH|a}!hQ+OrmdXcd5F^d=8UONCA6xmLL`ERpxy}`DN$Rj4#HOC_>}kpDp&kK)Fb%# zAtj86%`yqaX+BE1^!vAJPop|XPE*9X(_E>b-Sj)W-~q=eW*&9$*xY_9sr_(tI`q)3 zxFyUhG9pV}i=41U%ut-$Vno93gIx;J+ME=la8Nee^RATRkIOGzcZ1WoFF|$X^Rc7< zLml|($lvcuSSp%wbg2C)*a&e-kn~I77AM*-;F?%l{@(!}Va#vxOYE_{*Fkb^=bb+ zo)eB)9U4U|S5iN#L&q{x61VI8x%v3seUJUhzNxbPH%5#$j3|nc)WVjx6z5i0FRD!c?yIlRB@bW}vVIWScG57< z|LQhVnN)HkF%+M~RZFP)?700nJ-dLDpa1rQfT*a{U)sTS{F_3$UD*7>PNNNRz~olI z2X){#q%|pVNy|ZWqulRM&g01MWUM5w2F+jmFS3^(LY1bkLCO?{_p1N*z*dMFwDN=) zkbJ_-l2jQblQ8ZQMypmdgKyUYB9t^Pv?zf_lg6D5F_ zc2zK42!AKKQI%_gFhR@Kk`_jbPavR47XOZ}@TYD2h^-?_v|MUslx#BUAYJXz+a#!Y zMQS`#%v;uYap}EvMDfp35DQ@`b9kIY=&uinD#12PlamJ90Bu(d+_ErLOBU;vP{n8! zOKf)pT^fdpwX94KqcmGw0ZVW_%z@le(lwG7(%iFVB)}+Dfvmh_oGLGA_R>dTKn~Cz zF*dO32smR*^>V5eTKN8W3Kq@}f9?O$8Z{J6xvqx%E!Mc3|LpYXVbve;WSQ-^QzU$E za!4rrb=4gs&PE(6Li^|FO=zFhnPm>a%if~o88uidRV7twAUfQVzeTIwzq_s?UlyE zj@@ac#bG-CU|V>x*4Pb)o3vjk?>o&&C%u!}bG0@CVj`lqJ~@@bbAzQ*@WTdC8k7-b z8r~;peCj3gade2*+i7x~>reIK5G;)ytHMI>)u)+hrL1|wFZu@Ri7IZVm`DQ99QU|s5Luf{DO zoas0$c<-Cb=Qcc&#P4mtNJ4*n>-8I1F@HqYym*zTC*K>eQO-Z81JM7t=|Voz(H|w2 z()321x4wix!*-{PlmwRA#v;(qzr;S@KM`O3=tRVtv*tqX9od%7s`d0JWg2xXnIPm9 zrTgY{l)-P)Q*~cRIfN{1oS$hB6Unw#S8j}gxh3ZBgr=SHF2ktTP(LS|rM*;@p=K9yApgP$vzy12AIg)sypwb$e9@-|K`Iw@3=7$R{+gPgn#NChW zUjdBucP4<$QYqzmCbj9fGmQ%4iT+UnCLLpBf}6pv{s;d~Z9)fQRH{_eocLcf>cNE> z%+Bd&;$cdqx5kAknZj$*)%TPFizk z$We?sGTn4A{$834Om*S=OSJyTy4M$}U95uIkNLw&zX{I+c&r zvAfM=Eq>938qnjC8exeefk8FC08X6@#dP2iUYUl z)`mkmS3p3@{?H`yQWJtLsURp$ zpHLuVi-pV6DUVo11bjDv-fKpD7(YX;bN@m=;S|w)_#jG+*R0Q0*MC0d-|2eLw>zQt z{ngDyz5%D)LKF!5KRtHFg(vEV&(*_JaX@KaQKS%P#1;V`pwIVQE<; znw!e!X!sHe`TF*<`?Vc&3zN3ohzd#EjK6;|116@jJPN7P#zlV5^nlh z2=3;mkm4oR(*SymKVhrsKdE1D?7R(41Bx*sO~HXb+Fv!_M Date: Mon, 1 Nov 2021 13:39:25 +0100 Subject: [PATCH 66/72] Don't force timeframe in config in config generator --- freqtrade/commands/build_config_commands.py | 8 ++++++++ freqtrade/templates/base_config.json.j2 | 3 +-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/freqtrade/commands/build_config_commands.py b/freqtrade/commands/build_config_commands.py index 34ae35aff..ad38d2291 100644 --- a/freqtrade/commands/build_config_commands.py +++ b/freqtrade/commands/build_config_commands.py @@ -83,11 +83,19 @@ def ask_user_config() -> Dict[str, Any]: if val == UNLIMITED_STAKE_AMOUNT else val }, + { + "type": "select", + "name": "timeframe_in_config", + "message": "Tim", + "choices": ["Have the strategy define timeframe.", "Override in configuration."] + }, { "type": "text", "name": "timeframe", "message": "Please insert your desired timeframe (e.g. 5m):", "default": "5m", + "when": lambda x: x["timeframe_in_config"] == 'Override in configuration.' + }, { "type": "text", diff --git a/freqtrade/templates/base_config.json.j2 b/freqtrade/templates/base_config.json.j2 index 68eebdbd4..e2fa1c63e 100644 --- a/freqtrade/templates/base_config.json.j2 +++ b/freqtrade/templates/base_config.json.j2 @@ -10,8 +10,7 @@ "stake_currency": "{{ stake_currency }}", "stake_amount": {{ stake_amount }}, "tradable_balance_ratio": 0.99, - "fiat_display_currency": "{{ fiat_display_currency }}", - "timeframe": "{{ timeframe }}", + "fiat_display_currency": "{{ fiat_display_currency }}",{{ ('\n "timeframe": "' + timeframe + '",') if timeframe else '' }} "dry_run": {{ dry_run | lower }}, "cancel_open_orders_on_exit": false, "unfilledtimeout": { From 74e8b28991598957088a9e1f8eef2d6362535323 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 1 Nov 2021 13:54:12 +0100 Subject: [PATCH 67/72] Improve FAQ with outdated history message closes #5819 --- docs/faq.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/faq.md b/docs/faq.md index 99c0c2c75..10ae41870 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -93,6 +93,18 @@ If this happens for all pairs in the pairlist, this might indicate a recent exch Irrespectively of the reason, Freqtrade will fill up these candles with "empty" candles, where open, high, low and close are set to the previous candle close - and volume is empty. In a chart, this will look like a `_` - and is aligned with how exchanges usually represent 0 volume candles. +### I'm getting "Outdated history for pair xxx" in the log + +The bot is trying to tell you that it got an outdated last candle (not the last complete candle). +As a consequence, Freqtrade will not enter a trade for this pair - as trading on old information is usually not what is desired. + +This warning can point to one of the below problems: + +* Exchange downtime -> Check your exchange status page / blog / twitter feed for details. +* Wrong system time -> Ensure your system-time is correct. +* Barely traded pair -> Check the pair on the exchange webpage, look at the timeframe your strategy uses. If the pair does not have any volume in some candles (usually visualized with a "volume 0" bar, and a "_" as candle), this pair did not have any trades in this timeframe. These pairs should ideally be avoided, as they can cause problems with order-filling. +* API problem -> API returns wrong data (this only here for completeness, and should not happen with supported exchanges). + ### I'm getting the "RESTRICTED_MARKET" message in the log Currently known to happen for US Bittrex users. From 3056be3a1db4f509241703825cd29624ada97da5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 1 Nov 2021 20:04:52 +0100 Subject: [PATCH 68/72] document prerequisites for exchange listing --- docs/developer.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/developer.md b/docs/developer.md index a6c9ec322..da47d903c 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -252,6 +252,9 @@ Completing these tests successfully a good basis point (it's a requirement, actu Also try to use `freqtrade download-data` for an extended timerange and verify that the data downloaded correctly (no holes, the specified timerange was actually downloaded). +These are prerequisites to have an exchange listed as either Supported or Community tested (listed on the homepage). +The below are "extras", which will make an exchange better (feature-complete) - but are not absolutely necessary for either of the 2 categories. + ### Stoploss On Exchange Check if the new exchange supports Stoploss on Exchange orders through their API. From 7ae9b901747ca36861b67379ad65d146db9ec9bf Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 1 Nov 2021 20:12:34 +0100 Subject: [PATCH 69/72] Further clarify backtesting trailing stop logic part of #5816 --- docs/backtesting.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/backtesting.md b/docs/backtesting.md index 37724b02a..75f1ad6f8 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -478,6 +478,7 @@ Since backtesting lacks some detailed information about what happens within a ca - Low happens before high for stoploss, protecting capital first - Trailing stoploss - Trailing Stoploss is only adjusted if it's below the candle's low (otherwise it would be triggered) + - On trade entry candles that trigger trailing stoploss, the "minimum offset" (`stop_positive_offset`) is assumed (instead of high) - and the stop is calculated from this point - High happens first - adjusting stoploss - Low uses the adjusted stoploss (so sells with large high-low difference are backtested correctly) - ROI applies before trailing-stop, ensuring profits are "top-capped" at ROI if both ROI and trailing stop applies From e78df59e308965800c3c8185bbe2b920aa62c273 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 2 Nov 2021 19:49:53 +0100 Subject: [PATCH 70/72] Configure candle length for OKEX --- freqtrade/exchange/__init__.py | 1 + freqtrade/exchange/okex.py | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 freqtrade/exchange/okex.py diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index b08213d28..614e8ad68 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -19,3 +19,4 @@ from freqtrade.exchange.gateio import Gateio from freqtrade.exchange.hitbtc import Hitbtc from freqtrade.exchange.kraken import Kraken from freqtrade.exchange.kucoin import Kucoin +from freqtrade.exchange.okex import Okex diff --git a/freqtrade/exchange/okex.py b/freqtrade/exchange/okex.py new file mode 100644 index 000000000..5ab6f3147 --- /dev/null +++ b/freqtrade/exchange/okex.py @@ -0,0 +1,18 @@ +import logging +from typing import Dict + +from freqtrade.exchange import Exchange + + +logger = logging.getLogger(__name__) + + +class Okex(Exchange): + """ + Okex exchange class. Contains adjustments needed for Freqtrade to work + with this exchange. + """ + + _ft_has: Dict = { + "ohlcv_candle_limit": 100, + } From 161a3fac154770d1be998e182b65af3f15a65c75 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 2 Nov 2021 20:08:56 +0100 Subject: [PATCH 71/72] Run exchange-enabled tests against okex --- requirements.txt | 2 +- tests/exchange/test_ccxt_compat.py | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index ef43797bc..e2f6a3162 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.21.3 pandas==1.3.4 pandas-ta==0.3.14b -ccxt==1.59.77 +ccxt==1.60.11 # Pin cryptography for now due to rust build errors with piwheels cryptography==35.0.0 aiohttp==3.7.4.post0 diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index d71dbe015..2f629528c 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -47,6 +47,11 @@ EXCHANGES = { 'hasQuoteVolume': True, 'timeframe': '5m', }, + 'okex': { + 'pair': 'BTC/USDT', + 'hasQuoteVolume': True, + 'timeframe': '5m', + }, } From 1fefb132e00e6b92ac28f1a28f209422dd8b0a7e Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 2 Nov 2021 20:26:38 +0100 Subject: [PATCH 72/72] Improve wording in documentation --- .github/ISSUE_TEMPLATE/bug_report.md | 2 +- .github/ISSUE_TEMPLATE/question.md | 2 +- README.md | 14 +++++++------- docs/backtesting.md | 2 +- docs/faq.md | 2 +- docs/index.md | 2 +- docs/stoploss.md | 2 +- docs/strategy-advanced.md | 4 ++-- docs/strategy-customization.md | 6 +++--- docs/webhook-config.md | 2 +- 10 files changed, 19 insertions(+), 19 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index dacd9e673..54c9eab50 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -9,7 +9,7 @@ assignees: ''