From 85979c317679a58172fd15c2432d3c94cb1d9c4f Mon Sep 17 00:00:00 2001 From: Cryptomeister Nox Date: Thu, 17 Jun 2021 20:35:02 +0200 Subject: [PATCH 001/208] * 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 002/208] 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 003/208] 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 004/208] 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 005/208] 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 006/208] 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 007/208] 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 ca20e17d404c5aab902e765504aaa59aa42bc4a6 Mon Sep 17 00:00:00 2001 From: Robert Roman Date: Thu, 23 Sep 2021 21:48:08 -0500 Subject: [PATCH 009/208] 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 010/208] 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 011/208] 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 012/208] 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 013/208] 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 014/208] 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 015/208] 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 016/208] 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 017/208] 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 b898f86364787b3c1ba0686281a96254eb213579 Mon Sep 17 00:00:00 2001 From: theluxaz Date: Wed, 13 Oct 2021 00:02:28 +0300 Subject: [PATCH 018/208] Added sell_tag and buy/sell telegram performance functions --- freqtrade/data/btanalysis.py | 2 +- freqtrade/enums/signaltype.py | 1 + freqtrade/freqtradebot.py | 16 +- freqtrade/optimize/backtesting.py | 13 +- freqtrade/optimize/optimize_reports.py | 138 +- freqtrade/persistence/migrations.py | 6 +- freqtrade/persistence/models.py | 121 +- freqtrade/rpc/rpc.py | 31 + freqtrade/rpc/telegram.py | 152 +++ freqtrade/strategy/interface.py | 8 +- .../hyperopts/RuleNOTANDoptimizer.py | 1203 +++++++++++++++++ 11 files changed, 1673 insertions(+), 18 deletions(-) create mode 100644 freqtrade/user_data/hyperopts/RuleNOTANDoptimizer.py diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 7d97661c4..82b2bb3a9 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -30,7 +30,7 @@ BT_DATA_COLUMNS = ['pair', 'stake_amount', 'amount', 'open_date', 'close_date', 'fee_open', 'fee_close', 'trade_duration', 'profit_ratio', 'profit_abs', 'sell_reason', 'initial_stop_loss_abs', 'initial_stop_loss_ratio', 'stop_loss_abs', - 'stop_loss_ratio', 'min_rate', 'max_rate', 'is_open', 'buy_tag'] + 'stop_loss_ratio', 'min_rate', 'max_rate', 'is_open', 'buy_tag', 'sell_tag'] def get_latest_optimize_filename(directory: Union[Path, str], variant: str) -> str: diff --git a/freqtrade/enums/signaltype.py b/freqtrade/enums/signaltype.py index d2995d57a..32ac19ba4 100644 --- a/freqtrade/enums/signaltype.py +++ b/freqtrade/enums/signaltype.py @@ -14,3 +14,4 @@ class SignalTagType(Enum): Enum for signal columns """ BUY_TAG = "buy_tag" + SELL_TAG = "sell_tag" \ No newline at end of file diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 259270483..55828f763 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -420,7 +420,7 @@ class FreqtradeBot(LoggingMixin): return False # running get_signal on historical data fetched - (buy, sell, buy_tag) = self.strategy.get_signal( + (buy, sell, buy_tag,sell_tag) = self.strategy.get_signal( pair, self.strategy.timeframe, analyzed_df @@ -706,7 +706,7 @@ class FreqtradeBot(LoggingMixin): analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair, self.strategy.timeframe) - (buy, sell, _) = self.strategy.get_signal( + (buy, sell, buy_tag, sell_tag) = self.strategy.get_signal( trade.pair, self.strategy.timeframe, analyzed_df @@ -714,7 +714,7 @@ class FreqtradeBot(LoggingMixin): logger.debug('checking sell') sell_rate = self.exchange.get_rate(trade.pair, refresh=True, side="sell") - if self._check_and_execute_sell(trade, sell_rate, buy, sell): + if self._check_and_execute_sell(trade, sell_rate, buy, sell, sell_tag): return True logger.debug('Found no sell signal for %s.', trade) @@ -852,18 +852,19 @@ class FreqtradeBot(LoggingMixin): f"for pair {trade.pair}.") def _check_and_execute_sell(self, trade: Trade, sell_rate: float, - buy: bool, sell: bool) -> bool: + buy: bool, sell: bool, sell_tag: Optional[str]) -> bool: """ Check and execute sell """ + print(str(sell_tag)+"1") should_sell = self.strategy.should_sell( trade, sell_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, sell_rate, should_sell) + logger.info(f'Executing Sell for {trade.pair}. Reason: {should_sell.sell_type}. Tag: {sell_tag}') + self.execute_trade_exit(trade, sell_rate, should_sell,sell_tag) return True return False @@ -1064,7 +1065,7 @@ class FreqtradeBot(LoggingMixin): 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: + def execute_trade_exit(self, trade: Trade, limit: float, sell_reason: SellCheckTuple, sell_tag: Optional[str] = None) -> bool: """ Executes a trade exit for the given trade and limit :param trade: Trade instance @@ -1141,6 +1142,7 @@ class FreqtradeBot(LoggingMixin): trade.sell_order_status = '' trade.close_rate_requested = limit trade.sell_reason = sell_reason.sell_reason + trade.sell_tag = sell_tag # 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) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index eecc7af54..3bed3c540 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -44,7 +44,7 @@ SELL_IDX = 4 LOW_IDX = 5 HIGH_IDX = 6 BUY_TAG_IDX = 7 - +SELL_TAG_IDX = 8 class Backtesting: """ @@ -218,7 +218,7 @@ class Backtesting: """ # Every change to this headers list must evaluate further usages of the resulting tuple # and eventually change the constants for indexes at the top - headers = ['date', 'buy', 'open', 'close', 'sell', 'low', 'high', 'buy_tag'] + headers = ['date', 'buy', 'open', 'close', 'sell', 'low', 'high', 'buy_tag', 'sell_tag'] data: Dict = {} self.progress.init_step(BacktestState.CONVERT, len(processed)) @@ -230,6 +230,7 @@ class Backtesting: pair_data.loc[:, 'buy'] = 0 # cleanup if buy_signal is exist pair_data.loc[:, 'sell'] = 0 # cleanup if sell_signal is exist pair_data.loc[:, 'buy_tag'] = None # cleanup if buy_tag is exist + pair_data.loc[:, 'sell_tag'] = None # cleanup if sell_tag is exist df_analyzed = self.strategy.advise_sell( self.strategy.advise_buy(pair_data, {'pair': pair}), {'pair': pair}).copy() @@ -241,6 +242,7 @@ class Backtesting: df_analyzed.loc[:, 'buy'] = df_analyzed.loc[:, 'buy'].shift(1) df_analyzed.loc[:, 'sell'] = df_analyzed.loc[:, 'sell'].shift(1) df_analyzed.loc[:, 'buy_tag'] = df_analyzed.loc[:, 'buy_tag'].shift(1) + df_analyzed.loc[:, 'sell_tag'] = df_analyzed.loc[:, 'sell_tag'].shift(1) # Update dataprovider cache self.dataprovider._set_cached_df(pair, self.timeframe, df_analyzed) @@ -319,6 +321,9 @@ class Backtesting: return sell_row[OPEN_IDX] def _get_sell_trade_entry(self, trade: LocalTrade, sell_row: Tuple) -> Optional[LocalTrade]: + + + sell_candle_time = sell_row[DATE_IDX].to_pydatetime() sell = self.strategy.should_sell(trade, sell_row[OPEN_IDX], # type: ignore sell_candle_time, sell_row[BUY_IDX], @@ -327,6 +332,8 @@ class Backtesting: if sell.sell_flag: trade.close_date = sell_candle_time + if(sell_row[SELL_TAG_IDX] is not None): + trade.sell_tag = sell_row[SELL_TAG_IDX] trade.sell_reason = sell.sell_reason trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60) closerate = self._get_close_rate(sell_row, trade, sell, trade_dur) @@ -375,6 +382,7 @@ class Backtesting: if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount): # Enter trade has_buy_tag = len(row) >= BUY_TAG_IDX + 1 + has_sell_tag = len(row) >= SELL_TAG_IDX + 1 trade = LocalTrade( pair=pair, open_rate=row[OPEN_IDX], @@ -385,6 +393,7 @@ class Backtesting: fee_close=self.fee, is_open=True, buy_tag=row[BUY_TAG_IDX] if has_buy_tag else None, + sell_tag=row[SELL_TAG_IDX] if has_sell_tag else None, exchange='backtesting', ) return trade diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 7bb60228a..fcead07ba 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -82,7 +82,7 @@ def _generate_result_line(result: DataFrame, starting_balance: int, first_column 'profit_sum_pct': round(profit_sum * 100.0, 2), 'profit_total_abs': result['profit_abs'].sum(), 'profit_total': profit_total, - 'profit_total_pct': round(profit_total * 100.0, 2), + 'profit_total_pct': round(profit_sum * 100.0, 2), 'duration_avg': str(timedelta( minutes=round(result['trade_duration'].mean())) ) if not result.empty else '0:00', @@ -126,6 +126,92 @@ def generate_pair_metrics(data: Dict[str, Dict], stake_currency: str, starting_b tabular_data.append(_generate_result_line(results, starting_balance, 'TOTAL')) return tabular_data +def generate_tag_metrics(tag_type:str, data: Dict[str, Dict], stake_currency: str, starting_balance: int, + results: DataFrame, skip_nan: bool = False) -> List[Dict]: + """ + Generates and returns a list of metrics for the given tag trades and the results dataframe + :param data: Dict of containing data that was used during backtesting. + :param stake_currency: stake-currency - used to correctly name headers + :param starting_balance: Starting balance + :param results: Dataframe containing the backtest results + :param skip_nan: Print "left open" open trades + :return: List of Dicts containing the metrics per pair + """ + + tabular_data = [] + + # for tag, count in results[tag_type].value_counts().iteritems(): + # result = results.loc[results[tag_type] == tag] + # + # profit_mean = result['profit_ratio'].mean() + # profit_sum = result['profit_ratio'].sum() + # profit_total = profit_sum / max_open_trades + # + # tabular_data.append( + # { + # 'sell_reason': tag, + # 'trades': count, + # 'wins': len(result[result['profit_abs'] > 0]), + # 'draws': len(result[result['profit_abs'] == 0]), + # 'losses': len(result[result['profit_abs'] < 0]), + # 'profit_mean': profit_mean, + # 'profit_mean_pct': round(profit_mean * 100, 2), + # 'profit_sum': profit_sum, + # 'profit_sum_pct': round(profit_sum * 100, 2), + # 'profit_total_abs': result['profit_abs'].sum(), + # 'profit_total': profit_total, + # 'profit_total_pct': round(profit_total * 100, 2), + # } + # ) + # + # tabular_data = [] + + for tag, count in results[tag_type].value_counts().iteritems(): + result = results[results[tag_type] == tag] + if skip_nan and result['profit_abs'].isnull().all(): + continue + + tabular_data.append(_generate_tag_result_line(result, starting_balance, tag)) + + # Sort by total profit %: + tabular_data = sorted(tabular_data, key=lambda k: k['profit_total_abs'], reverse=True) + + # Append Total + tabular_data.append(_generate_result_line(results, starting_balance, 'TOTAL')) + return tabular_data + +def _generate_tag_result_line(result: DataFrame, starting_balance: int, first_column: str) -> Dict: + """ + Generate one result dict, with "first_column" as key. + """ + profit_sum = result['profit_ratio'].sum() + # (end-capital - starting capital) / starting capital + profit_total = result['profit_abs'].sum() / starting_balance + + return { + 'key': first_column, + 'trades': len(result), + 'profit_mean': result['profit_ratio'].mean() if len(result) > 0 else 0.0, + 'profit_mean_pct': result['profit_ratio'].mean() * 100.0 if len(result) > 0 else 0.0, + 'profit_sum': profit_sum, + 'profit_sum_pct': round(profit_sum * 100.0, 2), + 'profit_total_abs': result['profit_abs'].sum(), + 'profit_total': profit_total, + 'profit_total_pct': round(profit_total * 100.0, 2), + 'duration_avg': str(timedelta( + minutes=round(result['trade_duration'].mean())) + ) if not result.empty else '0:00', + # 'duration_max': str(timedelta( + # minutes=round(result['trade_duration'].max())) + # ) if not result.empty else '0:00', + # 'duration_min': str(timedelta( + # minutes=round(result['trade_duration'].min())) + # ) if not result.empty else '0:00', + 'wins': len(result[result['profit_abs'] > 0]), + 'draws': len(result[result['profit_abs'] == 0]), + 'losses': len(result[result['profit_abs'] < 0]), + } + def generate_sell_reason_stats(max_open_trades: int, results: DataFrame) -> List[Dict]: """ @@ -313,6 +399,13 @@ def generate_strategy_stats(btdata: Dict[str, DataFrame], pair_results = generate_pair_metrics(btdata, stake_currency=stake_currency, starting_balance=starting_balance, results=results, skip_nan=False) + buy_tag_results = generate_tag_metrics("buy_tag",btdata, stake_currency=stake_currency, + starting_balance=starting_balance, + results=results, skip_nan=False) + sell_tag_results = generate_tag_metrics("sell_tag",btdata, stake_currency=stake_currency, + starting_balance=starting_balance, + results=results, skip_nan=False) + sell_reason_stats = generate_sell_reason_stats(max_open_trades=max_open_trades, results=results) left_open_results = generate_pair_metrics(btdata, stake_currency=stake_currency, @@ -336,6 +429,8 @@ def generate_strategy_stats(btdata: Dict[str, DataFrame], 'best_pair': best_pair, 'worst_pair': worst_pair, 'results_per_pair': pair_results, + 'results_per_buy_tag': buy_tag_results, + 'results_per_sell_tag': sell_tag_results, 'sell_reason_summary': sell_reason_stats, 'left_open_trades': left_open_results, 'total_trades': len(results), @@ -504,6 +599,27 @@ def text_table_sell_reason(sell_reason_stats: List[Dict[str, Any]], stake_curren ] for t in sell_reason_stats] return tabulate(output, headers=headers, tablefmt="orgtbl", stralign="right") +def text_table_tags(tag_type:str, tag_results: List[Dict[str, Any]], stake_currency: str) -> str: + """ + Generates and returns a text table for the given backtest data and the results dataframe + :param pair_results: List of Dictionaries - one entry per pair + final TOTAL row + :param stake_currency: stake-currency - used to correctly name headers + :return: pretty printed table with tabulate as string + """ + + headers = _get_line_header("TAG", stake_currency) + floatfmt = _get_line_floatfmt(stake_currency) + output = [[ + t['key'], t['trades'], t['profit_mean_pct'], t['profit_sum_pct'], t['profit_total_abs'], + t['profit_total_pct'], t['duration_avg'], + _generate_wins_draws_losses(t['wins'], t['draws'], t['losses']) + ] for t in tag_results] + # Ignore type as floatfmt does allow tuples but mypy does not know that + return tabulate(output, headers=headers, + floatfmt=floatfmt, tablefmt="orgtbl", stralign="right") + + + def text_table_strategy(strategy_results, stake_currency: str) -> str: """ @@ -624,12 +740,24 @@ def show_backtest_result(strategy: str, results: Dict[str, Any], stake_currency: print(' BACKTESTING REPORT '.center(len(table.splitlines()[0]), '=')) print(table) + + table = text_table_tags("buy_tag", results['results_per_buy_tag'], stake_currency=stake_currency) + + if isinstance(table, str) and len(table) > 0: + print(' BUY TAG STATS '.center(len(table.splitlines()[0]), '=')) + print(table) + + table = text_table_sell_reason(sell_reason_stats=results['sell_reason_summary'], stake_currency=stake_currency) if isinstance(table, str) and len(table) > 0: print(' SELL REASON STATS '.center(len(table.splitlines()[0]), '=')) print(table) + + + + table = text_table_bt_results(results['left_open_trades'], stake_currency=stake_currency) if isinstance(table, str) and len(table) > 0: print(' LEFT OPEN TRADES REPORT '.center(len(table.splitlines()[0]), '=')) @@ -640,8 +768,16 @@ def show_backtest_result(strategy: str, results: Dict[str, Any], stake_currency: print(' SUMMARY METRICS '.center(len(table.splitlines()[0]), '=')) print(table) + table = text_table_tags("sell_tag",results['results_per_sell_tag'], stake_currency=stake_currency) + + if isinstance(table, str) and len(table) > 0: + print(' SELL TAG STATS '.center(len(table.splitlines()[0]), '=')) + print(table) + if isinstance(table, str) and len(table) > 0: print('=' * len(table.splitlines()[0])) + + print() diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index 1839c4130..db93cf8b0 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -82,7 +82,7 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col stake_amount, amount, amount_requested, open_date, close_date, open_order_id, stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct, stoploss_order_id, stoploss_last_update, - max_rate, min_rate, sell_reason, sell_order_status, strategy, buy_tag, + max_rate, min_rate, sell_reason, sell_order_status, strategy, buy_tag, sell_tag, timeframe, open_trade_value, close_profit_abs ) select id, lower(exchange), pair, @@ -98,7 +98,7 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col {stoploss_order_id} stoploss_order_id, {stoploss_last_update} stoploss_last_update, {max_rate} max_rate, {min_rate} min_rate, {sell_reason} sell_reason, {sell_order_status} sell_order_status, - {strategy} strategy, {buy_tag} buy_tag, {timeframe} timeframe, + {strategy} strategy, {buy_tag} buy_tag, {sell_tag} sell_tag, {timeframe} timeframe, {open_trade_value} open_trade_value, {close_profit_abs} close_profit_abs from {table_back_name} """)) @@ -157,7 +157,7 @@ def check_migrate(engine, decl_base, previous_tables) -> None: table_back_name = get_backup_name(tabs, 'trades_bak') # Check for latest column - if not has_column(cols, 'buy_tag'): + if not has_column(cols, 'sell_tag'): logger.info(f'Running database migration for trades - backup: {table_back_name}') migrate_trades_table(decl_base, inspector, engine, table_back_name, cols) # Reread columns - the above recreated the table! diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 8c8c1e0a9..b06386810 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -258,6 +258,7 @@ class LocalTrade(): sell_order_status: str = '' strategy: str = '' buy_tag: Optional[str] = None + sell_tag: Optional[str] = None timeframe: Optional[int] = None def __init__(self, **kwargs): @@ -324,7 +325,8 @@ class LocalTrade(): 'profit_pct': round(self.close_profit * 100, 2) if self.close_profit else None, 'profit_abs': self.close_profit_abs, - 'sell_reason': self.sell_reason, + 'sell_reason': (f' ({self.sell_reason})' if self.sell_reason else ''), #+str(self.sell_reason) ## CHANGE TO BUY TAG IF NEEDED + 'sell_tag': self.sell_tag, 'sell_order_status': self.sell_order_status, 'stop_loss_abs': self.stop_loss, 'stop_loss_ratio': self.stop_loss_pct if self.stop_loss_pct else None, @@ -706,6 +708,7 @@ class Trade(_DECL_BASE, LocalTrade): sell_order_status = Column(String(100), nullable=True) strategy = Column(String(100), nullable=True) buy_tag = Column(String(100), nullable=True) + sell_tag = Column(String(100), nullable=True) timeframe = Column(Integer, nullable=True) def __init__(self, **kwargs): @@ -856,6 +859,122 @@ class Trade(_DECL_BASE, LocalTrade): for pair, profit, profit_abs, count in pair_rates ] + @staticmethod + def get_buy_tag_performance(pair: str) -> List[Dict[str, Any]]: + """ + Returns List of dicts containing all Trades, based on buy tag performance + Can either be average for all pairs or a specific pair provided + NOTE: Not supported in Backtesting. + """ + + if(pair is not None): + tag_perf = Trade.query.with_entities( + Trade.buy_tag, + func.sum(Trade.close_profit).label('profit_sum'), + func.sum(Trade.close_profit_abs).label('profit_sum_abs'), + func.count(Trade.pair).label('count') + ).filter(Trade.is_open.is_(False))\ + .filter(Trade.pair.lower() == pair.lower()) \ + .order_by(desc('profit_sum_abs')) \ + .all() + else: + tag_perf = Trade.query.with_entities( + Trade.buy_tag, + func.sum(Trade.close_profit).label('profit_sum'), + func.sum(Trade.close_profit_abs).label('profit_sum_abs'), + func.count(Trade.pair).label('count') + ).filter(Trade.is_open.is_(False))\ + .group_by(Trade.pair) \ + .order_by(desc('profit_sum_abs')) \ + .all() + + return [ + { + 'buy_tag': buy_tag, + 'profit': profit, + 'profit_abs': profit_abs, + 'count': count + } + for buy_tag, profit, profit_abs, count in tag_perf + ] + + @staticmethod + def get_sell_tag_performance(pair: str) -> List[Dict[str, Any]]: + """ + Returns List of dicts containing all Trades, based on sell tag performance + Can either be average for all pairs or a specific pair provided + NOTE: Not supported in Backtesting. + """ + if(pair is not None): + tag_perf = Trade.query.with_entities( + Trade.sell_tag, + func.sum(Trade.close_profit).label('profit_sum'), + func.sum(Trade.close_profit_abs).label('profit_sum_abs'), + func.count(Trade.pair).label('count') + ).filter(Trade.is_open.is_(False))\ + .filter(Trade.pair.lower() == pair.lower()) \ + .order_by(desc('profit_sum_abs')) \ + .all() + else: + tag_perf = Trade.query.with_entities( + Trade.sell_tag, + func.sum(Trade.close_profit).label('profit_sum'), + func.sum(Trade.close_profit_abs).label('profit_sum_abs'), + func.count(Trade.pair).label('count') + ).filter(Trade.is_open.is_(False))\ + .group_by(Trade.pair) \ + .order_by(desc('profit_sum_abs')) \ + .all() + + return [ + { + 'sell_tag': sell_tag, + 'profit': profit, + 'profit_abs': profit_abs, + 'count': count + } + for sell_tag, profit, profit_abs, count in tag_perf + ] + + @staticmethod + def get_mix_tag_performance(pair: str) -> List[Dict[str, Any]]: + """ + Returns List of dicts containing all Trades, based on buy_tag + sell_tag performance + Can either be average for all pairs or a specific pair provided + NOTE: Not supported in Backtesting. + """ + if(pair is not None): + tag_perf = Trade.query.with_entities( + Trade.buy_tag, + Trade.sell_tag, + func.sum(Trade.close_profit).label('profit_sum'), + func.sum(Trade.close_profit_abs).label('profit_sum_abs'), + func.count(Trade.pair).label('count') + ).filter(Trade.is_open.is_(False))\ + .filter(Trade.pair.lower() == pair.lower()) \ + .order_by(desc('profit_sum_abs')) \ + .all() + else: + tag_perf = Trade.query.with_entities( + Trade.buy_tag, + Trade.sell_tag, + func.sum(Trade.close_profit).label('profit_sum'), + func.sum(Trade.close_profit_abs).label('profit_sum_abs'), + func.count(Trade.pair).label('count') + ).filter(Trade.is_open.is_(False))\ + .group_by(Trade.pair) \ + .order_by(desc('profit_sum_abs')) \ + .all() + + return [ + { 'mix_tag': str(buy_tag) + " " +str(sell_tag), + 'profit': profit, + 'profit_abs': profit_abs, + 'count': count + } + for buy_tag, sell_tag, profit, profit_abs, count in tag_perf + ] + @staticmethod def get_best_pair(start_date: datetime = datetime.fromtimestamp(0)): """ diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 95a37452b..a53ce2150 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -669,6 +669,37 @@ class RPC: [x.update({'profit': round(x['profit'] * 100, 2)}) for x in pair_rates] return pair_rates + def _rpc_buy_tag_performance(self, pair: str) -> List[Dict[str, Any]]: + """ + Handler for buy tag performance. + Shows a performance statistic from finished trades + """ + buy_tags = Trade.get_buy_tag_performance(pair) + # Round and convert to % + [x.update({'profit': round(x['profit'] * 100, 2)}) for x in buy_tags] + return buy_tags + + + def _rpc_sell_tag_performance(self, pair: str) -> List[Dict[str, Any]]: + """ + Handler for sell tag performance. + Shows a performance statistic from finished trades + """ + sell_tags = Trade.get_sell_tag_performance(pair) + # Round and convert to % + [x.update({'profit': round(x['profit'] * 100, 2)}) for x in sell_tags] + return sell_tags + + def _rpc_mix_tag_performance(self, pair: str) -> List[Dict[str, Any]]: + """ + Handler for mix tag performance. + Shows a performance statistic from finished trades + """ + mix_tags = Trade.get_mix_tag_performance(pair) + # Round and convert to % + [x.update({'profit': round(x['profit'] * 100, 2)}) for x in mix_tags] + return mix_tags + def _rpc_count(self) -> Dict[str, float]: """ Returns the number of trades running """ if self._freqtrade.state != State.RUNNING: diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index a988d2b60..1834abd64 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -108,6 +108,7 @@ class Telegram(RPCHandler): r'/trades$', r'/performance$', r'/daily$', r'/daily \d+$', r'/profit$', r'/profit \d+', r'/stats$', r'/count$', r'/locks$', r'/balance$', + r'/buys',r'/sells',r'/mix_tags', r'/stopbuy$', r'/reload_config$', r'/show_config$', r'/logs$', r'/whitelist$', r'/blacklist$', r'/edge$', r'/forcebuy$', r'/help$', r'/version$'] @@ -152,6 +153,9 @@ class Telegram(RPCHandler): CommandHandler('trades', self._trades), CommandHandler('delete', self._delete_trade), CommandHandler('performance', self._performance), + CommandHandler('buys', self._buy_tag_performance), + CommandHandler('sells', self._sell_tag_performance), + CommandHandler('mix_tags', self._mix_tag_performance), CommandHandler('stats', self._stats), CommandHandler('daily', self._daily), CommandHandler('count', self._count), @@ -173,6 +177,9 @@ class Telegram(RPCHandler): CallbackQueryHandler(self._profit, pattern='update_profit'), CallbackQueryHandler(self._balance, pattern='update_balance'), CallbackQueryHandler(self._performance, pattern='update_performance'), + CallbackQueryHandler(self._performance, pattern='update_buy_tag_performance'), + CallbackQueryHandler(self._performance, pattern='update_sell_tag_performance'), + CallbackQueryHandler(self._performance, pattern='update_mix_tag_performance'), CallbackQueryHandler(self._count, pattern='update_count'), CallbackQueryHandler(self._forcebuy_inline), ] @@ -258,6 +265,42 @@ class Telegram(RPCHandler): "*Current Rate:* `{current_rate:.8f}`\n" "*Close Rate:* `{limit:.8f}`").format(**msg) + sell_tag = msg['sell_tag'] + buy_tag = msg['buy_tag'] + + if sell_tag is not None and buy_tag is not None: + message = ("{emoji} *{exchange}:* Selling {pair} (#{trade_id})\n" + "*Profit:* `{profit_percent:.2f}%{profit_extra}`\n" + "*Buy Tag:* `{buy_tag}`\n" + "*Sell Tag:* `{sell_tag}`\n" + "*Sell Reason:* `{sell_reason}`\n" + "*Duration:* `{duration} ({duration_min:.1f} min)`\n" + "*Amount:* `{amount:.8f}`\n" + "*Open Rate:* `{open_rate:.8f}`\n" + "*Current Rate:* `{current_rate:.8f}`\n" + "*Close Rate:* `{limit:.8f}`").format(**msg) + elif sell_tag is None and buy_tag is not None: + message = ("{emoji} *{exchange}:* Selling {pair} (#{trade_id})\n" + "*Profit:* `{profit_percent:.2f}%{profit_extra}`\n" + "*Buy Tag:* `{buy_tag}`\n" + "*Sell Reason:* `{sell_reason}`\n" + "*Duration:* `{duration} ({duration_min:.1f} min)`\n" + "*Amount:* `{amount:.8f}`\n" + "*Open Rate:* `{open_rate:.8f}`\n" + "*Current Rate:* `{current_rate:.8f}`\n" + "*Close Rate:* `{limit:.8f}`").format(**msg) + elif sell_tag is not None and buy_tag is None: + message = ("{emoji} *{exchange}:* Selling {pair} (#{trade_id})\n" + "*Profit:* `{profit_percent:.2f}%{profit_extra}`\n" + "*Sell Tag:* `{sell_tag}`\n" + "*Sell Reason:* `{sell_reason}`\n" + "*Duration:* `{duration} ({duration_min:.1f} min)`\n" + "*Amount:* `{amount:.8f}`\n" + "*Open Rate:* `{open_rate:.8f}`\n" + "*Current Rate:* `{current_rate:.8f}`\n" + "*Close Rate:* `{limit:.8f}`").format(**msg) + + return message def send_msg(self, msg: Dict[str, Any]) -> None: @@ -364,6 +407,7 @@ class Telegram(RPCHandler): "*Current Pair:* {pair}", "*Amount:* `{amount} ({stake_amount} {base_currency})`", "*Buy Tag:* `{buy_tag}`" if r['buy_tag'] else "", + "*Sell Tag:* `{sell_tag}`" if r['sell_tag'] else "", "*Open Rate:* `{open_rate:.8f}`", "*Close Rate:* `{close_rate}`" if r['close_rate'] else "", "*Current Rate:* `{current_rate:.8f}`", @@ -845,6 +889,111 @@ class Telegram(RPCHandler): except RPCException as e: self._send_msg(str(e)) + @authorized_only + def _buy_tag_performance(self, update: Update, context: CallbackContext) -> None: + """ + Handler for /buys PAIR . + Shows a performance statistic from finished trades + :param bot: telegram bot + :param update: message update + :return: None + """ + try: + pair=None + if context.args: + pair = context.args[0] + + trades = self._rpc._rpc_buy_tag_performance(pair) + output = "Performance:\n" + for i, trade in enumerate(trades): + stat_line = ( + f"{i+1}.\t {trade['buy_tag']}\t" + f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} " + f"({trade['profit']:.2f}%) " + f"({trade['count']})\n") + + if len(output + stat_line) >= MAX_TELEGRAM_MESSAGE_LENGTH: + self._send_msg(output, parse_mode=ParseMode.HTML) + output = stat_line + else: + output += stat_line + + self._send_msg(output, parse_mode=ParseMode.HTML, + reload_able=True, callback_path="update_buy_tag_performance", + query=update.callback_query) + except RPCException as e: + self._send_msg(str(e)) + + @authorized_only + def _sell_tag_performance(self, update: Update, context: CallbackContext) -> None: + """ + Handler for /sells. + Shows a performance statistic from finished trades + :param bot: telegram bot + :param update: message update + :return: None + """ + try: + pair=None + if context.args: + pair = context.args[0] + + trades = self._rpc._rpc_sell_tag_performance(pair) + output = "Performance:\n" + for i, trade in enumerate(trades): + stat_line = ( + f"{i+1}.\t {trade['sell_tag']}\t" + f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} " + f"({trade['profit']:.2f}%) " + f"({trade['count']})\n") + + if len(output + stat_line) >= MAX_TELEGRAM_MESSAGE_LENGTH: + self._send_msg(output, parse_mode=ParseMode.HTML) + output = stat_line + else: + output += stat_line + + self._send_msg(output, parse_mode=ParseMode.HTML, + reload_able=True, callback_path="update_sell_tag_performance", + query=update.callback_query) + except RPCException as e: + self._send_msg(str(e)) + + @authorized_only + def _mix_tag_performance(self, update: Update, context: CallbackContext) -> None: + """ + Handler for /mix_tags. + Shows a performance statistic from finished trades + :param bot: telegram bot + :param update: message update + :return: None + """ + try: + pair=None + if context.args: + pair = context.args[0] + + trades = self._rpc._rpc_mix_tag_performance(pair) + output = "Performance:\n" + for i, trade in enumerate(trades): + stat_line = ( + f"{i+1}.\t {trade['mix_tag']}\t" + f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} " + f"({trade['profit']:.2f}%) " + f"({trade['count']})\n") + + if len(output + stat_line) >= MAX_TELEGRAM_MESSAGE_LENGTH: + self._send_msg(output, parse_mode=ParseMode.HTML) + output = stat_line + else: + output += stat_line + + self._send_msg(output, parse_mode=ParseMode.HTML, + reload_able=True, callback_path="update_mix_tag_performance", + query=update.callback_query) + except RPCException as e: + self._send_msg(str(e)) + @authorized_only def _count(self, update: Update, context: CallbackContext) -> None: """ @@ -1020,6 +1169,9 @@ class Telegram(RPCHandler): " *table :* `will display trades in a table`\n" " `pending buy orders are marked with an asterisk (*)`\n" " `pending sell orders are marked with a double asterisk (**)`\n" + "*/buys :* `Shows the buy_tag performance`\n" + "*/sells :* `Shows the sell reason performance`\n" + "*/mix_tag :* `Shows combined buy tag + sell reason performance`\n" "*/trades [limit]:* `Lists last closed trades (limited to 10 by default)`\n" "*/profit []:* `Lists cumulative profit from all finished trades, " "over the last n days`\n" diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index c51860011..68b65b293 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -460,6 +460,7 @@ class IStrategy(ABC, HyperStrategyMixin): dataframe['buy'] = 0 dataframe['sell'] = 0 dataframe['buy_tag'] = None + dataframe['sell_tag'] = None # Other Defs in strategy that want to be called every loop here # twitter_sell = self.watch_twitter_feed(dataframe, metadata) @@ -537,7 +538,7 @@ class IStrategy(ABC, HyperStrategyMixin): pair: str, timeframe: str, dataframe: DataFrame - ) -> Tuple[bool, bool, Optional[str]]: + ) -> Tuple[bool, bool, Optional[str], Optional[str]]: """ Calculates current signal based based on the buy / sell columns of the dataframe. Used by Bot to get the signal to buy or sell @@ -572,6 +573,7 @@ class IStrategy(ABC, HyperStrategyMixin): sell = latest[SignalType.SELL.value] == 1 buy_tag = latest.get(SignalTagType.BUY_TAG.value, None) + sell_tag = latest.get(SignalTagType.SELL_TAG.value, None) logger.debug('trigger: %s (pair=%s) buy=%s sell=%s', latest['date'], pair, str(buy), str(sell)) @@ -580,8 +582,8 @@ class IStrategy(ABC, HyperStrategyMixin): current_time=datetime.now(timezone.utc), timeframe_seconds=timeframe_seconds, buy=buy): - return False, sell, buy_tag - return buy, sell, buy_tag + return False, sell, buy_tag, sell_tag + return buy, sell, buy_tag, sell_tag def ignore_expired_candle(self, latest_date: datetime, current_time: datetime, timeframe_seconds: int, buy: bool): diff --git a/freqtrade/user_data/hyperopts/RuleNOTANDoptimizer.py b/freqtrade/user_data/hyperopts/RuleNOTANDoptimizer.py new file mode 100644 index 000000000..f720b59ca --- /dev/null +++ b/freqtrade/user_data/hyperopts/RuleNOTANDoptimizer.py @@ -0,0 +1,1203 @@ +# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement +# isort: skip_file +# --- Do not remove these libs --- +from functools import reduce +from typing import Any, Callable, Dict, List + +import numpy as np # noqa +import pandas as pd # noqa +from pandas import DataFrame +from skopt.space import Categorical, Dimension,Integer , Real # noqa +from freqtrade.optimize.space import SKDecimal +from freqtrade.optimize.hyperopt_interface import IHyperOpt + +# -------------------------------- +# Add your lib to import here +import talib.abstract as ta # noqa +import freqtrade.vendor.qtpylib.indicators as qtpylib + +##PYCHARM +import sys +sys.path.append(r"/freqtrade/user_data/strategies") + + +# ##HYPEROPT +# import sys,os +# file_dir = os.path.dirname(__file__) +# sys.path.append(file_dir) + + +from z_buyer_mid_volatility import mid_volatility_buyer +from z_seller_mid_volatility import mid_volatility_seller +from z_COMMON_FUNCTIONS import MID_VOLATILITY + + + + +class RuleOptimizer15min(IHyperOpt): + """ + This is a sample hyperopt to inspire you. + Feel free to customize it. + + More information in the documentation: https://www.freqtrade.io/en/latest/hyperopt/ + + You should: + - Rename the class name to some unique name. + - Add any methods you want to build your hyperopt. + - Add any lib you need to build your hyperopt. + + You must keep: + - The prototypes for the methods: populate_indicators, indicator_space, buy_strategy_generator. + + The methods roi_space, generate_roi_table and stoploss_space are not required + and are provided by default. + However, you may override them if you need the + 'roi' and the 'stoploss' spaces that differ from the defaults offered by Freqtrade. + + This sample illustrates how to override these methods. + """ + + + @staticmethod + def buy_strategy_generator(params: Dict[str, Any]) -> Callable: + """ + Define the buy strategy parameters to be used by hyperopt + """ + def populate_buy_trend(dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Buy strategy Hyperopt will build and use + """ + conditions = [] + + + +#--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +#--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + + + ##MAIN SELECTORS + +#-------------------- + + ##VOLATILITY + + conditions.append(dataframe['vol_mid'] > 0 ) + + # conditions.append((dataframe['vol_low'] > 0) |(dataframe['vol_mid'] > 0) ) + + # conditions.append((dataframe['vol_high'] > 0) |(dataframe['vol_mid'] > 0) ) + + +#-------------------- + + + ##PICKS TREND COMBO + + conditions.append( + + (dataframe['downtrend'] >= params['main_1_trend_strength']) + |#OR & + (dataframe['downtrendsmall'] >= params['main_2_trend_strength']) + + ) + + ##UPTREND + #conditions.append(dataframe['uptrend'] >= params['main_1_trend_strength']) + ##DOWNTREND + #conditions.append(dataframe['downtrend'] >= params['main_1_trend_strength']) + ##NOTREND + #conditions.append((dataframe['uptrend'] <1)&(dataframe['downtrend'] <1)) + +#------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +#------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + + ##ABOVE / BELOW THRESHOLDS + + #RSI ABOVE + if 'include_sell_ab_9_rsi_above_value' in params and params['include_sell_ab_9_rsi_above_value']: + conditions.append(dataframe['rsi'] > params['sell_ab_9_rsi_above_value']) + #RSI RECENT PIT 5 + if 'include_sell_ab_10_rsi_recent_pit_2_value' in params and params['include_sell_ab_10_rsi_recent_pit_2_value']: + conditions.append(dataframe['rsi'].rolling(2).min() < params['sell_ab_10_rsi_recent_pit_2_value']) + #RSI RECENT PIT 12 + if 'include_sell_ab_11_rsi_recent_pit_4_value' in params and params['include_sell_ab_11_rsi_recent_pit_4_value']: + conditions.append(dataframe['rsi'].rolling(4).min() < params['sell_ab_11_rsi_recent_pit_4_value']) + #RSI5 BELOW + if 'include_sell_ab_12_rsi5_above_value' in params and params['include_sell_ab_12_rsi5_above_value']: + conditions.append(dataframe['rsi5'] > params['sell_ab_12_rsi5_above_value']) + #RSI50 BELOW + if 'include_sell_ab_13_rsi50_above_value' in params and params['include_sell_ab_13_rsi50_above_value']: + conditions.append(dataframe['rsi50'] > params['sell_ab_13_rsi50_above_value']) + +#----------------------- + + #ROC BELOW + if 'include_sell_ab_14_roc_above_value' in params and params['include_sell_ab_14_roc_above_value']: + conditions.append(dataframe['roc'] > (params['sell_ab_14_roc_above_value']/2)) + #ROC50 BELOW + if 'include_sell_ab_15_roc50_above_value' in params and params['include_sell_ab_15_roc50_above_value']: + conditions.append(dataframe['roc50'] > (params['sell_ab_15_roc50_above_value'])) + #ROC2 BELOW + if 'include_sell_ab_16_roc2_above_value' in params and params['include_sell_ab_16_roc2_above_value']: + conditions.append(dataframe['roc2'] > (params['sell_ab_16_roc2_above_value']/2)) + +#----------------------- + + #PPO5 BELOW + if 'include_sell_ab_17_ppo5_above_value' in params and params['include_sell_ab_17_ppo5_above_value']: + conditions.append(dataframe['ppo5'] > (params['sell_ab_17_ppo5_above_value']/2)) + #PPO10 BELOW + if 'include_sell_ab_18_ppo10_above_value' in params and params['include_sell_ab_18_ppo10_above_value']: + conditions.append(dataframe['ppo10'] > (params['sell_ab_18_ppo10_above_value']/2)) + #PPO25 BELOW + if 'include_sell_ab_19_ppo25_above_value' in params and params['include_sell_ab_19_ppo25_above_value']: + conditions.append(dataframe['ppo25'] > (params['sell_ab_19_ppo25_above_value']/2)) + + #PPO50 BELOW + if 'include_sell_ab_20_ppo50_above_value' in params and params['include_sell_ab_20_ppo50_above_value']: + conditions.append(dataframe['ppo50'] > (params['sell_ab_20_ppo50_above_value']/2)) + #PPO100 BELOW + if 'include_sell_ab_21_ppo100_above_value' in params and params['include_sell_ab_21_ppo100_above_value']: + conditions.append(dataframe['ppo100'] > (params['sell_ab_21_ppo100_above_value'])) + #PPO200 BELOW + if 'include_sell_ab_22_ppo200_above_value' in params and params['include_sell_ab_22_ppo200_above_value']: + conditions.append(dataframe['ppo200'] > (params['sell_ab_22_ppo200_above_value'])) + #PPO500 BELOW + if 'include_sell_ab_23_ppo500_above_value' in params and params['include_sell_ab_23_ppo500_above_value']: + conditions.append(dataframe['ppo500'] > (params['sell_ab_23_ppo500_above_value']*2)) + + ##USE AT A LATER STEP + + #convsmall BELOW + if 'include_sell_ab_24_convsmall_above_value' in params and params['include_sell_ab_24_convsmall_above_value']: + conditions.append(dataframe['convsmall'] > (params['sell_ab_24_convsmall_above_value']/2)) + #convmedium BELOW + if 'include_sell_ab_25_convmedium_above_value' in params and params['include_sell_ab_25_convmedium_above_value']: + conditions.append(dataframe['convmedium'] >(params['sell_ab_25_convmedium_above_value'])) + #convlarge BELOW + if 'include_sell_ab_26_convlarge_above_value' in params and params['include_sell_ab_26_convlarge_above_value']: + conditions.append(dataframe['convlarge'] > (params['sell_ab_26_convlarge_above_value'])) + #convultra BELOW + if 'include_sell_ab_27_convultra_above_value' in params and params['include_sell_ab_27_convultra_above_value']: + conditions.append(dataframe['convultra'] > (params['sell_ab_27_convultra_above_value']/2)) + #convdist BELOW + if 'include_sell_ab_28_convdist_above_value' in params and params['include_sell_ab_28_convdist_above_value']: + conditions.append(dataframe['convdist'] > (params['sell_ab_28_convdist_above_value'])) + + +#------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +#------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + + ##SMA'S GOING DOWN + + if 'sell_down_0a_sma3' in params and params['sell_down_0a_sma3']: + conditions.append((dataframe['sma3'].shift(1) >dataframe['sma3'])) + if 'sell_down_0b_sma5' in params and params['sell_down_0b_sma5']: + conditions.append((dataframe['sma5'].shift(1) >dataframe['sma5'])) + if 'sell_down_1_sma10' in params and params['sell_down_1_sma10']: + conditions.append((dataframe['sma10'].shift(1) >dataframe['sma10'])) + if 'sell_down_2_sma25' in params and params['sell_down_2_sma25']: + conditions.append((dataframe['sma25'].shift(1) >dataframe['sma25'])) + if 'sell_down_3_sma50' in params and params['sell_down_3_sma50']: + conditions.append((dataframe['sma50'].shift(2) >dataframe['sma50'])) + if 'sell_down_4_sma100' in params and params['sell_down_4_sma100']: + conditions.append((dataframe['sma100'].shift(3) >dataframe['sma100'])) + if 'sell_down_5_sma200' in params and params['sell_down_5_sma200']: + conditions.append((dataframe['sma200'].shift(4) >dataframe['sma200'])) + + if 'sell_down_6_sma400' in params and params['sell_down_6_sma400']: + conditions.append((dataframe['sma400'].shift(4) >dataframe['sma400'])) + if 'sell_down_7_sma10k' in params and params['sell_down_7_sma10k']: + conditions.append((dataframe['sma10k'].shift(5) >dataframe['sma10k'])) + # if 'sell_down_8_sma20k' in params and params['sell_down_8_sma20k']: + # conditions.append((dataframe['sma20k'].shift(5) >dataframe['sma20k'])) + # if 'sell_down_9_sma30k' in params and params['sell_down_9_sma30k']: + # conditions.append((dataframe['sma30k'].shift(5) >dataframe['sma30k'])) + + if 'sell_down_10_convsmall' in params and params['sell_down_10_convsmall']: + conditions.append((dataframe['convsmall'].shift(2) >dataframe['convsmall'])) + if 'sell_down_11_convmedium' in params and params['sell_down_11_convmedium']: + conditions.append((dataframe['convmedium'].shift(3) >dataframe['convmedium'])) + if 'sell_down_12_convlarge' in params and params['sell_down_12_convlarge']: + conditions.append((dataframe['convlarge'].shift(4) >dataframe['convlarge'])) + if 'sell_down_13_convultra' in params and params['sell_down_13_convultra']: + conditions.append((dataframe['convultra'].shift(4) >dataframe['convultra'])) + if 'sell_down_14_convdist' in params and params['sell_down_14_convdist']: + conditions.append((dataframe['convdist'].shift(4) >dataframe['convdist'])) + + if 'sell_down_15_vol50' in params and params['sell_down_15_vol50']: + conditions.append((dataframe['vol50'].shift(2) >dataframe['vol50'])) + if 'sell_down_16_vol100' in params and params['sell_down_16_vol100']: + conditions.append((dataframe['vol100'].shift(3) >dataframe['vol100'])) + if 'sell_down_17_vol175' in params and params['sell_down_17_vol175']: + conditions.append((dataframe['vol175'].shift(4) >dataframe['vol175'])) + if 'sell_down_18_vol250' in params and params['sell_down_18_vol250']: + conditions.append((dataframe['vol250'].shift(4) >dataframe['vol250'])) + if 'sell_down_19_vol500' in params and params['sell_down_19_vol500']: + conditions.append((dataframe['vol500'].shift(4) >dataframe['vol500'])) + + if 'sell_down_20_vol1000' in params and params['sell_down_20_vol1000']: + conditions.append((dataframe['vol1000'].shift(4) >dataframe['vol1000'])) + if 'sell_down_21_vol100mean' in params and params['sell_down_21_vol100mean']: + conditions.append((dataframe['vol100mean'].shift(4) >dataframe['vol100mean'])) + if 'sell_down_22_vol250mean' in params and params['sell_down_22_vol250mean']: + conditions.append((dataframe['vol250mean'].shift(4) >dataframe['vol250mean'])) + + if 'up_20_conv3' in params and params['up_20_conv3']: + conditions.append(((dataframe['conv3'].shift(25) < dataframe['conv3'])&(dataframe['conv3'].shift(50) < dataframe['conv3']))) + if 'up_21_vol5' in params and params['up_21_vol5']: + conditions.append(((dataframe['vol5'].shift(25) < dataframe['vol5'])&(dataframe['vol5'].shift(50) < dataframe['vol5']))) + if 'up_22_vol5ultra' in params and params['up_22_vol5ultra']: + conditions.append(((dataframe['vol5ultra'].shift(25) < dataframe['vol5ultra'])&(dataframe['vol5ultra'].shift(50) < dataframe['vol5ultra']))) + if 'up_23_vol1ultra' in params and params['up_23_vol1ultra']: + conditions.append(((dataframe['vol1ultra'].shift(25) < dataframe['vol1ultra'])& (dataframe['vol1ultra'].shift(50) < dataframe['vol1ultra']))) + if 'up_24_vol1' in params and params['up_24_vol1']: + conditions.append(((dataframe['vol1'].shift(30) < dataframe['vol1'])&(dataframe['vol1'].shift(10) < dataframe['vol1']))) + if 'up_25_vol5inc24' in params and params['up_25_vol5inc24']: + conditions.append((dataframe['vol5inc24'].shift(50) < dataframe['vol5inc24'])) + + +#------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + + ##ABOVE / BELOW SMAS 1 above/ 0 None / -1 below + + #SMA10 + conditions.append((dataframe['close'] > dataframe['sma10'])|(0.5 > params['ab_1_sma10'])) + conditions.append((dataframe['close'] < dataframe['sma10'])|(-0.5 < params['ab_1_sma10'])) + #SMA25 + conditions.append((dataframe['close'] > dataframe['sma25'])|(0.5 > params['ab_2_sma25'])) + conditions.append((dataframe['close'] < dataframe['sma25'])|(-0.5 < params['ab_2_sma25'])) + #SMA50 + conditions.append((dataframe['close'] > dataframe['sma50'])|(0.5 > params['ab_3_sma50'])) + conditions.append((dataframe['close'] < dataframe['sma50'])|(-0.5 < params['ab_3_sma50'])) + + + #SMA100 + conditions.append((dataframe['close'] > dataframe['sma100'])|(0.5 > params['ab_4_sma100'])) + conditions.append((dataframe['close'] < dataframe['sma100'])|(-0.5 < params['ab_4_sma100'])) + #SMA100 + conditions.append((dataframe['close'] > dataframe['sma200'])|(0.5 > params['ab_5_sma200'])) + conditions.append((dataframe['close'] < dataframe['sma200'])|(-0.5 < params['ab_5_sma200'])) + #SMA400 + conditions.append((dataframe['close'] > dataframe['sma400'])|(0.5 > params['ab_6_sma400'])) + conditions.append((dataframe['close'] < dataframe['sma400'])|(-0.5 < params['ab_6_sma400'])) + #SMA10k + conditions.append((dataframe['close'] > dataframe['sma10k'])|(0.5 > params['ab_7_sma10k'])) + conditions.append((dataframe['close'] < dataframe['sma10k'])|(-0.5 < params['ab_7_sma10k'])) + +#------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + + ##DOWNSWINGS / UPSWINGS PPO'S + + #ppo5 UP OR DOWN (1 UP, 0 NOTHING, -1 DOWN) + conditions.append((dataframe['ppo5'].shift(2) params['sell_swings_1_ppo5_up_or_down_bool'])) + conditions.append((dataframe['ppo5'].shift(2) >dataframe['ppo5'])|(-0.5 < params['sell_swings_1_ppo5_up_or_down_bool'])) + #ppo10 + conditions.append((dataframe['ppo10'].shift(3) params['sell_swings_2_ppo10_up_or_down_bool'])) + conditions.append((dataframe['ppo10'].shift(3) >dataframe['ppo10'])|(-0.5 < params['sell_swings_2_ppo10_up_or_down_bool'])) + #ppo25 + #conditions.append((dataframe['ppo25'].shift(3) params['sell_swings_3_ppo25_up_or_down_bool'])) + conditions.append((dataframe['ppo25'].shift(3) >dataframe['ppo25'])|(-0.5 < params['sell_swings_3_ppo25_up_or_down_bool'])) + + #ppo50 + #conditions.append((dataframe['ppo50'].shift(3 params['sell_swings_4_ppo50_up_or_down_bool'])) + conditions.append((dataframe['ppo50'].shift(3) >dataframe['ppo50'])|(-0.5 < params['sell_swings_4_ppo50_up_or_down_bool'])) + #ppo100 + #conditions.append((dataframe['ppo100'].shift(4) params['sell_swings_5_ppo100_up_or_down_bool'])) + conditions.append((dataframe['ppo100'].shift(4) >dataframe['ppo100'])|(-0.5 < params['sell_swings_5_ppo100_up_or_down_bool'])) + #ppo200 + #conditions.append((dataframe['ppo200'].shift(4) params['sell_swings_6_ppo200_up_or_down_bool'])) + conditions.append((dataframe['ppo200'].shift(4) >dataframe['ppo200'])|(-0.5 < params['sell_swings_6_ppo200_up_or_down_bool'])) + + #ppo500 + #conditions.append((dataframe['ppo500'].shift(5) params['sell_swings_7_ppo500_up_or_down_bool'])) + conditions.append((dataframe['ppo500'].shift(5) >dataframe['ppo500'])|(-0.5 < params['sell_swings_7_ppo500_up_or_down_bool'])) + + #roc50 + #conditions.append((dataframe['roc50'].shift(3) params['sell_swings_8_roc50_up_or_down_bool'])) + conditions.append((dataframe['roc50'].shift(3) >dataframe['roc50'])|(-0.5 < params['sell_swings_8_roc50_up_or_down_bool'])) + #roc10 + #conditions.append((dataframe['roc10'].shift(2) params['sell_swings_9_roc10_up_or_down_bool'])) + conditions.append((dataframe['roc10'].shift(2) >dataframe['roc10'])|(-0.5 < params['sell_swings_9_roc10_up_or_down_bool'])) + + +#------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +#------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + + + ##DISTANCES/ROC + + ##FOR MORE TOP SELLERS + #dist50 MORE THAN + if 'include_sell_dist_1_dist50_more_value' in params and params['include_sell_dist_1_dist50_more_value']: + conditions.append(dataframe['dist50'] > (params['sell_dist_1_dist50_more_value'])) + #dist200 MORE THAN + if 'include_sell_dist_2_dist200_more_value' in params and params['include_sell_dist_2_dist200_more_value']: + conditions.append(dataframe['dist200'] > (params['sell_dist_2_dist200_more_value'])) + + #dist400 MORE THAN + if 'include_sell_dist_3_dist400_more_value' in params and params['include_sell_dist_3_dist400_more_value']: + conditions.append(dataframe['dist400'] > (params['sell_dist_3_dist400_more_value'])) + #dist10k MORE THAN + if 'include_sell_dist_4_dist10k_more_value' in params and params['include_sell_dist_4_dist10k_more_value']: + conditions.append(dataframe['dist10k'] > (params['sell_dist_4_dist10k_more_value'])) + + ##FOR MORE TOP SELLERS + #more =further from top bol up + #dist_upbol50 MORE THAN + if 'include_sell_dist_5_dist_upbol50_more_value' in params and params['include_sell_dist_5_dist_upbol50_more_value']: + conditions.append(dataframe['dist_upbol50'] > (params['sell_dist_5_dist_upbol50_more_value']/2)) + #dist_upbol100 MORE THAN + if 'include_sell_dist_6_dist_upbol100_more_value' in params and params['include_sell_dist_6_dist_upbol100_more_value']: + conditions.append(dataframe['dist_upbol100'] > (params['sell_dist_6_dist_upbol100_more_value']/2)) + + + ##for bot bol prevent seller + # #less =closer to bot bol + #dist_upbol50 LESS THAN. + #if 'include_sell_dist_7_dist_lowbol50_more_value' in params and params['include_sell_dist_7_dist_lowbol50_more_value']: + # conditions.append(dataframe['dist_lowbol50'] > (params['sell_dist_7_dist_lowbol50_more_value']/2)) + #dist_upbol100 LESS THAN + # if 'include_sell_dist_8_dist_lowbol100_more_value' in params and params['include_sell_dist_8_dist_lowbol100_more_value']: + # conditions.append(dataframe['dist_lowbol100'] > (params['sell_dist_8_dist_lowbol100_more_value']/2)) + + + + ##others + #roc50sma LESS THAN + if 'include_sell_dist_7_roc50sma_less_value' in params and params['include_sell_dist_7_roc50sma_less_value']: + conditions.append(dataframe['roc50sma'] < (params['sell_dist_7_roc50sma_less_value'])*2) + #roc200sma LESS THAN + if 'include_sell_dist_8_roc200sma_less_value' in params and params['include_sell_dist_8_roc200sma_less_value']: + conditions.append(dataframe['roc200sma'] < (params['sell_dist_8_roc200sma_less_value'])*2) + + ##ENABLE TO BUY AWAY FROM HIGH + # #HIGH500 TO CLOSE MORE THAN + #if 'include_sell_dist_9_high100_more_value' in params and params['include_sell_dist_9_high100_more_value']: + # conditions.append((dataframe['high100']-dataframe['close']) > ((dataframe['high100']/100* (params['sell_dist_9_high100_more_value'])) + +#------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + + + + + + + # Check that volume is not 0 + conditions.append(dataframe['volume'] > 0) + + + + + if conditions: + + + # ##ENABLE PRODUCTION BUYS + # dataframe.loc[ + # (add_production_buys_mid(dataframe)), + # 'buy'] = 1 + # + + + dataframe.loc[ + (~(reduce(lambda x, y: x & y, conditions)))&OPTIMIZED_RULE(dataframe,params), + 'buy'] = 1 + + return dataframe + + return populate_buy_trend + + @staticmethod + def indicator_space() -> List[Dimension]: + """ + Define your Hyperopt space for searching strategy parameters + """ + return [ + + +#------------------------------------------------------------------------------------------------------- + + ## CUSTOM RULE TRESHOLDS + + # SKDecimal(5.0, 7.0,decimals=1, name='sell_trigger_0_roc_ab_value'),# 5 range 5-7 or 4-7 + # SKDecimal(3.2, 4.5,decimals=1, name='sell_trigger_0_roc2_ab_value'),#3.8 range 3.2-4.5 + # Integer(77, 82, name='sell_trigger_0_rsi_ab_value'),#79 range 82-77 + # Integer(90, 95, name='sell_trigger_0_rsi5_ab_value'),#94 range 95-90 + # Integer(63, 67, name='sell_trigger_0_rsi50_ab_value'),#66 range 67-63 + +#------------------------------------------------------------------------------------------------------- + + ##MAIN + + Categorical([1, 2, 3], name='sell_main_1_trend_strength'), #BIG TREND STR + Categorical([1, 2, 3], name='sell_main_2_trend_strength'), #SMALL UPTREND STR + + + #Categorical([-1, 0, 1], name='sell_main_2_small_uptrend_downtrend'), #SMALL UPTREND ON/OFF 1 is on -1 is down + +#------------------------------------------------------------------------------------------------------- +#------------------------------------------------------------------------------------------------------- + + ##INCLUDE/EXCLUDE RULES + + Categorical([True, False], name='include_sell_ab_9_rsi_above_value'), + Categorical([True, False], name='include_sell_ab_10_rsi_recent_pit_2_value'), + Categorical([True, False], name='include_sell_ab_11_rsi_recent_pit_4_value'), + Categorical([True, False], name='include_sell_ab_12_rsi5_above_value'), + Categorical([True, False], name='include_sell_ab_13_rsi50_above_value'), + + Categorical([True, False], name='include_sell_ab_14_roc_above_value'), + Categorical([True, False], name='include_sell_ab_15_roc50_above_value'), + Categorical([True, False], name='include_sell_ab_16_roc2_above_value'), + + Categorical([True, False], name='include_sell_ab_17_ppo5_above_value'), + Categorical([True, False], name='include_sell_ab_18_ppo10_above_value'), + Categorical([True, False], name='include_sell_ab_19_ppo25_above_value'), + + Categorical([True, False], name='include_sell_ab_20_ppo50_above_value'), + Categorical([True, False], name='include_sell_ab_21_ppo100_above_value'), + Categorical([True, False], name='include_sell_ab_22_ppo200_above_value'), + Categorical([True, False], name='include_sell_ab_23_ppo500_above_value'), + + ##USE AT A LATER STEP + Categorical([True, False], name='include_sell_ab_24_convsmall_above_value'), + Categorical([True, False], name='include_sell_ab_25_convmedium_above_value'), + Categorical([True, False], name='include_sell_ab_26_convlarge_above_value'), + Categorical([True, False], name='include_sell_ab_27_convultra_above_value'), + Categorical([True, False], name='include_sell_ab_28_convdist_above_value'), + + Categorical([True, False], name='include_sell_dist_1_dist50_more_value'), + Categorical([True, False], name='include_sell_dist_2_dist200_more_value'), + Categorical([True, False], name='include_sell_dist_3_dist400_more_value'), + Categorical([True, False], name='include_sell_dist_4_dist10k_more_value'), + + Categorical([True, False], name='include_sell_dist_5_dist_upbol50_more_value'), + Categorical([True, False], name='include_sell_dist_6_dist_upbol100_more_value'), + + + # FOR MORE DOWNTREND BUYS LIKELY + # Categorical([True, False], name='include_sell_dist_7_dist_lowbol50_more_value'), + # Categorical([True, False], name='include_sell_dist_8_dist_lowbol100_more_value'), + + #MORE LIKE TRIGGERS + Categorical([True, False], name='include_sell_dist_7_roc50sma_less_value'), + Categorical([True, False], name='include_sell_dist_8_roc200sma_less_value'), + + ##below high 100 + #Categorical([True, False], name='include_sell_dist_9_high100_more_value'), + +#------------------------------------------------------------------------------------------------------- +#------------------------------------------------------------------------------------------------------- + + ##ABOVE/BELOW VALUES + + Integer(35, 82, name='sell_ab_9_rsi_above_value'), + Integer(18, 35, name='sell_ab_10_rsi_recent_pit_2_value'), + Integer(18, 35, name='sell_ab_11_rsi_recent_pit_4_value'), + Integer(70, 91, name='sell_ab_12_rsi5_above_value'), + Integer(37, 60, name='sell_ab_13_rsi50_above_value'), + + Integer(-4, 10, name='sell_ab_14_roc_above_value'),#/2 + Integer(-2, 8, name='sell_ab_15_roc50_above_value'), + Integer(-4, 8, name='sell_ab_16_roc2_above_value'),#/2 + +#-------------------------------- + + ##CHANGE DEPENDING WHAT TYPE OF SELL --> PEAK OR DOWTRENDS + Integer(-4, 6, name='sell_ab_17_ppo5_above_value'),#/2 + Integer(-4, 6, name='sell_ab_18_ppo10_above_value'),#/2 + Integer(-10, 8, name='sell_ab_19_ppo25_above_value'),#/2 + + Integer(-10, 8, name='sell_ab_20_ppo50_above_value'),#/2 + Integer(-6, 6, name='sell_ab_21_ppo100_above_value'), + Integer(-6, 6, name='sell_ab_22_ppo200_above_value'), + Integer(-4, 5, name='sell_ab_23_ppo500_above_value'),#*2 + + # ##USE AT A LATER STEP + # + # Integer(-1, 6, name='sell_ab_24_convsmall_above_value'),#/2 # extreme 12 + # Integer(-1, 4, name='sell_ab_25_convmedium_above_value'),# extreme 6 + # Integer(-1, 7, name='sell_ab_26_convlarge_above_value'),# extreme 12 + # Integer(-1, 8, name='sell_ab_27_convultra_above_value'),#/2# extreme 12 + # + # Integer(-1, 6, name='sell_ab_28_convdist_above_value'), #very extreme not useful 10+ + +#------------------------------------------------------------------------------------------------------- + + #SMA'S GOING DOWN + + Categorical([True, False], name='sell_down_0a_sma3'), + Categorical([True, False], name='sell_down_0b_sma5'), + Categorical([True, False], name='sell_down_1_sma10'), + Categorical([True, False], name='sell_down_2_sma25'), + Categorical([True, False], name='sell_down_3_sma50'), + Categorical([True, False], name='sell_down_4_sma100'), + Categorical([True, False], name='sell_down_5_sma200'), + + Categorical([True, False], name='sell_down_6_sma400'), + Categorical([True, False], name='sell_down_7_sma10k'), + # Categorical([True, False], name='sell_down_8_sma20k'), + # Categorical([True, False], name='sell_down_9_sma30k'), + + Categorical([True, False], name='sell_down_10_convsmall'), + Categorical([True, False], name='sell_down_11_convmedium'), + Categorical([True, False], name='sell_down_12_convlarge'), + Categorical([True, False], name='sell_down_13_convultra'), + Categorical([True, False], name='sell_down_14_convdist'), + + Categorical([True, False], name='sell_down_15_vol50'), + Categorical([True, False], name='sell_down_16_vol100'), + Categorical([True, False], name='sell_down_17_vol175'), + Categorical([True, False], name='sell_down_18_vol250'), + Categorical([True, False], name='sell_down_19_vol500'), + + Categorical([True, False], name='sell_down_20_vol1000'), + Categorical([True, False], name='sell_down_21_vol100mean'), + Categorical([True, False], name='sell_down_22_vol250mean'), + +#------------------------------------------------------------------------------------------------------- + + ##ABOVE/BELOW SMAS + + Categorical([-1, 0, 1], name='sell_ab_1_sma10'), + Categorical([-1, 0, 1], name='sell_ab_2_sma25'), + Categorical([-1, 0, 1], name='sell_ab_3_sma50'), + + Categorical([-1, 0, 1], name='sell_ab_4_sma100'), + Categorical([-1, 0, 1], name='sell_ab_5_sma200'), + Categorical([-1, 0, 1], name='sell_ab_6_sma400'), + Categorical([-1, 0, 1], name='sell_ab_7_sma10k'), + +#------------------------------------------------------------------------------------------------------- + + ##DOWNSWINGS / UPSWINGS PPO'S + + ##UP OR DOWN (1 UP, 0 NOTHING, -1 DOWN) + + Categorical([-1, 0, 1], name='sell_swings_1_ppo5_up_or_down_bool'), + Categorical([-1, 0, 1], name='sell_swings_2_ppo10_up_or_down_bool'), + Categorical([-1, 0], name='sell_swings_3_ppo25_up_or_down_bool'), + + Categorical([-1, 0], name='sell_swings_4_ppo50_up_or_down_bool'), + Categorical([-1, 0], name='sell_swings_5_ppo100_up_or_down_bool'), + Categorical([-1, 0], name='sell_swings_6_ppo200_up_or_down_bool'), + Categorical([-1, 0], name='sell_swings_7_ppo500_up_or_down_bool'), + + Categorical([-1, 0], name='sell_swings_8_roc50_up_or_down_bool'), + Categorical([-1, 0], name='sell_swings_9_roc10_up_or_down_bool'), + +#------------------------------------------------------------------------------------------------------- + + #DISTANCES + + #FOR MORE TOP SELLERS + Integer(-6, 14, name='sell_dist_1_dist50_more_value'), #extreme, useless -4 ,30 + Integer(-8, 20, name='sell_dist_2_dist200_more_value'), #extreme, useless -12-40 + Integer(-15, 30, name='sell_dist_3_dist400_more_value'), + Integer(-15, 35, name='sell_dist_4_dist10k_more_value'), + + #FOR MORE TOP SELLERS + Integer(-30, 25, name='sell_dist_5_dist_upbol50_more_value'),#/2 + Integer(-30, 25, name='sell_dist_6_dist_upbol100_more_value'),#/2 + + + #FOR MORE DOWNTREND BUYS LIKELY + # Integer(-8, 50, name='sell_dist_7_dist_lowbol50_more_value'),#/2 ##set to more, as in higher from lower boll + # Integer(-8, 50, name='sell_dist_8_dist_lowbol100_more_value'),#/2 ##set to more, as in higher from lower boll + + # Integer(-70, 40, name='sell_dist_7_roc50sma_more_value'),#*2 ##fix less more + # Integer(-40, 12, name='sell_dist_8_roc200sma_more_value'),#*2 + + ##below high 100 + #Integer(0, 0, name='sell_dist_9_high100_more_value'), + +#------------------------------------------------------------------------------------------------------- + + + + + ] + + + + @staticmethod + def sell_strategy_generator(params: Dict[str, Any]) -> Callable: + """ + Define the sell strategy parameters to be used by hyperopt + """ + def populate_sell_trend(dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Sell strategy Hyperopt will build and use + """ + # print(params) + conditions = [] + # GUARDS AND TRENDS + + +#--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +#--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + + + ##MAIN SELECTORS + +#-------------------- + + ##VOLATILITY + + conditions.append(dataframe['vol_mid'] > 0 ) + + # conditions.append((dataframe['vol_low'] > 0) |(dataframe['vol_mid'] > 0) ) + + # conditions.append((dataframe['vol_high'] > 0) |(dataframe['vol_mid'] > 0) ) + +#-------------------- + + + ##PICKS TREND COMBO + + conditions.append( + + (dataframe['uptrend'] >= params['main_1_trend_strength']) + |#OR & + (dataframe['uptrendsmall'] >= params['main_2_trend_strength']) + + ) + + ##UPTREND + #conditions.append(dataframe['uptrend'] >= params['main_1_trend_strength']) + ##DOWNTREND + #conditions.append(dataframe['downtrend'] >= params['main_1_trend_strength']) + ##NOTREND + #conditions.append((dataframe['uptrend'] <1)&(dataframe['downtrend'] <1)) + +#------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +#------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + + ##ABOVE/BELOW VALUES + + #RSI BELOW + if 'include_ab_9_rsi_below_value' in params and params['include_ab_9_rsi_below_value']: + conditions.append(dataframe['rsi'] < params['ab_9_rsi_below_value']) + #RSI RECENT PEAK 5 + if 'include_ab_10_rsi_recent_peak_2_value' in params and params['include_ab_10_rsi_recent_peak_2_value']: + conditions.append(dataframe['rsi'].rolling(2).max() < params['ab_10_rsi_recent_peak_2_value']) + + #RSI RECENT PEAK 12 + if 'include_ab_11_rsi_recent_peak_4_value' in params and params['include_ab_11_rsi_recent_peak_4_value']: + conditions.append(dataframe['rsi'].rolling(4).max() < params['ab_11_rsi_recent_peak_4_value']) + #RSI5 BELOW + if 'include_ab_12_rsi5_below_value' in params and params['include_ab_12_rsi5_below_value']: + conditions.append(dataframe['rsi5'] < params['ab_12_rsi5_below_value']) + #RSI50 BELOW + if 'include_ab_13_rsi50_below_value' in params and params['include_ab_13_rsi50_below_value']: + conditions.append(dataframe['rsi50'] < params['ab_13_rsi50_below_value']) + +#----------------------- + + #ROC BELOW + if 'include_ab_14_roc_below_value' in params and params['include_ab_14_roc_below_value']: + conditions.append(dataframe['roc'] < (params['ab_14_roc_below_value']/2)) + #ROC50 BELOW + if 'include_ab_15_roc50_below_value' in params and params['include_ab_15_roc50_below_value']: + conditions.append(dataframe['roc50'] < (params['ab_15_roc50_below_value'])) + #ROC2 BELOW + if 'include_ab_16_roc2_below_value' in params and params['include_ab_16_roc2_below_value']: + conditions.append(dataframe['roc2'] < (params['ab_16_roc2_below_value']/2)) + +#----------------------- + + #PPO5 BELOW + if 'include_ab_17_ppo5_below_value' in params and params['include_ab_17_ppo5_below_value']: + conditions.append(dataframe['ppo5'] < (params['ab_17_ppo5_below_value']/2)) + #PPO10 BELOW + if 'include_ab_18_ppo10_below_value' in params and params['include_ab_18_ppo10_below_value']: + conditions.append(dataframe['ppo10'] < (params['ab_18_ppo10_below_value']/2)) + #PPO25 BELOW + if 'include_ab_19_ppo25_below_value' in params and params['include_ab_19_ppo25_below_value']: + conditions.append(dataframe['ppo25'] < (params['ab_19_ppo25_below_value']/2)) + + #PPO50 BELOW + if 'include_ab_20_ppo50_below_value' in params and params['include_ab_20_ppo50_below_value']: + conditions.append(dataframe['ppo50'] < (params['ab_20_ppo50_below_value']/2)) + #PPO100 BELOW + if 'include_ab_21_ppo100_below_value' in params and params['include_ab_21_ppo100_below_value']: + conditions.append(dataframe['ppo100'] < (params['ab_21_ppo100_below_value'])) + #PPO200 BELOW + if 'include_ab_22_ppo200_below_value' in params and params['include_ab_22_ppo200_below_value']: + conditions.append(dataframe['ppo200'] < (params['ab_22_ppo200_below_value'])) + #PPO500 BELOW + if 'include_ab_23_ppo500_below_value' in params and params['include_ab_23_ppo500_below_value']: + conditions.append(dataframe['ppo500'] < (params['ab_23_ppo500_below_value']*2)) + + ##USE AT A LATER STEP + + #convsmall BELOW + if 'include_ab_24_convsmall_below_value' in params and params['include_ab_24_convsmall_below_value']: + conditions.append(dataframe['convsmall'] < (params['ab_24_convsmall_below_value']/2)) + #convmedium BELOW + if 'include_ab_25_convmedium_below_value' in params and params['include_ab_25_convmedium_below_value']: + conditions.append(dataframe['convmedium'] < (params['ab_25_convmedium_below_value'])) + #convlarge BELOW + if 'include_ab_26_convlarge_below_value' in params and params['include_ab_26_convlarge_below_value']: + conditions.append(dataframe['convlarge'] < (params['ab_26_convlarge_below_value'])) + #convultra BELOW + if 'include_ab_27_convultra_below_value' in params and params['include_ab_27_convultra_below_value']: + conditions.append(dataframe['convultra'] < (params['ab_27_convultra_below_value']/2)) + #convdist BELOW + if 'include_ab_28_convdist_below_value' in params and params['include_ab_28_convdist_below_value']: + conditions.append(dataframe['convdist'] < (params['ab_28_convdist_below_value'])) + +#------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + + + ##SMA'S GOING UP + + if 'up_0a_sma3' in params and params['up_0a_sma3']: + conditions.append((dataframe['sma3'].shift(1) dataframe['sma10'])|(0.5 > params['ab_1_sma10'])) + conditions.append((dataframe['close'] < dataframe['sma10'])|(-0.5 < params['ab_1_sma10'])) + #SMA25 + conditions.append((dataframe['close'] > dataframe['sma25'])|(0.5 > params['ab_2_sma25'])) + conditions.append((dataframe['close'] < dataframe['sma25'])|(-0.5 < params['ab_2_sma25'])) + #SMA50 + conditions.append((dataframe['close'] > dataframe['sma50'])|(0.5 > params['ab_3_sma50'])) + conditions.append((dataframe['close'] < dataframe['sma50'])|(-0.5 < params['ab_3_sma50'])) + + + #SMA100 + conditions.append((dataframe['close'] > dataframe['sma100'])|(0.5 > params['ab_4_sma100'])) + conditions.append((dataframe['close'] < dataframe['sma100'])|(-0.5 < params['ab_4_sma100'])) + #SMA100 + conditions.append((dataframe['close'] > dataframe['sma200'])|(0.5 > params['ab_5_sma200'])) + conditions.append((dataframe['close'] < dataframe['sma200'])|(-0.5 < params['ab_5_sma200'])) + #SMA400 + conditions.append((dataframe['close'] > dataframe['sma400'])|(0.5 > params['ab_6_sma400'])) + conditions.append((dataframe['close'] < dataframe['sma400'])|(-0.5 < params['ab_6_sma400'])) + #SMA10k + conditions.append((dataframe['close'] > dataframe['sma10k'])|(0.5 > params['ab_7_sma10k'])) + conditions.append((dataframe['close'] < dataframe['sma10k'])|(-0.5 < params['ab_7_sma10k'])) + +#------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + + ##DOWNSWINGS / UPSWINGS PPO'S + + #ppo5 UP OR DOWN (1 UP, 0 NOTHING, -1 DOWN) + conditions.append((dataframe['ppo5'].shift(1) params['swings_1_ppo5_up_or_down_bool'])) + conditions.append((dataframe['ppo5'].shift(1) >dataframe['ppo5'])|(-0.5 < params['swings_1_ppo5_up_or_down_bool'])) + #ppo10 + conditions.append((dataframe['ppo10'].shift(1) params['swings_2_ppo10_up_or_down_bool'])) + conditions.append((dataframe['ppo10'].shift(1) >dataframe['ppo10'])|(-0.5 < params['swings_2_ppo10_up_or_down_bool'])) + #ppo25 + conditions.append((dataframe['ppo25'].shift(1) params['swings_3_ppo25_up_or_down_bool'])) + #conditions.append((dataframe['ppo25'].shift(1) >dataframe['ppo25'])|(-0.5 < params['swings_3_ppo25_up_or_down_bool'])) + + #ppo50 + conditions.append((dataframe['ppo50'].shift(2) params['swings_4_ppo50_up_or_down_bool'])) + #conditions.append((dataframe['ppo50'].shift(2) >dataframe['ppo50'])|(-0.5 < params['swings_4_ppo50_up_or_down_bool'])) + #ppo100 + conditions.append((dataframe['ppo100'].shift(3) params['swings_5_ppo100_up_or_down_bool'])) + #conditions.append((dataframe['ppo100'].shift(3) >dataframe['ppo100'])|(-0.5 < params['swings_5_ppo100_up_or_down_bool'])) + #ppo200 + conditions.append((dataframe['ppo200'].shift(4) params['swings_6_ppo200_up_or_down_bool'])) + #conditions.append((dataframe['ppo200'].shift(4) >dataframe['ppo200'])|(-0.5 < params['swings_6_ppo200_up_or_down_bool'])) + #ppo500 + conditions.append((dataframe['ppo500'].shift(5) params['swings_7_ppo500_up_or_down_bool'])) + #conditions.append((dataframe['ppo500'].shift(5) >dataframe['ppo500'])|(-0.5 < params['swings_7_ppo500_up_or_down_bool'])) + + #roc50 + conditions.append((dataframe['roc50'].shift(2) params['swings_8_roc50_up_or_down_bool'])) + #conditions.append((dataframe['roc50'].shift(3) >dataframe['roc50'])|(-0.5 < params['swings_8_roc50_up_or_down_bool'])) + #roc10 + conditions.append((dataframe['roc10'].shift(1) params['swings_9_roc10_up_or_down_bool'])) + #conditions.append((dataframe['roc10'].shift(2) >dataframe['roc10'])|(-0.5 < params['swings_9_roc10_up_or_down_bool'])) + + +#------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + + + ##DISTANCES/ROC + + #dist50 LESS THAN + if 'include_dist_1_dist50_less_value' in params and params['include_dist_1_dist50_less_value']: + conditions.append(dataframe['dist50'] < (params['dist_1_dist50_less_value'])) + #dist200 LESS THAN + if 'include_dist_2_dist200_less_value' in params and params['include_dist_2_dist200_less_value']: + conditions.append(dataframe['dist200'] < (params['dist_2_dist200_less_value'])) + + #dist400 LESS THAN + if 'include_dist_3_dist400_less_value' in params and params['include_dist_3_dist400_less_value']: + conditions.append(dataframe['dist400'] < (params['dist_3_dist400_less_value'])) + #dist10k LESS THAN + if 'include_dist_4_dist10k_less_value' in params and params['include_dist_4_dist10k_less_value']: + conditions.append(dataframe['dist10k'] < (params['dist_4_dist10k_less_value'])) + + #less =further from top bol + #dist_upbol50 LESS THAN + if 'include_dist_5_dist_upbol50_less_value' in params and params['include_dist_5_dist_upbol50_less_value']: + conditions.append(dataframe['dist_upbol50'] < (params['dist_5_dist_upbol50_less_value']/2)) + #dist_upbol100 LESS THAN + if 'include_dist_6_dist_upbol100_less_value' in params and params['include_dist_6_dist_upbol100_less_value']: + conditions.append(dataframe['dist_upbol100'] < (params['dist_6_dist_upbol100_less_value']/2)) + + # #less =closer to bot bol + # #dist_upbol50 LESS THAN + # if 'include_dist_7_dist_lowbol50_less_value' in params and params['include_dist_7_dist_lowbol50_less_value']: + # conditions.append(dataframe['dist_lowbol50'] < (params['dist_7_dist_lowbol50_less_value']/2)) + # #dist_upbol100 LESS THAN + # if 'include_dist_8_dist_lowbol100_less_value' in params and params['include_dist_8_dist_lowbol100_less_value']: + # conditions.append(dataframe['dist_lowbol100'] < (params['dist_8_dist_lowbol100_less_value']/2)) + + + + #others + ##roc50sma MORE THAN + if 'include_dist_7_roc50sma_less_value' in params and params['include_dist_7_roc50sma_less_value']: + conditions.append(dataframe['roc50sma'] < (params['dist_7_roc50sma_less_value']*2)) + #roc200sma MORE THAN + if 'include_dist_8_roc200sma_less_value' in params and params['include_dist_8_roc200sma_less_value']: + conditions.append(dataframe['roc200sma'] < (params['dist_8_roc200sma_less_value']*2)) + + ##ENABLE TO BUY AWAY FROM HIGH + # #HIGH500 TO CLOSE MORE THAN + #if 'include_dist_9_high100_more_value' in params and params['include_dist_9_high100_more_value']: + # conditions.append((dataframe['high100']-dataframe['close']) > ((dataframe['high100']/100* (params['dist_9_high100_more_value'])) + +#------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + + + + + + # Check that volume is not 0 + conditions.append(dataframe['volume'] > 0) + + if conditions: + + + # ##ENABLE SELLS ALWAYS ON OTHER VOLATILITYS + # dataframe.loc[ + # ((dataframe['vol_low'] > 0) |(dataframe['vol_high'] > 0) ), + # 'sell'] = 1 + + + # ##ENABLE PRODUCTION SELLS + # dataframe.loc[ + # (add_production_sells_low(dataframe)), + # 'sell'] = 1 + # + + dataframe.loc[ + (~(reduce(lambda x, y: x & y, conditions)))&OPTIMIZED_RULE(dataframe,params), + 'sell'] = 1 + + return dataframe + + return populate_sell_trend + + @staticmethod + def sell_indicator_space() -> List[Dimension]: + """ + Define your Hyperopt space for searching sell strategy parameters + """ + return [ + + +#------------------------------------------------------------------------------------------------------- + + ## CUSTOM RULE TRESHOLDS + + # SKDecimal(5.0, 7.0,decimals=1, name='sell_trigger_0_roc_ab_value'),# 5 range 5-7 or 4-7 + # SKDecimal(3.2, 4.5,decimals=1, name='sell_trigger_0_roc2_ab_value'),#3.8 range 3.2-4.5 + # Integer(77, 82, name='sell_trigger_0_rsi_ab_value'),#79 range 82-77 + # Integer(90, 95, name='sell_trigger_0_rsi5_ab_value'),#94 range 95-90 + # Integer(63, 67, name='sell_trigger_0_rsi50_ab_value'),#66 range 67-63 + +#------------------------------------------------------------------------------------------------------- + + ##MAIN + + Categorical([1, 2, 3], name='main_1_trend_strength'), #UPTREND STR + Categorical([1, 2, 3], name='main_2_trend_strength'), #SMALL UPTREND STR + + + #Categorical([-1, 0, 1], name='main_2_small_uptrend_downtrend'), #SMALL UPTREND ON/OFF 1 is on -1 is down + +#------------------------------------------------------------------------------------------------------- + + ##INCLUDE/EXCLUDE RULES + + Categorical([True, False], name='include_ab_9_rsi_below_value'), + Categorical([True, False], name='include_ab_10_rsi_recent_peak_2_value'), + Categorical([True, False], name='include_ab_11_rsi_recent_peak_4_value'), + Categorical([True, False], name='include_ab_12_rsi5_below_value'), + Categorical([True, False], name='include_ab_13_rsi50_below_value'), + + Categorical([True, False], name='include_ab_14_roc_below_value'), + Categorical([True, False], name='include_ab_15_roc50_below_value'), + Categorical([True, False], name='include_ab_16_roc2_below_value'), + + Categorical([True, False], name='include_ab_17_ppo5_below_value'), + Categorical([True, False], name='include_ab_18_ppo10_below_value'), + Categorical([True, False], name='include_ab_19_ppo25_below_value'), + + Categorical([True, False], name='include_ab_20_ppo50_below_value'), + Categorical([True, False], name='include_ab_21_ppo100_below_value'), + Categorical([True, False], name='include_ab_22_ppo200_below_value'), + Categorical([True, False], name='include_ab_23_ppo500_below_value'), + + ##USE AT A LATER STEP + Categorical([True, False], name='include_ab_24_convsmall_below_value'), + Categorical([True, False], name='include_ab_25_convmedium_below_value'), + Categorical([True, False], name='include_ab_26_convlarge_below_value'), + Categorical([True, False], name='include_ab_27_convultra_below_value'), + + Categorical([True, False], name='include_ab_28_convdist_below_value'), + + Categorical([True, False], name='include_dist_1_dist50_less_value'), + Categorical([True, False], name='include_dist_2_dist200_less_value'), + Categorical([True, False], name='include_dist_3_dist400_less_value'), + Categorical([True, False], name='include_dist_4_dist10k_less_value'), + + Categorical([True, False], name='include_dist_5_dist_upbol50_less_value'), + Categorical([True, False], name='include_dist_6_dist_upbol100_less_value'), + + + # FOR MORE DOWNTREND BUYS LIKELY + # Categorical([True, False], name='include_dist_7_dist_lowbol50_less_value'), + # Categorical([True, False], name='include_dist_8_dist_lowbol100_less_value'), + + #MORE LIKE TRIGGERS + Categorical([True, False], name='include_dist_7_roc50sma_less_value'), + Categorical([True, False], name='include_dist_8_roc200sma_less_value'), + + ##below high 100 + #Categorical([True, False], name='include_dist_9_high100_more_value'), + + + +#------------------------------------------------------------------------------------------------------- + + ##ABOVE/BELOW VALUES + + Integer(35, 75, name='ab_9_rsi_below_value'), + Integer(60, 82, name='ab_10_rsi_recent_peak_2_value'), + Integer(60, 82, name='ab_11_rsi_recent_peak_4_value'), + Integer(40, 101, name='ab_12_rsi5_below_value'), + Integer(37, 73, name='ab_13_rsi50_below_value'), + + Integer(-6, 10, name='ab_14_roc_below_value'),#/2 + Integer(-8, 8, name='ab_15_roc50_below_value'), + Integer(-4, 6, name='ab_16_roc2_below_value'),#/2 + +#-------------------------------- + + Integer(-4, 4, name='ab_17_ppo5_below_value'),#/2 + Integer(-5, 5, name='ab_18_ppo10_below_value'),#/2 + Integer(-8, 10, name='ab_19_ppo25_below_value'),#/2 + + Integer(-6, 7, name='ab_20_ppo50_below_value'),#/2 + Integer(-6, 7, name='ab_21_ppo100_below_value'), + Integer(-5, 7, name='ab_22_ppo200_below_value'), + Integer(-4, 4, name='ab_23_ppo500_below_value'),#*2 + + ##USE AT A LATER STEP + + Integer(1, 12, name='ab_24_convsmall_below_value'),#/2 #final + Integer(1, 6, name='ab_25_convmedium_below_value'),#final + Integer(1, 15, name='ab_26_convlarge_below_value'), #final + Integer(2, 12, name='ab_27_convultra_below_value'),#/2 #final + + Integer(2, 30, name='ab_28_convdist_below_value'), + +#------------------------------------------------------------------------------------------------------- + + #SMA'S GOING UP + + Categorical([True, False], name='up_0a_sma3'), + Categorical([True, False], name='up_0b_sma5'), + Categorical([True, False], name='up_1_sma10'), + Categorical([True, False], name='up_2_sma25'), + Categorical([True, False], name='up_3_sma50'), + Categorical([True, False], name='up_4_sma100'), + Categorical([True, False], name='up_5_sma200'), + + Categorical([True, False], name='up_6_sma400'), + Categorical([True, False], name='up_7_sma10k'), + # Categorical([True, False], name='up_8_sma20k'), + # Categorical([True, False], name='up_9_sma30k'), + + Categorical([True, False], name='up_10_convsmall'), + Categorical([True, False], name='up_11_convmedium'), + Categorical([True, False], name='up_12_convlarge'), + Categorical([True, False], name='up_13_convultra'), + Categorical([True, False], name='up_14_convdist'), + + Categorical([True, False], name='up_15_vol50'), + Categorical([True, False], name='up_16_vol100'), + Categorical([True, False], name='up_17_vol175'), + Categorical([True, False], name='up_18_vol250'), + Categorical([True, False], name='up_19_vol500'), + + Categorical([True, False], name='up_20_vol1000'), + Categorical([True, False], name='up_21_vol100mean'), + Categorical([True, False], name='up_22_vol250mean'), + +#------------------------------------------------------------------------------------------------------- + + ##ABOVE/BELOW SMAS + + Categorical([-1, 0, 1], name='ab_1_sma10'), + Categorical([-1, 0, 1], name='ab_2_sma25'), + Categorical([-1, 0, 1], name='ab_3_sma50'), + + Categorical([-1, 0, 1], name='ab_4_sma100'), + Categorical([-1, 0, 1], name='ab_5_sma200'), + Categorical([-1, 0, 1], name='ab_6_sma400'), + Categorical([-1, 0, 1], name='ab_7_sma10k'), + +#------------------------------------------------------------------------------------------------------- + + ##DOWNSWINGS / UPSWINGS PPO'S + + ##UP OR DOWN (1 UP, 0 NOTHING, -1 DOWN) + + Categorical([-1, 0, 1], name='swings_1_ppo5_up_or_down_bool'), # -1 down, 1 up , 0 off + Categorical([-1, 0, 1],name='swings_2_ppo10_up_or_down_bool'), + Categorical([-1, 0, 1], name='swings_3_ppo25_up_or_down_bool'), #1 up , 0 off + + Categorical([0, 1], name='swings_4_ppo50_up_or_down_bool'), + Categorical([0, 1], name='swings_5_ppo100_up_or_down_bool'), + Categorical([0, 1], name='swings_6_ppo200_up_or_down_bool'), + Categorical([ 0, 1],name='swings_7_ppo500_up_or_down_bool'), + + Categorical([0, 1], name='swings_8_roc50_up_or_down_bool'), + Categorical([0, 1], name='swings_9_roc10_up_or_down_bool'), + +#------------------------------------------------------------------------------------------------------- + + ##DISTANCES + + Integer(-7, 14, name='dist_1_dist50_less_value'), ##extreme 8-30 + Integer(-8, 25, name='dist_2_dist200_less_value'), ##extreme 12 -40 + Integer(-12, 35, name='dist_3_dist400_less_value'), + Integer(-12, 40, name='dist_4_dist10k_less_value'), + + Integer(-25, 30, name='dist_5_dist_upbol50_less_value'),#/2 + Integer(-25, 30, name='dist_6_dist_upbol100_less_value'),#/2 + + + # FOR MORE DOWNTREND BUYS LIKELY + # Integer(-6, 100, name='dist_7_dist_lowbol50_less_value'),#/2 + # Integer(-6, 100, name='dist_8_dist_lowbol100_less_value'),#/2 + + ##MORE LIKE TRIGGERS + # Integer(-40, 70, name='dist_7_roc50sma_less_value'),#*2 ##pretty extreme + # Integer(-12, 40, name='dist_8_roc200sma_less_value'),#*2 + + ##below high 100 + #Integer(0, 0, name='dist_9_high100_more_value'), + +#------------------------------------------------------------------------------------------------------- + + + + + + ] + + +def OPTIMIZED_RULE(dataframe,params): + return( + + (dataframe['sma100'] < dataframe['close']) + + ) + +def add_production_buys_mid(dataframe): + return( + + MID_VOLATILITY(dataframe) + & + mid_volatility_buyer(dataframe) + ) + +def add_production_sells_mid(dataframe): + return( + + MID_VOLATILITY(dataframe) + & + mid_volatility_seller(dataframe) + ) + + From c9edf3bf4ac0b2ce5749ca994bb9ac97c1599fd3 Mon Sep 17 00:00:00 2001 From: theluxaz Date: Wed, 13 Oct 2021 00:09:30 +0300 Subject: [PATCH 019/208] Updated the gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 16df71194..9e4ce834b 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ config*.json *.sqlite logfile.txt user_data/* +freqtrade/user_data/* !user_data/strategy/sample_strategy.py !user_data/notebooks user_data/notebooks/* From 80b71790bc7612056b81630245384f8a19ef3ad4 Mon Sep 17 00:00:00 2001 From: theluxaz Date: Wed, 13 Oct 2021 01:22:53 +0300 Subject: [PATCH 020/208] Added some bigfixes for sell_tag --- freqtrade/freqtradebot.py | 7 ++++--- freqtrade/optimize/optimize_reports.py | 10 ++++++---- freqtrade/persistence/migrations.py | 1 + freqtrade/persistence/models.py | 2 +- freqtrade/strategy/interface.py | 4 ++-- 5 files changed, 14 insertions(+), 10 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 55828f763..3b973bb8b 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -856,14 +856,14 @@ class FreqtradeBot(LoggingMixin): """ Check and execute sell """ - print(str(sell_tag)+"1") + should_sell = self.strategy.should_sell( trade, sell_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}. Tag: {sell_tag}') + logger.info(f'Executing Sell for {trade.pair}. Reason: {should_sell.sell_type}. Tag: {sell_tag if sell_tag is not None else "None"}') self.execute_trade_exit(trade, sell_rate, should_sell,sell_tag) return True return False @@ -1142,7 +1142,8 @@ class FreqtradeBot(LoggingMixin): trade.sell_order_status = '' trade.close_rate_requested = limit trade.sell_reason = sell_reason.sell_reason - trade.sell_tag = sell_tag + if(sell_tag is not None): + trade.sell_tag = sell_tag # 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) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index fcead07ba..ee7af6844 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -399,6 +399,7 @@ def generate_strategy_stats(btdata: Dict[str, DataFrame], pair_results = generate_pair_metrics(btdata, stake_currency=stake_currency, starting_balance=starting_balance, results=results, skip_nan=False) + buy_tag_results = generate_tag_metrics("buy_tag",btdata, stake_currency=stake_currency, starting_balance=starting_balance, results=results, skip_nan=False) @@ -747,6 +748,11 @@ def show_backtest_result(strategy: str, results: Dict[str, Any], stake_currency: print(' BUY TAG STATS '.center(len(table.splitlines()[0]), '=')) print(table) + table = text_table_tags("sell_tag",results['results_per_sell_tag'], stake_currency=stake_currency) + + if isinstance(table, str) and len(table) > 0: + print(' SELL TAG STATS '.center(len(table.splitlines()[0]), '=')) + print(table) table = text_table_sell_reason(sell_reason_stats=results['sell_reason_summary'], stake_currency=stake_currency) @@ -768,11 +774,7 @@ def show_backtest_result(strategy: str, results: Dict[str, Any], stake_currency: print(' SUMMARY METRICS '.center(len(table.splitlines()[0]), '=')) print(table) - table = text_table_tags("sell_tag",results['results_per_sell_tag'], stake_currency=stake_currency) - if isinstance(table, str) and len(table) > 0: - print(' SELL TAG STATS '.center(len(table.splitlines()[0]), '=')) - print(table) if isinstance(table, str) and len(table) > 0: print('=' * len(table.splitlines()[0])) diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index db93cf8b0..0f07c13b5 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -48,6 +48,7 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col sell_reason = get_column_def(cols, 'sell_reason', 'null') strategy = get_column_def(cols, 'strategy', 'null') buy_tag = get_column_def(cols, 'buy_tag', 'null') + sell_tag = get_column_def(cols, 'sell_tag', 'null') # If ticker-interval existed use that, else null. if has_column(cols, 'ticker_interval'): timeframe = get_column_def(cols, 'timeframe', 'ticker_interval') diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index b06386810..0fdaba5ac 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -326,7 +326,7 @@ class LocalTrade(): 'profit_abs': self.close_profit_abs, 'sell_reason': (f' ({self.sell_reason})' if self.sell_reason else ''), #+str(self.sell_reason) ## CHANGE TO BUY TAG IF NEEDED - 'sell_tag': self.sell_tag, + 'sell_tag': (f' ({self.sell_tag})' if self.sell_tag else '') , 'sell_order_status': self.sell_order_status, 'stop_loss_abs': self.stop_loss, 'stop_loss_ratio': self.stop_loss_pct if self.stop_loss_pct else None, diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 68b65b293..be552282d 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -549,7 +549,7 @@ class IStrategy(ABC, HyperStrategyMixin): """ if not isinstance(dataframe, DataFrame) or dataframe.empty: logger.warning(f'Empty candle (OHLCV) data for pair {pair}') - return False, False, None + return False, False, None, None latest_date = dataframe['date'].max() latest = dataframe.loc[dataframe['date'] == latest_date].iloc[-1] @@ -564,7 +564,7 @@ class IStrategy(ABC, HyperStrategyMixin): 'Outdated history for pair %s. Last tick is %s minutes old', pair, int((arrow.utcnow() - latest_date).total_seconds() // 60) ) - return False, False, None + return False, False, None, None buy = latest[SignalType.BUY.value] == 1 From 02243b1a2ba6bae679f0bc4aff5a832f9a7bbff9 Mon Sep 17 00:00:00 2001 From: theluxaz Date: Wed, 13 Oct 2021 01:34:29 +0300 Subject: [PATCH 021/208] minifix --- freqtrade/freqtradebot.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 3b973bb8b..e1734926c 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -701,6 +701,8 @@ class FreqtradeBot(LoggingMixin): (buy, sell) = (False, False) + sell_tag=None + 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, From af74850e794997ad58ffcaf4bb78b20f754a2ed1 Mon Sep 17 00:00:00 2001 From: theluxaz <37055144+theluxaz@users.noreply.github.com> Date: Wed, 13 Oct 2021 02:07:23 +0300 Subject: [PATCH 022/208] Update README.md --- README.md | 205 +----------------------------------------------------- 1 file changed, 1 insertion(+), 204 deletions(-) diff --git a/README.md b/README.md index 0a4d6424e..28f764d75 100644 --- a/README.md +++ b/README.md @@ -1,204 +1 @@ -# ![freqtrade](https://raw.githubusercontent.com/freqtrade/freqtrade/develop/docs/assets/freqtrade_poweredby.svg) - -[![Freqtrade CI](https://github.com/freqtrade/freqtrade/workflows/Freqtrade%20CI/badge.svg)](https://github.com/freqtrade/freqtrade/actions/) -[![Coverage Status](https://coveralls.io/repos/github/freqtrade/freqtrade/badge.svg?branch=develop&service=github)](https://coveralls.io/github/freqtrade/freqtrade?branch=develop) -[![Documentation](https://readthedocs.org/projects/freqtrade/badge/)](https://www.freqtrade.io) -[![Maintainability](https://api.codeclimate.com/v1/badges/5737e6d668200b7518ff/maintainability)](https://codeclimate.com/github/freqtrade/freqtrade/maintainability) - -Freqtrade is a free and open source crypto trading bot written in Python. It is designed to support all major exchanges and be controlled via Telegram. It contains backtesting, plotting and money management tools as well as strategy optimization by machine learning. - -![freqtrade](https://raw.githubusercontent.com/freqtrade/freqtrade/develop/docs/assets/freqtrade-screenshot.png) - -## Disclaimer - -This software is for educational purposes only. Do not risk money which -you are afraid to lose. USE THE SOFTWARE AT YOUR OWN RISK. THE AUTHORS -AND ALL AFFILIATES ASSUME NO RESPONSIBILITY FOR YOUR TRADING RESULTS. - -Always start by running a trading bot in Dry-run and do not engage money -before you understand how it works and what profit/loss you should -expect. - -We strongly recommend you to have coding and Python knowledge. Do not -hesitate to read the source code and understand the mechanism of this bot. - -## Supported Exchange marketplaces - -Please read the [exchange specific notes](docs/exchanges.md) to learn about eventual, special configurations needed for each exchange. - -- [X] [Binance](https://www.binance.com/) ([*Note for binance users](docs/exchanges.md#binance-blacklist)) -- [X] [Bittrex](https://bittrex.com/) -- [X] [Kraken](https://kraken.com/) -- [X] [FTX](https://ftx.com) -- [X] [Gate.io](https://www.gate.io/ref/6266643) -- [ ] [potentially many others](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_ - -### Community tested - -Exchanges confirmed working by the community: - -- [X] [Bitvavo](https://bitvavo.com/) -- [X] [Kucoin](https://www.kucoin.com/) - -## Documentation - -We invite you to read the bot documentation to ensure you understand how the bot is working. - -Please find the complete documentation on our [website](https://www.freqtrade.io). - -## Features - -- [x] **Based on Python 3.7+**: For botting on any operating system - Windows, macOS and Linux. -- [x] **Persistence**: Persistence is achieved through sqlite. -- [x] **Dry-run**: Run the bot without paying money. -- [x] **Backtesting**: Run a simulation of your buy/sell strategy. -- [x] **Strategy Optimization by machine learning**: Use machine learning to optimize your buy/sell strategy parameters with real exchange data. -- [x] **Edge position sizing** Calculate your win rate, risk reward ratio, the best stoploss and adjust your position size before taking a position for each specific market. [Learn more](https://www.freqtrade.io/en/stable/edge/). -- [x] **Whitelist crypto-currencies**: Select which crypto-currency you want to trade or use dynamic whitelists. -- [x] **Blacklist crypto-currencies**: Select which crypto-currency you want to avoid. -- [x] **Manageable via Telegram**: Manage the bot with Telegram. -- [x] **Display profit/loss in fiat**: Display your profit/loss in 33 fiat. -- [x] **Daily summary of profit/loss**: Provide a daily summary of your profit/loss. -- [x] **Performance status report**: Provide a performance status of your current trades. - -## Quick start - -Freqtrade provides a Linux/macOS script to install all dependencies and help you to configure the bot. - -```bash -git clone -b develop https://github.com/freqtrade/freqtrade.git -cd freqtrade -./setup.sh --install -``` - -For any other type of installation please refer to [Installation doc](https://www.freqtrade.io/en/stable/installation/). - -## Basic Usage - -### Bot commands - -``` -usage: freqtrade [-h] [-V] - {trade,create-userdir,new-config,new-strategy,download-data,convert-data,convert-trade-data,list-data,backtesting,edge,hyperopt,hyperopt-list,hyperopt-show,list-exchanges,list-hyperopts,list-markets,list-pairs,list-strategies,list-timeframes,show-trades,test-pairlist,install-ui,plot-dataframe,plot-profit,webserver} - ... - -Free, open source crypto trading bot - -positional arguments: - {trade,create-userdir,new-config,new-strategy,download-data,convert-data,convert-trade-data,list-data,backtesting,edge,hyperopt,hyperopt-list,hyperopt-show,list-exchanges,list-hyperopts,list-markets,list-pairs,list-strategies,list-timeframes,show-trades,test-pairlist,install-ui,plot-dataframe,plot-profit,webserver} - trade Trade module. - create-userdir Create user-data directory. - new-config Create new config - new-strategy Create new strategy - download-data Download backtesting data. - convert-data Convert candle (OHLCV) data from one format to - another. - convert-trade-data Convert trade data from one format to another. - list-data List downloaded data. - backtesting Backtesting module. - edge Edge module. - hyperopt Hyperopt module. - hyperopt-list List Hyperopt results - hyperopt-show Show details of Hyperopt results - list-exchanges Print available exchanges. - list-hyperopts Print available hyperopt classes. - list-markets Print markets on exchange. - list-pairs Print pairs on exchange. - list-strategies Print available strategies. - list-timeframes Print available timeframes for the exchange. - show-trades Show trades. - test-pairlist Test your pairlist configuration. - install-ui Install FreqUI - plot-dataframe Plot candles with indicators. - plot-profit Generate plot showing profits. - webserver Webserver module. - -optional arguments: - -h, --help show this help message and exit - -V, --version show program's version number and exit - -``` - -### Telegram RPC commands - -Telegram is not mandatory. However, this is a great way to control your bot. More details and the full command list on our [documentation](https://www.freqtrade.io/en/latest/telegram-usage/) - -- `/start`: Starts the trader. -- `/stop`: Stops the trader. -- `/stopbuy`: Stop entering new trades. -- `/status |[table]`: Lists all or specific open trades. -- `/profit []`: Lists cumulative profit from all finished trades, over the last n days. -- `/forcesell |all`: Instantly sells the given trade (Ignoring `minimum_roi`). -- `/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. -- `/help`: Show help message. -- `/version`: Show version. - -## Development branches - -The project is currently setup in two main branches: - -- `develop` - This branch has often new features, but might also contain breaking changes. We try hard to keep this branch as stable as possible. -- `stable` - This branch contains the latest stable release. This branch is generally well tested. -- `feat/*` - These are feature branches, which are being worked on heavily. Please don't use these unless you want to test a specific feature. - -## Support - -### Help / Discord - -For any questions not covered by the documentation or for further information about the bot, or to simply engage with like-minded individuals, we encourage you to join the Freqtrade [discord server](https://discord.gg/p7nuUNVfP7). - -### [Bugs / Issues](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue) - -If you discover a bug in the bot, please -[search our issue tracker](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue) -first. If it hasn't been reported, please -[create a new issue](https://github.com/freqtrade/freqtrade/issues/new/choose) and -ensure you follow the template guide so that our team can assist you as -quickly as possible. - -### [Feature Requests](https://github.com/freqtrade/freqtrade/labels/enhancement) - -Have you a great idea to improve the bot you want to share? Please, -first search if this feature was not [already discussed](https://github.com/freqtrade/freqtrade/labels/enhancement). -If it hasn't been requested, please -[create a new request](https://github.com/freqtrade/freqtrade/issues/new/choose) -and ensure you follow the template guide so that it does not get lost -in the bug reports. - -### [Pull Requests](https://github.com/freqtrade/freqtrade/pulls) - -Feel like our bot is missing a feature? We welcome your pull requests! - -Please read our -[Contributing document](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md) -to understand the requirements before sending your pull-requests. - -Coding is not a necessity to contribute - maybe start with improving our documentation? -Issues labeled [good first issue](https://github.com/freqtrade/freqtrade/labels/good%20first%20issue) can be good first contributions, and will help get you familiar with the codebase. - -**Note** before starting any major new feature work, *please open an issue describing what you are planning to do* or talk to us on [discord](https://discord.gg/p7nuUNVfP7) (please use the #dev channel for this). This will ensure that interested parties can give valuable feedback on the feature, and let others know that you are working on it. - -**Important:** Always create your PR against the `develop` branch, not `stable`. - -## Requirements - -### Up-to-date clock - -The clock must be accurate, synchronized to a NTP server very frequently to avoid problems with communication to the exchanges. - -### Min hardware required - -To run this bot we recommend you a cloud instance with a minimum of: - -- Minimal (advised) system requirements: 2GB RAM, 1GB disk space, 2vCPU - -### Software requirements - -- [Python 3.7.x](http://docs.python-guide.org/en/latest/starting/installation/) -- [pip](https://pip.pypa.io/en/stable/installing/) -- [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) -- [TA-Lib](https://mrjbq7.github.io/ta-lib/install.html) -- [virtualenv](https://virtualenv.pypa.io/en/stable/installation.html) (Recommended) -- [Docker](https://www.docker.com/products/docker) (Recommended) +Freqtrade fork From 3ee9674bb7d9fffe7a1e1730a1361f01d5aeef19 Mon Sep 17 00:00:00 2001 From: theluxaz <37055144+theluxaz@users.noreply.github.com> Date: Wed, 13 Oct 2021 02:07:45 +0300 Subject: [PATCH 023/208] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 28f764d75..f468e9a9c 100644 --- a/README.md +++ b/README.md @@ -1 +1 @@ -Freqtrade fork +fork From 0f670189ebfaac0817c8726d607295870d9ba576 Mon Sep 17 00:00:00 2001 From: theluxaz Date: Wed, 13 Oct 2021 02:14:07 +0300 Subject: [PATCH 024/208] quick typo fix --- freqtrade/freqtradebot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index f0a54500e..f8b9dbb5e 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -716,7 +716,7 @@ class FreqtradeBot(LoggingMixin): logger.debug('checking sell') sell_rate = self.exchange.get_rate(trade.pair, refresh=True, side="sell") - if self._check_and_execute_sell(trade, sell_rate, buy, sell, sell_tag): + if self._check_and_execute_exit(trade, sell_rate, buy, sell, sell_tag): return True logger.debug('Found no sell signal for %s.', trade) From 96cab22a8c2999c9f224a783f17052d302cfa955 Mon Sep 17 00:00:00 2001 From: theluxaz Date: Thu, 14 Oct 2021 01:03:15 +0300 Subject: [PATCH 025/208] Fixed some bugs for live sell_tags. --- freqtrade/freqtradebot.py | 2 ++ freqtrade/rpc/telegram.py | 8 ++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index f8b9dbb5e..d415c9d93 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1187,6 +1187,7 @@ class FreqtradeBot(LoggingMixin): 'profit_amount': profit_trade, 'profit_ratio': profit_ratio, 'sell_reason': trade.sell_reason, + 'sell_tag': trade.sell_tag, 'open_date': trade.open_date, 'close_date': trade.close_date or datetime.utcnow(), 'stake_currency': self.config['stake_currency'], @@ -1230,6 +1231,7 @@ class FreqtradeBot(LoggingMixin): 'profit_amount': profit_trade, 'profit_ratio': profit_ratio, 'sell_reason': trade.sell_reason, + 'sell_tag': trade.sell_tag, 'open_date': trade.open_date, 'close_date': trade.close_date or datetime.now(timezone.utc), 'stake_currency': self.config['stake_currency'], diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index bd8c83315..db745ff37 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -265,8 +265,12 @@ class Telegram(RPCHandler): "*Current Rate:* `{current_rate:.8f}`\n" "*Close Rate:* `{limit:.8f}`").format(**msg) - sell_tag = msg['sell_tag'] - buy_tag = msg['buy_tag'] + sell_tag =None + if("sell_tag" in msg.keys()): + sell_tag = msg['sell_tag'] + buy_tag =None + if("buy_tag" in msg.keys()): + buy_tag = msg['buy_tag'] if sell_tag is not None and buy_tag is not None: message = ("{emoji} *{exchange}:* Selling {pair} (#{trade_id})\n" From d341d85079af2a77ee2122ee654ac08ae7d1a6d8 Mon Sep 17 00:00:00 2001 From: theluxaz Date: Thu, 14 Oct 2021 01:13:28 +0300 Subject: [PATCH 026/208] Refixed some files for the pull request --- .gitignore | 1 - README.md | 201 ++- .../hyperopts/RuleNOTANDoptimizer.py | 1203 ----------------- 3 files changed, 200 insertions(+), 1205 deletions(-) delete mode 100644 freqtrade/user_data/hyperopts/RuleNOTANDoptimizer.py diff --git a/.gitignore b/.gitignore index 9e4ce834b..16df71194 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,6 @@ config*.json *.sqlite logfile.txt user_data/* -freqtrade/user_data/* !user_data/strategy/sample_strategy.py !user_data/notebooks user_data/notebooks/* diff --git a/README.md b/README.md index f468e9a9c..1eb96f200 100644 --- a/README.md +++ b/README.md @@ -1 +1,200 @@ -fork +# ![freqtrade](https://raw.githubusercontent.com/freqtrade/freqtrade/develop/docs/assets/freqtrade_poweredby.svg) + +[![Freqtrade CI](https://github.com/freqtrade/freqtrade/workflows/Freqtrade%20CI/badge.svg)](https://github.com/freqtrade/freqtrade/actions/) +[![Coverage Status](https://coveralls.io/repos/github/freqtrade/freqtrade/badge.svg?branch=develop&service=github)](https://coveralls.io/github/freqtrade/freqtrade?branch=develop) +[![Documentation](https://readthedocs.org/projects/freqtrade/badge/)](https://www.freqtrade.io) +[![Maintainability](https://api.codeclimate.com/v1/badges/5737e6d668200b7518ff/maintainability)](https://codeclimate.com/github/freqtrade/freqtrade/maintainability) + +Freqtrade is a free and open source crypto trading bot written in Python. It is designed to support all major exchanges and be controlled via Telegram. It contains backtesting, plotting and money management tools as well as strategy optimization by machine learning. + +![freqtrade](https://raw.githubusercontent.com/freqtrade/freqtrade/develop/docs/assets/freqtrade-screenshot.png) + +## Disclaimer + +This software is for educational purposes only. Do not risk money which +you are afraid to lose. USE THE SOFTWARE AT YOUR OWN RISK. THE AUTHORS +AND ALL AFFILIATES ASSUME NO RESPONSIBILITY FOR YOUR TRADING RESULTS. + +Always start by running a trading bot in Dry-run and do not engage money +before you understand how it works and what profit/loss you should +expect. + +We strongly recommend you to have coding and Python knowledge. Do not +hesitate to read the source code and understand the mechanism of this bot. + +## Supported Exchange marketplaces + +Please read the [exchange specific notes](docs/exchanges.md) to learn about eventual, special configurations needed for each exchange. + +- [X] [Binance](https://www.binance.com/) ([*Note for binance users](docs/exchanges.md#binance-blacklist)) +- [X] [Bittrex](https://bittrex.com/) +- [X] [Kraken](https://kraken.com/) +- [X] [FTX](https://ftx.com) +- [X] [Gate.io](https://www.gate.io/ref/6266643) +- [ ] [potentially many others](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_ + +### Community tested + +Exchanges confirmed working by the community: + +- [X] [Bitvavo](https://bitvavo.com/) +- [X] [Kucoin](https://www.kucoin.com/) + +## Documentation + +We invite you to read the bot documentation to ensure you understand how the bot is working. + +Please find the complete documentation on our [website](https://www.freqtrade.io). + +## Features + +- [x] **Based on Python 3.7+**: For botting on any operating system - Windows, macOS and Linux. +- [x] **Persistence**: Persistence is achieved through sqlite. +- [x] **Dry-run**: Run the bot without paying money. +- [x] **Backtesting**: Run a simulation of your buy/sell strategy. +- [x] **Strategy Optimization by machine learning**: Use machine learning to optimize your buy/sell strategy parameters with real exchange data. +- [x] **Edge position sizing** Calculate your win rate, risk reward ratio, the best stoploss and adjust your position size before taking a position for each specific market. [Learn more](https://www.freqtrade.io/en/stable/edge/). +- [x] **Whitelist crypto-currencies**: Select which crypto-currency you want to trade or use dynamic whitelists. +- [x] **Blacklist crypto-currencies**: Select which crypto-currency you want to avoid. +- [x] **Manageable via Telegram**: Manage the bot with Telegram. +- [x] **Display profit/loss in fiat**: Display your profit/loss in 33 fiat. +- [x] **Daily summary of profit/loss**: Provide a daily summary of your profit/loss. +- [x] **Performance status report**: Provide a performance status of your current trades. + +## Quick start + +Freqtrade provides a Linux/macOS script to install all dependencies and help you to configure the bot. + +```bash +git clone -b develop https://github.com/freqtrade/freqtrade.git +cd freqtrade +./setup.sh --install +``` + +For any other type of installation please refer to [Installation doc](https://www.freqtrade.io/en/stable/installation/). + +## Basic Usage + +### Bot commands + +``` +usage: freqtrade [-h] [-V] + {trade,create-userdir,new-config,new-strategy,download-data,convert-data,convert-trade-data,list-data,backtesting,edge,hyperopt,hyperopt-list,hyperopt-show,list-exchanges,list-hyperopts,list-markets,list-pairs,list-strategies,list-timeframes,show-trades,test-pairlist,install-ui,plot-dataframe,plot-profit,webserver} + ... +Free, open source crypto trading bot +positional arguments: + {trade,create-userdir,new-config,new-strategy,download-data,convert-data,convert-trade-data,list-data,backtesting,edge,hyperopt,hyperopt-list,hyperopt-show,list-exchanges,list-hyperopts,list-markets,list-pairs,list-strategies,list-timeframes,show-trades,test-pairlist,install-ui,plot-dataframe,plot-profit,webserver} + trade Trade module. + create-userdir Create user-data directory. + new-config Create new config + new-strategy Create new strategy + download-data Download backtesting data. + convert-data Convert candle (OHLCV) data from one format to + another. + convert-trade-data Convert trade data from one format to another. + list-data List downloaded data. + backtesting Backtesting module. + edge Edge module. + hyperopt Hyperopt module. + hyperopt-list List Hyperopt results + hyperopt-show Show details of Hyperopt results + list-exchanges Print available exchanges. + list-hyperopts Print available hyperopt classes. + list-markets Print markets on exchange. + list-pairs Print pairs on exchange. + list-strategies Print available strategies. + list-timeframes Print available timeframes for the exchange. + show-trades Show trades. + test-pairlist Test your pairlist configuration. + install-ui Install FreqUI + plot-dataframe Plot candles with indicators. + plot-profit Generate plot showing profits. + webserver Webserver module. +optional arguments: + -h, --help show this help message and exit + -V, --version show program's version number and exit +``` + +### Telegram RPC commands + +Telegram is not mandatory. However, this is a great way to control your bot. More details and the full command list on our [documentation](https://www.freqtrade.io/en/latest/telegram-usage/) + +- `/start`: Starts the trader. +- `/stop`: Stops the trader. +- `/stopbuy`: Stop entering new trades. +- `/status |[table]`: Lists all or specific open trades. +- `/profit []`: Lists cumulative profit from all finished trades, over the last n days. +- `/forcesell |all`: Instantly sells the given trade (Ignoring `minimum_roi`). +- `/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. +- `/help`: Show help message. +- `/version`: Show version. + +## Development branches + +The project is currently setup in two main branches: + +- `develop` - This branch has often new features, but might also contain breaking changes. We try hard to keep this branch as stable as possible. +- `stable` - This branch contains the latest stable release. This branch is generally well tested. +- `feat/*` - These are feature branches, which are being worked on heavily. Please don't use these unless you want to test a specific feature. + +## Support + +### Help / Discord + +For any questions not covered by the documentation or for further information about the bot, or to simply engage with like-minded individuals, we encourage you to join the Freqtrade [discord server](https://discord.gg/p7nuUNVfP7). + +### [Bugs / Issues](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue) + +If you discover a bug in the bot, please +[search our issue tracker](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue) +first. If it hasn't been reported, please +[create a new issue](https://github.com/freqtrade/freqtrade/issues/new/choose) and +ensure you follow the template guide so that our team can assist you as +quickly as possible. + +### [Feature Requests](https://github.com/freqtrade/freqtrade/labels/enhancement) + +Have you a great idea to improve the bot you want to share? Please, +first search if this feature was not [already discussed](https://github.com/freqtrade/freqtrade/labels/enhancement). +If it hasn't been requested, please +[create a new request](https://github.com/freqtrade/freqtrade/issues/new/choose) +and ensure you follow the template guide so that it does not get lost +in the bug reports. + +### [Pull Requests](https://github.com/freqtrade/freqtrade/pulls) + +Feel like our bot is missing a feature? We welcome your pull requests! + +Please read our +[Contributing document](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md) +to understand the requirements before sending your pull-requests. + +Coding is not a necessity to contribute - maybe start with improving our documentation? +Issues labeled [good first issue](https://github.com/freqtrade/freqtrade/labels/good%20first%20issue) can be good first contributions, and will help get you familiar with the codebase. + +**Note** before starting any major new feature work, *please open an issue describing what you are planning to do* or talk to us on [discord](https://discord.gg/p7nuUNVfP7) (please use the #dev channel for this). This will ensure that interested parties can give valuable feedback on the feature, and let others know that you are working on it. + +**Important:** Always create your PR against the `develop` branch, not `stable`. + +## Requirements + +### Up-to-date clock + +The clock must be accurate, synchronized to a NTP server very frequently to avoid problems with communication to the exchanges. + +### Min hardware required + +To run this bot we recommend you a cloud instance with a minimum of: + +- Minimal (advised) system requirements: 2GB RAM, 1GB disk space, 2vCPU + +### Software requirements + +- [Python 3.7.x](http://docs.python-guide.org/en/latest/starting/installation/) +- [pip](https://pip.pypa.io/en/stable/installing/) +- [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) +- [TA-Lib](https://mrjbq7.github.io/ta-lib/install.html) +- [virtualenv](https://virtualenv.pypa.io/en/stable/installation.html) (Recommended) +- [Docker](https://www.docker.com/products/docker) (Recommended) \ No newline at end of file diff --git a/freqtrade/user_data/hyperopts/RuleNOTANDoptimizer.py b/freqtrade/user_data/hyperopts/RuleNOTANDoptimizer.py deleted file mode 100644 index f720b59ca..000000000 --- a/freqtrade/user_data/hyperopts/RuleNOTANDoptimizer.py +++ /dev/null @@ -1,1203 +0,0 @@ -# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement -# isort: skip_file -# --- Do not remove these libs --- -from functools import reduce -from typing import Any, Callable, Dict, List - -import numpy as np # noqa -import pandas as pd # noqa -from pandas import DataFrame -from skopt.space import Categorical, Dimension,Integer , Real # noqa -from freqtrade.optimize.space import SKDecimal -from freqtrade.optimize.hyperopt_interface import IHyperOpt - -# -------------------------------- -# Add your lib to import here -import talib.abstract as ta # noqa -import freqtrade.vendor.qtpylib.indicators as qtpylib - -##PYCHARM -import sys -sys.path.append(r"/freqtrade/user_data/strategies") - - -# ##HYPEROPT -# import sys,os -# file_dir = os.path.dirname(__file__) -# sys.path.append(file_dir) - - -from z_buyer_mid_volatility import mid_volatility_buyer -from z_seller_mid_volatility import mid_volatility_seller -from z_COMMON_FUNCTIONS import MID_VOLATILITY - - - - -class RuleOptimizer15min(IHyperOpt): - """ - This is a sample hyperopt to inspire you. - Feel free to customize it. - - More information in the documentation: https://www.freqtrade.io/en/latest/hyperopt/ - - You should: - - Rename the class name to some unique name. - - Add any methods you want to build your hyperopt. - - Add any lib you need to build your hyperopt. - - You must keep: - - The prototypes for the methods: populate_indicators, indicator_space, buy_strategy_generator. - - The methods roi_space, generate_roi_table and stoploss_space are not required - and are provided by default. - However, you may override them if you need the - 'roi' and the 'stoploss' spaces that differ from the defaults offered by Freqtrade. - - This sample illustrates how to override these methods. - """ - - - @staticmethod - def buy_strategy_generator(params: Dict[str, Any]) -> Callable: - """ - Define the buy strategy parameters to be used by hyperopt - """ - def populate_buy_trend(dataframe: DataFrame, metadata: dict) -> DataFrame: - """ - Buy strategy Hyperopt will build and use - """ - conditions = [] - - - -#--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -#--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - - - ##MAIN SELECTORS - -#-------------------- - - ##VOLATILITY - - conditions.append(dataframe['vol_mid'] > 0 ) - - # conditions.append((dataframe['vol_low'] > 0) |(dataframe['vol_mid'] > 0) ) - - # conditions.append((dataframe['vol_high'] > 0) |(dataframe['vol_mid'] > 0) ) - - -#-------------------- - - - ##PICKS TREND COMBO - - conditions.append( - - (dataframe['downtrend'] >= params['main_1_trend_strength']) - |#OR & - (dataframe['downtrendsmall'] >= params['main_2_trend_strength']) - - ) - - ##UPTREND - #conditions.append(dataframe['uptrend'] >= params['main_1_trend_strength']) - ##DOWNTREND - #conditions.append(dataframe['downtrend'] >= params['main_1_trend_strength']) - ##NOTREND - #conditions.append((dataframe['uptrend'] <1)&(dataframe['downtrend'] <1)) - -#------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -#------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - - ##ABOVE / BELOW THRESHOLDS - - #RSI ABOVE - if 'include_sell_ab_9_rsi_above_value' in params and params['include_sell_ab_9_rsi_above_value']: - conditions.append(dataframe['rsi'] > params['sell_ab_9_rsi_above_value']) - #RSI RECENT PIT 5 - if 'include_sell_ab_10_rsi_recent_pit_2_value' in params and params['include_sell_ab_10_rsi_recent_pit_2_value']: - conditions.append(dataframe['rsi'].rolling(2).min() < params['sell_ab_10_rsi_recent_pit_2_value']) - #RSI RECENT PIT 12 - if 'include_sell_ab_11_rsi_recent_pit_4_value' in params and params['include_sell_ab_11_rsi_recent_pit_4_value']: - conditions.append(dataframe['rsi'].rolling(4).min() < params['sell_ab_11_rsi_recent_pit_4_value']) - #RSI5 BELOW - if 'include_sell_ab_12_rsi5_above_value' in params and params['include_sell_ab_12_rsi5_above_value']: - conditions.append(dataframe['rsi5'] > params['sell_ab_12_rsi5_above_value']) - #RSI50 BELOW - if 'include_sell_ab_13_rsi50_above_value' in params and params['include_sell_ab_13_rsi50_above_value']: - conditions.append(dataframe['rsi50'] > params['sell_ab_13_rsi50_above_value']) - -#----------------------- - - #ROC BELOW - if 'include_sell_ab_14_roc_above_value' in params and params['include_sell_ab_14_roc_above_value']: - conditions.append(dataframe['roc'] > (params['sell_ab_14_roc_above_value']/2)) - #ROC50 BELOW - if 'include_sell_ab_15_roc50_above_value' in params and params['include_sell_ab_15_roc50_above_value']: - conditions.append(dataframe['roc50'] > (params['sell_ab_15_roc50_above_value'])) - #ROC2 BELOW - if 'include_sell_ab_16_roc2_above_value' in params and params['include_sell_ab_16_roc2_above_value']: - conditions.append(dataframe['roc2'] > (params['sell_ab_16_roc2_above_value']/2)) - -#----------------------- - - #PPO5 BELOW - if 'include_sell_ab_17_ppo5_above_value' in params and params['include_sell_ab_17_ppo5_above_value']: - conditions.append(dataframe['ppo5'] > (params['sell_ab_17_ppo5_above_value']/2)) - #PPO10 BELOW - if 'include_sell_ab_18_ppo10_above_value' in params and params['include_sell_ab_18_ppo10_above_value']: - conditions.append(dataframe['ppo10'] > (params['sell_ab_18_ppo10_above_value']/2)) - #PPO25 BELOW - if 'include_sell_ab_19_ppo25_above_value' in params and params['include_sell_ab_19_ppo25_above_value']: - conditions.append(dataframe['ppo25'] > (params['sell_ab_19_ppo25_above_value']/2)) - - #PPO50 BELOW - if 'include_sell_ab_20_ppo50_above_value' in params and params['include_sell_ab_20_ppo50_above_value']: - conditions.append(dataframe['ppo50'] > (params['sell_ab_20_ppo50_above_value']/2)) - #PPO100 BELOW - if 'include_sell_ab_21_ppo100_above_value' in params and params['include_sell_ab_21_ppo100_above_value']: - conditions.append(dataframe['ppo100'] > (params['sell_ab_21_ppo100_above_value'])) - #PPO200 BELOW - if 'include_sell_ab_22_ppo200_above_value' in params and params['include_sell_ab_22_ppo200_above_value']: - conditions.append(dataframe['ppo200'] > (params['sell_ab_22_ppo200_above_value'])) - #PPO500 BELOW - if 'include_sell_ab_23_ppo500_above_value' in params and params['include_sell_ab_23_ppo500_above_value']: - conditions.append(dataframe['ppo500'] > (params['sell_ab_23_ppo500_above_value']*2)) - - ##USE AT A LATER STEP - - #convsmall BELOW - if 'include_sell_ab_24_convsmall_above_value' in params and params['include_sell_ab_24_convsmall_above_value']: - conditions.append(dataframe['convsmall'] > (params['sell_ab_24_convsmall_above_value']/2)) - #convmedium BELOW - if 'include_sell_ab_25_convmedium_above_value' in params and params['include_sell_ab_25_convmedium_above_value']: - conditions.append(dataframe['convmedium'] >(params['sell_ab_25_convmedium_above_value'])) - #convlarge BELOW - if 'include_sell_ab_26_convlarge_above_value' in params and params['include_sell_ab_26_convlarge_above_value']: - conditions.append(dataframe['convlarge'] > (params['sell_ab_26_convlarge_above_value'])) - #convultra BELOW - if 'include_sell_ab_27_convultra_above_value' in params and params['include_sell_ab_27_convultra_above_value']: - conditions.append(dataframe['convultra'] > (params['sell_ab_27_convultra_above_value']/2)) - #convdist BELOW - if 'include_sell_ab_28_convdist_above_value' in params and params['include_sell_ab_28_convdist_above_value']: - conditions.append(dataframe['convdist'] > (params['sell_ab_28_convdist_above_value'])) - - -#------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -#------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - - ##SMA'S GOING DOWN - - if 'sell_down_0a_sma3' in params and params['sell_down_0a_sma3']: - conditions.append((dataframe['sma3'].shift(1) >dataframe['sma3'])) - if 'sell_down_0b_sma5' in params and params['sell_down_0b_sma5']: - conditions.append((dataframe['sma5'].shift(1) >dataframe['sma5'])) - if 'sell_down_1_sma10' in params and params['sell_down_1_sma10']: - conditions.append((dataframe['sma10'].shift(1) >dataframe['sma10'])) - if 'sell_down_2_sma25' in params and params['sell_down_2_sma25']: - conditions.append((dataframe['sma25'].shift(1) >dataframe['sma25'])) - if 'sell_down_3_sma50' in params and params['sell_down_3_sma50']: - conditions.append((dataframe['sma50'].shift(2) >dataframe['sma50'])) - if 'sell_down_4_sma100' in params and params['sell_down_4_sma100']: - conditions.append((dataframe['sma100'].shift(3) >dataframe['sma100'])) - if 'sell_down_5_sma200' in params and params['sell_down_5_sma200']: - conditions.append((dataframe['sma200'].shift(4) >dataframe['sma200'])) - - if 'sell_down_6_sma400' in params and params['sell_down_6_sma400']: - conditions.append((dataframe['sma400'].shift(4) >dataframe['sma400'])) - if 'sell_down_7_sma10k' in params and params['sell_down_7_sma10k']: - conditions.append((dataframe['sma10k'].shift(5) >dataframe['sma10k'])) - # if 'sell_down_8_sma20k' in params and params['sell_down_8_sma20k']: - # conditions.append((dataframe['sma20k'].shift(5) >dataframe['sma20k'])) - # if 'sell_down_9_sma30k' in params and params['sell_down_9_sma30k']: - # conditions.append((dataframe['sma30k'].shift(5) >dataframe['sma30k'])) - - if 'sell_down_10_convsmall' in params and params['sell_down_10_convsmall']: - conditions.append((dataframe['convsmall'].shift(2) >dataframe['convsmall'])) - if 'sell_down_11_convmedium' in params and params['sell_down_11_convmedium']: - conditions.append((dataframe['convmedium'].shift(3) >dataframe['convmedium'])) - if 'sell_down_12_convlarge' in params and params['sell_down_12_convlarge']: - conditions.append((dataframe['convlarge'].shift(4) >dataframe['convlarge'])) - if 'sell_down_13_convultra' in params and params['sell_down_13_convultra']: - conditions.append((dataframe['convultra'].shift(4) >dataframe['convultra'])) - if 'sell_down_14_convdist' in params and params['sell_down_14_convdist']: - conditions.append((dataframe['convdist'].shift(4) >dataframe['convdist'])) - - if 'sell_down_15_vol50' in params and params['sell_down_15_vol50']: - conditions.append((dataframe['vol50'].shift(2) >dataframe['vol50'])) - if 'sell_down_16_vol100' in params and params['sell_down_16_vol100']: - conditions.append((dataframe['vol100'].shift(3) >dataframe['vol100'])) - if 'sell_down_17_vol175' in params and params['sell_down_17_vol175']: - conditions.append((dataframe['vol175'].shift(4) >dataframe['vol175'])) - if 'sell_down_18_vol250' in params and params['sell_down_18_vol250']: - conditions.append((dataframe['vol250'].shift(4) >dataframe['vol250'])) - if 'sell_down_19_vol500' in params and params['sell_down_19_vol500']: - conditions.append((dataframe['vol500'].shift(4) >dataframe['vol500'])) - - if 'sell_down_20_vol1000' in params and params['sell_down_20_vol1000']: - conditions.append((dataframe['vol1000'].shift(4) >dataframe['vol1000'])) - if 'sell_down_21_vol100mean' in params and params['sell_down_21_vol100mean']: - conditions.append((dataframe['vol100mean'].shift(4) >dataframe['vol100mean'])) - if 'sell_down_22_vol250mean' in params and params['sell_down_22_vol250mean']: - conditions.append((dataframe['vol250mean'].shift(4) >dataframe['vol250mean'])) - - if 'up_20_conv3' in params and params['up_20_conv3']: - conditions.append(((dataframe['conv3'].shift(25) < dataframe['conv3'])&(dataframe['conv3'].shift(50) < dataframe['conv3']))) - if 'up_21_vol5' in params and params['up_21_vol5']: - conditions.append(((dataframe['vol5'].shift(25) < dataframe['vol5'])&(dataframe['vol5'].shift(50) < dataframe['vol5']))) - if 'up_22_vol5ultra' in params and params['up_22_vol5ultra']: - conditions.append(((dataframe['vol5ultra'].shift(25) < dataframe['vol5ultra'])&(dataframe['vol5ultra'].shift(50) < dataframe['vol5ultra']))) - if 'up_23_vol1ultra' in params and params['up_23_vol1ultra']: - conditions.append(((dataframe['vol1ultra'].shift(25) < dataframe['vol1ultra'])& (dataframe['vol1ultra'].shift(50) < dataframe['vol1ultra']))) - if 'up_24_vol1' in params and params['up_24_vol1']: - conditions.append(((dataframe['vol1'].shift(30) < dataframe['vol1'])&(dataframe['vol1'].shift(10) < dataframe['vol1']))) - if 'up_25_vol5inc24' in params and params['up_25_vol5inc24']: - conditions.append((dataframe['vol5inc24'].shift(50) < dataframe['vol5inc24'])) - - -#------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - - ##ABOVE / BELOW SMAS 1 above/ 0 None / -1 below - - #SMA10 - conditions.append((dataframe['close'] > dataframe['sma10'])|(0.5 > params['ab_1_sma10'])) - conditions.append((dataframe['close'] < dataframe['sma10'])|(-0.5 < params['ab_1_sma10'])) - #SMA25 - conditions.append((dataframe['close'] > dataframe['sma25'])|(0.5 > params['ab_2_sma25'])) - conditions.append((dataframe['close'] < dataframe['sma25'])|(-0.5 < params['ab_2_sma25'])) - #SMA50 - conditions.append((dataframe['close'] > dataframe['sma50'])|(0.5 > params['ab_3_sma50'])) - conditions.append((dataframe['close'] < dataframe['sma50'])|(-0.5 < params['ab_3_sma50'])) - - - #SMA100 - conditions.append((dataframe['close'] > dataframe['sma100'])|(0.5 > params['ab_4_sma100'])) - conditions.append((dataframe['close'] < dataframe['sma100'])|(-0.5 < params['ab_4_sma100'])) - #SMA100 - conditions.append((dataframe['close'] > dataframe['sma200'])|(0.5 > params['ab_5_sma200'])) - conditions.append((dataframe['close'] < dataframe['sma200'])|(-0.5 < params['ab_5_sma200'])) - #SMA400 - conditions.append((dataframe['close'] > dataframe['sma400'])|(0.5 > params['ab_6_sma400'])) - conditions.append((dataframe['close'] < dataframe['sma400'])|(-0.5 < params['ab_6_sma400'])) - #SMA10k - conditions.append((dataframe['close'] > dataframe['sma10k'])|(0.5 > params['ab_7_sma10k'])) - conditions.append((dataframe['close'] < dataframe['sma10k'])|(-0.5 < params['ab_7_sma10k'])) - -#------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - - ##DOWNSWINGS / UPSWINGS PPO'S - - #ppo5 UP OR DOWN (1 UP, 0 NOTHING, -1 DOWN) - conditions.append((dataframe['ppo5'].shift(2) params['sell_swings_1_ppo5_up_or_down_bool'])) - conditions.append((dataframe['ppo5'].shift(2) >dataframe['ppo5'])|(-0.5 < params['sell_swings_1_ppo5_up_or_down_bool'])) - #ppo10 - conditions.append((dataframe['ppo10'].shift(3) params['sell_swings_2_ppo10_up_or_down_bool'])) - conditions.append((dataframe['ppo10'].shift(3) >dataframe['ppo10'])|(-0.5 < params['sell_swings_2_ppo10_up_or_down_bool'])) - #ppo25 - #conditions.append((dataframe['ppo25'].shift(3) params['sell_swings_3_ppo25_up_or_down_bool'])) - conditions.append((dataframe['ppo25'].shift(3) >dataframe['ppo25'])|(-0.5 < params['sell_swings_3_ppo25_up_or_down_bool'])) - - #ppo50 - #conditions.append((dataframe['ppo50'].shift(3 params['sell_swings_4_ppo50_up_or_down_bool'])) - conditions.append((dataframe['ppo50'].shift(3) >dataframe['ppo50'])|(-0.5 < params['sell_swings_4_ppo50_up_or_down_bool'])) - #ppo100 - #conditions.append((dataframe['ppo100'].shift(4) params['sell_swings_5_ppo100_up_or_down_bool'])) - conditions.append((dataframe['ppo100'].shift(4) >dataframe['ppo100'])|(-0.5 < params['sell_swings_5_ppo100_up_or_down_bool'])) - #ppo200 - #conditions.append((dataframe['ppo200'].shift(4) params['sell_swings_6_ppo200_up_or_down_bool'])) - conditions.append((dataframe['ppo200'].shift(4) >dataframe['ppo200'])|(-0.5 < params['sell_swings_6_ppo200_up_or_down_bool'])) - - #ppo500 - #conditions.append((dataframe['ppo500'].shift(5) params['sell_swings_7_ppo500_up_or_down_bool'])) - conditions.append((dataframe['ppo500'].shift(5) >dataframe['ppo500'])|(-0.5 < params['sell_swings_7_ppo500_up_or_down_bool'])) - - #roc50 - #conditions.append((dataframe['roc50'].shift(3) params['sell_swings_8_roc50_up_or_down_bool'])) - conditions.append((dataframe['roc50'].shift(3) >dataframe['roc50'])|(-0.5 < params['sell_swings_8_roc50_up_or_down_bool'])) - #roc10 - #conditions.append((dataframe['roc10'].shift(2) params['sell_swings_9_roc10_up_or_down_bool'])) - conditions.append((dataframe['roc10'].shift(2) >dataframe['roc10'])|(-0.5 < params['sell_swings_9_roc10_up_or_down_bool'])) - - -#------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -#------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - - - ##DISTANCES/ROC - - ##FOR MORE TOP SELLERS - #dist50 MORE THAN - if 'include_sell_dist_1_dist50_more_value' in params and params['include_sell_dist_1_dist50_more_value']: - conditions.append(dataframe['dist50'] > (params['sell_dist_1_dist50_more_value'])) - #dist200 MORE THAN - if 'include_sell_dist_2_dist200_more_value' in params and params['include_sell_dist_2_dist200_more_value']: - conditions.append(dataframe['dist200'] > (params['sell_dist_2_dist200_more_value'])) - - #dist400 MORE THAN - if 'include_sell_dist_3_dist400_more_value' in params and params['include_sell_dist_3_dist400_more_value']: - conditions.append(dataframe['dist400'] > (params['sell_dist_3_dist400_more_value'])) - #dist10k MORE THAN - if 'include_sell_dist_4_dist10k_more_value' in params and params['include_sell_dist_4_dist10k_more_value']: - conditions.append(dataframe['dist10k'] > (params['sell_dist_4_dist10k_more_value'])) - - ##FOR MORE TOP SELLERS - #more =further from top bol up - #dist_upbol50 MORE THAN - if 'include_sell_dist_5_dist_upbol50_more_value' in params and params['include_sell_dist_5_dist_upbol50_more_value']: - conditions.append(dataframe['dist_upbol50'] > (params['sell_dist_5_dist_upbol50_more_value']/2)) - #dist_upbol100 MORE THAN - if 'include_sell_dist_6_dist_upbol100_more_value' in params and params['include_sell_dist_6_dist_upbol100_more_value']: - conditions.append(dataframe['dist_upbol100'] > (params['sell_dist_6_dist_upbol100_more_value']/2)) - - - ##for bot bol prevent seller - # #less =closer to bot bol - #dist_upbol50 LESS THAN. - #if 'include_sell_dist_7_dist_lowbol50_more_value' in params and params['include_sell_dist_7_dist_lowbol50_more_value']: - # conditions.append(dataframe['dist_lowbol50'] > (params['sell_dist_7_dist_lowbol50_more_value']/2)) - #dist_upbol100 LESS THAN - # if 'include_sell_dist_8_dist_lowbol100_more_value' in params and params['include_sell_dist_8_dist_lowbol100_more_value']: - # conditions.append(dataframe['dist_lowbol100'] > (params['sell_dist_8_dist_lowbol100_more_value']/2)) - - - - ##others - #roc50sma LESS THAN - if 'include_sell_dist_7_roc50sma_less_value' in params and params['include_sell_dist_7_roc50sma_less_value']: - conditions.append(dataframe['roc50sma'] < (params['sell_dist_7_roc50sma_less_value'])*2) - #roc200sma LESS THAN - if 'include_sell_dist_8_roc200sma_less_value' in params and params['include_sell_dist_8_roc200sma_less_value']: - conditions.append(dataframe['roc200sma'] < (params['sell_dist_8_roc200sma_less_value'])*2) - - ##ENABLE TO BUY AWAY FROM HIGH - # #HIGH500 TO CLOSE MORE THAN - #if 'include_sell_dist_9_high100_more_value' in params and params['include_sell_dist_9_high100_more_value']: - # conditions.append((dataframe['high100']-dataframe['close']) > ((dataframe['high100']/100* (params['sell_dist_9_high100_more_value'])) - -#------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - - - - - - - # Check that volume is not 0 - conditions.append(dataframe['volume'] > 0) - - - - - if conditions: - - - # ##ENABLE PRODUCTION BUYS - # dataframe.loc[ - # (add_production_buys_mid(dataframe)), - # 'buy'] = 1 - # - - - dataframe.loc[ - (~(reduce(lambda x, y: x & y, conditions)))&OPTIMIZED_RULE(dataframe,params), - 'buy'] = 1 - - return dataframe - - return populate_buy_trend - - @staticmethod - def indicator_space() -> List[Dimension]: - """ - Define your Hyperopt space for searching strategy parameters - """ - return [ - - -#------------------------------------------------------------------------------------------------------- - - ## CUSTOM RULE TRESHOLDS - - # SKDecimal(5.0, 7.0,decimals=1, name='sell_trigger_0_roc_ab_value'),# 5 range 5-7 or 4-7 - # SKDecimal(3.2, 4.5,decimals=1, name='sell_trigger_0_roc2_ab_value'),#3.8 range 3.2-4.5 - # Integer(77, 82, name='sell_trigger_0_rsi_ab_value'),#79 range 82-77 - # Integer(90, 95, name='sell_trigger_0_rsi5_ab_value'),#94 range 95-90 - # Integer(63, 67, name='sell_trigger_0_rsi50_ab_value'),#66 range 67-63 - -#------------------------------------------------------------------------------------------------------- - - ##MAIN - - Categorical([1, 2, 3], name='sell_main_1_trend_strength'), #BIG TREND STR - Categorical([1, 2, 3], name='sell_main_2_trend_strength'), #SMALL UPTREND STR - - - #Categorical([-1, 0, 1], name='sell_main_2_small_uptrend_downtrend'), #SMALL UPTREND ON/OFF 1 is on -1 is down - -#------------------------------------------------------------------------------------------------------- -#------------------------------------------------------------------------------------------------------- - - ##INCLUDE/EXCLUDE RULES - - Categorical([True, False], name='include_sell_ab_9_rsi_above_value'), - Categorical([True, False], name='include_sell_ab_10_rsi_recent_pit_2_value'), - Categorical([True, False], name='include_sell_ab_11_rsi_recent_pit_4_value'), - Categorical([True, False], name='include_sell_ab_12_rsi5_above_value'), - Categorical([True, False], name='include_sell_ab_13_rsi50_above_value'), - - Categorical([True, False], name='include_sell_ab_14_roc_above_value'), - Categorical([True, False], name='include_sell_ab_15_roc50_above_value'), - Categorical([True, False], name='include_sell_ab_16_roc2_above_value'), - - Categorical([True, False], name='include_sell_ab_17_ppo5_above_value'), - Categorical([True, False], name='include_sell_ab_18_ppo10_above_value'), - Categorical([True, False], name='include_sell_ab_19_ppo25_above_value'), - - Categorical([True, False], name='include_sell_ab_20_ppo50_above_value'), - Categorical([True, False], name='include_sell_ab_21_ppo100_above_value'), - Categorical([True, False], name='include_sell_ab_22_ppo200_above_value'), - Categorical([True, False], name='include_sell_ab_23_ppo500_above_value'), - - ##USE AT A LATER STEP - Categorical([True, False], name='include_sell_ab_24_convsmall_above_value'), - Categorical([True, False], name='include_sell_ab_25_convmedium_above_value'), - Categorical([True, False], name='include_sell_ab_26_convlarge_above_value'), - Categorical([True, False], name='include_sell_ab_27_convultra_above_value'), - Categorical([True, False], name='include_sell_ab_28_convdist_above_value'), - - Categorical([True, False], name='include_sell_dist_1_dist50_more_value'), - Categorical([True, False], name='include_sell_dist_2_dist200_more_value'), - Categorical([True, False], name='include_sell_dist_3_dist400_more_value'), - Categorical([True, False], name='include_sell_dist_4_dist10k_more_value'), - - Categorical([True, False], name='include_sell_dist_5_dist_upbol50_more_value'), - Categorical([True, False], name='include_sell_dist_6_dist_upbol100_more_value'), - - - # FOR MORE DOWNTREND BUYS LIKELY - # Categorical([True, False], name='include_sell_dist_7_dist_lowbol50_more_value'), - # Categorical([True, False], name='include_sell_dist_8_dist_lowbol100_more_value'), - - #MORE LIKE TRIGGERS - Categorical([True, False], name='include_sell_dist_7_roc50sma_less_value'), - Categorical([True, False], name='include_sell_dist_8_roc200sma_less_value'), - - ##below high 100 - #Categorical([True, False], name='include_sell_dist_9_high100_more_value'), - -#------------------------------------------------------------------------------------------------------- -#------------------------------------------------------------------------------------------------------- - - ##ABOVE/BELOW VALUES - - Integer(35, 82, name='sell_ab_9_rsi_above_value'), - Integer(18, 35, name='sell_ab_10_rsi_recent_pit_2_value'), - Integer(18, 35, name='sell_ab_11_rsi_recent_pit_4_value'), - Integer(70, 91, name='sell_ab_12_rsi5_above_value'), - Integer(37, 60, name='sell_ab_13_rsi50_above_value'), - - Integer(-4, 10, name='sell_ab_14_roc_above_value'),#/2 - Integer(-2, 8, name='sell_ab_15_roc50_above_value'), - Integer(-4, 8, name='sell_ab_16_roc2_above_value'),#/2 - -#-------------------------------- - - ##CHANGE DEPENDING WHAT TYPE OF SELL --> PEAK OR DOWTRENDS - Integer(-4, 6, name='sell_ab_17_ppo5_above_value'),#/2 - Integer(-4, 6, name='sell_ab_18_ppo10_above_value'),#/2 - Integer(-10, 8, name='sell_ab_19_ppo25_above_value'),#/2 - - Integer(-10, 8, name='sell_ab_20_ppo50_above_value'),#/2 - Integer(-6, 6, name='sell_ab_21_ppo100_above_value'), - Integer(-6, 6, name='sell_ab_22_ppo200_above_value'), - Integer(-4, 5, name='sell_ab_23_ppo500_above_value'),#*2 - - # ##USE AT A LATER STEP - # - # Integer(-1, 6, name='sell_ab_24_convsmall_above_value'),#/2 # extreme 12 - # Integer(-1, 4, name='sell_ab_25_convmedium_above_value'),# extreme 6 - # Integer(-1, 7, name='sell_ab_26_convlarge_above_value'),# extreme 12 - # Integer(-1, 8, name='sell_ab_27_convultra_above_value'),#/2# extreme 12 - # - # Integer(-1, 6, name='sell_ab_28_convdist_above_value'), #very extreme not useful 10+ - -#------------------------------------------------------------------------------------------------------- - - #SMA'S GOING DOWN - - Categorical([True, False], name='sell_down_0a_sma3'), - Categorical([True, False], name='sell_down_0b_sma5'), - Categorical([True, False], name='sell_down_1_sma10'), - Categorical([True, False], name='sell_down_2_sma25'), - Categorical([True, False], name='sell_down_3_sma50'), - Categorical([True, False], name='sell_down_4_sma100'), - Categorical([True, False], name='sell_down_5_sma200'), - - Categorical([True, False], name='sell_down_6_sma400'), - Categorical([True, False], name='sell_down_7_sma10k'), - # Categorical([True, False], name='sell_down_8_sma20k'), - # Categorical([True, False], name='sell_down_9_sma30k'), - - Categorical([True, False], name='sell_down_10_convsmall'), - Categorical([True, False], name='sell_down_11_convmedium'), - Categorical([True, False], name='sell_down_12_convlarge'), - Categorical([True, False], name='sell_down_13_convultra'), - Categorical([True, False], name='sell_down_14_convdist'), - - Categorical([True, False], name='sell_down_15_vol50'), - Categorical([True, False], name='sell_down_16_vol100'), - Categorical([True, False], name='sell_down_17_vol175'), - Categorical([True, False], name='sell_down_18_vol250'), - Categorical([True, False], name='sell_down_19_vol500'), - - Categorical([True, False], name='sell_down_20_vol1000'), - Categorical([True, False], name='sell_down_21_vol100mean'), - Categorical([True, False], name='sell_down_22_vol250mean'), - -#------------------------------------------------------------------------------------------------------- - - ##ABOVE/BELOW SMAS - - Categorical([-1, 0, 1], name='sell_ab_1_sma10'), - Categorical([-1, 0, 1], name='sell_ab_2_sma25'), - Categorical([-1, 0, 1], name='sell_ab_3_sma50'), - - Categorical([-1, 0, 1], name='sell_ab_4_sma100'), - Categorical([-1, 0, 1], name='sell_ab_5_sma200'), - Categorical([-1, 0, 1], name='sell_ab_6_sma400'), - Categorical([-1, 0, 1], name='sell_ab_7_sma10k'), - -#------------------------------------------------------------------------------------------------------- - - ##DOWNSWINGS / UPSWINGS PPO'S - - ##UP OR DOWN (1 UP, 0 NOTHING, -1 DOWN) - - Categorical([-1, 0, 1], name='sell_swings_1_ppo5_up_or_down_bool'), - Categorical([-1, 0, 1], name='sell_swings_2_ppo10_up_or_down_bool'), - Categorical([-1, 0], name='sell_swings_3_ppo25_up_or_down_bool'), - - Categorical([-1, 0], name='sell_swings_4_ppo50_up_or_down_bool'), - Categorical([-1, 0], name='sell_swings_5_ppo100_up_or_down_bool'), - Categorical([-1, 0], name='sell_swings_6_ppo200_up_or_down_bool'), - Categorical([-1, 0], name='sell_swings_7_ppo500_up_or_down_bool'), - - Categorical([-1, 0], name='sell_swings_8_roc50_up_or_down_bool'), - Categorical([-1, 0], name='sell_swings_9_roc10_up_or_down_bool'), - -#------------------------------------------------------------------------------------------------------- - - #DISTANCES - - #FOR MORE TOP SELLERS - Integer(-6, 14, name='sell_dist_1_dist50_more_value'), #extreme, useless -4 ,30 - Integer(-8, 20, name='sell_dist_2_dist200_more_value'), #extreme, useless -12-40 - Integer(-15, 30, name='sell_dist_3_dist400_more_value'), - Integer(-15, 35, name='sell_dist_4_dist10k_more_value'), - - #FOR MORE TOP SELLERS - Integer(-30, 25, name='sell_dist_5_dist_upbol50_more_value'),#/2 - Integer(-30, 25, name='sell_dist_6_dist_upbol100_more_value'),#/2 - - - #FOR MORE DOWNTREND BUYS LIKELY - # Integer(-8, 50, name='sell_dist_7_dist_lowbol50_more_value'),#/2 ##set to more, as in higher from lower boll - # Integer(-8, 50, name='sell_dist_8_dist_lowbol100_more_value'),#/2 ##set to more, as in higher from lower boll - - # Integer(-70, 40, name='sell_dist_7_roc50sma_more_value'),#*2 ##fix less more - # Integer(-40, 12, name='sell_dist_8_roc200sma_more_value'),#*2 - - ##below high 100 - #Integer(0, 0, name='sell_dist_9_high100_more_value'), - -#------------------------------------------------------------------------------------------------------- - - - - - ] - - - - @staticmethod - def sell_strategy_generator(params: Dict[str, Any]) -> Callable: - """ - Define the sell strategy parameters to be used by hyperopt - """ - def populate_sell_trend(dataframe: DataFrame, metadata: dict) -> DataFrame: - """ - Sell strategy Hyperopt will build and use - """ - # print(params) - conditions = [] - # GUARDS AND TRENDS - - -#--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -#--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - - - ##MAIN SELECTORS - -#-------------------- - - ##VOLATILITY - - conditions.append(dataframe['vol_mid'] > 0 ) - - # conditions.append((dataframe['vol_low'] > 0) |(dataframe['vol_mid'] > 0) ) - - # conditions.append((dataframe['vol_high'] > 0) |(dataframe['vol_mid'] > 0) ) - -#-------------------- - - - ##PICKS TREND COMBO - - conditions.append( - - (dataframe['uptrend'] >= params['main_1_trend_strength']) - |#OR & - (dataframe['uptrendsmall'] >= params['main_2_trend_strength']) - - ) - - ##UPTREND - #conditions.append(dataframe['uptrend'] >= params['main_1_trend_strength']) - ##DOWNTREND - #conditions.append(dataframe['downtrend'] >= params['main_1_trend_strength']) - ##NOTREND - #conditions.append((dataframe['uptrend'] <1)&(dataframe['downtrend'] <1)) - -#------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -#------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - - ##ABOVE/BELOW VALUES - - #RSI BELOW - if 'include_ab_9_rsi_below_value' in params and params['include_ab_9_rsi_below_value']: - conditions.append(dataframe['rsi'] < params['ab_9_rsi_below_value']) - #RSI RECENT PEAK 5 - if 'include_ab_10_rsi_recent_peak_2_value' in params and params['include_ab_10_rsi_recent_peak_2_value']: - conditions.append(dataframe['rsi'].rolling(2).max() < params['ab_10_rsi_recent_peak_2_value']) - - #RSI RECENT PEAK 12 - if 'include_ab_11_rsi_recent_peak_4_value' in params and params['include_ab_11_rsi_recent_peak_4_value']: - conditions.append(dataframe['rsi'].rolling(4).max() < params['ab_11_rsi_recent_peak_4_value']) - #RSI5 BELOW - if 'include_ab_12_rsi5_below_value' in params and params['include_ab_12_rsi5_below_value']: - conditions.append(dataframe['rsi5'] < params['ab_12_rsi5_below_value']) - #RSI50 BELOW - if 'include_ab_13_rsi50_below_value' in params and params['include_ab_13_rsi50_below_value']: - conditions.append(dataframe['rsi50'] < params['ab_13_rsi50_below_value']) - -#----------------------- - - #ROC BELOW - if 'include_ab_14_roc_below_value' in params and params['include_ab_14_roc_below_value']: - conditions.append(dataframe['roc'] < (params['ab_14_roc_below_value']/2)) - #ROC50 BELOW - if 'include_ab_15_roc50_below_value' in params and params['include_ab_15_roc50_below_value']: - conditions.append(dataframe['roc50'] < (params['ab_15_roc50_below_value'])) - #ROC2 BELOW - if 'include_ab_16_roc2_below_value' in params and params['include_ab_16_roc2_below_value']: - conditions.append(dataframe['roc2'] < (params['ab_16_roc2_below_value']/2)) - -#----------------------- - - #PPO5 BELOW - if 'include_ab_17_ppo5_below_value' in params and params['include_ab_17_ppo5_below_value']: - conditions.append(dataframe['ppo5'] < (params['ab_17_ppo5_below_value']/2)) - #PPO10 BELOW - if 'include_ab_18_ppo10_below_value' in params and params['include_ab_18_ppo10_below_value']: - conditions.append(dataframe['ppo10'] < (params['ab_18_ppo10_below_value']/2)) - #PPO25 BELOW - if 'include_ab_19_ppo25_below_value' in params and params['include_ab_19_ppo25_below_value']: - conditions.append(dataframe['ppo25'] < (params['ab_19_ppo25_below_value']/2)) - - #PPO50 BELOW - if 'include_ab_20_ppo50_below_value' in params and params['include_ab_20_ppo50_below_value']: - conditions.append(dataframe['ppo50'] < (params['ab_20_ppo50_below_value']/2)) - #PPO100 BELOW - if 'include_ab_21_ppo100_below_value' in params and params['include_ab_21_ppo100_below_value']: - conditions.append(dataframe['ppo100'] < (params['ab_21_ppo100_below_value'])) - #PPO200 BELOW - if 'include_ab_22_ppo200_below_value' in params and params['include_ab_22_ppo200_below_value']: - conditions.append(dataframe['ppo200'] < (params['ab_22_ppo200_below_value'])) - #PPO500 BELOW - if 'include_ab_23_ppo500_below_value' in params and params['include_ab_23_ppo500_below_value']: - conditions.append(dataframe['ppo500'] < (params['ab_23_ppo500_below_value']*2)) - - ##USE AT A LATER STEP - - #convsmall BELOW - if 'include_ab_24_convsmall_below_value' in params and params['include_ab_24_convsmall_below_value']: - conditions.append(dataframe['convsmall'] < (params['ab_24_convsmall_below_value']/2)) - #convmedium BELOW - if 'include_ab_25_convmedium_below_value' in params and params['include_ab_25_convmedium_below_value']: - conditions.append(dataframe['convmedium'] < (params['ab_25_convmedium_below_value'])) - #convlarge BELOW - if 'include_ab_26_convlarge_below_value' in params and params['include_ab_26_convlarge_below_value']: - conditions.append(dataframe['convlarge'] < (params['ab_26_convlarge_below_value'])) - #convultra BELOW - if 'include_ab_27_convultra_below_value' in params and params['include_ab_27_convultra_below_value']: - conditions.append(dataframe['convultra'] < (params['ab_27_convultra_below_value']/2)) - #convdist BELOW - if 'include_ab_28_convdist_below_value' in params and params['include_ab_28_convdist_below_value']: - conditions.append(dataframe['convdist'] < (params['ab_28_convdist_below_value'])) - -#------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - - - ##SMA'S GOING UP - - if 'up_0a_sma3' in params and params['up_0a_sma3']: - conditions.append((dataframe['sma3'].shift(1) dataframe['sma10'])|(0.5 > params['ab_1_sma10'])) - conditions.append((dataframe['close'] < dataframe['sma10'])|(-0.5 < params['ab_1_sma10'])) - #SMA25 - conditions.append((dataframe['close'] > dataframe['sma25'])|(0.5 > params['ab_2_sma25'])) - conditions.append((dataframe['close'] < dataframe['sma25'])|(-0.5 < params['ab_2_sma25'])) - #SMA50 - conditions.append((dataframe['close'] > dataframe['sma50'])|(0.5 > params['ab_3_sma50'])) - conditions.append((dataframe['close'] < dataframe['sma50'])|(-0.5 < params['ab_3_sma50'])) - - - #SMA100 - conditions.append((dataframe['close'] > dataframe['sma100'])|(0.5 > params['ab_4_sma100'])) - conditions.append((dataframe['close'] < dataframe['sma100'])|(-0.5 < params['ab_4_sma100'])) - #SMA100 - conditions.append((dataframe['close'] > dataframe['sma200'])|(0.5 > params['ab_5_sma200'])) - conditions.append((dataframe['close'] < dataframe['sma200'])|(-0.5 < params['ab_5_sma200'])) - #SMA400 - conditions.append((dataframe['close'] > dataframe['sma400'])|(0.5 > params['ab_6_sma400'])) - conditions.append((dataframe['close'] < dataframe['sma400'])|(-0.5 < params['ab_6_sma400'])) - #SMA10k - conditions.append((dataframe['close'] > dataframe['sma10k'])|(0.5 > params['ab_7_sma10k'])) - conditions.append((dataframe['close'] < dataframe['sma10k'])|(-0.5 < params['ab_7_sma10k'])) - -#------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - - ##DOWNSWINGS / UPSWINGS PPO'S - - #ppo5 UP OR DOWN (1 UP, 0 NOTHING, -1 DOWN) - conditions.append((dataframe['ppo5'].shift(1) params['swings_1_ppo5_up_or_down_bool'])) - conditions.append((dataframe['ppo5'].shift(1) >dataframe['ppo5'])|(-0.5 < params['swings_1_ppo5_up_or_down_bool'])) - #ppo10 - conditions.append((dataframe['ppo10'].shift(1) params['swings_2_ppo10_up_or_down_bool'])) - conditions.append((dataframe['ppo10'].shift(1) >dataframe['ppo10'])|(-0.5 < params['swings_2_ppo10_up_or_down_bool'])) - #ppo25 - conditions.append((dataframe['ppo25'].shift(1) params['swings_3_ppo25_up_or_down_bool'])) - #conditions.append((dataframe['ppo25'].shift(1) >dataframe['ppo25'])|(-0.5 < params['swings_3_ppo25_up_or_down_bool'])) - - #ppo50 - conditions.append((dataframe['ppo50'].shift(2) params['swings_4_ppo50_up_or_down_bool'])) - #conditions.append((dataframe['ppo50'].shift(2) >dataframe['ppo50'])|(-0.5 < params['swings_4_ppo50_up_or_down_bool'])) - #ppo100 - conditions.append((dataframe['ppo100'].shift(3) params['swings_5_ppo100_up_or_down_bool'])) - #conditions.append((dataframe['ppo100'].shift(3) >dataframe['ppo100'])|(-0.5 < params['swings_5_ppo100_up_or_down_bool'])) - #ppo200 - conditions.append((dataframe['ppo200'].shift(4) params['swings_6_ppo200_up_or_down_bool'])) - #conditions.append((dataframe['ppo200'].shift(4) >dataframe['ppo200'])|(-0.5 < params['swings_6_ppo200_up_or_down_bool'])) - #ppo500 - conditions.append((dataframe['ppo500'].shift(5) params['swings_7_ppo500_up_or_down_bool'])) - #conditions.append((dataframe['ppo500'].shift(5) >dataframe['ppo500'])|(-0.5 < params['swings_7_ppo500_up_or_down_bool'])) - - #roc50 - conditions.append((dataframe['roc50'].shift(2) params['swings_8_roc50_up_or_down_bool'])) - #conditions.append((dataframe['roc50'].shift(3) >dataframe['roc50'])|(-0.5 < params['swings_8_roc50_up_or_down_bool'])) - #roc10 - conditions.append((dataframe['roc10'].shift(1) params['swings_9_roc10_up_or_down_bool'])) - #conditions.append((dataframe['roc10'].shift(2) >dataframe['roc10'])|(-0.5 < params['swings_9_roc10_up_or_down_bool'])) - - -#------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - - - ##DISTANCES/ROC - - #dist50 LESS THAN - if 'include_dist_1_dist50_less_value' in params and params['include_dist_1_dist50_less_value']: - conditions.append(dataframe['dist50'] < (params['dist_1_dist50_less_value'])) - #dist200 LESS THAN - if 'include_dist_2_dist200_less_value' in params and params['include_dist_2_dist200_less_value']: - conditions.append(dataframe['dist200'] < (params['dist_2_dist200_less_value'])) - - #dist400 LESS THAN - if 'include_dist_3_dist400_less_value' in params and params['include_dist_3_dist400_less_value']: - conditions.append(dataframe['dist400'] < (params['dist_3_dist400_less_value'])) - #dist10k LESS THAN - if 'include_dist_4_dist10k_less_value' in params and params['include_dist_4_dist10k_less_value']: - conditions.append(dataframe['dist10k'] < (params['dist_4_dist10k_less_value'])) - - #less =further from top bol - #dist_upbol50 LESS THAN - if 'include_dist_5_dist_upbol50_less_value' in params and params['include_dist_5_dist_upbol50_less_value']: - conditions.append(dataframe['dist_upbol50'] < (params['dist_5_dist_upbol50_less_value']/2)) - #dist_upbol100 LESS THAN - if 'include_dist_6_dist_upbol100_less_value' in params and params['include_dist_6_dist_upbol100_less_value']: - conditions.append(dataframe['dist_upbol100'] < (params['dist_6_dist_upbol100_less_value']/2)) - - # #less =closer to bot bol - # #dist_upbol50 LESS THAN - # if 'include_dist_7_dist_lowbol50_less_value' in params and params['include_dist_7_dist_lowbol50_less_value']: - # conditions.append(dataframe['dist_lowbol50'] < (params['dist_7_dist_lowbol50_less_value']/2)) - # #dist_upbol100 LESS THAN - # if 'include_dist_8_dist_lowbol100_less_value' in params and params['include_dist_8_dist_lowbol100_less_value']: - # conditions.append(dataframe['dist_lowbol100'] < (params['dist_8_dist_lowbol100_less_value']/2)) - - - - #others - ##roc50sma MORE THAN - if 'include_dist_7_roc50sma_less_value' in params and params['include_dist_7_roc50sma_less_value']: - conditions.append(dataframe['roc50sma'] < (params['dist_7_roc50sma_less_value']*2)) - #roc200sma MORE THAN - if 'include_dist_8_roc200sma_less_value' in params and params['include_dist_8_roc200sma_less_value']: - conditions.append(dataframe['roc200sma'] < (params['dist_8_roc200sma_less_value']*2)) - - ##ENABLE TO BUY AWAY FROM HIGH - # #HIGH500 TO CLOSE MORE THAN - #if 'include_dist_9_high100_more_value' in params and params['include_dist_9_high100_more_value']: - # conditions.append((dataframe['high100']-dataframe['close']) > ((dataframe['high100']/100* (params['dist_9_high100_more_value'])) - -#------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - - - - - - # Check that volume is not 0 - conditions.append(dataframe['volume'] > 0) - - if conditions: - - - # ##ENABLE SELLS ALWAYS ON OTHER VOLATILITYS - # dataframe.loc[ - # ((dataframe['vol_low'] > 0) |(dataframe['vol_high'] > 0) ), - # 'sell'] = 1 - - - # ##ENABLE PRODUCTION SELLS - # dataframe.loc[ - # (add_production_sells_low(dataframe)), - # 'sell'] = 1 - # - - dataframe.loc[ - (~(reduce(lambda x, y: x & y, conditions)))&OPTIMIZED_RULE(dataframe,params), - 'sell'] = 1 - - return dataframe - - return populate_sell_trend - - @staticmethod - def sell_indicator_space() -> List[Dimension]: - """ - Define your Hyperopt space for searching sell strategy parameters - """ - return [ - - -#------------------------------------------------------------------------------------------------------- - - ## CUSTOM RULE TRESHOLDS - - # SKDecimal(5.0, 7.0,decimals=1, name='sell_trigger_0_roc_ab_value'),# 5 range 5-7 or 4-7 - # SKDecimal(3.2, 4.5,decimals=1, name='sell_trigger_0_roc2_ab_value'),#3.8 range 3.2-4.5 - # Integer(77, 82, name='sell_trigger_0_rsi_ab_value'),#79 range 82-77 - # Integer(90, 95, name='sell_trigger_0_rsi5_ab_value'),#94 range 95-90 - # Integer(63, 67, name='sell_trigger_0_rsi50_ab_value'),#66 range 67-63 - -#------------------------------------------------------------------------------------------------------- - - ##MAIN - - Categorical([1, 2, 3], name='main_1_trend_strength'), #UPTREND STR - Categorical([1, 2, 3], name='main_2_trend_strength'), #SMALL UPTREND STR - - - #Categorical([-1, 0, 1], name='main_2_small_uptrend_downtrend'), #SMALL UPTREND ON/OFF 1 is on -1 is down - -#------------------------------------------------------------------------------------------------------- - - ##INCLUDE/EXCLUDE RULES - - Categorical([True, False], name='include_ab_9_rsi_below_value'), - Categorical([True, False], name='include_ab_10_rsi_recent_peak_2_value'), - Categorical([True, False], name='include_ab_11_rsi_recent_peak_4_value'), - Categorical([True, False], name='include_ab_12_rsi5_below_value'), - Categorical([True, False], name='include_ab_13_rsi50_below_value'), - - Categorical([True, False], name='include_ab_14_roc_below_value'), - Categorical([True, False], name='include_ab_15_roc50_below_value'), - Categorical([True, False], name='include_ab_16_roc2_below_value'), - - Categorical([True, False], name='include_ab_17_ppo5_below_value'), - Categorical([True, False], name='include_ab_18_ppo10_below_value'), - Categorical([True, False], name='include_ab_19_ppo25_below_value'), - - Categorical([True, False], name='include_ab_20_ppo50_below_value'), - Categorical([True, False], name='include_ab_21_ppo100_below_value'), - Categorical([True, False], name='include_ab_22_ppo200_below_value'), - Categorical([True, False], name='include_ab_23_ppo500_below_value'), - - ##USE AT A LATER STEP - Categorical([True, False], name='include_ab_24_convsmall_below_value'), - Categorical([True, False], name='include_ab_25_convmedium_below_value'), - Categorical([True, False], name='include_ab_26_convlarge_below_value'), - Categorical([True, False], name='include_ab_27_convultra_below_value'), - - Categorical([True, False], name='include_ab_28_convdist_below_value'), - - Categorical([True, False], name='include_dist_1_dist50_less_value'), - Categorical([True, False], name='include_dist_2_dist200_less_value'), - Categorical([True, False], name='include_dist_3_dist400_less_value'), - Categorical([True, False], name='include_dist_4_dist10k_less_value'), - - Categorical([True, False], name='include_dist_5_dist_upbol50_less_value'), - Categorical([True, False], name='include_dist_6_dist_upbol100_less_value'), - - - # FOR MORE DOWNTREND BUYS LIKELY - # Categorical([True, False], name='include_dist_7_dist_lowbol50_less_value'), - # Categorical([True, False], name='include_dist_8_dist_lowbol100_less_value'), - - #MORE LIKE TRIGGERS - Categorical([True, False], name='include_dist_7_roc50sma_less_value'), - Categorical([True, False], name='include_dist_8_roc200sma_less_value'), - - ##below high 100 - #Categorical([True, False], name='include_dist_9_high100_more_value'), - - - -#------------------------------------------------------------------------------------------------------- - - ##ABOVE/BELOW VALUES - - Integer(35, 75, name='ab_9_rsi_below_value'), - Integer(60, 82, name='ab_10_rsi_recent_peak_2_value'), - Integer(60, 82, name='ab_11_rsi_recent_peak_4_value'), - Integer(40, 101, name='ab_12_rsi5_below_value'), - Integer(37, 73, name='ab_13_rsi50_below_value'), - - Integer(-6, 10, name='ab_14_roc_below_value'),#/2 - Integer(-8, 8, name='ab_15_roc50_below_value'), - Integer(-4, 6, name='ab_16_roc2_below_value'),#/2 - -#-------------------------------- - - Integer(-4, 4, name='ab_17_ppo5_below_value'),#/2 - Integer(-5, 5, name='ab_18_ppo10_below_value'),#/2 - Integer(-8, 10, name='ab_19_ppo25_below_value'),#/2 - - Integer(-6, 7, name='ab_20_ppo50_below_value'),#/2 - Integer(-6, 7, name='ab_21_ppo100_below_value'), - Integer(-5, 7, name='ab_22_ppo200_below_value'), - Integer(-4, 4, name='ab_23_ppo500_below_value'),#*2 - - ##USE AT A LATER STEP - - Integer(1, 12, name='ab_24_convsmall_below_value'),#/2 #final - Integer(1, 6, name='ab_25_convmedium_below_value'),#final - Integer(1, 15, name='ab_26_convlarge_below_value'), #final - Integer(2, 12, name='ab_27_convultra_below_value'),#/2 #final - - Integer(2, 30, name='ab_28_convdist_below_value'), - -#------------------------------------------------------------------------------------------------------- - - #SMA'S GOING UP - - Categorical([True, False], name='up_0a_sma3'), - Categorical([True, False], name='up_0b_sma5'), - Categorical([True, False], name='up_1_sma10'), - Categorical([True, False], name='up_2_sma25'), - Categorical([True, False], name='up_3_sma50'), - Categorical([True, False], name='up_4_sma100'), - Categorical([True, False], name='up_5_sma200'), - - Categorical([True, False], name='up_6_sma400'), - Categorical([True, False], name='up_7_sma10k'), - # Categorical([True, False], name='up_8_sma20k'), - # Categorical([True, False], name='up_9_sma30k'), - - Categorical([True, False], name='up_10_convsmall'), - Categorical([True, False], name='up_11_convmedium'), - Categorical([True, False], name='up_12_convlarge'), - Categorical([True, False], name='up_13_convultra'), - Categorical([True, False], name='up_14_convdist'), - - Categorical([True, False], name='up_15_vol50'), - Categorical([True, False], name='up_16_vol100'), - Categorical([True, False], name='up_17_vol175'), - Categorical([True, False], name='up_18_vol250'), - Categorical([True, False], name='up_19_vol500'), - - Categorical([True, False], name='up_20_vol1000'), - Categorical([True, False], name='up_21_vol100mean'), - Categorical([True, False], name='up_22_vol250mean'), - -#------------------------------------------------------------------------------------------------------- - - ##ABOVE/BELOW SMAS - - Categorical([-1, 0, 1], name='ab_1_sma10'), - Categorical([-1, 0, 1], name='ab_2_sma25'), - Categorical([-1, 0, 1], name='ab_3_sma50'), - - Categorical([-1, 0, 1], name='ab_4_sma100'), - Categorical([-1, 0, 1], name='ab_5_sma200'), - Categorical([-1, 0, 1], name='ab_6_sma400'), - Categorical([-1, 0, 1], name='ab_7_sma10k'), - -#------------------------------------------------------------------------------------------------------- - - ##DOWNSWINGS / UPSWINGS PPO'S - - ##UP OR DOWN (1 UP, 0 NOTHING, -1 DOWN) - - Categorical([-1, 0, 1], name='swings_1_ppo5_up_or_down_bool'), # -1 down, 1 up , 0 off - Categorical([-1, 0, 1],name='swings_2_ppo10_up_or_down_bool'), - Categorical([-1, 0, 1], name='swings_3_ppo25_up_or_down_bool'), #1 up , 0 off - - Categorical([0, 1], name='swings_4_ppo50_up_or_down_bool'), - Categorical([0, 1], name='swings_5_ppo100_up_or_down_bool'), - Categorical([0, 1], name='swings_6_ppo200_up_or_down_bool'), - Categorical([ 0, 1],name='swings_7_ppo500_up_or_down_bool'), - - Categorical([0, 1], name='swings_8_roc50_up_or_down_bool'), - Categorical([0, 1], name='swings_9_roc10_up_or_down_bool'), - -#------------------------------------------------------------------------------------------------------- - - ##DISTANCES - - Integer(-7, 14, name='dist_1_dist50_less_value'), ##extreme 8-30 - Integer(-8, 25, name='dist_2_dist200_less_value'), ##extreme 12 -40 - Integer(-12, 35, name='dist_3_dist400_less_value'), - Integer(-12, 40, name='dist_4_dist10k_less_value'), - - Integer(-25, 30, name='dist_5_dist_upbol50_less_value'),#/2 - Integer(-25, 30, name='dist_6_dist_upbol100_less_value'),#/2 - - - # FOR MORE DOWNTREND BUYS LIKELY - # Integer(-6, 100, name='dist_7_dist_lowbol50_less_value'),#/2 - # Integer(-6, 100, name='dist_8_dist_lowbol100_less_value'),#/2 - - ##MORE LIKE TRIGGERS - # Integer(-40, 70, name='dist_7_roc50sma_less_value'),#*2 ##pretty extreme - # Integer(-12, 40, name='dist_8_roc200sma_less_value'),#*2 - - ##below high 100 - #Integer(0, 0, name='dist_9_high100_more_value'), - -#------------------------------------------------------------------------------------------------------- - - - - - - ] - - -def OPTIMIZED_RULE(dataframe,params): - return( - - (dataframe['sma100'] < dataframe['close']) - - ) - -def add_production_buys_mid(dataframe): - return( - - MID_VOLATILITY(dataframe) - & - mid_volatility_buyer(dataframe) - ) - -def add_production_sells_mid(dataframe): - return( - - MID_VOLATILITY(dataframe) - & - mid_volatility_seller(dataframe) - ) - - From 8b2c14a6fa65d31d8496c5991a64380ef81b8127 Mon Sep 17 00:00:00 2001 From: theluxaz Date: Thu, 14 Oct 2021 01:15:43 +0300 Subject: [PATCH 027/208] Readme fix --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 1eb96f200..906e19ef7 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,9 @@ For any other type of installation please refer to [Installation doc](https://ww usage: freqtrade [-h] [-V] {trade,create-userdir,new-config,new-strategy,download-data,convert-data,convert-trade-data,list-data,backtesting,edge,hyperopt,hyperopt-list,hyperopt-show,list-exchanges,list-hyperopts,list-markets,list-pairs,list-strategies,list-timeframes,show-trades,test-pairlist,install-ui,plot-dataframe,plot-profit,webserver} ... + Free, open source crypto trading bot + positional arguments: {trade,create-userdir,new-config,new-strategy,download-data,convert-data,convert-trade-data,list-data,backtesting,edge,hyperopt,hyperopt-list,hyperopt-show,list-exchanges,list-hyperopts,list-markets,list-pairs,list-strategies,list-timeframes,show-trades,test-pairlist,install-ui,plot-dataframe,plot-profit,webserver} trade Trade module. @@ -110,9 +112,11 @@ positional arguments: plot-dataframe Plot candles with indicators. plot-profit Generate plot showing profits. webserver Webserver module. + optional arguments: -h, --help show this help message and exit -V, --version show program's version number and exit + ``` ### Telegram RPC commands From ed39b8dab06e4b1676710b402de4117ddfc4659f Mon Sep 17 00:00:00 2001 From: theluxaz Date: Thu, 14 Oct 2021 01:18:16 +0300 Subject: [PATCH 028/208] fixed profit total calculation --- freqtrade/optimize/optimize_reports.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 5187cb0ba..0d001b230 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -82,7 +82,7 @@ def _generate_result_line(result: DataFrame, starting_balance: int, first_column 'profit_sum_pct': round(profit_sum * 100.0, 2), 'profit_total_abs': result['profit_abs'].sum(), 'profit_total': profit_total, - 'profit_total_pct': round(profit_sum * 100.0, 2), + 'profit_total_pct': round(profit_total * 100.0, 2), 'duration_avg': str(timedelta( minutes=round(result['trade_duration'].mean())) ) if not result.empty else '0:00', From 0bb7ea10ab03034c05e33443889c10e8fa8fd5dc Mon Sep 17 00:00:00 2001 From: theluxaz Date: Thu, 14 Oct 2021 01:34:30 +0300 Subject: [PATCH 029/208] Fixed minor header for backtesting --- freqtrade/optimize/optimize_reports.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 0d001b230..0e8467788 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -54,6 +54,14 @@ def _get_line_header(first_column: str, stake_currency: str) -> List[str]: f'Tot Profit {stake_currency}', 'Tot Profit %', 'Avg Duration', 'Win Draw Loss Win%'] +def _get_line_header_sell(first_column: str, stake_currency: str) -> List[str]: + """ + Generate header lines (goes in line with _generate_result_line()) + """ + return [first_column, 'Sells', 'Avg Profit %', 'Cum Profit %', + f'Tot Profit {stake_currency}', 'Tot Profit %', 'Avg Duration', + 'Win Draw Loss Win%'] + def _generate_wins_draws_losses(wins, draws, losses): if wins > 0 and losses == 0: @@ -608,8 +616,10 @@ def text_table_tags(tag_type:str, tag_results: List[Dict[str, Any]], stake_curre :param stake_currency: stake-currency - used to correctly name headers :return: pretty printed table with tabulate as string """ - - headers = _get_line_header("TAG", stake_currency) + if(tag_type=="buy_tag"): + headers = _get_line_header("TAG", stake_currency) + else: + headers = _get_line_header_sell("TAG", stake_currency) floatfmt = _get_line_floatfmt(stake_currency) output = [[ t['key'], t['trades'], t['profit_mean_pct'], t['profit_sum_pct'], t['profit_total_abs'], From 69a59cdf37e79de1e476dc32d5c9a482bcd9ea51 Mon Sep 17 00:00:00 2001 From: theluxaz Date: Mon, 18 Oct 2021 23:56:41 +0300 Subject: [PATCH 030/208] Fixed flake 8, changed sell_tag to exit_tag and fixed telegram functions --- freqtrade/data/btanalysis.py | 2 +- freqtrade/enums/signaltype.py | 2 +- freqtrade/freqtradebot.py | 41 ++++--- freqtrade/optimize/backtesting.py | 21 ++-- freqtrade/optimize/optimize_reports.py | 93 ++++++--------- freqtrade/persistence/migrations.py | 8 +- freqtrade/persistence/models.py | 158 ++++++++++++++----------- freqtrade/rpc/rpc.py | 11 +- freqtrade/rpc/telegram.py | 83 +++++-------- freqtrade/strategy/interface.py | 8 +- 10 files changed, 206 insertions(+), 221 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 82b2bb3a9..3dba635e6 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -30,7 +30,7 @@ BT_DATA_COLUMNS = ['pair', 'stake_amount', 'amount', 'open_date', 'close_date', 'fee_open', 'fee_close', 'trade_duration', 'profit_ratio', 'profit_abs', 'sell_reason', 'initial_stop_loss_abs', 'initial_stop_loss_ratio', 'stop_loss_abs', - 'stop_loss_ratio', 'min_rate', 'max_rate', 'is_open', 'buy_tag', 'sell_tag'] + 'stop_loss_ratio', 'min_rate', 'max_rate', 'is_open', 'buy_tag', 'exit_tag'] def get_latest_optimize_filename(directory: Union[Path, str], variant: str) -> str: diff --git a/freqtrade/enums/signaltype.py b/freqtrade/enums/signaltype.py index 32ac19ba4..4437f49e3 100644 --- a/freqtrade/enums/signaltype.py +++ b/freqtrade/enums/signaltype.py @@ -14,4 +14,4 @@ class SignalTagType(Enum): Enum for signal columns """ BUY_TAG = "buy_tag" - SELL_TAG = "sell_tag" \ No newline at end of file + EXIT_TAG = "exit_tag" diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index d415c9d93..73d9bb382 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -201,11 +201,11 @@ class FreqtradeBot(LoggingMixin): 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 ''}", + '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) @@ -420,7 +420,7 @@ class FreqtradeBot(LoggingMixin): return False # running get_signal on historical data fetched - (buy, sell, buy_tag,sell_tag) = self.strategy.get_signal( + (buy, sell, buy_tag, exit_tag) = self.strategy.get_signal( pair, self.strategy.timeframe, analyzed_df @@ -700,15 +700,14 @@ class FreqtradeBot(LoggingMixin): logger.debug('Handling %s ...', trade) (buy, sell) = (False, False) - - sell_tag=None + exit_tag = None 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, buy_tag, sell_tag) = self.strategy.get_signal( + (buy, sell, buy_tag, exit_tag) = self.strategy.get_signal( trade.pair, self.strategy.timeframe, analyzed_df @@ -716,7 +715,7 @@ class FreqtradeBot(LoggingMixin): logger.debug('checking sell') sell_rate = self.exchange.get_rate(trade.pair, refresh=True, side="sell") - if self._check_and_execute_exit(trade, sell_rate, buy, sell, sell_tag): + if self._check_and_execute_exit(trade, sell_rate, buy, sell, exit_tag): return True logger.debug('Found no sell signal for %s.', trade) @@ -854,7 +853,7 @@ class FreqtradeBot(LoggingMixin): f"for pair {trade.pair}.") def _check_and_execute_exit(self, trade: Trade, exit_rate: float, - buy: bool, sell: bool, sell_tag: Optional[str]) -> bool: + buy: bool, sell: bool, exit_tag: Optional[str]) -> bool: """ Check and execute exit """ @@ -865,8 +864,9 @@ class FreqtradeBot(LoggingMixin): ) if should_sell.sell_flag: - logger.info(f'Executing Sell for {trade.pair}. Reason: {should_sell.sell_type}. Tag: {sell_tag if sell_tag is not None else "None"}') - self.execute_trade_exit(trade, exit_rate, should_sell,sell_tag) + logger.info( + f'Executing Sell for {trade.pair}. Reason: {should_sell.sell_type}. Tag: {exit_tag if exit_tag is not None else "None"}') + self.execute_trade_exit(trade, exit_rate, should_sell, exit_tag) return True return False @@ -1067,7 +1067,12 @@ class FreqtradeBot(LoggingMixin): 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, sell_tag: Optional[str] = None) -> bool: + def execute_trade_exit( + self, + trade: Trade, + limit: float, + sell_reason: SellCheckTuple, + exit_tag: Optional[str] = None) -> bool: """ Executes a trade exit for the given trade and limit :param trade: Trade instance @@ -1144,8 +1149,8 @@ class FreqtradeBot(LoggingMixin): trade.sell_order_status = '' trade.close_rate_requested = limit trade.sell_reason = sell_reason.sell_reason - if(sell_tag is not None): - trade.sell_tag = sell_tag + if(exit_tag is not None): + trade.exit_tag = exit_tag # 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) @@ -1187,7 +1192,7 @@ class FreqtradeBot(LoggingMixin): 'profit_amount': profit_trade, 'profit_ratio': profit_ratio, 'sell_reason': trade.sell_reason, - 'sell_tag': trade.sell_tag, + 'exit_tag': trade.exit_tag, 'open_date': trade.open_date, 'close_date': trade.close_date or datetime.utcnow(), 'stake_currency': self.config['stake_currency'], @@ -1231,7 +1236,7 @@ class FreqtradeBot(LoggingMixin): 'profit_amount': profit_trade, 'profit_ratio': profit_ratio, 'sell_reason': trade.sell_reason, - 'sell_tag': trade.sell_tag, + 'exit_tag': trade.exit_tag, 'open_date': trade.open_date, 'close_date': trade.close_date or datetime.now(timezone.utc), 'stake_currency': self.config['stake_currency'], diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 69f2d2580..6c2a20cb1 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -44,7 +44,8 @@ SELL_IDX = 4 LOW_IDX = 5 HIGH_IDX = 6 BUY_TAG_IDX = 7 -SELL_TAG_IDX = 8 +EXIT_TAG_IDX = 8 + class Backtesting: """ @@ -247,7 +248,7 @@ class Backtesting: """ # Every change to this headers list must evaluate further usages of the resulting tuple # and eventually change the constants for indexes at the top - headers = ['date', 'buy', 'open', 'close', 'sell', 'low', 'high', 'buy_tag', 'sell_tag'] + headers = ['date', 'buy', 'open', 'close', 'sell', 'low', 'high', 'buy_tag', 'exit_tag'] data: Dict = {} self.progress.init_step(BacktestState.CONVERT, len(processed)) @@ -259,7 +260,7 @@ class Backtesting: pair_data.loc[:, 'buy'] = 0 # cleanup if buy_signal is exist pair_data.loc[:, 'sell'] = 0 # cleanup if sell_signal is exist pair_data.loc[:, 'buy_tag'] = None # cleanup if buy_tag is exist - pair_data.loc[:, 'sell_tag'] = None # cleanup if sell_tag is exist + pair_data.loc[:, 'exit_tag'] = None # cleanup if exit_tag is exist df_analyzed = self.strategy.advise_sell( self.strategy.advise_buy(pair_data, {'pair': pair}), {'pair': pair}).copy() @@ -271,7 +272,7 @@ class Backtesting: df_analyzed.loc[:, 'buy'] = df_analyzed.loc[:, 'buy'].shift(1) df_analyzed.loc[:, 'sell'] = df_analyzed.loc[:, 'sell'].shift(1) df_analyzed.loc[:, 'buy_tag'] = df_analyzed.loc[:, 'buy_tag'].shift(1) - df_analyzed.loc[:, 'sell_tag'] = df_analyzed.loc[:, 'sell_tag'].shift(1) + df_analyzed.loc[:, 'exit_tag'] = df_analyzed.loc[:, 'exit_tag'].shift(1) # Update dataprovider cache self.dataprovider._set_cached_df(pair, self.timeframe, df_analyzed) @@ -359,8 +360,10 @@ class Backtesting: if sell.sell_flag: trade.close_date = sell_candle_time - if(sell_row[SELL_TAG_IDX] is not None): - trade.sell_tag = sell_row[SELL_TAG_IDX] + if(sell_row[EXIT_TAG_IDX] is not None): + trade.exit_tag = sell_row[EXIT_TAG_IDX] + else: + trade.exit_tag = None trade.sell_reason = sell.sell_reason trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60) closerate = self._get_close_rate(sell_row, trade, sell, trade_dur) @@ -389,7 +392,7 @@ class Backtesting: detail_data = detail_data.loc[ (detail_data['date'] >= sell_candle_time) & (detail_data['date'] < sell_candle_end) - ].copy() + ].copy() if len(detail_data) == 0: # Fall back to "regular" data if no detail data was found for this candle return self._get_sell_trade_entry_for_candle(trade, sell_row) @@ -435,7 +438,7 @@ class Backtesting: if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount): # Enter trade has_buy_tag = len(row) >= BUY_TAG_IDX + 1 - has_sell_tag = len(row) >= SELL_TAG_IDX + 1 + has_exit_tag = len(row) >= EXIT_TAG_IDX + 1 trade = LocalTrade( pair=pair, open_rate=row[OPEN_IDX], @@ -446,7 +449,7 @@ class Backtesting: fee_close=self.fee, is_open=True, buy_tag=row[BUY_TAG_IDX] if has_buy_tag else None, - sell_tag=row[SELL_TAG_IDX] if has_sell_tag else None, + exit_tag=row[EXIT_TAG_IDX] if has_exit_tag else None, exchange='backtesting', ) return trade diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 0e8467788..30005f524 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -54,6 +54,7 @@ def _get_line_header(first_column: str, stake_currency: str) -> List[str]: f'Tot Profit {stake_currency}', 'Tot Profit %', 'Avg Duration', 'Win Draw Loss Win%'] + def _get_line_header_sell(first_column: str, stake_currency: str) -> List[str]: """ Generate header lines (goes in line with _generate_result_line()) @@ -134,12 +135,13 @@ def generate_pair_metrics(data: Dict[str, Dict], stake_currency: str, starting_b tabular_data.append(_generate_result_line(results, starting_balance, 'TOTAL')) return tabular_data -def generate_tag_metrics(tag_type:str, data: Dict[str, Dict], stake_currency: str, starting_balance: int, - results: DataFrame, skip_nan: bool = False) -> List[Dict]: + +def generate_tag_metrics(tag_type: str, + starting_balance: int, + results: DataFrame, + skip_nan: bool = False) -> List[Dict]: """ Generates and returns a list of metrics for the given tag trades and the results dataframe - :param data: Dict of containing data that was used during backtesting. - :param stake_currency: stake-currency - used to correctly name headers :param starting_balance: Starting balance :param results: Dataframe containing the backtest results :param skip_nan: Print "left open" open trades @@ -148,32 +150,6 @@ def generate_tag_metrics(tag_type:str, data: Dict[str, Dict], stake_currency: st tabular_data = [] - # for tag, count in results[tag_type].value_counts().iteritems(): - # result = results.loc[results[tag_type] == tag] - # - # profit_mean = result['profit_ratio'].mean() - # profit_sum = result['profit_ratio'].sum() - # profit_total = profit_sum / max_open_trades - # - # tabular_data.append( - # { - # 'sell_reason': tag, - # 'trades': count, - # 'wins': len(result[result['profit_abs'] > 0]), - # 'draws': len(result[result['profit_abs'] == 0]), - # 'losses': len(result[result['profit_abs'] < 0]), - # 'profit_mean': profit_mean, - # 'profit_mean_pct': round(profit_mean * 100, 2), - # 'profit_sum': profit_sum, - # 'profit_sum_pct': round(profit_sum * 100, 2), - # 'profit_total_abs': result['profit_abs'].sum(), - # 'profit_total': profit_total, - # 'profit_total_pct': round(profit_total * 100, 2), - # } - # ) - # - # tabular_data = [] - for tag, count in results[tag_type].value_counts().iteritems(): result = results[results[tag_type] == tag] if skip_nan and result['profit_abs'].isnull().all(): @@ -188,6 +164,7 @@ def generate_tag_metrics(tag_type:str, data: Dict[str, Dict], stake_currency: st tabular_data.append(_generate_result_line(results, starting_balance, 'TOTAL')) return tabular_data + def _generate_tag_result_line(result: DataFrame, starting_balance: int, first_column: str) -> Dict: """ Generate one result dict, with "first_column" as key. @@ -408,12 +385,10 @@ def generate_strategy_stats(btdata: Dict[str, DataFrame], starting_balance=starting_balance, results=results, skip_nan=False) - buy_tag_results = generate_tag_metrics("buy_tag",btdata, stake_currency=stake_currency, - starting_balance=starting_balance, - results=results, skip_nan=False) - sell_tag_results = generate_tag_metrics("sell_tag",btdata, stake_currency=stake_currency, - starting_balance=starting_balance, - results=results, skip_nan=False) + buy_tag_results = generate_tag_metrics("buy_tag", starting_balance=starting_balance, + results=results, skip_nan=False) + exit_tag_results = generate_tag_metrics("exit_tag", starting_balance=starting_balance, + results=results, skip_nan=False) sell_reason_stats = generate_sell_reason_stats(max_open_trades=max_open_trades, results=results) @@ -439,7 +414,7 @@ def generate_strategy_stats(btdata: Dict[str, DataFrame], 'worst_pair': worst_pair, 'results_per_pair': pair_results, 'results_per_buy_tag': buy_tag_results, - 'results_per_sell_tag': sell_tag_results, + 'results_per_exit_tag': exit_tag_results, 'sell_reason_summary': sell_reason_stats, 'left_open_trades': left_open_results, 'total_trades': len(results), @@ -609,30 +584,38 @@ def text_table_sell_reason(sell_reason_stats: List[Dict[str, Any]], stake_curren ] for t in sell_reason_stats] return tabulate(output, headers=headers, tablefmt="orgtbl", stralign="right") -def text_table_tags(tag_type:str, tag_results: List[Dict[str, Any]], stake_currency: str) -> str: + +def text_table_tags(tag_type: str, tag_results: List[Dict[str, Any]], stake_currency: str) -> str: """ Generates and returns a text table for the given backtest data and the results dataframe :param pair_results: List of Dictionaries - one entry per pair + final TOTAL row :param stake_currency: stake-currency - used to correctly name headers :return: pretty printed table with tabulate as string """ - if(tag_type=="buy_tag"): + if(tag_type == "buy_tag"): headers = _get_line_header("TAG", stake_currency) else: headers = _get_line_header_sell("TAG", stake_currency) floatfmt = _get_line_floatfmt(stake_currency) - output = [[ - t['key'], t['trades'], t['profit_mean_pct'], t['profit_sum_pct'], t['profit_total_abs'], - t['profit_total_pct'], t['duration_avg'], - _generate_wins_draws_losses(t['wins'], t['draws'], t['losses']) - ] for t in tag_results] + output = [ + [ + t['key'] if t['key'] is not None and len( + t['key']) > 0 else "OTHER", + t['trades'], + t['profit_mean_pct'], + t['profit_sum_pct'], + t['profit_total_abs'], + t['profit_total_pct'], + t['duration_avg'], + _generate_wins_draws_losses( + t['wins'], + t['draws'], + t['losses'])] for t in tag_results] # Ignore type as floatfmt does allow tuples but mypy does not know that return tabulate(output, headers=headers, floatfmt=floatfmt, tablefmt="orgtbl", stralign="right") - - def text_table_strategy(strategy_results, stake_currency: str) -> str: """ Generate summary table per strategy @@ -752,14 +735,19 @@ def show_backtest_result(strategy: str, results: Dict[str, Any], stake_currency: print(' BACKTESTING REPORT '.center(len(table.splitlines()[0]), '=')) print(table) - - table = text_table_tags("buy_tag", results['results_per_buy_tag'], stake_currency=stake_currency) + table = text_table_tags( + "buy_tag", + results['results_per_buy_tag'], + stake_currency=stake_currency) if isinstance(table, str) and len(table) > 0: print(' BUY TAG STATS '.center(len(table.splitlines()[0]), '=')) print(table) - table = text_table_tags("sell_tag",results['results_per_sell_tag'], stake_currency=stake_currency) + table = text_table_tags( + "exit_tag", + results['results_per_exit_tag'], + stake_currency=stake_currency) if isinstance(table, str) and len(table) > 0: print(' SELL TAG STATS '.center(len(table.splitlines()[0]), '=')) @@ -771,10 +759,6 @@ def show_backtest_result(strategy: str, results: Dict[str, Any], stake_currency: print(' SELL REASON STATS '.center(len(table.splitlines()[0]), '=')) print(table) - - - - table = text_table_bt_results(results['left_open_trades'], stake_currency=stake_currency) if isinstance(table, str) and len(table) > 0: print(' LEFT OPEN TRADES REPORT '.center(len(table.splitlines()[0]), '=')) @@ -785,12 +769,9 @@ def show_backtest_result(strategy: str, results: Dict[str, Any], stake_currency: print(' SUMMARY METRICS '.center(len(table.splitlines()[0]), '=')) print(table) - - if isinstance(table, str) and len(table) > 0: print('=' * len(table.splitlines()[0])) - print() diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index 0f07c13b5..d0b3add3c 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -48,7 +48,7 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col sell_reason = get_column_def(cols, 'sell_reason', 'null') strategy = get_column_def(cols, 'strategy', 'null') buy_tag = get_column_def(cols, 'buy_tag', 'null') - sell_tag = get_column_def(cols, 'sell_tag', 'null') + exit_tag = get_column_def(cols, 'exit_tag', 'null') # If ticker-interval existed use that, else null. if has_column(cols, 'ticker_interval'): timeframe = get_column_def(cols, 'timeframe', 'ticker_interval') @@ -83,7 +83,7 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col stake_amount, amount, amount_requested, open_date, close_date, open_order_id, stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct, stoploss_order_id, stoploss_last_update, - max_rate, min_rate, sell_reason, sell_order_status, strategy, buy_tag, sell_tag, + max_rate, min_rate, sell_reason, sell_order_status, strategy, buy_tag, exit_tag, timeframe, open_trade_value, close_profit_abs ) select id, lower(exchange), pair, @@ -99,7 +99,7 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col {stoploss_order_id} stoploss_order_id, {stoploss_last_update} stoploss_last_update, {max_rate} max_rate, {min_rate} min_rate, {sell_reason} sell_reason, {sell_order_status} sell_order_status, - {strategy} strategy, {buy_tag} buy_tag, {sell_tag} sell_tag, {timeframe} timeframe, + {strategy} strategy, {buy_tag} buy_tag, {exit_tag} exit_tag, {timeframe} timeframe, {open_trade_value} open_trade_value, {close_profit_abs} close_profit_abs from {table_back_name} """)) @@ -158,7 +158,7 @@ def check_migrate(engine, decl_base, previous_tables) -> None: table_back_name = get_backup_name(tabs, 'trades_bak') # Check for latest column - if not has_column(cols, 'sell_tag'): + if not has_column(cols, 'exit_tag'): logger.info(f'Running database migration for trades - backup: {table_back_name}') migrate_trades_table(decl_base, inspector, engine, table_back_name, cols) # Reread columns - the above recreated the table! diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 33a4429c0..945201982 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -258,7 +258,7 @@ class LocalTrade(): sell_order_status: str = '' strategy: str = '' buy_tag: Optional[str] = None - sell_tag: Optional[str] = None + exit_tag: Optional[str] = None timeframe: Optional[int] = None def __init__(self, **kwargs): @@ -325,8 +325,9 @@ class LocalTrade(): 'profit_pct': round(self.close_profit * 100, 2) if self.close_profit else None, 'profit_abs': self.close_profit_abs, - 'sell_reason': (f' ({self.sell_reason})' if self.sell_reason else ''), #+str(self.sell_reason) ## CHANGE TO BUY TAG IF NEEDED - 'sell_tag': (f' ({self.sell_tag})' if self.sell_tag else '') , + # +str(self.sell_reason) ## CHANGE TO BUY TAG IF NEEDED + 'sell_reason': (f' ({self.sell_reason})' if self.sell_reason else ''), + 'exit_tag': (f' ({self.exit_tag})' if self.exit_tag else ''), 'sell_order_status': self.sell_order_status, 'stop_loss_abs': self.stop_loss, 'stop_loss_ratio': self.stop_loss_pct if self.stop_loss_pct else None, @@ -708,7 +709,7 @@ class Trade(_DECL_BASE, LocalTrade): sell_order_status = Column(String(100), nullable=True) strategy = Column(String(100), nullable=True) buy_tag = Column(String(100), nullable=True) - sell_tag = Column(String(100), nullable=True) + exit_tag = Column(String(100), nullable=True) timeframe = Column(Integer, nullable=True) def __init__(self, **kwargs): @@ -873,28 +874,28 @@ class Trade(_DECL_BASE, LocalTrade): if(pair is not None): tag_perf = Trade.query.with_entities( - Trade.buy_tag, - func.sum(Trade.close_profit).label('profit_sum'), - func.sum(Trade.close_profit_abs).label('profit_sum_abs'), - func.count(Trade.pair).label('count') - ).filter(Trade.is_open.is_(False))\ - .filter(Trade.pair.lower() == pair.lower()) \ - .order_by(desc('profit_sum_abs')) \ - .all() + Trade.buy_tag, + func.sum(Trade.close_profit).label('profit_sum'), + func.sum(Trade.close_profit_abs).label('profit_sum_abs'), + func.count(Trade.pair).label('count') + ).filter(Trade.is_open.is_(False))\ + .filter(Trade.pair == pair) \ + .order_by(desc('profit_sum_abs')) \ + .all() else: tag_perf = Trade.query.with_entities( - Trade.buy_tag, - func.sum(Trade.close_profit).label('profit_sum'), - func.sum(Trade.close_profit_abs).label('profit_sum_abs'), - func.count(Trade.pair).label('count') - ).filter(Trade.is_open.is_(False))\ - .group_by(Trade.pair) \ - .order_by(desc('profit_sum_abs')) \ - .all() + Trade.buy_tag, + func.sum(Trade.close_profit).label('profit_sum'), + func.sum(Trade.close_profit_abs).label('profit_sum_abs'), + func.count(Trade.pair).label('count') + ).filter(Trade.is_open.is_(False))\ + .group_by(Trade.buy_tag) \ + .order_by(desc('profit_sum_abs')) \ + .all() return [ { - 'buy_tag': buy_tag, + 'buy_tag': buy_tag if buy_tag is not None else "Other", 'profit': profit, 'profit_abs': profit_abs, 'count': count @@ -903,81 +904,102 @@ class Trade(_DECL_BASE, LocalTrade): ] @staticmethod - def get_sell_tag_performance(pair: str) -> List[Dict[str, Any]]: + def get_exit_tag_performance(pair: str) -> List[Dict[str, Any]]: """ - Returns List of dicts containing all Trades, based on sell tag performance + Returns List of dicts containing all Trades, based on exit tag performance Can either be average for all pairs or a specific pair provided NOTE: Not supported in Backtesting. """ if(pair is not None): tag_perf = Trade.query.with_entities( - Trade.sell_tag, - func.sum(Trade.close_profit).label('profit_sum'), - func.sum(Trade.close_profit_abs).label('profit_sum_abs'), - func.count(Trade.pair).label('count') - ).filter(Trade.is_open.is_(False))\ - .filter(Trade.pair.lower() == pair.lower()) \ - .order_by(desc('profit_sum_abs')) \ - .all() + Trade.exit_tag, + func.sum(Trade.close_profit).label('profit_sum'), + func.sum(Trade.close_profit_abs).label('profit_sum_abs'), + func.count(Trade.pair).label('count') + ).filter(Trade.is_open.is_(False))\ + .filter(Trade.pair == pair) \ + .order_by(desc('profit_sum_abs')) \ + .all() else: tag_perf = Trade.query.with_entities( - Trade.sell_tag, - func.sum(Trade.close_profit).label('profit_sum'), - func.sum(Trade.close_profit_abs).label('profit_sum_abs'), - func.count(Trade.pair).label('count') - ).filter(Trade.is_open.is_(False))\ - .group_by(Trade.pair) \ - .order_by(desc('profit_sum_abs')) \ - .all() + Trade.exit_tag, + func.sum(Trade.close_profit).label('profit_sum'), + func.sum(Trade.close_profit_abs).label('profit_sum_abs'), + func.count(Trade.pair).label('count') + ).filter(Trade.is_open.is_(False))\ + .group_by(Trade.exit_tag) \ + .order_by(desc('profit_sum_abs')) \ + .all() return [ { - 'sell_tag': sell_tag, + 'exit_tag': exit_tag if exit_tag is not None else "Other", 'profit': profit, 'profit_abs': profit_abs, 'count': count } - for sell_tag, profit, profit_abs, count in tag_perf + for exit_tag, profit, profit_abs, count in tag_perf ] @staticmethod def get_mix_tag_performance(pair: str) -> List[Dict[str, Any]]: """ - Returns List of dicts containing all Trades, based on buy_tag + sell_tag performance + Returns List of dicts containing all Trades, based on buy_tag + exit_tag performance Can either be average for all pairs or a specific pair provided NOTE: Not supported in Backtesting. """ if(pair is not None): tag_perf = Trade.query.with_entities( - Trade.buy_tag, - Trade.sell_tag, - func.sum(Trade.close_profit).label('profit_sum'), - func.sum(Trade.close_profit_abs).label('profit_sum_abs'), - func.count(Trade.pair).label('count') - ).filter(Trade.is_open.is_(False))\ - .filter(Trade.pair.lower() == pair.lower()) \ - .order_by(desc('profit_sum_abs')) \ - .all() + Trade.id, + Trade.buy_tag, + Trade.exit_tag, + func.sum(Trade.close_profit).label('profit_sum'), + func.sum(Trade.close_profit_abs).label('profit_sum_abs'), + func.count(Trade.pair).label('count') + ).filter(Trade.is_open.is_(False))\ + .filter(Trade.pair == pair) \ + .order_by(desc('profit_sum_abs')) \ + .all() + else: tag_perf = Trade.query.with_entities( - Trade.buy_tag, - Trade.sell_tag, - func.sum(Trade.close_profit).label('profit_sum'), - func.sum(Trade.close_profit_abs).label('profit_sum_abs'), - func.count(Trade.pair).label('count') - ).filter(Trade.is_open.is_(False))\ - .group_by(Trade.pair) \ - .order_by(desc('profit_sum_abs')) \ - .all() + Trade.id, + Trade.buy_tag, + Trade.exit_tag, + func.sum(Trade.close_profit).label('profit_sum'), + func.sum(Trade.close_profit_abs).label('profit_sum_abs'), + func.count(Trade.pair).label('count') + ).filter(Trade.is_open.is_(False))\ + .group_by(Trade.id) \ + .order_by(desc('profit_sum_abs')) \ + .all() - return [ - { 'mix_tag': str(buy_tag) + " " +str(sell_tag), - 'profit': profit, - 'profit_abs': profit_abs, - 'count': count - } - for buy_tag, sell_tag, profit, profit_abs, count in tag_perf - ] + return_list = [] + for id, buy_tag, exit_tag, profit, profit_abs, count in tag_perf: + buy_tag = buy_tag if buy_tag is not None else "Other" + exit_tag = exit_tag if exit_tag is not None else "Other" + + if(exit_tag is not None and buy_tag is not None): + mix_tag = buy_tag + " " + exit_tag + i = 0 + if not any(item["mix_tag"] == mix_tag for item in return_list): + return_list.append({'mix_tag': mix_tag, + 'profit': profit, + 'profit_abs': profit_abs, + 'count': count}) + else: + while i < len(return_list): + if return_list[i]["mix_tag"] == mix_tag: + print("item below") + print(return_list[i]) + return_list[i] = { + 'mix_tag': mix_tag, + 'profit': profit + return_list[i]["profit"], + 'profit_abs': profit_abs + return_list[i]["profit_abs"], + 'count': 1 + return_list[i]["count"]} + i += 1 + + return return_list @staticmethod def get_best_pair(start_date: datetime = datetime.fromtimestamp(0)): diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 85973add6..508ce6894 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -105,7 +105,7 @@ class RPC: val = { 'dry_run': config['dry_run'], 'stake_currency': config['stake_currency'], - 'stake_currency_decimals': decimals_per_coin(config['stake_currency']), + 'stake_currency_decimals': decimals_per_coin(config['stake_currency']), 'stake_amount': config['stake_amount'], 'available_capital': config.get('available_capital'), 'max_open_trades': (config['max_open_trades'] @@ -696,16 +696,15 @@ class RPC: [x.update({'profit': round(x['profit'] * 100, 2)}) for x in buy_tags] return buy_tags - - def _rpc_sell_tag_performance(self, pair: str) -> List[Dict[str, Any]]: + def _rpc_exit_tag_performance(self, pair: str) -> List[Dict[str, Any]]: """ Handler for sell tag performance. Shows a performance statistic from finished trades """ - sell_tags = Trade.get_sell_tag_performance(pair) + exit_tags = Trade.get_exit_tag_performance(pair) # Round and convert to % - [x.update({'profit': round(x['profit'] * 100, 2)}) for x in sell_tags] - return sell_tags + [x.update({'profit': round(x['profit'] * 100, 2)}) for x in exit_tags] + return exit_tags def _rpc_mix_tag_performance(self, pair: str) -> List[Dict[str, Any]]: """ diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index db745ff37..85a91a10e 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -108,7 +108,7 @@ class Telegram(RPCHandler): r'/trades$', r'/performance$', r'/daily$', r'/daily \d+$', r'/profit$', r'/profit \d+', r'/stats$', r'/count$', r'/locks$', r'/balance$', - r'/buys',r'/sells',r'/mix_tags', + r'/buys', r'/sells', r'/mix_tags', r'/stopbuy$', r'/reload_config$', r'/show_config$', r'/logs$', r'/whitelist$', r'/blacklist$', r'/edge$', r'/forcebuy$', r'/help$', r'/version$'] @@ -154,7 +154,7 @@ class Telegram(RPCHandler): CommandHandler('delete', self._delete_trade), CommandHandler('performance', self._performance), CommandHandler('buys', self._buy_tag_performance), - CommandHandler('sells', self._sell_tag_performance), + CommandHandler('sells', self._exit_tag_performance), CommandHandler('mix_tags', self._mix_tag_performance), CommandHandler('stats', self._stats), CommandHandler('daily', self._daily), @@ -178,7 +178,7 @@ class Telegram(RPCHandler): CallbackQueryHandler(self._balance, pattern='update_balance'), CallbackQueryHandler(self._performance, pattern='update_performance'), CallbackQueryHandler(self._performance, pattern='update_buy_tag_performance'), - CallbackQueryHandler(self._performance, pattern='update_sell_tag_performance'), + CallbackQueryHandler(self._performance, pattern='update_exit_tag_performance'), CallbackQueryHandler(self._performance, pattern='update_mix_tag_performance'), CallbackQueryHandler(self._count, pattern='update_count'), CallbackQueryHandler(self._forcebuy_inline), @@ -242,6 +242,7 @@ class Telegram(RPCHandler): msg['duration'] = msg['close_date'].replace( microsecond=0) - msg['open_date'].replace(microsecond=0) msg['duration_min'] = msg['duration'].total_seconds() / 60 + msg['tags'] = self._get_tags_string(msg) msg['emoji'] = self._get_sell_emoji(msg) @@ -258,6 +259,7 @@ class Telegram(RPCHandler): message = ("{emoji} *{exchange}:* Selling {pair} (#{trade_id})\n" "*Profit:* `{profit_percent:.2f}%{profit_extra}`\n" + "{tags}" "*Sell Reason:* `{sell_reason}`\n" "*Duration:* `{duration} ({duration_min:.1f} min)`\n" "*Amount:* `{amount:.8f}`\n" @@ -265,46 +267,6 @@ class Telegram(RPCHandler): "*Current Rate:* `{current_rate:.8f}`\n" "*Close Rate:* `{limit:.8f}`").format(**msg) - sell_tag =None - if("sell_tag" in msg.keys()): - sell_tag = msg['sell_tag'] - buy_tag =None - if("buy_tag" in msg.keys()): - buy_tag = msg['buy_tag'] - - if sell_tag is not None and buy_tag is not None: - message = ("{emoji} *{exchange}:* Selling {pair} (#{trade_id})\n" - "*Profit:* `{profit_percent:.2f}%{profit_extra}`\n" - "*Buy Tag:* `{buy_tag}`\n" - "*Sell Tag:* `{sell_tag}`\n" - "*Sell Reason:* `{sell_reason}`\n" - "*Duration:* `{duration} ({duration_min:.1f} min)`\n" - "*Amount:* `{amount:.8f}`\n" - "*Open Rate:* `{open_rate:.8f}`\n" - "*Current Rate:* `{current_rate:.8f}`\n" - "*Close Rate:* `{limit:.8f}`").format(**msg) - elif sell_tag is None and buy_tag is not None: - message = ("{emoji} *{exchange}:* Selling {pair} (#{trade_id})\n" - "*Profit:* `{profit_percent:.2f}%{profit_extra}`\n" - "*Buy Tag:* `{buy_tag}`\n" - "*Sell Reason:* `{sell_reason}`\n" - "*Duration:* `{duration} ({duration_min:.1f} min)`\n" - "*Amount:* `{amount:.8f}`\n" - "*Open Rate:* `{open_rate:.8f}`\n" - "*Current Rate:* `{current_rate:.8f}`\n" - "*Close Rate:* `{limit:.8f}`").format(**msg) - elif sell_tag is not None and buy_tag is None: - message = ("{emoji} *{exchange}:* Selling {pair} (#{trade_id})\n" - "*Profit:* `{profit_percent:.2f}%{profit_extra}`\n" - "*Sell Tag:* `{sell_tag}`\n" - "*Sell Reason:* `{sell_reason}`\n" - "*Duration:* `{duration} ({duration_min:.1f} min)`\n" - "*Amount:* `{amount:.8f}`\n" - "*Open Rate:* `{open_rate:.8f}`\n" - "*Current Rate:* `{current_rate:.8f}`\n" - "*Close Rate:* `{limit:.8f}`").format(**msg) - - return message def compose_message(self, msg: Dict[str, Any], msg_type: RPCMessageType) -> str: @@ -393,6 +355,18 @@ class Telegram(RPCHandler): else: return "\N{CROSS MARK}" + def _get_tags_string(self, msg): + """ + Get string lines for buy/sell tags to display when a sell is made + """ + tag_lines = "" + + if ("buy_tag" in msg.keys() and msg['buy_tag'] is not None): + tag_lines += ("*Buy Tag:* `{buy_tag}`\n").format(msg['buy_tag']) + if ("exit_tag" in msg.keys() and msg['exit_tag'] is not None): + tag_lines += ("*Sell Tag:* `{exit_tag}`\n").format(msg['exit_tag']) + return tag_lines + @authorized_only def _status(self, update: Update, context: CallbackContext) -> None: """ @@ -425,7 +399,7 @@ class Telegram(RPCHandler): "*Current Pair:* {pair}", "*Amount:* `{amount} ({stake_amount} {base_currency})`", "*Buy Tag:* `{buy_tag}`" if r['buy_tag'] else "", - "*Sell Tag:* `{sell_tag}`" if r['sell_tag'] else "", + "*Sell Tag:* `{exit_tag}`" if r['exit_tag'] else "", "*Open Rate:* `{open_rate:.8f}`", "*Close Rate:* `{close_rate}`" if r['close_rate'] else "", "*Current Rate:* `{current_rate:.8f}`", @@ -923,12 +897,12 @@ class Telegram(RPCHandler): :return: None """ try: - pair=None + pair = None if context.args: pair = context.args[0] trades = self._rpc._rpc_buy_tag_performance(pair) - output = "Performance:\n" + output = "Buy Tag Performance:\n" for i, trade in enumerate(trades): stat_line = ( f"{i+1}.\t {trade['buy_tag']}\t" @@ -949,7 +923,7 @@ class Telegram(RPCHandler): self._send_msg(str(e)) @authorized_only - def _sell_tag_performance(self, update: Update, context: CallbackContext) -> None: + def _exit_tag_performance(self, update: Update, context: CallbackContext) -> None: """ Handler for /sells. Shows a performance statistic from finished trades @@ -958,15 +932,15 @@ class Telegram(RPCHandler): :return: None """ try: - pair=None + pair = None if context.args: pair = context.args[0] - trades = self._rpc._rpc_sell_tag_performance(pair) - output = "Performance:\n" + trades = self._rpc._rpc_exit_tag_performance(pair) + output = "Sell Tag Performance:\n" for i, trade in enumerate(trades): stat_line = ( - f"{i+1}.\t {trade['sell_tag']}\t" + f"{i+1}.\t {trade['exit_tag']}\t" f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} " f"({trade['profit']:.2f}%) " f"({trade['count']})\n") @@ -978,7 +952,7 @@ class Telegram(RPCHandler): output += stat_line self._send_msg(output, parse_mode=ParseMode.HTML, - reload_able=True, callback_path="update_sell_tag_performance", + reload_able=True, callback_path="update_exit_tag_performance", query=update.callback_query) except RPCException as e: self._send_msg(str(e)) @@ -993,13 +967,14 @@ class Telegram(RPCHandler): :return: None """ try: - pair=None + pair = None if context.args: pair = context.args[0] trades = self._rpc._rpc_mix_tag_performance(pair) - output = "Performance:\n" + output = "Mix Tag Performance:\n" for i, trade in enumerate(trades): + print(str(trade)) stat_line = ( f"{i+1}.\t {trade['mix_tag']}\t" f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} " diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 3c82d4d25..e4bf6ca69 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -500,7 +500,7 @@ class IStrategy(ABC, HyperStrategyMixin): dataframe['buy'] = 0 dataframe['sell'] = 0 dataframe['buy_tag'] = None - dataframe['sell_tag'] = None + dataframe['exit_tag'] = None # Other Defs in strategy that want to be called every loop here # twitter_sell = self.watch_twitter_feed(dataframe, metadata) @@ -613,7 +613,7 @@ class IStrategy(ABC, HyperStrategyMixin): sell = latest[SignalType.SELL.value] == 1 buy_tag = latest.get(SignalTagType.BUY_TAG.value, None) - sell_tag = latest.get(SignalTagType.SELL_TAG.value, None) + exit_tag = latest.get(SignalTagType.EXIT_TAG.value, None) logger.debug('trigger: %s (pair=%s) buy=%s sell=%s', latest['date'], pair, str(buy), str(sell)) @@ -622,8 +622,8 @@ class IStrategy(ABC, HyperStrategyMixin): current_time=datetime.now(timezone.utc), timeframe_seconds=timeframe_seconds, buy=buy): - return False, sell, buy_tag, sell_tag - return buy, sell, buy_tag, sell_tag + return False, sell, buy_tag, exit_tag + return buy, sell, buy_tag, exit_tag def ignore_expired_candle(self, latest_date: datetime, current_time: datetime, timeframe_seconds: int, buy: bool): From 1fdc4425dd8ac0fce2ec32198bb125e5f8a6f1e6 Mon Sep 17 00:00:00 2001 From: theluxaz Date: Wed, 20 Oct 2021 01:26:15 +0300 Subject: [PATCH 031/208] Changed exit_tag to be represented as sell_reason --- freqtrade/freqtradebot.py | 5 +++-- freqtrade/optimize/backtesting.py | 7 +++--- freqtrade/optimize/optimize_reports.py | 12 ---------- freqtrade/persistence/models.py | 31 ++++++++++++-------------- freqtrade/rpc/rpc.py | 15 ++++++++----- freqtrade/rpc/telegram.py | 29 +++++++----------------- 6 files changed, 37 insertions(+), 62 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 73d9bb382..5ecf5b2a3 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1150,6 +1150,7 @@ class FreqtradeBot(LoggingMixin): trade.close_rate_requested = limit trade.sell_reason = sell_reason.sell_reason if(exit_tag is not None): + trade.sell_reason = exit_tag trade.exit_tag = exit_tag # In case of market sell orders the order can be closed immediately if order.get('status', 'unknown') in ('closed', 'expired'): @@ -1191,8 +1192,8 @@ class FreqtradeBot(LoggingMixin): 'current_rate': current_rate, 'profit_amount': profit_trade, 'profit_ratio': profit_ratio, + 'buy_tag': trade.buy_tag, 'sell_reason': trade.sell_reason, - 'exit_tag': trade.exit_tag, 'open_date': trade.open_date, 'close_date': trade.close_date or datetime.utcnow(), 'stake_currency': self.config['stake_currency'], @@ -1235,8 +1236,8 @@ class FreqtradeBot(LoggingMixin): 'current_rate': current_rate, 'profit_amount': profit_trade, 'profit_ratio': profit_ratio, + 'buy_tag': trade.buy_tag, 'sell_reason': trade.sell_reason, - 'exit_tag': trade.exit_tag, 'open_date': trade.open_date, 'close_date': trade.close_date or datetime.now(timezone.utc), 'stake_currency': self.config['stake_currency'], diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 6c2a20cb1..827be4d76 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -360,11 +360,10 @@ class Backtesting: if sell.sell_flag: trade.close_date = sell_candle_time - if(sell_row[EXIT_TAG_IDX] is not None): - trade.exit_tag = sell_row[EXIT_TAG_IDX] - else: - trade.exit_tag = None trade.sell_reason = sell.sell_reason + if(sell_row[EXIT_TAG_IDX] is not None): + trade.sell_reason = sell_row[EXIT_TAG_IDX] + trade.exit_tag = sell_row[EXIT_TAG_IDX] trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60) closerate = self._get_close_rate(sell_row, trade, sell, trade_dur) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 30005f524..67dacd7c6 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -387,8 +387,6 @@ def generate_strategy_stats(btdata: Dict[str, DataFrame], buy_tag_results = generate_tag_metrics("buy_tag", starting_balance=starting_balance, results=results, skip_nan=False) - exit_tag_results = generate_tag_metrics("exit_tag", starting_balance=starting_balance, - results=results, skip_nan=False) sell_reason_stats = generate_sell_reason_stats(max_open_trades=max_open_trades, results=results) @@ -414,7 +412,6 @@ def generate_strategy_stats(btdata: Dict[str, DataFrame], 'worst_pair': worst_pair, 'results_per_pair': pair_results, 'results_per_buy_tag': buy_tag_results, - 'results_per_exit_tag': exit_tag_results, 'sell_reason_summary': sell_reason_stats, 'left_open_trades': left_open_results, 'total_trades': len(results), @@ -744,15 +741,6 @@ def show_backtest_result(strategy: str, results: Dict[str, Any], stake_currency: print(' BUY TAG STATS '.center(len(table.splitlines()[0]), '=')) print(table) - table = text_table_tags( - "exit_tag", - results['results_per_exit_tag'], - stake_currency=stake_currency) - - if isinstance(table, str) and len(table) > 0: - print(' SELL TAG STATS '.center(len(table.splitlines()[0]), '=')) - print(table) - table = text_table_sell_reason(sell_reason_stats=results['sell_reason_summary'], stake_currency=stake_currency) if isinstance(table, str) and len(table) > 0: diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 945201982..e03830d7f 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -325,7 +325,6 @@ class LocalTrade(): 'profit_pct': round(self.close_profit * 100, 2) if self.close_profit else None, 'profit_abs': self.close_profit_abs, - # +str(self.sell_reason) ## CHANGE TO BUY TAG IF NEEDED 'sell_reason': (f' ({self.sell_reason})' if self.sell_reason else ''), 'exit_tag': (f' ({self.exit_tag})' if self.exit_tag else ''), 'sell_order_status': self.sell_order_status, @@ -904,15 +903,15 @@ class Trade(_DECL_BASE, LocalTrade): ] @staticmethod - def get_exit_tag_performance(pair: str) -> List[Dict[str, Any]]: + def get_sell_reason_performance(pair: str) -> List[Dict[str, Any]]: """ - Returns List of dicts containing all Trades, based on exit tag performance + Returns List of dicts containing all Trades, based on sell reason performance Can either be average for all pairs or a specific pair provided NOTE: Not supported in Backtesting. """ if(pair is not None): tag_perf = Trade.query.with_entities( - Trade.exit_tag, + Trade.sell_reason, func.sum(Trade.close_profit).label('profit_sum'), func.sum(Trade.close_profit_abs).label('profit_sum_abs'), func.count(Trade.pair).label('count') @@ -922,29 +921,29 @@ class Trade(_DECL_BASE, LocalTrade): .all() else: tag_perf = Trade.query.with_entities( - Trade.exit_tag, + Trade.sell_reason, func.sum(Trade.close_profit).label('profit_sum'), func.sum(Trade.close_profit_abs).label('profit_sum_abs'), func.count(Trade.pair).label('count') ).filter(Trade.is_open.is_(False))\ - .group_by(Trade.exit_tag) \ + .group_by(Trade.sell_reason) \ .order_by(desc('profit_sum_abs')) \ .all() return [ { - 'exit_tag': exit_tag if exit_tag is not None else "Other", + 'sell_reason': sell_reason if sell_reason is not None else "Other", 'profit': profit, 'profit_abs': profit_abs, 'count': count } - for exit_tag, profit, profit_abs, count in tag_perf + for sell_reason, profit, profit_abs, count in tag_perf ] @staticmethod def get_mix_tag_performance(pair: str) -> List[Dict[str, Any]]: """ - Returns List of dicts containing all Trades, based on buy_tag + exit_tag performance + Returns List of dicts containing all Trades, based on buy_tag + sell_reason performance Can either be average for all pairs or a specific pair provided NOTE: Not supported in Backtesting. """ @@ -952,7 +951,7 @@ class Trade(_DECL_BASE, LocalTrade): tag_perf = Trade.query.with_entities( Trade.id, Trade.buy_tag, - Trade.exit_tag, + Trade.sell_reason, func.sum(Trade.close_profit).label('profit_sum'), func.sum(Trade.close_profit_abs).label('profit_sum_abs'), func.count(Trade.pair).label('count') @@ -965,7 +964,7 @@ class Trade(_DECL_BASE, LocalTrade): tag_perf = Trade.query.with_entities( Trade.id, Trade.buy_tag, - Trade.exit_tag, + Trade.sell_reason, func.sum(Trade.close_profit).label('profit_sum'), func.sum(Trade.close_profit_abs).label('profit_sum_abs'), func.count(Trade.pair).label('count') @@ -975,12 +974,12 @@ class Trade(_DECL_BASE, LocalTrade): .all() return_list = [] - for id, buy_tag, exit_tag, profit, profit_abs, count in tag_perf: + for id, buy_tag, sell_reason, profit, profit_abs, count in tag_perf: buy_tag = buy_tag if buy_tag is not None else "Other" - exit_tag = exit_tag if exit_tag is not None else "Other" + sell_reason = sell_reason if sell_reason is not None else "Other" - if(exit_tag is not None and buy_tag is not None): - mix_tag = buy_tag + " " + exit_tag + if(sell_reason is not None and buy_tag is not None): + mix_tag = buy_tag + " " + sell_reason i = 0 if not any(item["mix_tag"] == mix_tag for item in return_list): return_list.append({'mix_tag': mix_tag, @@ -990,8 +989,6 @@ class Trade(_DECL_BASE, LocalTrade): else: while i < len(return_list): if return_list[i]["mix_tag"] == mix_tag: - print("item below") - print(return_list[i]) return_list[i] = { 'mix_tag': mix_tag, 'profit': profit + return_list[i]["profit"], diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 508ce6894..2a664e7bc 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -161,6 +161,8 @@ class RPC: current_rate = NAN else: current_rate = trade.close_rate + + buy_tag = trade.buy_tag current_profit = trade.calc_profit_ratio(current_rate) current_profit_abs = trade.calc_profit(current_rate) current_profit_fiat: Optional[float] = None @@ -191,6 +193,7 @@ class RPC: profit_pct=round(current_profit * 100, 2), profit_abs=current_profit_abs, profit_fiat=current_profit_fiat, + buy_tag=buy_tag, stoploss_current_dist=stoploss_current_dist, stoploss_current_dist_ratio=round(stoploss_current_dist_ratio, 8), @@ -696,19 +699,19 @@ class RPC: [x.update({'profit': round(x['profit'] * 100, 2)}) for x in buy_tags] return buy_tags - def _rpc_exit_tag_performance(self, pair: str) -> List[Dict[str, Any]]: + def _rpc_sell_reason_performance(self, pair: str) -> List[Dict[str, Any]]: """ - Handler for sell tag performance. + Handler for sell reason performance. Shows a performance statistic from finished trades """ - exit_tags = Trade.get_exit_tag_performance(pair) + sell_reasons = Trade.get_sell_reason_performance(pair) # Round and convert to % - [x.update({'profit': round(x['profit'] * 100, 2)}) for x in exit_tags] - return exit_tags + [x.update({'profit': round(x['profit'] * 100, 2)}) for x in sell_reasons] + return sell_reasons def _rpc_mix_tag_performance(self, pair: str) -> List[Dict[str, Any]]: """ - Handler for mix tag performance. + Handler for mix tag (buy_tag + exit_tag) performance. Shows a performance statistic from finished trades """ mix_tags = Trade.get_mix_tag_performance(pair) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 0a84b588a..2352d366a 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -156,7 +156,7 @@ class Telegram(RPCHandler): CommandHandler('delete', self._delete_trade), CommandHandler('performance', self._performance), CommandHandler('buys', self._buy_tag_performance), - CommandHandler('sells', self._exit_tag_performance), + CommandHandler('sells', self._sell_reason_performance), CommandHandler('mix_tags', self._mix_tag_performance), CommandHandler('stats', self._stats), CommandHandler('daily', self._daily), @@ -244,8 +244,8 @@ class Telegram(RPCHandler): msg['duration'] = msg['close_date'].replace( microsecond=0) - msg['open_date'].replace(microsecond=0) msg['duration_min'] = msg['duration'].total_seconds() / 60 - msg['tags'] = self._get_tags_string(msg) + msg['buy_tag'] = msg['buy_tag'] if "buy_tag" in msg.keys() else None msg['emoji'] = self._get_sell_emoji(msg) # Check if all sell properties are available. @@ -261,7 +261,7 @@ class Telegram(RPCHandler): message = ("{emoji} *{exchange}:* Selling {pair} (#{trade_id})\n" "*Profit:* `{profit_percent:.2f}%{profit_extra}`\n" - "{tags}" + "*Buy Tag:* `{buy_tag}`\n" "*Sell Reason:* `{sell_reason}`\n" "*Duration:* `{duration} ({duration_min:.1f} min)`\n" "*Amount:* `{amount:.8f}`\n" @@ -357,18 +357,6 @@ class Telegram(RPCHandler): else: return "\N{CROSS MARK}" - def _get_tags_string(self, msg): - """ - Get string lines for buy/sell tags to display when a sell is made - """ - tag_lines = "" - - if ("buy_tag" in msg.keys() and msg['buy_tag'] is not None): - tag_lines += ("*Buy Tag:* `{buy_tag}`\n").format(msg['buy_tag']) - if ("exit_tag" in msg.keys() and msg['exit_tag'] is not None): - tag_lines += ("*Sell Tag:* `{exit_tag}`\n").format(msg['exit_tag']) - return tag_lines - @authorized_only def _status(self, update: Update, context: CallbackContext) -> None: """ @@ -401,7 +389,6 @@ class Telegram(RPCHandler): "*Current Pair:* {pair}", "*Amount:* `{amount} ({stake_amount} {base_currency})`", "*Buy Tag:* `{buy_tag}`" if r['buy_tag'] else "", - "*Sell Tag:* `{exit_tag}`" if r['exit_tag'] else "", "*Open Rate:* `{open_rate:.8f}`", "*Close Rate:* `{close_rate}`" if r['close_rate'] else "", "*Current Rate:* `{current_rate:.8f}`", @@ -925,7 +912,7 @@ class Telegram(RPCHandler): self._send_msg(str(e)) @authorized_only - def _exit_tag_performance(self, update: Update, context: CallbackContext) -> None: + def _sell_reason_performance(self, update: Update, context: CallbackContext) -> None: """ Handler for /sells. Shows a performance statistic from finished trades @@ -938,11 +925,11 @@ class Telegram(RPCHandler): if context.args: pair = context.args[0] - trades = self._rpc._rpc_exit_tag_performance(pair) - output = "Sell Tag Performance:\n" + trades = self._rpc._rpc_sell_reason_performance(pair) + output = "Sell Reason Performance:\n" for i, trade in enumerate(trades): stat_line = ( - f"{i+1}.\t {trade['exit_tag']}\t" + f"{i+1}.\t {trade['sell_reason']}\t" f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} " f"({trade['profit']:.2f}%) " f"({trade['count']})\n") @@ -954,7 +941,7 @@ class Telegram(RPCHandler): output += stat_line self._send_msg(output, parse_mode=ParseMode.HTML, - reload_able=True, callback_path="update_exit_tag_performance", + reload_able=True, callback_path="update_sell_reason_performance", query=update.callback_query) except RPCException as e: self._send_msg(str(e)) From 905f3a1a5083d0feb357429870486f8b4fda50d1 Mon Sep 17 00:00:00 2001 From: theluxaz Date: Wed, 20 Oct 2021 17:58:50 +0300 Subject: [PATCH 032/208] Removed exit_tag from Trade objects. --- freqtrade/data/btanalysis.py | 2 +- freqtrade/freqtradebot.py | 1 - freqtrade/optimize/backtesting.py | 3 --- freqtrade/persistence/migrations.py | 7 +++---- freqtrade/persistence/models.py | 3 --- freqtrade/rpc/rpc.py | 2 +- freqtrade/rpc/telegram.py | 3 +-- 7 files changed, 6 insertions(+), 15 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 3dba635e6..7d97661c4 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -30,7 +30,7 @@ BT_DATA_COLUMNS = ['pair', 'stake_amount', 'amount', 'open_date', 'close_date', 'fee_open', 'fee_close', 'trade_duration', 'profit_ratio', 'profit_abs', 'sell_reason', 'initial_stop_loss_abs', 'initial_stop_loss_ratio', 'stop_loss_abs', - 'stop_loss_ratio', 'min_rate', 'max_rate', 'is_open', 'buy_tag', 'exit_tag'] + 'stop_loss_ratio', 'min_rate', 'max_rate', 'is_open', 'buy_tag'] def get_latest_optimize_filename(directory: Union[Path, str], variant: str) -> str: diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 5ecf5b2a3..b7449d884 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1151,7 +1151,6 @@ class FreqtradeBot(LoggingMixin): trade.sell_reason = sell_reason.sell_reason if(exit_tag is not None): trade.sell_reason = exit_tag - trade.exit_tag = exit_tag # 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) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 827be4d76..5566127c3 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -363,7 +363,6 @@ class Backtesting: trade.sell_reason = sell.sell_reason if(sell_row[EXIT_TAG_IDX] is not None): trade.sell_reason = sell_row[EXIT_TAG_IDX] - trade.exit_tag = sell_row[EXIT_TAG_IDX] trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60) closerate = self._get_close_rate(sell_row, trade, sell, trade_dur) @@ -437,7 +436,6 @@ class Backtesting: if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount): # Enter trade has_buy_tag = len(row) >= BUY_TAG_IDX + 1 - has_exit_tag = len(row) >= EXIT_TAG_IDX + 1 trade = LocalTrade( pair=pair, open_rate=row[OPEN_IDX], @@ -448,7 +446,6 @@ class Backtesting: fee_close=self.fee, is_open=True, buy_tag=row[BUY_TAG_IDX] if has_buy_tag else None, - exit_tag=row[EXIT_TAG_IDX] if has_exit_tag else None, exchange='backtesting', ) return trade diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index d0b3add3c..1839c4130 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -48,7 +48,6 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col sell_reason = get_column_def(cols, 'sell_reason', 'null') strategy = get_column_def(cols, 'strategy', 'null') buy_tag = get_column_def(cols, 'buy_tag', 'null') - exit_tag = get_column_def(cols, 'exit_tag', 'null') # If ticker-interval existed use that, else null. if has_column(cols, 'ticker_interval'): timeframe = get_column_def(cols, 'timeframe', 'ticker_interval') @@ -83,7 +82,7 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col stake_amount, amount, amount_requested, open_date, close_date, open_order_id, stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct, stoploss_order_id, stoploss_last_update, - max_rate, min_rate, sell_reason, sell_order_status, strategy, buy_tag, exit_tag, + max_rate, min_rate, sell_reason, sell_order_status, strategy, buy_tag, timeframe, open_trade_value, close_profit_abs ) select id, lower(exchange), pair, @@ -99,7 +98,7 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col {stoploss_order_id} stoploss_order_id, {stoploss_last_update} stoploss_last_update, {max_rate} max_rate, {min_rate} min_rate, {sell_reason} sell_reason, {sell_order_status} sell_order_status, - {strategy} strategy, {buy_tag} buy_tag, {exit_tag} exit_tag, {timeframe} timeframe, + {strategy} strategy, {buy_tag} buy_tag, {timeframe} timeframe, {open_trade_value} open_trade_value, {close_profit_abs} close_profit_abs from {table_back_name} """)) @@ -158,7 +157,7 @@ def check_migrate(engine, decl_base, previous_tables) -> None: table_back_name = get_backup_name(tabs, 'trades_bak') # Check for latest column - if not has_column(cols, 'exit_tag'): + if not has_column(cols, 'buy_tag'): logger.info(f'Running database migration for trades - backup: {table_back_name}') migrate_trades_table(decl_base, inspector, engine, table_back_name, cols) # Reread columns - the above recreated the table! diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index e03830d7f..ed0c2bf9d 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -258,7 +258,6 @@ class LocalTrade(): sell_order_status: str = '' strategy: str = '' buy_tag: Optional[str] = None - exit_tag: Optional[str] = None timeframe: Optional[int] = None def __init__(self, **kwargs): @@ -326,7 +325,6 @@ class LocalTrade(): 'profit_abs': self.close_profit_abs, 'sell_reason': (f' ({self.sell_reason})' if self.sell_reason else ''), - 'exit_tag': (f' ({self.exit_tag})' if self.exit_tag else ''), 'sell_order_status': self.sell_order_status, 'stop_loss_abs': self.stop_loss, 'stop_loss_ratio': self.stop_loss_pct if self.stop_loss_pct else None, @@ -708,7 +706,6 @@ class Trade(_DECL_BASE, LocalTrade): sell_order_status = Column(String(100), nullable=True) strategy = Column(String(100), nullable=True) buy_tag = Column(String(100), nullable=True) - exit_tag = Column(String(100), nullable=True) timeframe = Column(Integer, nullable=True) def __init__(self, **kwargs): diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 2a664e7bc..310b0ad07 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -711,7 +711,7 @@ class RPC: def _rpc_mix_tag_performance(self, pair: str) -> List[Dict[str, Any]]: """ - Handler for mix tag (buy_tag + exit_tag) performance. + Handler for mix tag (buy_tag + sell_reason) performance. Shows a performance statistic from finished trades """ mix_tags = Trade.get_mix_tag_performance(pair) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 2352d366a..341eec5dd 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -180,7 +180,7 @@ class Telegram(RPCHandler): CallbackQueryHandler(self._balance, pattern='update_balance'), CallbackQueryHandler(self._performance, pattern='update_performance'), CallbackQueryHandler(self._performance, pattern='update_buy_tag_performance'), - CallbackQueryHandler(self._performance, pattern='update_exit_tag_performance'), + CallbackQueryHandler(self._performance, pattern='update_sell_reason_performance'), CallbackQueryHandler(self._performance, pattern='update_mix_tag_performance'), CallbackQueryHandler(self._count, pattern='update_count'), CallbackQueryHandler(self._forcebuy_inline), @@ -963,7 +963,6 @@ class Telegram(RPCHandler): trades = self._rpc._rpc_mix_tag_performance(pair) output = "Mix Tag Performance:\n" for i, trade in enumerate(trades): - print(str(trade)) stat_line = ( f"{i+1}.\t {trade['mix_tag']}\t" f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} " From 1267374c8a2665dd74179f95eb52edabd66634a6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 20 Oct 2021 19:13:34 +0200 Subject: [PATCH 033/208] Small fixes to tests --- freqtrade/freqtradebot.py | 3 ++- freqtrade/persistence/models.py | 4 ++-- tests/conftest.py | 2 +- tests/optimize/__init__.py | 2 ++ tests/test_freqtradebot.py | 41 ++++++++++++++++++--------------- tests/test_persistence.py | 4 ++++ 6 files changed, 34 insertions(+), 22 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index b7449d884..99373ae74 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -865,7 +865,8 @@ class FreqtradeBot(LoggingMixin): if should_sell.sell_flag: logger.info( - f'Executing Sell for {trade.pair}. Reason: {should_sell.sell_type}. Tag: {exit_tag if exit_tag is not None else "None"}') + f'Executing Sell for {trade.pair}. Reason: {should_sell.sell_type}. ' + f'Tag: {exit_tag if exit_tag is not None else "None"}') self.execute_trade_exit(trade, exit_rate, should_sell, exit_tag) return True return False diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index ed0c2bf9d..9a1f04429 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -324,7 +324,7 @@ class LocalTrade(): 'profit_pct': round(self.close_profit * 100, 2) if self.close_profit else None, 'profit_abs': self.close_profit_abs, - 'sell_reason': (f' ({self.sell_reason})' if self.sell_reason else ''), + 'sell_reason': self.sell_reason, 'sell_order_status': self.sell_order_status, 'stop_loss_abs': self.stop_loss, 'stop_loss_ratio': self.stop_loss_pct if self.stop_loss_pct else None, @@ -970,7 +970,7 @@ class Trade(_DECL_BASE, LocalTrade): .order_by(desc('profit_sum_abs')) \ .all() - return_list = [] + return_list: List[Dict] = [] for id, buy_tag, sell_reason, profit, profit_abs, count in tag_perf: buy_tag = buy_tag if buy_tag is not None else "Other" sell_reason = sell_reason if sell_reason is not None else "Other" diff --git a/tests/conftest.py b/tests/conftest.py index b35a220df..698c464ed 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -186,7 +186,7 @@ def get_patched_worker(mocker, config) -> Worker: return Worker(args=None, config=config) -def patch_get_signal(freqtrade: FreqtradeBot, value=(True, False, None)) -> None: +def patch_get_signal(freqtrade: FreqtradeBot, value=(True, False, None, None)) -> None: """ :param mocker: mocker to patch IStrategy class :param value: which value IStrategy.get_signal() must return diff --git a/tests/optimize/__init__.py b/tests/optimize/__init__.py index 6ad2d300b..50e7162f4 100644 --- a/tests/optimize/__init__.py +++ b/tests/optimize/__init__.py @@ -54,4 +54,6 @@ def _build_backtest_dataframe(data): frame[column] = frame[column].astype('float64') if 'buy_tag' not in columns: frame['buy_tag'] = None + if 'exit_tag' not in columns: + frame['exit_tag'] = None return frame diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 838a158e0..e590f4f74 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -236,7 +236,7 @@ def test_edge_overrides_stoploss(limit_buy_order_usdt, fee, caplog, mocker, # stoploss shoud be hit assert freqtrade.handle_trade(trade) is not ignore_strat_sl if not ignore_strat_sl: - assert log_has('Executing Sell for NEO/BTC. Reason: stop_loss', caplog) + assert log_has_re(r'Executing Sell for NEO/BTC. Reason: stop_loss.*', caplog) assert trade.sell_reason == SellType.STOP_LOSS.value @@ -450,7 +450,7 @@ def test_create_trade_no_signal(default_conf_usdt, fee, mocker) -> None: ) default_conf_usdt['stake_amount'] = 10 freqtrade = FreqtradeBot(default_conf_usdt) - patch_get_signal(freqtrade, value=(False, False, None)) + patch_get_signal(freqtrade, value=(False, False, None, None)) Trade.query = MagicMock() Trade.query.filter = MagicMock() @@ -677,7 +677,7 @@ def test_process_informative_pairs_added(default_conf_usdt, ticker_usdt, mocker) inf_pairs = MagicMock(return_value=[("BTC/ETH", '1m'), ("ETH/USDT", "1h")]) mocker.patch( 'freqtrade.strategy.interface.IStrategy.get_signal', - return_value=(False, False, '') + return_value=(False, False, '', '') ) mocker.patch('time.sleep', return_value=None) @@ -1808,7 +1808,7 @@ def test_handle_trade(default_conf_usdt, limit_buy_order_usdt, limit_sell_order_ assert trade.is_open is True freqtrade.wallets.update() - patch_get_signal(freqtrade, value=(False, True, None)) + patch_get_signal(freqtrade, value=(False, True, None, None)) assert freqtrade.handle_trade(trade) is True assert trade.open_order_id == limit_sell_order_usdt['id'] @@ -1836,7 +1836,7 @@ def test_handle_overlapping_signals(default_conf_usdt, ticker_usdt, limit_buy_or ) freqtrade = FreqtradeBot(default_conf_usdt) - patch_get_signal(freqtrade, value=(True, True, None)) + patch_get_signal(freqtrade, value=(True, True, None, None)) freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) freqtrade.enter_positions() @@ -1855,7 +1855,7 @@ def test_handle_overlapping_signals(default_conf_usdt, ticker_usdt, limit_buy_or assert trades[0].is_open is True # Buy and Sell are not triggering, so doing nothing ... - patch_get_signal(freqtrade, value=(False, False, None)) + patch_get_signal(freqtrade, value=(False, False, None, None)) assert freqtrade.handle_trade(trades[0]) is False trades = Trade.query.all() nb_trades = len(trades) @@ -1863,7 +1863,7 @@ def test_handle_overlapping_signals(default_conf_usdt, ticker_usdt, limit_buy_or assert trades[0].is_open is True # Buy and Sell are triggering, so doing nothing ... - patch_get_signal(freqtrade, value=(True, True, None)) + patch_get_signal(freqtrade, value=(True, True, None, None)) assert freqtrade.handle_trade(trades[0]) is False trades = Trade.query.all() nb_trades = len(trades) @@ -1871,7 +1871,7 @@ def test_handle_overlapping_signals(default_conf_usdt, ticker_usdt, limit_buy_or assert trades[0].is_open is True # Sell is triggering, guess what : we are Selling! - patch_get_signal(freqtrade, value=(False, True, None)) + patch_get_signal(freqtrade, value=(False, True, None, None)) trades = Trade.query.all() assert freqtrade.handle_trade(trades[0]) is True @@ -1905,7 +1905,7 @@ def test_handle_trade_roi(default_conf_usdt, ticker_usdt, limit_buy_order_usdt_o # we might just want to check if we are in a sell condition without # executing # if ROI is reached we must sell - patch_get_signal(freqtrade, value=(False, True, None)) + patch_get_signal(freqtrade, value=(False, True, None, None)) assert freqtrade.handle_trade(trade) assert log_has("ETH/USDT - Required profit reached. sell_type=SellType.ROI", caplog) @@ -1934,10 +1934,10 @@ def test_handle_trade_use_sell_signal(default_conf_usdt, ticker_usdt, limit_buy_ trade = Trade.query.first() trade.is_open = True - patch_get_signal(freqtrade, value=(False, False, None)) + patch_get_signal(freqtrade, value=(False, False, None, None)) assert not freqtrade.handle_trade(trade) - patch_get_signal(freqtrade, value=(False, True, None)) + patch_get_signal(freqtrade, value=(False, True, None, None)) assert freqtrade.handle_trade(trade) assert log_has("ETH/USDT - Sell signal received. sell_type=SellType.SELL_SIGNAL", caplog) @@ -2579,6 +2579,7 @@ def test_execute_trade_exit_up(default_conf_usdt, ticker_usdt, fee, ticker_usdt_ 'limit': 2.2, 'amount': 30.0, 'order_type': 'limit', + 'buy_tag': None, 'open_rate': 2.0, 'current_rate': 2.3, 'profit_amount': 5.685, @@ -2632,6 +2633,7 @@ def test_execute_trade_exit_down(default_conf_usdt, ticker_usdt, fee, ticker_usd 'limit': 2.01, 'amount': 30.0, 'order_type': 'limit', + 'buy_tag': None, 'open_rate': 2.0, 'current_rate': 2.0, 'profit_amount': -0.00075, @@ -2699,6 +2701,7 @@ def test_execute_trade_exit_custom_exit_price(default_conf_usdt, ticker_usdt, fe 'limit': 2.25, 'amount': 30.0, 'order_type': 'limit', + 'buy_tag': None, 'open_rate': 2.0, 'current_rate': 2.3, 'profit_amount': 7.18125, @@ -2758,6 +2761,7 @@ def test_execute_trade_exit_down_stoploss_on_exchange_dry_run( 'limit': 1.98, 'amount': 30.0, 'order_type': 'limit', + 'buy_tag': None, 'open_rate': 2.0, 'current_rate': 2.0, 'profit_amount': -0.8985, @@ -2975,6 +2979,7 @@ def test_execute_trade_exit_market_order(default_conf_usdt, ticker_usdt, fee, 'limit': 2.2, 'amount': 30.0, 'order_type': 'market', + 'buy_tag': None, 'open_rate': 2.0, 'current_rate': 2.3, 'profit_amount': 5.685, @@ -3068,7 +3073,7 @@ def test_sell_profit_only( trade = Trade.query.first() trade.update(limit_buy_order_usdt) freqtrade.wallets.update() - patch_get_signal(freqtrade, value=(False, True, None)) + patch_get_signal(freqtrade, value=(False, True, None, None)) assert freqtrade.handle_trade(trade) is handle_first if handle_second: @@ -3103,7 +3108,7 @@ def test_sell_not_enough_balance(default_conf_usdt, limit_buy_order_usdt, limit_ trade = Trade.query.first() amnt = trade.amount trade.update(limit_buy_order_usdt) - patch_get_signal(freqtrade, value=(False, True, None)) + patch_get_signal(freqtrade, value=(False, True, None, None)) mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=trade.amount * 0.985)) assert freqtrade.handle_trade(trade) is True @@ -3212,11 +3217,11 @@ def test_ignore_roi_if_buy_signal(default_conf_usdt, limit_buy_order_usdt, trade = Trade.query.first() trade.update(limit_buy_order_usdt) freqtrade.wallets.update() - patch_get_signal(freqtrade, value=(True, True, None)) + patch_get_signal(freqtrade, value=(True, True, None, None)) assert freqtrade.handle_trade(trade) is False # Test if buy-signal is absent (should sell due to roi = true) - patch_get_signal(freqtrade, value=(False, True, None)) + patch_get_signal(freqtrade, value=(False, True, None, None)) assert freqtrade.handle_trade(trade) is True assert trade.sell_reason == SellType.ROI.value @@ -3402,11 +3407,11 @@ def test_disable_ignore_roi_if_buy_signal(default_conf_usdt, limit_buy_order_usd trade = Trade.query.first() trade.update(limit_buy_order_usdt) # Sell due to min_roi_reached - patch_get_signal(freqtrade, value=(True, True, None)) + patch_get_signal(freqtrade, value=(True, True, None, None)) assert freqtrade.handle_trade(trade) is True # Test if buy-signal is absent - patch_get_signal(freqtrade, value=(False, True, None)) + patch_get_signal(freqtrade, value=(False, True, None, None)) assert freqtrade.handle_trade(trade) is True assert trade.sell_reason == SellType.ROI.value @@ -3848,7 +3853,7 @@ def test_order_book_ask_strategy( freqtrade.wallets.update() assert trade.is_open is True - patch_get_signal(freqtrade, value=(False, True, None)) + patch_get_signal(freqtrade, value=(False, True, None, None)) assert freqtrade.handle_trade(trade) is True assert trade.close_rate_requested == order_book_l2.return_value['asks'][0][0] diff --git a/tests/test_persistence.py b/tests/test_persistence.py index d036b045e..719dc8263 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -1317,6 +1317,10 @@ def test_Trade_object_idem(): 'get_open_trades_without_assigned_fees', 'get_open_order_trades', 'get_trades', + 'get_sell_reason_performance', + 'get_buy_tag_performance', + 'get_mix_tag_performance', + ) # Parent (LocalTrade) should have the same attributes From 0e085298e9576fa2d88c47c2a70a9124dcc4dcfb Mon Sep 17 00:00:00 2001 From: theluxaz Date: Thu, 21 Oct 2021 17:25:38 +0300 Subject: [PATCH 034/208] Fixed test failures. --- freqtrade/optimize/backtesting.py | 6 +++- freqtrade/optimize/optimize_reports.py | 38 ++++++++++++++------------ freqtrade/rpc/telegram.py | 11 ++++---- tests/optimize/test_backtesting.py | 18 ++++++------ tests/rpc/test_rpc_telegram.py | 20 +++++++++++--- tests/strategy/test_interface.py | 34 ++++++++++++++++------- 6 files changed, 81 insertions(+), 46 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 5566127c3..d23b6bdc3 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -361,8 +361,12 @@ class Backtesting: if sell.sell_flag: trade.close_date = sell_candle_time trade.sell_reason = sell.sell_reason - if(sell_row[EXIT_TAG_IDX] is not None): + + # Checks and adds an exit tag, after checking that the length of the + # sell_row has the length for an exit tag column + if(len(sell_row) > EXIT_TAG_IDX and sell_row[EXIT_TAG_IDX] is not None and len(sell_row[EXIT_TAG_IDX]) > 0): trade.sell_reason = sell_row[EXIT_TAG_IDX] + trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60) closerate = self._get_close_rate(sell_row, trade, sell, trade_dur) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 67dacd7c6..6e0926660 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -150,19 +150,22 @@ def generate_tag_metrics(tag_type: str, tabular_data = [] - for tag, count in results[tag_type].value_counts().iteritems(): - result = results[results[tag_type] == tag] - if skip_nan and result['profit_abs'].isnull().all(): - continue + if tag_type in results.columns: + for tag, count in results[tag_type].value_counts().iteritems(): + result = results[results[tag_type] == tag] + if skip_nan and result['profit_abs'].isnull().all(): + continue - tabular_data.append(_generate_tag_result_line(result, starting_balance, tag)) + tabular_data.append(_generate_tag_result_line(result, starting_balance, tag)) - # Sort by total profit %: - tabular_data = sorted(tabular_data, key=lambda k: k['profit_total_abs'], reverse=True) + # Sort by total profit %: + tabular_data = sorted(tabular_data, key=lambda k: k['profit_total_abs'], reverse=True) - # Append Total - tabular_data.append(_generate_result_line(results, starting_balance, 'TOTAL')) - return tabular_data + # Append Total + tabular_data.append(_generate_result_line(results, starting_balance, 'TOTAL')) + return tabular_data + else: + return None def _generate_tag_result_line(result: DataFrame, starting_balance: int, first_column: str) -> Dict: @@ -732,14 +735,15 @@ def show_backtest_result(strategy: str, results: Dict[str, Any], stake_currency: print(' BACKTESTING REPORT '.center(len(table.splitlines()[0]), '=')) print(table) - table = text_table_tags( - "buy_tag", - results['results_per_buy_tag'], - stake_currency=stake_currency) + if(results['results_per_buy_tag'] is not None): + table = text_table_tags( + "buy_tag", + results['results_per_buy_tag'], + stake_currency=stake_currency) - if isinstance(table, str) and len(table) > 0: - print(' BUY TAG STATS '.center(len(table.splitlines()[0]), '=')) - print(table) + if isinstance(table, str) and len(table) > 0: + print(' BUY TAG STATS '.center(len(table.splitlines()[0]), '=')) + print(table) table = text_table_sell_reason(sell_reason_stats=results['sell_reason_summary'], stake_currency=stake_currency) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 341eec5dd..96124ff45 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -107,10 +107,9 @@ class Telegram(RPCHandler): # this needs refactoring of the whole telegram module (same # problem in _help()). valid_keys: List[str] = [r'/start$', r'/stop$', r'/status$', r'/status table$', - r'/trades$', r'/performance$', r'/daily$', r'/daily \d+$', - r'/profit$', r'/profit \d+', + r'/trades$', r'/performance$', r'/buys', r'/sells', r'/mix_tags', + r'/daily$', r'/daily \d+$', r'/profit$', r'/profit \d+', r'/stats$', r'/count$', r'/locks$', r'/balance$', - r'/buys', r'/sells', r'/mix_tags', r'/stopbuy$', r'/reload_config$', r'/show_config$', r'/logs$', r'/whitelist$', r'/blacklist$', r'/edge$', r'/forcebuy$', r'/help$', r'/version$'] @@ -179,9 +178,9 @@ class Telegram(RPCHandler): CallbackQueryHandler(self._profit, pattern='update_profit'), CallbackQueryHandler(self._balance, pattern='update_balance'), CallbackQueryHandler(self._performance, pattern='update_performance'), - CallbackQueryHandler(self._performance, pattern='update_buy_tag_performance'), - CallbackQueryHandler(self._performance, pattern='update_sell_reason_performance'), - CallbackQueryHandler(self._performance, pattern='update_mix_tag_performance'), + CallbackQueryHandler(self._buy_tag_performance, pattern='update_buy_tag_performance'), + CallbackQueryHandler(self._sell_reason_performance, pattern='update_sell_reason_performance'), + CallbackQueryHandler(self._mix_tag_performance, pattern='update_mix_tag_performance'), CallbackQueryHandler(self._count, pattern='update_count'), CallbackQueryHandler(self._forcebuy_inline), ] diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 2248cd4c1..9d3ca01a9 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -567,6 +567,7 @@ def test_backtest__get_sell_trade_entry(default_conf, fee, mocker) -> None: 195, # Low 201.5, # High '', # Buy Signal Name + '', # Exit Signal Name ] trade = backtesting._enter_trade(pair, row=row) @@ -581,26 +582,27 @@ def test_backtest__get_sell_trade_entry(default_conf, fee, mocker) -> None: 195, # Low 210.5, # High '', # Buy Signal Name + '', # Exit Signal Name ] row_detail = pd.DataFrame( [ [ pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=0, tzinfo=timezone.utc), - 1, 200, 199, 0, 197, 200.1, '', + 1, 200, 199, 0, 197, 200.1, '', '', ], [ pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=1, tzinfo=timezone.utc), - 0, 199, 199.5, 0, 199, 199.7, '', + 0, 199, 199.5, 0, 199, 199.7, '', '', ], [ pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=2, tzinfo=timezone.utc), - 0, 199.5, 200.5, 0, 199, 200.8, '', + 0, 199.5, 200.5, 0, 199, 200.8, '', '', ], [ pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=3, tzinfo=timezone.utc), - 0, 200.5, 210.5, 0, 193, 210.5, '', # ROI sell (?) + 0, 200.5, 210.5, 0, 193, 210.5, '', '', # ROI sell (?) ], [ pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=4, tzinfo=timezone.utc), - 0, 200, 199, 0, 193, 200.1, '', + 0, 200, 199, 0, 193, 200.1, '', '', ], - ], columns=["date", "buy", "open", "close", "sell", "low", "high", "buy_tag"] + ], columns=["date", "buy", "open", "close", "sell", "low", "high", "buy_tag", "exit_tag"] ) # No data available. @@ -614,7 +616,7 @@ def test_backtest__get_sell_trade_entry(default_conf, fee, mocker) -> None: assert isinstance(trade, LocalTrade) # Assign empty ... no result. backtesting.detail_data[pair] = pd.DataFrame( - [], columns=["date", "buy", "open", "close", "sell", "low", "high", "buy_tag"]) + [], columns=["date", "buy", "open", "close", "sell", "low", "high", "buy_tag", "exit_tag"]) res = backtesting._get_sell_trade_entry(trade, row) assert res is None @@ -678,7 +680,7 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None: 'min_rate': [0.10370188, 0.10300000000000001], 'max_rate': [0.10501, 0.1038888], 'is_open': [False, False], - 'buy_tag': [None, None], + 'buy_tag': [None, None] }) pd.testing.assert_frame_equal(results, expected) data_pair = processed[pair] diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 7dde7b803..01d6d92cf 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -33,6 +33,7 @@ class DummyCls(Telegram): """ Dummy class for testing the Telegram @authorized_only decorator """ + def __init__(self, rpc: RPC, config) -> None: super().__init__(rpc, config) self.state = {'called': False} @@ -92,7 +93,8 @@ def test_telegram_init(default_conf, mocker, caplog) -> None: message_str = ("rpc.telegram is listening for following commands: [['status'], ['profit'], " "['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], ['trades'], " - "['delete'], ['performance'], ['stats'], ['daily'], ['count'], ['locks'], " + "['delete'], ['performance'], ['buys'], ['sells'], ['mix_tags'], " + "['stats'], ['daily'], ['count'], ['locks'], " "['unlock', 'delete_locks'], ['reload_config', 'reload_conf'], " "['show_config', 'show_conf'], ['stopbuy'], " "['whitelist'], ['blacklist'], ['logs'], ['edge'], ['help'], ['version']" @@ -713,6 +715,7 @@ def test_telegram_forcesell_handle(default_conf, update, ticker, fee, 'profit_ratio': 0.0629778, 'stake_currency': 'BTC', 'fiat_currency': 'USD', + 'buy_tag': ANY, 'sell_reason': SellType.FORCE_SELL.value, 'open_date': ANY, 'close_date': ANY, @@ -776,6 +779,7 @@ def test_telegram_forcesell_down_handle(default_conf, update, ticker, fee, 'profit_ratio': -0.05482878, 'stake_currency': 'BTC', 'fiat_currency': 'USD', + 'buy_tag': ANY, 'sell_reason': SellType.FORCE_SELL.value, 'open_date': ANY, 'close_date': ANY, @@ -829,6 +833,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None 'profit_ratio': -0.00408133, 'stake_currency': 'BTC', 'fiat_currency': 'USD', + 'buy_tag': ANY, 'sell_reason': SellType.FORCE_SELL.value, 'open_date': ANY, 'close_date': ANY, @@ -997,9 +1002,9 @@ def test_count_handle(default_conf, update, ticker, fee, mocker) -> None: msg = ('
  current    max    total stake\n---------  -----  -------------\n'
            '        1      {}          {}
').format( - default_conf['max_open_trades'], - default_conf['stake_amount'] - ) + default_conf['max_open_trades'], + default_conf['stake_amount'] + ) assert msg in msg_mock.call_args_list[0][0][0] @@ -1382,6 +1387,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: 'profit_ratio': -0.57405275, 'stake_currency': 'ETH', 'fiat_currency': 'USD', + 'buy_tag': 'buy_signal1', 'sell_reason': SellType.STOP_LOSS.value, 'open_date': arrow.utcnow().shift(hours=-1), 'close_date': arrow.utcnow(), @@ -1389,6 +1395,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: assert msg_mock.call_args[0][0] \ == ('\N{WARNING SIGN} *Binance:* Selling KEY/ETH (#1)\n' '*Profit:* `-57.41% (loss: -0.05746268 ETH / -24.812 USD)`\n' + '*Buy Tag:* `buy_signal1`\n' '*Sell Reason:* `stop_loss`\n' '*Duration:* `1:00:00 (60.0 min)`\n' '*Amount:* `1333.33333333`\n' @@ -1412,6 +1419,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: 'profit_amount': -0.05746268, 'profit_ratio': -0.57405275, 'stake_currency': 'ETH', + 'buy_tag': 'buy_signal1', 'sell_reason': SellType.STOP_LOSS.value, 'open_date': arrow.utcnow().shift(days=-1, hours=-2, minutes=-30), 'close_date': arrow.utcnow(), @@ -1419,6 +1427,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: assert msg_mock.call_args[0][0] \ == ('\N{WARNING SIGN} *Binance:* Selling KEY/ETH (#1)\n' '*Profit:* `-57.41%`\n' + '*Buy Tag:* `buy_signal1`\n' '*Sell Reason:* `stop_loss`\n' '*Duration:* `1 day, 2:30:00 (1590.0 min)`\n' '*Amount:* `1333.33333333`\n' @@ -1483,6 +1492,7 @@ def test_send_msg_sell_fill_notification(default_conf, mocker) -> None: 'profit_ratio': -0.57405275, 'stake_currency': 'ETH', 'fiat_currency': 'USD', + 'buy_tag': 'buy_signal1', 'sell_reason': SellType.STOP_LOSS.value, 'open_date': arrow.utcnow().shift(hours=-1), 'close_date': arrow.utcnow(), @@ -1574,12 +1584,14 @@ def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None: 'profit_ratio': -0.57405275, 'stake_currency': 'ETH', 'fiat_currency': 'USD', + 'buy_tag': 'buy_signal1', 'sell_reason': SellType.STOP_LOSS.value, 'open_date': arrow.utcnow().shift(hours=-2, minutes=-35, seconds=-3), 'close_date': arrow.utcnow(), }) assert msg_mock.call_args[0][0] == ('\N{WARNING SIGN} *Binance:* Selling KEY/ETH (#1)\n' '*Profit:* `-57.41%`\n' + '*Buy Tag:* `buy_signal1`\n' '*Sell Reason:* `stop_loss`\n' '*Duration:* `2:35:03 (155.1 min)`\n' '*Amount:* `1333.33333333`\n' diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index dcb9e3e64..62510b370 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -38,20 +38,27 @@ def test_returns_latest_signal(mocker, default_conf, ohlcv_history): mocked_history['buy'] = 0 mocked_history.loc[1, 'sell'] = 1 - assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_history) == (False, True, None) + assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_history) == (False, True, None, None) mocked_history.loc[1, 'sell'] = 0 mocked_history.loc[1, 'buy'] = 1 - assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_history) == (True, False, None) + assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_history) == (True, False, None, None) mocked_history.loc[1, 'sell'] = 0 mocked_history.loc[1, 'buy'] = 0 - assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_history) == (False, False, None) + assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_history) == (False, False, None, None) mocked_history.loc[1, 'sell'] = 0 mocked_history.loc[1, 'buy'] = 1 mocked_history.loc[1, 'buy_tag'] = 'buy_signal_01' - assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_history) == (True, False, 'buy_signal_01') + assert _STRATEGY.get_signal( + 'ETH/BTC', + '5m', + mocked_history) == ( + True, + False, + 'buy_signal_01', + None) def test_analyze_pair_empty(default_conf, mocker, caplog, ohlcv_history): @@ -68,17 +75,24 @@ def test_analyze_pair_empty(default_conf, mocker, caplog, ohlcv_history): def test_get_signal_empty(default_conf, mocker, caplog): - assert (False, False, None) == _STRATEGY.get_signal( + assert (False, False, None, None) == _STRATEGY.get_signal( 'foo', default_conf['timeframe'], DataFrame() ) assert log_has('Empty candle (OHLCV) data for pair foo', caplog) caplog.clear() - assert (False, False, None) == _STRATEGY.get_signal('bar', default_conf['timeframe'], None) + assert ( + False, + False, + None, + None) == _STRATEGY.get_signal( + 'bar', + default_conf['timeframe'], + None) assert log_has('Empty candle (OHLCV) data for pair bar', caplog) caplog.clear() - assert (False, False, None) == _STRATEGY.get_signal( + assert (False, False, None, None) == _STRATEGY.get_signal( 'baz', default_conf['timeframe'], DataFrame([]) @@ -118,7 +132,7 @@ def test_get_signal_old_dataframe(default_conf, mocker, caplog, ohlcv_history): caplog.set_level(logging.INFO) mocker.patch.object(_STRATEGY, 'assert_df') - assert (False, False, None) == _STRATEGY.get_signal( + assert (False, False, None, None) == _STRATEGY.get_signal( 'xyz', default_conf['timeframe'], mocked_history @@ -140,7 +154,7 @@ def test_get_signal_no_sell_column(default_conf, mocker, caplog, ohlcv_history): caplog.set_level(logging.INFO) mocker.patch.object(_STRATEGY, 'assert_df') - assert (True, False, None) == _STRATEGY.get_signal( + assert (True, False, None, None) == _STRATEGY.get_signal( 'xyz', default_conf['timeframe'], mocked_history @@ -646,7 +660,7 @@ def test_strategy_safe_wrapper(value): ret = strategy_safe_wrapper(working_method, message='DeadBeef')(value) - assert type(ret) == type(value) + assert isinstance(ret, type(value)) assert ret == value From 5f309627eac656b65bfee9ce5761a138c3f809cd Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 24 Oct 2021 09:01:13 +0200 Subject: [PATCH 035/208] 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 17432b2823fdb6fd54b9fd7fde2e88a146c41ce5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 24 Oct 2021 09:15:05 +0200 Subject: [PATCH 036/208] Improve some stylings --- README.md | 2 +- freqtrade/freqtradebot.py | 4 +--- freqtrade/optimize/backtesting.py | 6 +++++- freqtrade/rpc/telegram.py | 3 ++- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 906e19ef7..0a4d6424e 100644 --- a/README.md +++ b/README.md @@ -201,4 +201,4 @@ To run this bot we recommend you a cloud instance with a minimum of: - [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) - [TA-Lib](https://mrjbq7.github.io/ta-lib/install.html) - [virtualenv](https://virtualenv.pypa.io/en/stable/installation.html) (Recommended) -- [Docker](https://www.docker.com/products/docker) (Recommended) \ No newline at end of file +- [Docker](https://www.docker.com/products/docker) (Recommended) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 99373ae74..fb42a8924 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1149,9 +1149,7 @@ class FreqtradeBot(LoggingMixin): trade.open_order_id = order['id'] trade.sell_order_status = '' trade.close_rate_requested = limit - trade.sell_reason = sell_reason.sell_reason - if(exit_tag is not None): - trade.sell_reason = exit_tag + trade.sell_reason = exit_tag or 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) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index d23b6bdc3..6fea716a0 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -364,7 +364,11 @@ class Backtesting: # Checks and adds an exit tag, after checking that the length of the # sell_row has the length for an exit tag column - if(len(sell_row) > EXIT_TAG_IDX and sell_row[EXIT_TAG_IDX] is not None and len(sell_row[EXIT_TAG_IDX]) > 0): + if( + len(sell_row) > EXIT_TAG_IDX + and sell_row[EXIT_TAG_IDX] is not None + and len(sell_row[EXIT_TAG_IDX]) > 0 + ): trade.sell_reason = sell_row[EXIT_TAG_IDX] trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 96124ff45..f79f8d457 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -179,7 +179,8 @@ class Telegram(RPCHandler): CallbackQueryHandler(self._balance, pattern='update_balance'), CallbackQueryHandler(self._performance, pattern='update_performance'), CallbackQueryHandler(self._buy_tag_performance, pattern='update_buy_tag_performance'), - CallbackQueryHandler(self._sell_reason_performance, pattern='update_sell_reason_performance'), + CallbackQueryHandler(self._sell_reason_performance, + pattern='update_sell_reason_performance'), CallbackQueryHandler(self._mix_tag_performance, pattern='update_mix_tag_performance'), CallbackQueryHandler(self._count, pattern='update_count'), CallbackQueryHandler(self._forcebuy_inline), From 22dd2ca003726868ba45158a9e9bee33dee5aeb8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 24 Oct 2021 15:18:29 +0200 Subject: [PATCH 037/208] Fix mypy type errors --- freqtrade/optimize/optimize_reports.py | 2 +- freqtrade/persistence/models.py | 6 +++--- freqtrade/rpc/rpc.py | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 7fb6a14a0..4e51e80c2 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -165,7 +165,7 @@ def generate_tag_metrics(tag_type: str, tabular_data.append(_generate_result_line(results, starting_balance, 'TOTAL')) return tabular_data else: - return None + return [] def _generate_tag_result_line(result: DataFrame, starting_balance: int, first_column: str) -> Dict: diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 9a1f04429..a3c6656af 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -861,7 +861,7 @@ class Trade(_DECL_BASE, LocalTrade): ] @staticmethod - def get_buy_tag_performance(pair: str) -> List[Dict[str, Any]]: + def get_buy_tag_performance(pair: Optional[str]) -> List[Dict[str, Any]]: """ Returns List of dicts containing all Trades, based on buy tag performance Can either be average for all pairs or a specific pair provided @@ -900,7 +900,7 @@ class Trade(_DECL_BASE, LocalTrade): ] @staticmethod - def get_sell_reason_performance(pair: str) -> List[Dict[str, Any]]: + def get_sell_reason_performance(pair: Optional[str]) -> List[Dict[str, Any]]: """ Returns List of dicts containing all Trades, based on sell reason performance Can either be average for all pairs or a specific pair provided @@ -938,7 +938,7 @@ class Trade(_DECL_BASE, LocalTrade): ] @staticmethod - def get_mix_tag_performance(pair: str) -> List[Dict[str, Any]]: + def get_mix_tag_performance(pair: Optional[str]) -> List[Dict[str, Any]]: """ Returns List of dicts containing all Trades, based on buy_tag + sell_reason performance Can either be average for all pairs or a specific pair provided diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 310b0ad07..4ef9213eb 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -689,7 +689,7 @@ class RPC: [x.update({'profit': round(x['profit'] * 100, 2)}) for x in pair_rates] return pair_rates - def _rpc_buy_tag_performance(self, pair: str) -> List[Dict[str, Any]]: + def _rpc_buy_tag_performance(self, pair: Optional[str]) -> List[Dict[str, Any]]: """ Handler for buy tag performance. Shows a performance statistic from finished trades @@ -699,7 +699,7 @@ class RPC: [x.update({'profit': round(x['profit'] * 100, 2)}) for x in buy_tags] return buy_tags - def _rpc_sell_reason_performance(self, pair: str) -> List[Dict[str, Any]]: + def _rpc_sell_reason_performance(self, pair: Optional[str]) -> List[Dict[str, Any]]: """ Handler for sell reason performance. Shows a performance statistic from finished trades @@ -709,7 +709,7 @@ class RPC: [x.update({'profit': round(x['profit'] * 100, 2)}) for x in sell_reasons] return sell_reasons - def _rpc_mix_tag_performance(self, pair: str) -> List[Dict[str, Any]]: + def _rpc_mix_tag_performance(self, pair: Optional[str]) -> List[Dict[str, Any]]: """ Handler for mix tag (buy_tag + sell_reason) performance. Shows a performance statistic from finished trades From 88b96d5d1b1aea451331bd92a664e7a92b00dfed Mon Sep 17 00:00:00 2001 From: Robert Roman Date: Mon, 25 Oct 2021 00:45:10 -0500 Subject: [PATCH 038/208] 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 7ff16997e9b440d68d3e1c57a60cc31c7591d5ac Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sun, 24 Oct 2021 23:27:08 -0600 Subject: [PATCH 039/208] Wrote echo block method for setup script --- setup.sh | 50 +++++++++++++++++--------------------------------- 1 file changed, 17 insertions(+), 33 deletions(-) diff --git a/setup.sh b/setup.sh index 1173b59b9..3af358468 100755 --- a/setup.sh +++ b/setup.sh @@ -1,12 +1,16 @@ #!/usr/bin/env bash #encoding=utf8 +function echo_block() { + echo "----------------------------" + echo $1 + echo "----------------------------" +} + function check_installed_pip() { ${PYTHON} -m pip > /dev/null if [ $? -ne 0 ]; then - echo "-----------------------------" - echo "Installing Pip for ${PYTHON}" - echo "-----------------------------" + echo_block "Installing Pip for ${PYTHON}" curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py ${PYTHON} get-pip.py rm get-pip.py @@ -37,9 +41,7 @@ function check_installed_python() { } function updateenv() { - echo "-------------------------" - echo "Updating your virtual env" - echo "-------------------------" + echo_block "Updating your virtual env" if [ ! -f .env/bin/activate ]; then echo "Something went wrong, no virtual environment found." exit 1 @@ -110,18 +112,14 @@ function install_mac_newer_python_dependencies() { if [ ! $(brew --prefix --installed hdf5 2>/dev/null) ] then - echo "-------------------------" - echo "Installing hdf5" - echo "-------------------------" + echo_block "Installing hdf5" brew install hdf5 fi export HDF5_DIR=$(brew --prefix) if [ ! $(brew --prefix --installed c-blosc 2>/dev/null) ] then - echo "-------------------------" - echo "Installing c-blosc" - echo "-------------------------" + echo_block "Installing c-blosc" brew install c-blosc fi export CBLOSC_DIR=$(brew --prefix) @@ -131,9 +129,7 @@ function install_mac_newer_python_dependencies() { function install_macos() { if [ ! -x "$(command -v brew)" ] then - echo "-------------------------" - echo "Installing Brew" - echo "-------------------------" + echo_block "Installing Brew" /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" fi #Gets number after decimal in python version @@ -160,9 +156,7 @@ function update() { # Reset Develop or Stable branch function reset() { - echo "----------------------------" - echo "Resetting branch and virtual env" - echo "----------------------------" + echo_block "Resetting branch and virtual env" if [ "1" == $(git branch -vv |grep -cE "\* develop|\* stable") ] then @@ -200,16 +194,12 @@ function reset() { } function config() { - - echo "-------------------------" - echo "Please use 'freqtrade new-config -c config.json' to generate a new configuration file." - echo "-------------------------" + echo_block "Please use 'freqtrade new-config -c config.json' to generate a new configuration file." } function install() { - echo "-------------------------" - echo "Installing mandatory dependencies" - echo "-------------------------" + + echo_block "Installing mandatory dependencies" if [ "$(uname -s)" == "Darwin" ] then @@ -228,20 +218,14 @@ function install() { echo reset config - echo "-------------------------" - echo "Run the bot !" - echo "-------------------------" + echo_block "Run the bot !" echo "You can now use the bot by executing 'source .env/bin/activate; freqtrade '." echo "You can see the list of available bot sub-commands by executing 'source .env/bin/activate; freqtrade --help'." echo "You verify that freqtrade is installed successfully by running 'source .env/bin/activate; freqtrade --version'." } function plot() { - echo " - ----------------------------------------- - Installing dependencies for Plotting scripts - ----------------------------------------- - " + echo_block "Installing dependencies for Plotting scripts" ${PYTHON} -m pip install plotly --upgrade } From d1e2a53267bdd0a6b2d1a2d7bae72734ed80ce01 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sun, 24 Oct 2021 23:33:02 -0600 Subject: [PATCH 040/208] Added centOS support to setup.sh script --- setup.sh | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/setup.sh b/setup.sh index 3af358468..1d76c6d2a 100755 --- a/setup.sh +++ b/setup.sh @@ -144,7 +144,14 @@ function install_macos() { # Install bot Debian_ubuntu function install_debian() { sudo apt-get update - sudo apt-get install -y build-essential autoconf libtool pkg-config make wget git $(echo lib${PYTHON}-dev ${PYTHON}-venv) + sudo apt-get install -y gcc build-essential autoconf libtool pkg-config make wget git $(echo lib${PYTHON}-dev ${PYTHON}-venv) + install_talib +} + +# Install bot RedHat_CentOS +function install_redhat() { + sudo yum update + sudo yum install -y gcc gcc-c++ build-essential autoconf libtool pkg-config make wget git $(echo ${PYTHON}-devel | sed 's/\.//g') install_talib } @@ -201,17 +208,18 @@ function install() { echo_block "Installing mandatory dependencies" - if [ "$(uname -s)" == "Darwin" ] - then + if [ "$(uname -s)" == "Darwin" ]; then echo "macOS detected. Setup for this system in-progress" install_macos - elif [ -x "$(command -v apt-get)" ] - then + elif [ -x "$(command -v apt-get)" ]; then echo "Debian/Ubuntu detected. Setup for this system in-progress" install_debian + elif [ -x "$(command -v yum)" ]; then + echo "Red Hat/CentOS detected. Setup for this system in-progress" + install_redhat else echo "This script does not support your OS." - echo "If you have Python3.6 or Python3.7, pip, virtualenv, ta-lib you can continue." + echo "If you have Python version 3.6 - 3.9, pip, virtualenv, ta-lib you can continue." echo "Wait 10 seconds to continue the next install steps or use ctrl+c to interrupt this shell." sleep 10 fi From b51f946ee07d579a09f0b0368412454fc6e92ef7 Mon Sep 17 00:00:00 2001 From: theluxaz Date: Mon, 25 Oct 2021 23:43:22 +0300 Subject: [PATCH 041/208] Fixed models and rpc performance functions, added skeletons for tests. --- freqtrade/persistence/models.py | 124 ++++++++++++++------------------ freqtrade/rpc/rpc.py | 12 ++-- tests/rpc/test_rpc.py | 114 +++++++++++++++++++++++++++++ tests/rpc/test_rpc_telegram.py | 99 +++++++++++++++++++++++++ 4 files changed, 272 insertions(+), 77 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index a3c6656af..8ccf8bbef 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -850,7 +850,8 @@ class Trade(_DECL_BASE, LocalTrade): .group_by(Trade.pair) \ .order_by(desc('profit_sum_abs')) \ .all() - return [ + + response = [ { 'pair': pair, 'profit': profit, @@ -859,6 +860,8 @@ class Trade(_DECL_BASE, LocalTrade): } for pair, profit, profit_abs, count in pair_rates ] + [x.update({'profit': round(x['profit'] * 100, 2)}) for x in response] + return response @staticmethod def get_buy_tag_performance(pair: Optional[str]) -> List[Dict[str, Any]]: @@ -868,36 +871,31 @@ class Trade(_DECL_BASE, LocalTrade): NOTE: Not supported in Backtesting. """ + filters = [Trade.is_open.is_(False)] if(pair is not None): - tag_perf = Trade.query.with_entities( - Trade.buy_tag, - func.sum(Trade.close_profit).label('profit_sum'), - func.sum(Trade.close_profit_abs).label('profit_sum_abs'), - func.count(Trade.pair).label('count') - ).filter(Trade.is_open.is_(False))\ - .filter(Trade.pair == pair) \ - .order_by(desc('profit_sum_abs')) \ - .all() - else: - tag_perf = Trade.query.with_entities( - Trade.buy_tag, - func.sum(Trade.close_profit).label('profit_sum'), - func.sum(Trade.close_profit_abs).label('profit_sum_abs'), - func.count(Trade.pair).label('count') - ).filter(Trade.is_open.is_(False))\ - .group_by(Trade.buy_tag) \ - .order_by(desc('profit_sum_abs')) \ - .all() + filters.append(Trade.pair == pair) - return [ + buy_tag_perf = Trade.query.with_entities( + Trade.buy_tag, + func.sum(Trade.close_profit).label('profit_sum'), + func.sum(Trade.close_profit_abs).label('profit_sum_abs'), + func.count(Trade.pair).label('count') + ).filter(*filters)\ + .group_by(Trade.buy_tag) \ + .order_by(desc('profit_sum_abs')) \ + .all() + + response = [ { 'buy_tag': buy_tag if buy_tag is not None else "Other", 'profit': profit, 'profit_abs': profit_abs, 'count': count } - for buy_tag, profit, profit_abs, count in tag_perf + for buy_tag, profit, profit_abs, count in buy_tag_perf ] + [x.update({'profit': round(x['profit'] * 100, 2)}) for x in response] + return response @staticmethod def get_sell_reason_performance(pair: Optional[str]) -> List[Dict[str, Any]]: @@ -906,36 +904,32 @@ class Trade(_DECL_BASE, LocalTrade): Can either be average for all pairs or a specific pair provided NOTE: Not supported in Backtesting. """ - if(pair is not None): - tag_perf = Trade.query.with_entities( - Trade.sell_reason, - func.sum(Trade.close_profit).label('profit_sum'), - func.sum(Trade.close_profit_abs).label('profit_sum_abs'), - func.count(Trade.pair).label('count') - ).filter(Trade.is_open.is_(False))\ - .filter(Trade.pair == pair) \ - .order_by(desc('profit_sum_abs')) \ - .all() - else: - tag_perf = Trade.query.with_entities( - Trade.sell_reason, - func.sum(Trade.close_profit).label('profit_sum'), - func.sum(Trade.close_profit_abs).label('profit_sum_abs'), - func.count(Trade.pair).label('count') - ).filter(Trade.is_open.is_(False))\ - .group_by(Trade.sell_reason) \ - .order_by(desc('profit_sum_abs')) \ - .all() - return [ + filters = [Trade.is_open.is_(False)] + if(pair is not None): + filters.append(Trade.pair == pair) + + sell_tag_perf = Trade.query.with_entities( + Trade.sell_reason, + func.sum(Trade.close_profit).label('profit_sum'), + func.sum(Trade.close_profit_abs).label('profit_sum_abs'), + func.count(Trade.pair).label('count') + ).filter(*filters)\ + .group_by(Trade.sell_reason) \ + .order_by(desc('profit_sum_abs')) \ + .all() + + response = [ { 'sell_reason': sell_reason if sell_reason is not None else "Other", 'profit': profit, 'profit_abs': profit_abs, 'count': count } - for sell_reason, profit, profit_abs, count in tag_perf + for sell_reason, profit, profit_abs, count in sell_tag_perf ] + [x.update({'profit': round(x['profit'] * 100, 2)}) for x in response] + return response @staticmethod def get_mix_tag_performance(pair: Optional[str]) -> List[Dict[str, Any]]: @@ -944,34 +938,25 @@ class Trade(_DECL_BASE, LocalTrade): Can either be average for all pairs or a specific pair provided NOTE: Not supported in Backtesting. """ - if(pair is not None): - tag_perf = Trade.query.with_entities( - Trade.id, - Trade.buy_tag, - Trade.sell_reason, - func.sum(Trade.close_profit).label('profit_sum'), - func.sum(Trade.close_profit_abs).label('profit_sum_abs'), - func.count(Trade.pair).label('count') - ).filter(Trade.is_open.is_(False))\ - .filter(Trade.pair == pair) \ - .order_by(desc('profit_sum_abs')) \ - .all() - else: - tag_perf = Trade.query.with_entities( - Trade.id, - Trade.buy_tag, - Trade.sell_reason, - func.sum(Trade.close_profit).label('profit_sum'), - func.sum(Trade.close_profit_abs).label('profit_sum_abs'), - func.count(Trade.pair).label('count') - ).filter(Trade.is_open.is_(False))\ - .group_by(Trade.id) \ - .order_by(desc('profit_sum_abs')) \ - .all() + filters = [Trade.is_open.is_(False)] + if(pair is not None): + filters.append(Trade.pair == pair) + + mix_tag_perf = Trade.query.with_entities( + Trade.id, + Trade.buy_tag, + Trade.sell_reason, + func.sum(Trade.close_profit).label('profit_sum'), + func.sum(Trade.close_profit_abs).label('profit_sum_abs'), + func.count(Trade.pair).label('count') + ).filter(*filters)\ + .group_by(Trade.id) \ + .order_by(desc('profit_sum_abs')) \ + .all() return_list: List[Dict] = [] - for id, buy_tag, sell_reason, profit, profit_abs, count in tag_perf: + for id, buy_tag, sell_reason, profit, profit_abs, count in mix_tag_perf: buy_tag = buy_tag if buy_tag is not None else "Other" sell_reason = sell_reason if sell_reason is not None else "Other" @@ -993,6 +978,7 @@ class Trade(_DECL_BASE, LocalTrade): 'count': 1 + return_list[i]["count"]} i += 1 + [x.update({'profit': round(x['profit'] * 100, 2)}) for x in return_list] return return_list @staticmethod diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 4ef9213eb..42d502cd8 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -685,8 +685,7 @@ class RPC: Shows a performance statistic from finished trades """ pair_rates = Trade.get_overall_performance() - # Round and convert to % - [x.update({'profit': round(x['profit'] * 100, 2)}) for x in pair_rates] + return pair_rates def _rpc_buy_tag_performance(self, pair: Optional[str]) -> List[Dict[str, Any]]: @@ -695,8 +694,7 @@ class RPC: Shows a performance statistic from finished trades """ buy_tags = Trade.get_buy_tag_performance(pair) - # Round and convert to % - [x.update({'profit': round(x['profit'] * 100, 2)}) for x in buy_tags] + return buy_tags def _rpc_sell_reason_performance(self, pair: Optional[str]) -> List[Dict[str, Any]]: @@ -705,8 +703,7 @@ class RPC: Shows a performance statistic from finished trades """ sell_reasons = Trade.get_sell_reason_performance(pair) - # Round and convert to % - [x.update({'profit': round(x['profit'] * 100, 2)}) for x in sell_reasons] + return sell_reasons def _rpc_mix_tag_performance(self, pair: Optional[str]) -> List[Dict[str, Any]]: @@ -715,8 +712,7 @@ class RPC: Shows a performance statistic from finished trades """ mix_tags = Trade.get_mix_tag_performance(pair) - # Round and convert to % - [x.update({'profit': round(x['profit'] * 100, 2)}) for x in mix_tags] + return mix_tags def _rpc_count(self) -> Dict[str, float]: diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index f8c923958..78805a456 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -827,6 +827,120 @@ def test_performance_handle(default_conf, ticker, limit_buy_order, fee, assert res[0]['count'] == 1 assert prec_satoshi(res[0]['profit'], 6.2) + # TEST FOR TRADES WITH NO BUY TAG + # TEST TRADE WITH ONE BUY_TAG AND OTHER TWO TRADES WITH THE SAME TAG + # TEST THE SAME FOR A PAIR + + +def test_buy_tag_performance_handle(default_conf, ticker, limit_buy_order, fee, + limit_sell_order, mocker) -> None: + mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_balances=MagicMock(return_value=ticker), + fetch_ticker=ticker, + get_fee=fee, + ) + + freqtradebot = get_patched_freqtradebot(mocker, default_conf) + patch_get_signal(freqtradebot) + rpc = RPC(freqtradebot) + + # Create some test data + freqtradebot.enter_positions() + trade = Trade.query.first() + assert trade + + # Simulate fulfilled LIMIT_BUY order for trade + trade.update(limit_buy_order) + + # Simulate fulfilled LIMIT_SELL order for trade + trade.update(limit_sell_order) + + trade.close_date = datetime.utcnow() + trade.is_open = False + res = rpc._rpc_buy_tag_performance(None) + assert len(res) == 1 + assert res[0]['pair'] == 'ETH/BTC' + assert res[0]['count'] == 1 + assert prec_satoshi(res[0]['profit'], 6.2) + + # TEST FOR TRADES WITH NO SELL REASON + # TEST TRADE WITH ONE SELL REASON AND OTHER TWO TRADES WITH THE SAME reason + # TEST THE SAME FOR A PAIR + + +def test_sell_reason_performance_handle(default_conf, ticker, limit_buy_order, fee, + limit_sell_order, mocker) -> None: + mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_balances=MagicMock(return_value=ticker), + fetch_ticker=ticker, + get_fee=fee, + ) + + freqtradebot = get_patched_freqtradebot(mocker, default_conf) + patch_get_signal(freqtradebot) + rpc = RPC(freqtradebot) + + # Create some test data + freqtradebot.enter_positions() + trade = Trade.query.first() + assert trade + + # Simulate fulfilled LIMIT_BUY order for trade + trade.update(limit_buy_order) + + # Simulate fulfilled LIMIT_SELL order for trade + trade.update(limit_sell_order) + + trade.close_date = datetime.utcnow() + trade.is_open = False + res = rpc._rpc_sell_reason_performance(None) + assert len(res) == 1 + assert res[0]['pair'] == 'ETH/BTC' + assert res[0]['count'] == 1 + assert prec_satoshi(res[0]['profit'], 6.2) + + # TEST FOR TRADES WITH NO TAGS + # TEST TRADE WITH ONE TAG MIX AND OTHER TWO TRADES WITH THE SAME TAG MIX + # TEST THE SAME FOR A PAIR + + +def test_mix_tag_performance_handle(default_conf, ticker, limit_buy_order, fee, + limit_sell_order, mocker) -> None: + mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_balances=MagicMock(return_value=ticker), + fetch_ticker=ticker, + get_fee=fee, + ) + + freqtradebot = get_patched_freqtradebot(mocker, default_conf) + patch_get_signal(freqtradebot) + rpc = RPC(freqtradebot) + + # Create some test data + freqtradebot.enter_positions() + trade = Trade.query.first() + assert trade + + # Simulate fulfilled LIMIT_BUY order for trade + trade.update(limit_buy_order) + + # Simulate fulfilled LIMIT_SELL order for trade + trade.update(limit_sell_order) + + trade.close_date = datetime.utcnow() + trade.is_open = False + res = rpc._rpc_mix_tag_performance(None) + assert len(res) == 1 + assert res[0]['pair'] == 'ETH/BTC' + assert res[0]['count'] == 1 + assert prec_satoshi(res[0]['profit'], 6.2) + def test_rpc_count(mocker, default_conf, ticker, fee) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 01d6d92cf..306181eae 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -978,6 +978,105 @@ def test_performance_handle(default_conf, update, ticker, fee, assert 'Performance' in msg_mock.call_args_list[0][0][0] assert 'ETH/BTC\t0.00006217 BTC (6.20%) (1)' in msg_mock.call_args_list[0][0][0] + # TEST FOR TRADES WITH NO BUY TAG + # TEST TRADE WITH ONE BUY_TAG AND OTHER TWO TRADES WITH THE SAME TAG + # TEST THE SAME FOR A PAIR + + +def test_buy_tag_performance_handle(default_conf, update, ticker, fee, + limit_buy_order, limit_sell_order, mocker) -> None: + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + fetch_ticker=ticker, + get_fee=fee, + ) + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) + patch_get_signal(freqtradebot) + + # Create some test data + freqtradebot.enter_positions() + trade = Trade.query.first() + assert trade + + # Simulate fulfilled LIMIT_BUY order for trade + trade.update(limit_buy_order) + + # Simulate fulfilled LIMIT_SELL order for trade + trade.update(limit_sell_order) + + trade.close_date = datetime.utcnow() + trade.is_open = False + telegram._buy_tag_performance(update=update, context=MagicMock()) + assert msg_mock.call_count == 1 + assert 'Performance' in msg_mock.call_args_list[0][0][0] + assert 'ETH/BTC\t0.00006217 BTC (6.20%) (1)' in msg_mock.call_args_list[0][0][0] + + # TEST FOR TRADES WITH NO SELL REASON + # TEST TRADE WITH ONE SELL REASON AND OTHER TWO TRADES WITH THE SAME reason + # TEST THE SAME FOR A PAIR + + +def test_sell_reason_performance_handle(default_conf, update, ticker, fee, + limit_buy_order, limit_sell_order, mocker) -> None: + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + fetch_ticker=ticker, + get_fee=fee, + ) + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) + patch_get_signal(freqtradebot) + + # Create some test data + freqtradebot.enter_positions() + trade = Trade.query.first() + assert trade + + # Simulate fulfilled LIMIT_BUY order for trade + trade.update(limit_buy_order) + + # Simulate fulfilled LIMIT_SELL order for trade + trade.update(limit_sell_order) + + trade.close_date = datetime.utcnow() + trade.is_open = False + telegram._sell_reason_performance(update=update, context=MagicMock()) + assert msg_mock.call_count == 1 + assert 'Performance' in msg_mock.call_args_list[0][0][0] + assert 'ETH/BTC\t0.00006217 BTC (6.20%) (1)' in msg_mock.call_args_list[0][0][0] + + # TEST FOR TRADES WITH NO TAGS + # TEST TRADE WITH ONE TAG MIX AND OTHER TWO TRADES WITH THE SAME TAG MIX + # TEST THE SAME FOR A PAIR + + +def test_mix_tag_performance_handle(default_conf, update, ticker, fee, + limit_buy_order, limit_sell_order, mocker) -> None: + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + fetch_ticker=ticker, + get_fee=fee, + ) + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) + patch_get_signal(freqtradebot) + + # Create some test data + freqtradebot.enter_positions() + trade = Trade.query.first() + assert trade + + # Simulate fulfilled LIMIT_BUY order for trade + trade.update(limit_buy_order) + + # Simulate fulfilled LIMIT_SELL order for trade + trade.update(limit_sell_order) + + trade.close_date = datetime.utcnow() + trade.is_open = False + telegram._mix_tag_performance(update=update, context=MagicMock()) + assert msg_mock.call_count == 1 + assert 'Performance' in msg_mock.call_args_list[0][0][0] + assert 'ETH/BTC\t0.00006217 BTC (6.20%) (1)' in msg_mock.call_args_list[0][0][0] + def test_count_handle(default_conf, update, ticker, fee, mocker) -> None: mocker.patch.multiple( From c3f3bdaa2ae03ebffba27ba71cf1b7276fa77e76 Mon Sep 17 00:00:00 2001 From: incrementby1 Date: Tue, 26 Oct 2021 00:04:40 +0200 Subject: [PATCH 042/208] 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 043/208] 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 044/208] 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 045/208] 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 e4e75d4861247c726781373de54a1e37ba95352d Mon Sep 17 00:00:00 2001 From: theluxaz Date: Wed, 27 Oct 2021 01:29:19 +0300 Subject: [PATCH 046/208] Added test data for buy_tag/sell_reason testing --- tests/conftest.py | 38 +++++++- tests/conftest_trades_tags.py | 165 ++++++++++++++++++++++++++++++++++ tests/rpc/test_rpc.py | 51 +++++++++-- 3 files changed, 242 insertions(+), 12 deletions(-) create mode 100644 tests/conftest_trades_tags.py diff --git a/tests/conftest.py b/tests/conftest.py index 698c464ed..6afda47d5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -23,11 +23,19 @@ from freqtrade.freqtradebot import FreqtradeBot from freqtrade.persistence import LocalTrade, Trade, init_db from freqtrade.resolvers import ExchangeResolver from freqtrade.worker import Worker -from tests.conftest_trades import (mock_trade_1, mock_trade_2, mock_trade_3, mock_trade_4, - mock_trade_5, mock_trade_6) +from tests.conftest_trades import ( + mock_trade_1, + mock_trade_2, + mock_trade_3, + mock_trade_4, + mock_trade_5, + mock_trade_6, + mock_trade_7, + mock_trade_8, + mock_trade_9) from tests.conftest_trades_usdt import (mock_trade_usdt_1, mock_trade_usdt_2, mock_trade_usdt_3, mock_trade_usdt_4, mock_trade_usdt_5, mock_trade_usdt_6) - +from tests.conftest_trades_tags import (mock_trade_tags_1, mock_trade_tags_2, mock_trade_tags_3) logging.getLogger('').setLevel(logging.INFO) @@ -229,6 +237,30 @@ def create_mock_trades(fee, use_db: bool = True): Trade.commit() +def create_mock_trades_tags(fee, use_db: bool = True): + """ + Create some fake trades to simulate buy tags and sell reasons + """ + def add_trade(trade): + if use_db: + Trade.query.session.add(trade) + else: + LocalTrade.add_bt_trade(trade) + + # Simulate dry_run entries + trade = mock_trade_tags_1(fee) + add_trade(trade) + + trade = mock_trade_tags_2(fee) + add_trade(trade) + + trade = mock_trade_tags_3(fee) + add_trade(trade) + + if use_db: + Trade.commit() + + def create_mock_trades_usdt(fee, use_db: bool = True): """ Create some fake trades ... diff --git a/tests/conftest_trades_tags.py b/tests/conftest_trades_tags.py new file mode 100644 index 000000000..db0d3d3bd --- /dev/null +++ b/tests/conftest_trades_tags.py @@ -0,0 +1,165 @@ +from datetime import datetime, timedelta, timezone + +from freqtrade.persistence.models import Order, Trade + + +MOCK_TRADE_COUNT = 3 + + +def mock_order_1(): + return { + 'id': 'prod_buy_1', + 'symbol': 'LTC/BTC', + 'status': 'closed', + 'side': 'buy', + 'type': 'limit', + 'price': 0.15, + 'amount': 2.0, + 'filled': 2.0, + 'remaining': 0.0, + } + + +def mock_order_1_sell(): + return { + 'id': 'prod_sell_1', + 'symbol': 'LTC/BTC', + 'status': 'open', + 'side': 'sell', + 'type': 'limit', + 'price': 0.20, + 'amount': 2.0, + 'filled': 0.0, + 'remaining': 2.0, + } + + +def mock_trade_tags_1(fee): + trade = Trade( + pair='LTC/BTC', + stake_amount=0.001, + amount=2.0, + amount_requested=2.0, + fee_open=fee.return_value, + fee_close=fee.return_value, + is_open=True, + open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=15), + open_rate=0.15, + exchange='binance', + open_order_id='dry_run_buy_123455', + strategy='StrategyTestV2', + timeframe=5, + buy_tag="BUY_TAG1", + sell_reason="SELL_REASON2" + ) + o = Order.parse_from_ccxt_object(mock_order_1(), 'LTC/BTC', 'buy') + trade.orders.append(o) + o = Order.parse_from_ccxt_object(mock_order_1_sell(), 'LTC/BTC', 'sell') + trade.orders.append(o) + return trade + + +def mock_order_2(): + return { + 'id': '1239', + 'symbol': 'LTC/BTC', + 'status': 'closed', + 'side': 'buy', + 'type': 'limit', + 'price': 0.120, + 'amount': 100.0, + 'filled': 100.0, + 'remaining': 0.0, + } + + +def mock_order_2_sell(): + return { + 'id': '12392', + 'symbol': 'LTC/BTC', + 'status': 'closed', + 'side': 'sell', + 'type': 'limit', + 'price': 0.138, + 'amount': 100.0, + 'filled': 100.0, + 'remaining': 0.0, + } + + +def mock_trade_tags_2(fee): + trade = Trade( + pair='LTC/BTC', + stake_amount=0.001, + amount=100.0, + amount_requested=100.0, + fee_open=fee.return_value, + fee_close=fee.return_value, + is_open=True, + open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=13), + open_rate=0.120, + exchange='binance', + open_order_id='dry_run_buy_123456', + strategy='StrategyTestV2', + timeframe=5, + buy_tag="BUY_TAG2", + sell_reason="SELL_REASON1" + ) + o = Order.parse_from_ccxt_object(mock_order_2(), 'LTC/BTC', 'buy') + trade.orders.append(o) + o = Order.parse_from_ccxt_object(mock_order_2_sell(), 'LTC/BTC', 'sell') + trade.orders.append(o) + return trade + + +def mock_order_3(): + return { + 'id': '1235', + 'symbol': 'ETC/BTC', + 'status': 'closed', + 'side': 'buy', + 'type': 'limit', + 'price': 0.123, + 'amount': 123.0, + 'filled': 123.0, + 'remaining': 0.0, + } + + +def mock_order_3_sell(): + return { + 'id': '12352', + 'symbol': 'ETC/BTC', + 'status': 'closed', + 'side': 'sell', + 'type': 'limit', + 'price': 0.128, + 'amount': 123.0, + 'filled': 123.0, + 'remaining': 0.0, + } + + +def mock_trade_tags_3(fee): + trade = Trade( + pair='ETC/BTC', + stake_amount=0.001, + amount=123.0, + amount_requested=123.0, + fee_open=fee.return_value, + fee_close=fee.return_value, + is_open=True, + open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=12), + open_rate=0.123, + exchange='binance', + open_order_id='dry_run_buy_123457', + strategy='StrategyTestV2', + timeframe=5, + buy_tag="BUY_TAG1", + sell_reason="SELL_REASON2" + ) + o = Order.parse_from_ccxt_object(mock_order_3(), 'ETC/BTC', 'buy') + trade.orders.append(o) + o = Order.parse_from_ccxt_object(mock_order_3_sell(), 'ETC/BTC', 'sell') + trade.orders.append(o) + return trade diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 78805a456..294b5eac8 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -14,7 +14,7 @@ from freqtrade.persistence import Trade from freqtrade.persistence.pairlock_middleware import PairLocks from freqtrade.rpc import RPC, RPCException from freqtrade.rpc.fiat_convert import CryptoToFiatConverter -from tests.conftest import create_mock_trades, get_patched_freqtradebot, patch_get_signal +from tests.conftest import create_mock_trades, get_patched_freqtradebot, patch_get_signal, create_mock_trades_tags # Functions for recurrent object patching @@ -822,10 +822,11 @@ def test_performance_handle(default_conf, ticker, limit_buy_order, fee, trade.close_date = datetime.utcnow() trade.is_open = False res = rpc._rpc_performance() + print(str(res)) assert len(res) == 1 assert res[0]['pair'] == 'ETH/BTC' assert res[0]['count'] == 1 - assert prec_satoshi(res[0]['profit'], 6.2) + assert prec_satoshi(res[0]['profit'], 6.3) # TEST FOR TRADES WITH NO BUY TAG # TEST TRADE WITH ONE BUY_TAG AND OTHER TWO TRADES WITH THE SAME TAG @@ -860,11 +861,43 @@ def test_buy_tag_performance_handle(default_conf, ticker, limit_buy_order, fee, trade.close_date = datetime.utcnow() trade.is_open = False res = rpc._rpc_buy_tag_performance(None) + print(str(res)) assert len(res) == 1 - assert res[0]['pair'] == 'ETH/BTC' + assert res[0]['buy_tag'] == 'Other' assert res[0]['count'] == 1 assert prec_satoshi(res[0]['profit'], 6.2) + print(Trade.pair) + trade.buy_tag = "TEST_TAG" + res = rpc._rpc_buy_tag_performance(None) + print(str(res)) + assert len(res) == 1 + assert res[0]['buy_tag'] == 'TEST_TAG' + assert res[0]['count'] == 1 + assert prec_satoshi(res[0]['profit'], 6.3) + + +def test_buy_tag_performance_handle2(mocker, default_conf, markets, fee): + mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + markets=PropertyMock(return_value=markets) + ) + + freqtradebot = get_patched_freqtradebot(mocker, default_conf) + create_mock_trades_tags(fee) + rpc = RPC(freqtradebot) + + trades = Trade.query.all() + print(str(trades[0].buy_tag)) + + res = rpc._rpc_performance() + print(res) + assert len(trades) == 1 + assert trades[0]['buy_tag'] == 'TEST_TAG' + assert trades[0]['count'] == 1 + assert prec_satoshi(trades[0]['profit'], 6.3) + # TEST FOR TRADES WITH NO SELL REASON # TEST TRADE WITH ONE SELL REASON AND OTHER TWO TRADES WITH THE SAME reason # TEST THE SAME FOR A PAIR @@ -899,9 +932,9 @@ def test_sell_reason_performance_handle(default_conf, ticker, limit_buy_order, f trade.is_open = False res = rpc._rpc_sell_reason_performance(None) assert len(res) == 1 - assert res[0]['pair'] == 'ETH/BTC' - assert res[0]['count'] == 1 - assert prec_satoshi(res[0]['profit'], 6.2) + # assert res[0]['pair'] == 'ETH/BTC' + # assert res[0]['count'] == 1 + # assert prec_satoshi(res[0]['profit'], 6.2) # TEST FOR TRADES WITH NO TAGS # TEST TRADE WITH ONE TAG MIX AND OTHER TWO TRADES WITH THE SAME TAG MIX @@ -937,9 +970,9 @@ def test_mix_tag_performance_handle(default_conf, ticker, limit_buy_order, fee, trade.is_open = False res = rpc._rpc_mix_tag_performance(None) assert len(res) == 1 - assert res[0]['pair'] == 'ETH/BTC' - assert res[0]['count'] == 1 - assert prec_satoshi(res[0]['profit'], 6.2) + # assert res[0]['pair'] == 'ETH/BTC' + # assert res[0]['count'] == 1 + # assert prec_satoshi(res[0]['profit'], 6.2) def test_rpc_count(mocker, default_conf, ticker, fee) -> None: From 21ab83163d3a6fd7d7bc16ca7534e187ecbf8c8a Mon Sep 17 00:00:00 2001 From: theluxaz Date: Wed, 27 Oct 2021 01:35:47 +0300 Subject: [PATCH 047/208] Quick import/clarity fix --- tests/conftest.py | 5 +---- tests/rpc/test_rpc.py | 4 ++-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 6afda47d5..a2a9f77c6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -29,10 +29,7 @@ from tests.conftest_trades import ( mock_trade_3, mock_trade_4, mock_trade_5, - mock_trade_6, - mock_trade_7, - mock_trade_8, - mock_trade_9) + mock_trade_6) from tests.conftest_trades_usdt import (mock_trade_usdt_1, mock_trade_usdt_2, mock_trade_usdt_3, mock_trade_usdt_4, mock_trade_usdt_5, mock_trade_usdt_6) from tests.conftest_trades_tags import (mock_trade_tags_1, mock_trade_tags_2, mock_trade_tags_3) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 294b5eac8..bce618f30 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -885,11 +885,11 @@ def test_buy_tag_performance_handle2(mocker, default_conf, markets, fee): ) freqtradebot = get_patched_freqtradebot(mocker, default_conf) - create_mock_trades_tags(fee) + #create_mock_trades(fee) #this works + create_mock_trades_tags(fee) #this doesn't rpc = RPC(freqtradebot) trades = Trade.query.all() - print(str(trades[0].buy_tag)) res = rpc._rpc_performance() print(res) 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 048/208] 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 049/208] 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 050/208] 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 051/208] 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 052/208] 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 dc605e29aa8ade2cff0d6e105b243df13b65c7fd Mon Sep 17 00:00:00 2001 From: incrementby1 Date: Wed, 27 Oct 2021 21:04:08 +0200 Subject: [PATCH 053/208] 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 054/208] 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 055/208] 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 056/208] 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 560802c326e9e8baed1e4873a7794531e40988ec Mon Sep 17 00:00:00 2001 From: theluxaz Date: Thu, 28 Oct 2021 21:39:42 +0300 Subject: [PATCH 057/208] Added tests for the new rpc/telegram functions --- freqtrade/rpc/telegram.py | 6 +- tests/conftest.py | 25 ----- tests/conftest_trades.py | 3 + tests/conftest_trades_tags.py | 165 --------------------------------- tests/rpc/test_rpc.py | 138 ++++++++++++++++++++------- tests/rpc/test_rpc_telegram.py | 32 +++---- 6 files changed, 125 insertions(+), 244 deletions(-) delete mode 100644 tests/conftest_trades_tags.py diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index f79f8d457..23938c686 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -887,7 +887,7 @@ class Telegram(RPCHandler): """ try: pair = None - if context.args: + if context.args and isinstance(context.args[0], str): pair = context.args[0] trades = self._rpc._rpc_buy_tag_performance(pair) @@ -922,7 +922,7 @@ class Telegram(RPCHandler): """ try: pair = None - if context.args: + if context.args and isinstance(context.args[0], str): pair = context.args[0] trades = self._rpc._rpc_sell_reason_performance(pair) @@ -957,7 +957,7 @@ class Telegram(RPCHandler): """ try: pair = None - if context.args: + if context.args and isinstance(context.args[0], str): pair = context.args[0] trades = self._rpc._rpc_mix_tag_performance(pair) diff --git a/tests/conftest.py b/tests/conftest.py index a2a9f77c6..501cfc9b9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -32,7 +32,6 @@ from tests.conftest_trades import ( mock_trade_6) from tests.conftest_trades_usdt import (mock_trade_usdt_1, mock_trade_usdt_2, mock_trade_usdt_3, mock_trade_usdt_4, mock_trade_usdt_5, mock_trade_usdt_6) -from tests.conftest_trades_tags import (mock_trade_tags_1, mock_trade_tags_2, mock_trade_tags_3) logging.getLogger('').setLevel(logging.INFO) @@ -234,30 +233,6 @@ def create_mock_trades(fee, use_db: bool = True): Trade.commit() -def create_mock_trades_tags(fee, use_db: bool = True): - """ - Create some fake trades to simulate buy tags and sell reasons - """ - def add_trade(trade): - if use_db: - Trade.query.session.add(trade) - else: - LocalTrade.add_bt_trade(trade) - - # Simulate dry_run entries - trade = mock_trade_tags_1(fee) - add_trade(trade) - - trade = mock_trade_tags_2(fee) - add_trade(trade) - - trade = mock_trade_tags_3(fee) - add_trade(trade) - - if use_db: - Trade.commit() - - def create_mock_trades_usdt(fee, use_db: bool = True): """ Create some fake trades ... diff --git a/tests/conftest_trades.py b/tests/conftest_trades.py index 024803be0..4496df37d 100644 --- a/tests/conftest_trades.py +++ b/tests/conftest_trades.py @@ -89,6 +89,7 @@ def mock_trade_2(fee): open_order_id='dry_run_sell_12345', strategy='StrategyTestV2', timeframe=5, + buy_tag='TEST1', sell_reason='sell_signal', open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20), close_date=datetime.now(tz=timezone.utc) - timedelta(minutes=2), @@ -241,6 +242,7 @@ def mock_trade_5(fee): open_rate=0.123, exchange='binance', strategy='SampleStrategy', + buy_tag='TEST1', stoploss_order_id='prod_stoploss_3455', timeframe=5, ) @@ -295,6 +297,7 @@ def mock_trade_6(fee): open_rate=0.15, exchange='binance', strategy='SampleStrategy', + buy_tag='TEST2', open_order_id="prod_sell_6", timeframe=5, ) diff --git a/tests/conftest_trades_tags.py b/tests/conftest_trades_tags.py deleted file mode 100644 index db0d3d3bd..000000000 --- a/tests/conftest_trades_tags.py +++ /dev/null @@ -1,165 +0,0 @@ -from datetime import datetime, timedelta, timezone - -from freqtrade.persistence.models import Order, Trade - - -MOCK_TRADE_COUNT = 3 - - -def mock_order_1(): - return { - 'id': 'prod_buy_1', - 'symbol': 'LTC/BTC', - 'status': 'closed', - 'side': 'buy', - 'type': 'limit', - 'price': 0.15, - 'amount': 2.0, - 'filled': 2.0, - 'remaining': 0.0, - } - - -def mock_order_1_sell(): - return { - 'id': 'prod_sell_1', - 'symbol': 'LTC/BTC', - 'status': 'open', - 'side': 'sell', - 'type': 'limit', - 'price': 0.20, - 'amount': 2.0, - 'filled': 0.0, - 'remaining': 2.0, - } - - -def mock_trade_tags_1(fee): - trade = Trade( - pair='LTC/BTC', - stake_amount=0.001, - amount=2.0, - amount_requested=2.0, - fee_open=fee.return_value, - fee_close=fee.return_value, - is_open=True, - open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=15), - open_rate=0.15, - exchange='binance', - open_order_id='dry_run_buy_123455', - strategy='StrategyTestV2', - timeframe=5, - buy_tag="BUY_TAG1", - sell_reason="SELL_REASON2" - ) - o = Order.parse_from_ccxt_object(mock_order_1(), 'LTC/BTC', 'buy') - trade.orders.append(o) - o = Order.parse_from_ccxt_object(mock_order_1_sell(), 'LTC/BTC', 'sell') - trade.orders.append(o) - return trade - - -def mock_order_2(): - return { - 'id': '1239', - 'symbol': 'LTC/BTC', - 'status': 'closed', - 'side': 'buy', - 'type': 'limit', - 'price': 0.120, - 'amount': 100.0, - 'filled': 100.0, - 'remaining': 0.0, - } - - -def mock_order_2_sell(): - return { - 'id': '12392', - 'symbol': 'LTC/BTC', - 'status': 'closed', - 'side': 'sell', - 'type': 'limit', - 'price': 0.138, - 'amount': 100.0, - 'filled': 100.0, - 'remaining': 0.0, - } - - -def mock_trade_tags_2(fee): - trade = Trade( - pair='LTC/BTC', - stake_amount=0.001, - amount=100.0, - amount_requested=100.0, - fee_open=fee.return_value, - fee_close=fee.return_value, - is_open=True, - open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=13), - open_rate=0.120, - exchange='binance', - open_order_id='dry_run_buy_123456', - strategy='StrategyTestV2', - timeframe=5, - buy_tag="BUY_TAG2", - sell_reason="SELL_REASON1" - ) - o = Order.parse_from_ccxt_object(mock_order_2(), 'LTC/BTC', 'buy') - trade.orders.append(o) - o = Order.parse_from_ccxt_object(mock_order_2_sell(), 'LTC/BTC', 'sell') - trade.orders.append(o) - return trade - - -def mock_order_3(): - return { - 'id': '1235', - 'symbol': 'ETC/BTC', - 'status': 'closed', - 'side': 'buy', - 'type': 'limit', - 'price': 0.123, - 'amount': 123.0, - 'filled': 123.0, - 'remaining': 0.0, - } - - -def mock_order_3_sell(): - return { - 'id': '12352', - 'symbol': 'ETC/BTC', - 'status': 'closed', - 'side': 'sell', - 'type': 'limit', - 'price': 0.128, - 'amount': 123.0, - 'filled': 123.0, - 'remaining': 0.0, - } - - -def mock_trade_tags_3(fee): - trade = Trade( - pair='ETC/BTC', - stake_amount=0.001, - amount=123.0, - amount_requested=123.0, - fee_open=fee.return_value, - fee_close=fee.return_value, - is_open=True, - open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=12), - open_rate=0.123, - exchange='binance', - open_order_id='dry_run_buy_123457', - strategy='StrategyTestV2', - timeframe=5, - buy_tag="BUY_TAG1", - sell_reason="SELL_REASON2" - ) - o = Order.parse_from_ccxt_object(mock_order_3(), 'ETC/BTC', 'buy') - trade.orders.append(o) - o = Order.parse_from_ccxt_object(mock_order_3_sell(), 'ETC/BTC', 'sell') - trade.orders.append(o) - return trade diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index bce618f30..aeb0483de 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -14,7 +14,7 @@ from freqtrade.persistence import Trade from freqtrade.persistence.pairlock_middleware import PairLocks from freqtrade.rpc import RPC, RPCException from freqtrade.rpc.fiat_convert import CryptoToFiatConverter -from tests.conftest import create_mock_trades, get_patched_freqtradebot, patch_get_signal, create_mock_trades_tags +from tests.conftest import create_mock_trades, get_patched_freqtradebot, patch_get_signal # Functions for recurrent object patching @@ -826,11 +826,7 @@ def test_performance_handle(default_conf, ticker, limit_buy_order, fee, assert len(res) == 1 assert res[0]['pair'] == 'ETH/BTC' assert res[0]['count'] == 1 - assert prec_satoshi(res[0]['profit'], 6.3) - - # TEST FOR TRADES WITH NO BUY TAG - # TEST TRADE WITH ONE BUY_TAG AND OTHER TWO TRADES WITH THE SAME TAG - # TEST THE SAME FOR A PAIR + assert prec_satoshi(res[0]['profit'], 6.2) def test_buy_tag_performance_handle(default_conf, ticker, limit_buy_order, fee, @@ -861,23 +857,22 @@ def test_buy_tag_performance_handle(default_conf, ticker, limit_buy_order, fee, trade.close_date = datetime.utcnow() trade.is_open = False res = rpc._rpc_buy_tag_performance(None) - print(str(res)) + assert len(res) == 1 assert res[0]['buy_tag'] == 'Other' assert res[0]['count'] == 1 assert prec_satoshi(res[0]['profit'], 6.2) - print(Trade.pair) trade.buy_tag = "TEST_TAG" res = rpc._rpc_buy_tag_performance(None) - print(str(res)) + assert len(res) == 1 assert res[0]['buy_tag'] == 'TEST_TAG' assert res[0]['count'] == 1 - assert prec_satoshi(res[0]['profit'], 6.3) + assert prec_satoshi(res[0]['profit'], 6.2) -def test_buy_tag_performance_handle2(mocker, default_conf, markets, fee): +def test_buy_tag_performance_handle_2(mocker, default_conf, markets, fee): mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -885,22 +880,25 @@ def test_buy_tag_performance_handle2(mocker, default_conf, markets, fee): ) freqtradebot = get_patched_freqtradebot(mocker, default_conf) - #create_mock_trades(fee) #this works - create_mock_trades_tags(fee) #this doesn't + create_mock_trades(fee) rpc = RPC(freqtradebot) - trades = Trade.query.all() + res = rpc._rpc_buy_tag_performance(None) - res = rpc._rpc_performance() - print(res) - assert len(trades) == 1 - assert trades[0]['buy_tag'] == 'TEST_TAG' - assert trades[0]['count'] == 1 - assert prec_satoshi(trades[0]['profit'], 6.3) + assert len(res) == 2 + assert res[0]['buy_tag'] == 'TEST1' + assert res[0]['count'] == 1 + assert prec_satoshi(res[0]['profit'], 0.5) + assert res[1]['buy_tag'] == 'Other' + assert res[1]['count'] == 1 + assert prec_satoshi(res[1]['profit'], 1.0) - # TEST FOR TRADES WITH NO SELL REASON - # TEST TRADE WITH ONE SELL REASON AND OTHER TWO TRADES WITH THE SAME reason - # TEST THE SAME FOR A PAIR + # Test for a specific pair + res = rpc._rpc_buy_tag_performance('ETC/BTC') + assert len(res) == 1 + assert res[0]['count'] == 1 + assert res[0]['buy_tag'] == 'TEST1' + assert prec_satoshi(res[0]['profit'], 0.5) def test_sell_reason_performance_handle(default_conf, ticker, limit_buy_order, fee, @@ -931,14 +929,48 @@ def test_sell_reason_performance_handle(default_conf, ticker, limit_buy_order, f trade.close_date = datetime.utcnow() trade.is_open = False res = rpc._rpc_sell_reason_performance(None) - assert len(res) == 1 - # assert res[0]['pair'] == 'ETH/BTC' - # assert res[0]['count'] == 1 - # assert prec_satoshi(res[0]['profit'], 6.2) - # TEST FOR TRADES WITH NO TAGS - # TEST TRADE WITH ONE TAG MIX AND OTHER TWO TRADES WITH THE SAME TAG MIX - # TEST THE SAME FOR A PAIR + assert len(res) == 1 + assert res[0]['sell_reason'] == 'Other' + assert res[0]['count'] == 1 + assert prec_satoshi(res[0]['profit'], 6.2) + + trade.sell_reason = "TEST1" + res = rpc._rpc_sell_reason_performance(None) + + assert len(res) == 1 + assert res[0]['sell_reason'] == 'TEST1' + assert res[0]['count'] == 1 + assert prec_satoshi(res[0]['profit'], 6.2) + + +def test_sell_reason_performance_handle_2(mocker, default_conf, markets, fee): + mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + markets=PropertyMock(return_value=markets) + ) + + freqtradebot = get_patched_freqtradebot(mocker, default_conf) + create_mock_trades(fee) + rpc = RPC(freqtradebot) + + res = rpc._rpc_sell_reason_performance(None) + + assert len(res) == 2 + assert res[0]['sell_reason'] == 'sell_signal' + assert res[0]['count'] == 1 + assert prec_satoshi(res[0]['profit'], 0.5) + assert res[1]['sell_reason'] == 'roi' + assert res[1]['count'] == 1 + assert prec_satoshi(res[1]['profit'], 1.0) + + # Test for a specific pair + res = rpc._rpc_sell_reason_performance('ETC/BTC') + assert len(res) == 1 + assert res[0]['count'] == 1 + assert res[0]['sell_reason'] == 'sell_signal' + assert prec_satoshi(res[0]['profit'], 0.5) def test_mix_tag_performance_handle(default_conf, ticker, limit_buy_order, fee, @@ -969,10 +1001,50 @@ def test_mix_tag_performance_handle(default_conf, ticker, limit_buy_order, fee, trade.close_date = datetime.utcnow() trade.is_open = False res = rpc._rpc_mix_tag_performance(None) + assert len(res) == 1 - # assert res[0]['pair'] == 'ETH/BTC' - # assert res[0]['count'] == 1 - # assert prec_satoshi(res[0]['profit'], 6.2) + assert res[0]['mix_tag'] == 'Other Other' + assert res[0]['count'] == 1 + assert prec_satoshi(res[0]['profit'], 6.2) + + trade.buy_tag = "TESTBUY" + trade.sell_reason = "TESTSELL" + res = rpc._rpc_mix_tag_performance(None) + + assert len(res) == 1 + assert res[0]['mix_tag'] == 'TESTBUY TESTSELL' + assert res[0]['count'] == 1 + assert prec_satoshi(res[0]['profit'], 6.2) + + +def test_mix_tag_performance_handle_2(mocker, default_conf, markets, fee): + mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + markets=PropertyMock(return_value=markets) + ) + + freqtradebot = get_patched_freqtradebot(mocker, default_conf) + create_mock_trades(fee) + rpc = RPC(freqtradebot) + + res = rpc._rpc_mix_tag_performance(None) + + assert len(res) == 2 + assert res[0]['mix_tag'] == 'TEST1 sell_signal' + assert res[0]['count'] == 1 + assert prec_satoshi(res[0]['profit'], 0.5) + assert res[1]['mix_tag'] == 'Other roi' + assert res[1]['count'] == 1 + assert prec_satoshi(res[1]['profit'], 1.0) + + # Test for a specific pair + res = rpc._rpc_mix_tag_performance('ETC/BTC') + + assert len(res) == 1 + assert res[0]['count'] == 1 + assert res[0]['mix_tag'] == 'TEST1 sell_signal' + assert prec_satoshi(res[0]['profit'], 0.5) def test_rpc_count(mocker, default_conf, ticker, fee) -> None: diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 306181eae..f669e9411 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -978,10 +978,6 @@ def test_performance_handle(default_conf, update, ticker, fee, assert 'Performance' in msg_mock.call_args_list[0][0][0] assert 'ETH/BTC\t0.00006217 BTC (6.20%) (1)' in msg_mock.call_args_list[0][0][0] - # TEST FOR TRADES WITH NO BUY TAG - # TEST TRADE WITH ONE BUY_TAG AND OTHER TWO TRADES WITH THE SAME TAG - # TEST THE SAME FOR A PAIR - def test_buy_tag_performance_handle(default_conf, update, ticker, fee, limit_buy_order, limit_sell_order, mocker) -> None: @@ -1001,19 +997,17 @@ def test_buy_tag_performance_handle(default_conf, update, ticker, fee, # Simulate fulfilled LIMIT_BUY order for trade trade.update(limit_buy_order) + trade.buy_tag = "TESTBUY" # Simulate fulfilled LIMIT_SELL order for trade trade.update(limit_sell_order) trade.close_date = datetime.utcnow() trade.is_open = False + telegram._buy_tag_performance(update=update, context=MagicMock()) assert msg_mock.call_count == 1 - assert 'Performance' in msg_mock.call_args_list[0][0][0] - assert 'ETH/BTC\t0.00006217 BTC (6.20%) (1)' in msg_mock.call_args_list[0][0][0] - - # TEST FOR TRADES WITH NO SELL REASON - # TEST TRADE WITH ONE SELL REASON AND OTHER TWO TRADES WITH THE SAME reason - # TEST THE SAME FOR A PAIR + assert 'Buy Tag Performance' in msg_mock.call_args_list[0][0][0] + assert 'TESTBUY\t0.00006217 BTC (6.20%) (1)' in msg_mock.call_args_list[0][0][0] def test_sell_reason_performance_handle(default_conf, update, ticker, fee, @@ -1034,19 +1028,17 @@ def test_sell_reason_performance_handle(default_conf, update, ticker, fee, # Simulate fulfilled LIMIT_BUY order for trade trade.update(limit_buy_order) + trade.sell_reason = 'TESTSELL' # Simulate fulfilled LIMIT_SELL order for trade trade.update(limit_sell_order) trade.close_date = datetime.utcnow() trade.is_open = False + telegram._sell_reason_performance(update=update, context=MagicMock()) assert msg_mock.call_count == 1 - assert 'Performance' in msg_mock.call_args_list[0][0][0] - assert 'ETH/BTC\t0.00006217 BTC (6.20%) (1)' in msg_mock.call_args_list[0][0][0] - - # TEST FOR TRADES WITH NO TAGS - # TEST TRADE WITH ONE TAG MIX AND OTHER TWO TRADES WITH THE SAME TAG MIX - # TEST THE SAME FOR A PAIR + assert 'Sell Reason Performance' in msg_mock.call_args_list[0][0][0] + assert 'TESTSELL\t0.00006217 BTC (6.20%) (1)' in msg_mock.call_args_list[0][0][0] def test_mix_tag_performance_handle(default_conf, update, ticker, fee, @@ -1067,15 +1059,19 @@ def test_mix_tag_performance_handle(default_conf, update, ticker, fee, # Simulate fulfilled LIMIT_BUY order for trade trade.update(limit_buy_order) + trade.buy_tag = "TESTBUY" + trade.sell_reason = "TESTSELL" + # Simulate fulfilled LIMIT_SELL order for trade trade.update(limit_sell_order) trade.close_date = datetime.utcnow() trade.is_open = False + telegram._mix_tag_performance(update=update, context=MagicMock()) assert msg_mock.call_count == 1 - assert 'Performance' in msg_mock.call_args_list[0][0][0] - assert 'ETH/BTC\t0.00006217 BTC (6.20%) (1)' in msg_mock.call_args_list[0][0][0] + assert 'Mix Tag Performance' in msg_mock.call_args_list[0][0][0] + assert 'TESTBUY TESTSELL\t0.00006217 BTC (6.20%) (1)' in msg_mock.call_args_list[0][0][0] def test_count_handle(default_conf, update, ticker, fee, mocker) -> None: From 658006e7eedfd6a09fa7ee439e5fee1dbc81752b Mon Sep 17 00:00:00 2001 From: incrementby1 Date: Thu, 28 Oct 2021 23:29:26 +0200 Subject: [PATCH 058/208] 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 059/208] 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 060/208] 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 240923341b580f0e719c614ddb3ffc2faf93154e Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 29 Oct 2021 07:04:20 +0200 Subject: [PATCH 061/208] Reformat telegram test --- tests/conftest.py | 10 +++------- tests/rpc/test_rpc_telegram.py | 3 ++- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 501cfc9b9..698c464ed 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -23,16 +23,12 @@ from freqtrade.freqtradebot import FreqtradeBot from freqtrade.persistence import LocalTrade, Trade, init_db from freqtrade.resolvers import ExchangeResolver from freqtrade.worker import Worker -from tests.conftest_trades import ( - mock_trade_1, - mock_trade_2, - mock_trade_3, - mock_trade_4, - mock_trade_5, - mock_trade_6) +from tests.conftest_trades import (mock_trade_1, mock_trade_2, mock_trade_3, mock_trade_4, + mock_trade_5, mock_trade_6) from tests.conftest_trades_usdt import (mock_trade_usdt_1, mock_trade_usdt_2, mock_trade_usdt_3, mock_trade_usdt_4, mock_trade_usdt_5, mock_trade_usdt_6) + logging.getLogger('').setLevel(logging.INFO) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index f669e9411..5f49c8bf7 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1071,7 +1071,8 @@ def test_mix_tag_performance_handle(default_conf, update, ticker, fee, telegram._mix_tag_performance(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert 'Mix Tag Performance' in msg_mock.call_args_list[0][0][0] - assert 'TESTBUY TESTSELL\t0.00006217 BTC (6.20%) (1)' in msg_mock.call_args_list[0][0][0] + assert ('TESTBUY TESTSELL\t0.00006217 BTC (6.20%) (1)' + in msg_mock.call_args_list[0][0][0]) def test_count_handle(default_conf, update, ticker, fee, mocker) -> None: From c579fcfc19ca91e5567eca1ad8b8d9f16a577f3d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 30 Oct 2021 09:39:40 +0200 Subject: [PATCH 062/208] 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 063/208] 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 064/208] 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 065/208] 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 066/208] 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 067/208] 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 068/208] 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 069/208] 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 070/208] 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 071/208] 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 072/208] 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 dffe76f10935e1c88141d85e00c3ad1204863738 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 31 Oct 2021 10:42:42 +0100 Subject: [PATCH 073/208] Don't double-loop to generate profits --- freqtrade/freqtradebot.py | 8 ++++---- freqtrade/persistence/models.py | 23 ++++++++++------------- freqtrade/rpc/api_server/api_schemas.py | 2 ++ freqtrade/rpc/rpc.py | 3 --- freqtrade/rpc/telegram.py | 8 ++++---- tests/rpc/test_rpc.py | 23 +++++++++++------------ tests/rpc/test_rpc_apiserver.py | 6 ++++-- 7 files changed, 35 insertions(+), 38 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index fb42a8924..a0b773a16 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -202,10 +202,10 @@ class FreqtradeBot(LoggingMixin): 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 ''}", + 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) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 8ccf8bbef..3da415e9b 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -850,18 +850,17 @@ class Trade(_DECL_BASE, LocalTrade): .group_by(Trade.pair) \ .order_by(desc('profit_sum_abs')) \ .all() - - response = [ + return [ { 'pair': pair, - 'profit': profit, + 'profit_ratio': profit, + 'profit': round(profit * 100, 2), # Compatibility mode + 'profit_pct': round(profit * 100, 2), 'profit_abs': profit_abs, 'count': count } for pair, profit, profit_abs, count in pair_rates ] - [x.update({'profit': round(x['profit'] * 100, 2)}) for x in response] - return response @staticmethod def get_buy_tag_performance(pair: Optional[str]) -> List[Dict[str, Any]]: @@ -885,17 +884,16 @@ class Trade(_DECL_BASE, LocalTrade): .order_by(desc('profit_sum_abs')) \ .all() - response = [ + return [ { 'buy_tag': buy_tag if buy_tag is not None else "Other", - 'profit': profit, + 'profit_ratio': profit, + 'profit_pct': round(profit * 100, 2), 'profit_abs': profit_abs, 'count': count } for buy_tag, profit, profit_abs, count in buy_tag_perf ] - [x.update({'profit': round(x['profit'] * 100, 2)}) for x in response] - return response @staticmethod def get_sell_reason_performance(pair: Optional[str]) -> List[Dict[str, Any]]: @@ -919,17 +917,16 @@ class Trade(_DECL_BASE, LocalTrade): .order_by(desc('profit_sum_abs')) \ .all() - response = [ + return [ { 'sell_reason': sell_reason if sell_reason is not None else "Other", - 'profit': profit, + 'profit_ratio': profit, + 'profit_pct': round(profit * 100, 2), 'profit_abs': profit_abs, 'count': count } for sell_reason, profit, profit_abs, count in sell_tag_perf ] - [x.update({'profit': round(x['profit'] * 100, 2)}) for x in response] - return response @staticmethod def get_mix_tag_performance(pair: Optional[str]) -> List[Dict[str, Any]]: diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index e9985c3c6..ff1915fca 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -63,6 +63,8 @@ class Count(BaseModel): class PerformanceEntry(BaseModel): pair: str profit: float + profit_ratio: float + profit_pct: float profit_abs: float count: int diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 42d502cd8..da8d23b7a 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -161,8 +161,6 @@ class RPC: current_rate = NAN else: current_rate = trade.close_rate - - buy_tag = trade.buy_tag current_profit = trade.calc_profit_ratio(current_rate) current_profit_abs = trade.calc_profit(current_rate) current_profit_fiat: Optional[float] = None @@ -193,7 +191,6 @@ class RPC: profit_pct=round(current_profit * 100, 2), profit_abs=current_profit_abs, profit_fiat=current_profit_fiat, - buy_tag=buy_tag, stoploss_current_dist=stoploss_current_dist, stoploss_current_dist_ratio=round(stoploss_current_dist_ratio, 8), diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 23938c686..e07734fda 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -861,7 +861,7 @@ class Telegram(RPCHandler): stat_line = ( f"{i+1}.\t {trade['pair']}\t" f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} " - f"({trade['profit']:.2f}%) " + f"({trade['profit_pct']:.2f}%) " f"({trade['count']})\n") if len(output + stat_line) >= MAX_TELEGRAM_MESSAGE_LENGTH: @@ -896,7 +896,7 @@ class Telegram(RPCHandler): stat_line = ( f"{i+1}.\t {trade['buy_tag']}\t" f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} " - f"({trade['profit']:.2f}%) " + f"({trade['profit_pct']:.2f}%) " f"({trade['count']})\n") if len(output + stat_line) >= MAX_TELEGRAM_MESSAGE_LENGTH: @@ -931,7 +931,7 @@ class Telegram(RPCHandler): stat_line = ( f"{i+1}.\t {trade['sell_reason']}\t" f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} " - f"({trade['profit']:.2f}%) " + f"({trade['profit_pct']:.2f}%) " f"({trade['count']})\n") if len(output + stat_line) >= MAX_TELEGRAM_MESSAGE_LENGTH: @@ -1158,7 +1158,7 @@ class Telegram(RPCHandler): " `pending sell orders are marked with a double asterisk (**)`\n" "*/buys :* `Shows the buy_tag performance`\n" "*/sells :* `Shows the sell reason performance`\n" - "*/mix_tag :* `Shows combined buy tag + sell reason performance`\n" + "*/mix_tags :* `Shows combined buy tag + sell reason performance`\n" "*/trades [limit]:* `Lists last closed trades (limited to 10 by default)`\n" "*/profit []:* `Lists cumulative profit from all finished trades, " "over the last n days`\n" diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index aeb0483de..945217b8a 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -822,11 +822,10 @@ def test_performance_handle(default_conf, ticker, limit_buy_order, fee, trade.close_date = datetime.utcnow() trade.is_open = False res = rpc._rpc_performance() - print(str(res)) assert len(res) == 1 assert res[0]['pair'] == 'ETH/BTC' assert res[0]['count'] == 1 - assert prec_satoshi(res[0]['profit'], 6.2) + assert prec_satoshi(res[0]['profit_pct'], 6.2) def test_buy_tag_performance_handle(default_conf, ticker, limit_buy_order, fee, @@ -861,7 +860,7 @@ def test_buy_tag_performance_handle(default_conf, ticker, limit_buy_order, fee, assert len(res) == 1 assert res[0]['buy_tag'] == 'Other' assert res[0]['count'] == 1 - assert prec_satoshi(res[0]['profit'], 6.2) + assert prec_satoshi(res[0]['profit_pct'], 6.2) trade.buy_tag = "TEST_TAG" res = rpc._rpc_buy_tag_performance(None) @@ -869,7 +868,7 @@ def test_buy_tag_performance_handle(default_conf, ticker, limit_buy_order, fee, assert len(res) == 1 assert res[0]['buy_tag'] == 'TEST_TAG' assert res[0]['count'] == 1 - assert prec_satoshi(res[0]['profit'], 6.2) + assert prec_satoshi(res[0]['profit_pct'], 6.2) def test_buy_tag_performance_handle_2(mocker, default_conf, markets, fee): @@ -888,17 +887,17 @@ def test_buy_tag_performance_handle_2(mocker, default_conf, markets, fee): assert len(res) == 2 assert res[0]['buy_tag'] == 'TEST1' assert res[0]['count'] == 1 - assert prec_satoshi(res[0]['profit'], 0.5) + assert prec_satoshi(res[0]['profit_pct'], 0.5) assert res[1]['buy_tag'] == 'Other' assert res[1]['count'] == 1 - assert prec_satoshi(res[1]['profit'], 1.0) + assert prec_satoshi(res[1]['profit_pct'], 1.0) # Test for a specific pair res = rpc._rpc_buy_tag_performance('ETC/BTC') assert len(res) == 1 assert res[0]['count'] == 1 assert res[0]['buy_tag'] == 'TEST1' - assert prec_satoshi(res[0]['profit'], 0.5) + assert prec_satoshi(res[0]['profit_pct'], 0.5) def test_sell_reason_performance_handle(default_conf, ticker, limit_buy_order, fee, @@ -933,7 +932,7 @@ def test_sell_reason_performance_handle(default_conf, ticker, limit_buy_order, f assert len(res) == 1 assert res[0]['sell_reason'] == 'Other' assert res[0]['count'] == 1 - assert prec_satoshi(res[0]['profit'], 6.2) + assert prec_satoshi(res[0]['profit_pct'], 6.2) trade.sell_reason = "TEST1" res = rpc._rpc_sell_reason_performance(None) @@ -941,7 +940,7 @@ def test_sell_reason_performance_handle(default_conf, ticker, limit_buy_order, f assert len(res) == 1 assert res[0]['sell_reason'] == 'TEST1' assert res[0]['count'] == 1 - assert prec_satoshi(res[0]['profit'], 6.2) + assert prec_satoshi(res[0]['profit_pct'], 6.2) def test_sell_reason_performance_handle_2(mocker, default_conf, markets, fee): @@ -960,17 +959,17 @@ def test_sell_reason_performance_handle_2(mocker, default_conf, markets, fee): assert len(res) == 2 assert res[0]['sell_reason'] == 'sell_signal' assert res[0]['count'] == 1 - assert prec_satoshi(res[0]['profit'], 0.5) + assert prec_satoshi(res[0]['profit_pct'], 0.5) assert res[1]['sell_reason'] == 'roi' assert res[1]['count'] == 1 - assert prec_satoshi(res[1]['profit'], 1.0) + assert prec_satoshi(res[1]['profit_pct'], 1.0) # Test for a specific pair res = rpc._rpc_sell_reason_performance('ETC/BTC') assert len(res) == 1 assert res[0]['count'] == 1 assert res[0]['sell_reason'] == 'sell_signal' - assert prec_satoshi(res[0]['profit'], 0.5) + assert prec_satoshi(res[0]['profit_pct'], 0.5) def test_mix_tag_performance_handle(default_conf, ticker, limit_buy_order, fee, diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 02ed26459..e0bbee861 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -812,8 +812,10 @@ def test_api_performance(botclient, fee): rc = client_get(client, f"{BASE_URI}/performance") assert_response(rc) assert len(rc.json()) == 2 - assert rc.json() == [{'count': 1, 'pair': 'LTC/ETH', 'profit': 7.61, 'profit_abs': 0.01872279}, - {'count': 1, 'pair': 'XRP/ETH', 'profit': -5.57, 'profit_abs': -0.1150375}] + assert rc.json() == [{'count': 1, 'pair': 'LTC/ETH', 'profit': 7.61, 'profit_pct': 7.61, + 'profit_ratio': 0.07609203, 'profit_abs': 0.01872279}, + {'count': 1, 'pair': 'XRP/ETH', 'profit': -5.57, 'profit_pct': -5.57, + 'profit_ratio': -0.05570419, 'profit_abs': -0.1150375}] def test_api_status(botclient, mocker, ticker, fee, markets): From 6b90b4a144e559ffc81318788116be6cfd2ac0a2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 31 Oct 2021 10:51:56 +0100 Subject: [PATCH 074/208] Test "get-signal" --- freqtrade/freqtradebot.py | 4 ++-- tests/strategy/test_interface.py | 14 +++++++++++++- tests/test_freqtradebot.py | 3 ++- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index a0b773a16..d23ba270d 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -420,7 +420,7 @@ class FreqtradeBot(LoggingMixin): return False # running get_signal on historical data fetched - (buy, sell, buy_tag, exit_tag) = self.strategy.get_signal( + (buy, sell, buy_tag, _) = self.strategy.get_signal( pair, self.strategy.timeframe, analyzed_df @@ -707,7 +707,7 @@ class FreqtradeBot(LoggingMixin): analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair, self.strategy.timeframe) - (buy, sell, buy_tag, exit_tag) = self.strategy.get_signal( + (buy, sell, _, exit_tag) = self.strategy.get_signal( trade.pair, self.strategy.timeframe, analyzed_df diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 62510b370..e8ee0bfed 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -30,7 +30,7 @@ _STRATEGY = StrategyTestV2(config={}) _STRATEGY.dp = DataProvider({}, None, None) -def test_returns_latest_signal(mocker, default_conf, ohlcv_history): +def test_returns_latest_signal(ohlcv_history): ohlcv_history.loc[1, 'date'] = arrow.utcnow() # Take a copy to correctly modify the call mocked_history = ohlcv_history.copy() @@ -60,6 +60,18 @@ def test_returns_latest_signal(mocker, default_conf, ohlcv_history): 'buy_signal_01', None) + mocked_history.loc[1, 'buy_tag'] = None + mocked_history.loc[1, 'exit_tag'] = 'sell_signal_01' + + assert _STRATEGY.get_signal( + 'ETH/BTC', + '5m', + mocked_history) == ( + True, + False, + None, + 'sell_signal_01') + def test_analyze_pair_empty(default_conf, mocker, caplog, ohlcv_history): mocker.patch.object(_STRATEGY.dp, 'ohlcv', return_value=ohlcv_history) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index e590f4f74..0435dc3a2 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1808,7 +1808,7 @@ def test_handle_trade(default_conf_usdt, limit_buy_order_usdt, limit_sell_order_ assert trade.is_open is True freqtrade.wallets.update() - patch_get_signal(freqtrade, value=(False, True, None, None)) + patch_get_signal(freqtrade, value=(False, True, None, 'sell_signal1')) assert freqtrade.handle_trade(trade) is True assert trade.open_order_id == limit_sell_order_usdt['id'] @@ -1819,6 +1819,7 @@ def test_handle_trade(default_conf_usdt, limit_buy_order_usdt, limit_sell_order_ assert trade.close_profit == 0.09451372 assert trade.calc_profit() == 5.685 assert trade.close_date is not None + assert trade.sell_reason == 'sell_signal1' def test_handle_overlapping_signals(default_conf_usdt, ticker_usdt, limit_buy_order_usdt_open, 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 075/208] 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 076/208] 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 077/208] 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 078/208] 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 079/208] 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 080/208] 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 081/208] 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 082/208] 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 083/208] 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 f365e68706099acbb4b83b87d06f8182bf84ebb1 Mon Sep 17 00:00:00 2001 From: Theagainmen <24569139+Theagainmen@users.noreply.github.com> Date: Mon, 1 Nov 2021 23:07:16 +0100 Subject: [PATCH 084/208] [docs] Update RateLimit value [small] ## Summary Fix very small mistake in docs, that might confuse people. Let me know if this is the correct value now, there is still another 3100 in there, which I think makes sense there and is correct. ## Quick changelog Changed the `rateLimit` 3100 value to 200, to match the 200ms and thus 0.2s delay. --- docs/exchanges.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/exchanges.md b/docs/exchanges.md index badaa484a..d00c88d8e 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -41,12 +41,12 @@ In case of problems related to rate-limits (usually DDOS Exceptions in your logs "ccxt_config": {"enableRateLimit": true}, "ccxt_async_config": { "enableRateLimit": true, - "rateLimit": 3100 + "rateLimit": 200 }, ``` This configuration enables kraken, as well as rate-limiting to avoid bans from the exchange. -`"rateLimit": 3100` defines a wait-event of 0.2s between each call. This can also be completely disabled by setting `"enableRateLimit"` to false. +`"rateLimit": 200` defines a wait-event of 0.2s between each call. This can also be completely disabled by setting `"enableRateLimit"` to false. !!! Note Optimal settings for rate-limiting depend on the exchange and the size of the whitelist, so an ideal parameter will vary on many other settings. From e78df59e308965800c3c8185bbe2b920aa62c273 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 2 Nov 2021 19:49:53 +0100 Subject: [PATCH 085/208] 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 086/208] 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 087/208] 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: ''