From 70e9fa6136a0d7705dfe6a72e0315467d6c0a838 Mon Sep 17 00:00:00 2001 From: hippocritical Date: Tue, 27 Dec 2022 20:14:39 +0100 Subject: [PATCH 001/360] implementing the strategy_updater in a first version --- freqtrade/commands/__init__.py | 1 + freqtrade/commands/arguments.py | 14 +- freqtrade/commands/strategy_utils_commands.py | 37 +++ freqtrade/strategy/strategy_updater.py | 213 ++++++++++++++++++ 4 files changed, 263 insertions(+), 2 deletions(-) create mode 100644 freqtrade/commands/strategy_utils_commands.py create mode 100644 freqtrade/strategy/strategy_updater.py diff --git a/freqtrade/commands/__init__.py b/freqtrade/commands/__init__.py index 788657cc8..66a9c995b 100644 --- a/freqtrade/commands/__init__.py +++ b/freqtrade/commands/__init__.py @@ -22,5 +22,6 @@ from freqtrade.commands.optimize_commands import (start_backtesting, start_backt 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.strategy_utils_commands import start_strategy_update from freqtrade.commands.trade_commands import start_trading from freqtrade.commands.webserver_commands import start_webserver diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index b53a1022d..9990ad230 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -115,6 +115,8 @@ NO_CONF_REQURIED = ["convert-data", "convert-trade-data", "download-data", "list NO_CONF_ALLOWED = ["create-userdir", "list-exchanges", "new-strategy"] +ARGS_STRATEGY_UTILS = ARGS_COMMON_OPTIMIZE + ["strategy_list"] + class Arguments: """ @@ -198,8 +200,8 @@ class Arguments: start_list_freqAI_models, 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) + start_plot_profit, start_show_trades, start_strategy_update, + start_test_pairlist, start_trading, start_webserver) subparsers = self.parser.add_subparsers(dest='command', # Use custom message when no subhandler is added @@ -440,3 +442,11 @@ class Arguments: parents=[_common_parser]) webserver_cmd.set_defaults(func=start_webserver) self._build_args(optionlist=ARGS_WEBSERVER, parser=webserver_cmd) + + # Add strategy_updater subcommand + strategy_updater_cmd = subparsers.add_parser('strategy-updater', + help='updates outdated strategy' + 'files to the current version', + parents=[_common_parser]) + strategy_updater_cmd.set_defaults(func=start_strategy_update) + self._build_args(optionlist=ARGS_STRATEGY_UTILS, parser=strategy_updater_cmd) diff --git a/freqtrade/commands/strategy_utils_commands.py b/freqtrade/commands/strategy_utils_commands.py new file mode 100644 index 000000000..b9431fecf --- /dev/null +++ b/freqtrade/commands/strategy_utils_commands.py @@ -0,0 +1,37 @@ +import logging +from typing import Any, Dict + +from freqtrade.configuration import setup_utils_configuration +from freqtrade.enums import RunMode +from freqtrade.resolvers import StrategyResolver + + +logger = logging.getLogger(__name__) + + +def start_strategy_update(args: Dict[str, Any]) -> None: + """ + Start the strategy updating script + :param args: Cli args from Arguments() + :return: None + """ + + # Import here to avoid loading backtesting module when it's not used + from freqtrade.strategy.strategy_updater import strategy_updater + + config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) + + strategy_objs = StrategyResolver.search_all_objects( + config, enum_failed=True, recursive=config.get('recursive_strategy_search', False)) + + filtered_strategy_objs = [] + for args_strategy in args['strategy_list']: + for strategy_obj in strategy_objs: + if strategy_obj['name'] == args_strategy and strategy_obj not in filtered_strategy_objs: + filtered_strategy_objs.append(strategy_obj) + break + + for filtered_strategy_obj in filtered_strategy_objs: + # Initialize backtesting object + instance_strategy_updater = strategy_updater() + strategy_updater.start(instance_strategy_updater, filtered_strategy_obj) diff --git a/freqtrade/strategy/strategy_updater.py b/freqtrade/strategy/strategy_updater.py new file mode 100644 index 000000000..fea8edf31 --- /dev/null +++ b/freqtrade/strategy/strategy_updater.py @@ -0,0 +1,213 @@ +import ast +import os +import shutil + + +class strategy_updater: + name_mapping = { + 'ticker_interval': 'timeframe', + 'buy': 'enter_long', + 'sell': 'exit_long', + 'buy_tag': 'enter_tag', + 'sell_reason': 'exit_reason', + + 'sell_signal': 'exit_signal', + 'custom_sell': 'custom_exit', + 'force_sell': 'force_exit', + 'emergency_sell': 'emergency_exit', + + # Strategy/config settings: + 'use_sell_signal': 'use_exit_signal', + 'sell_profit_only': 'exit_profit_only', + 'sell_profit_offset': 'exit_profit_offset', + 'ignore_roi_if_buy_signal': 'ignore_roi_if_entry_signal', + 'forcebuy_enable': 'force_entry_enable', + } + + function_mapping = { + 'populate_buy_trend': 'populate_entry_trend', + 'populate_sell_trend': 'populate_exit_trend', + 'custom_sell': 'custom_exit', + 'check_buy_timeout': 'check_entry_timeout', + 'check_sell_timeout': 'check_exit_timeout', + # '': '', + } + # order_time_in_force, order_types, unfilledtimeout + otif_ot_unfilledtimeout = { + 'buy': 'entry', + 'sell': 'exit', + } + + # create a dictionary that maps the old column names to the new ones + rename_dict = {'buy': 'enter_long', 'sell': 'exit_long', 'buy_tag': 'enter_tag'} + + def start(self, strategy_obj: dict) -> None: + """ + Run strategy updater + It updates a strategy to v3 with the help of the ast-module + :return: None + """ + + self.cwd = os.getcwd() + self.strategies_backup_folder = f'{os.getcwd()}user_data/strategies_orig_updater' + source_file = strategy_obj['location'] + + # read the file + with open(source_file, 'r') as f: + old_code = f.read() + if not os.path.exists(self.strategies_backup_folder): + os.makedirs(self.strategies_backup_folder) + + # backup original + # => currently no date after the filename, + # could get overridden pretty fast if this is fired twice! + shutil.copy(source_file, f"{self.strategies_backup_folder}/{strategy_obj['location_rel']}") + + # update the code + new_code = strategy_updater.update_code(self, old_code, + strategy_updater.name_mapping, + strategy_updater.function_mapping, + strategy_updater.rename_dict) + + # write the modified code to the destination folder + with open(source_file, 'w') as f: + f.write(new_code) + print(f"conversion of file {source_file} successful.") + + # define the function to update the code + def update_code(self, code, _name_mapping, _function_mapping, _rename_dict): + # parse the code into an AST + tree = ast.parse(code) + + # use the AST to update the code + updated_code = strategy_updater.modify_ast( + tree, + _name_mapping, + _function_mapping, + _rename_dict) + + # return the modified code without executing it + return updated_code + + # function that uses the ast module to update the code + def modify_ast(node, _name_mapping, _function_mapping, _rename_dict): # noqa + # create a visitor that will update the names and functions + class NameUpdater(ast.NodeTransformer): + def generic_visit(self, node): + # traverse the AST recursively by calling the visitor method for each child node + if hasattr(node, "_fields"): + for field_name, field_value in ast.iter_fields(node): + self.check_fields(field_value) + self.check_strategy_and_config_settings(field_value) + # add this check to handle the case where field_value is a slice + if isinstance(field_value, ast.Slice): + self.visit(field_value) + # add this check to handle the case where field_value is a target + if isinstance(field_value, ast.expr_context): + self.visit(field_value) + + def check_fields(self, field_value): + if isinstance(field_value, list): + for item in field_value: + if isinstance(item, ast.AST): + self.visit(item) + + def check_strategy_and_config_settings(self, field_value): + if (isinstance(field_value, ast.AST) and + hasattr(node, "targets") and + isinstance(node.targets, list)): + for target in node.targets: + if (hasattr(target, "id") and + (target.id == "order_time_in_force" or + target.id == "order_types" or + target.id == "unfilledtimeout") and + hasattr(field_value, "keys") and + isinstance(field_value.keys, list)): + for key in field_value.keys: + self.visit(key) + + def visit_Name(self, node): + # if the name is in the mapping, update it + if node.id in _name_mapping: + node.id = _name_mapping[node.id] + return node + + def visit_Import(self, node): + # do not update the names in import statements + return node + + def visit_ImportFrom(self, node): + # do not update the names in import statements + if hasattr(node, "module"): + if node.module == "freqtrade.strategy.hyper": + node.module = "freqtrade.strategy" + return node + + def visit_FunctionDef(self, node): + # if the function name is in the mapping, update it + if node.name in _function_mapping: + node.name = _function_mapping[node.name] + return self.generic_visit(node) + + def visit_Attribute(self, node): + # if the attribute name is 'nr_of_successful_buys', + # update it to 'nr_of_successful_entries' + if isinstance(node.value, ast.Name) and \ + node.value.id == 'trades' and \ + node.attr == 'nr_of_successful_buys': + node.attr = 'nr_of_successful_entries' + return self.generic_visit(node) + + def visit_ClassDef(self, node): + # check if the class is derived from IStrategy + if any(isinstance(base, ast.Name) and + base.id == 'IStrategy' for base in node.bases): + # check if the INTERFACE_VERSION variable exists + has_interface_version = any( + isinstance(child, ast.Assign) and + isinstance(child.targets[0], ast.Name) and + child.targets[0].id == 'INTERFACE_VERSION' + for child in node.body + ) + + # if the INTERFACE_VERSION variable does not exist, add it as the first child + if not has_interface_version: + node.body.insert(0, ast.parse('INTERFACE_VERSION = 3').body[0]) + # otherwise, update its value to 3 + else: + for child in node.body: + if isinstance(child, ast.Assign) and \ + isinstance(child.targets[0], ast.Name) and \ + child.targets[0].id == 'INTERFACE_VERSION': + child.value = ast.parse('3').body[0].value + return self.generic_visit(node) + + def visit_Subscript(self, node): + if isinstance(node.slice, ast.Constant): + if node.slice.value in strategy_updater.rename_dict: + # Replace the slice attributes with the values from rename_dict + node.slice.value = strategy_updater.rename_dict[node.slice.value] + if hasattr(node.slice, "elts"): + for elt in node.slice.elts: + if isinstance(elt, ast.Constant) and \ + elt.value in strategy_updater.rename_dict: + elt.value = strategy_updater.rename_dict[elt.value] + return node + + def visit_Constant(self, node): + # do not update the names in import statements + if node.value in \ + strategy_updater.otif_ot_unfilledtimeout: + node.value = \ + strategy_updater.otif_ot_unfilledtimeout[node.value] + return node + + # use the visitor to update the names and functions in the AST + NameUpdater().visit(node) + + # first fix the comments so it understands "\n" properly inside multi line comments. + ast.fix_missing_locations(node) + ast.increment_lineno(node, n=1) + + # generate the new code from the updated AST + return ast.unparse(node) From c6f045afa96c30bbd612af4b5877aeb41834d8fe Mon Sep 17 00:00:00 2001 From: hippocritical Date: Thu, 29 Dec 2022 22:17:52 +0100 Subject: [PATCH 002/360] fixing issues of the maintainer found a bug meaning elts could contain lists of elts (now recurively gone through) Next in line: writing tests based on StrategyUpdater.update_code --- freqtrade/commands/strategy_utils_commands.py | 6 +- freqtrade/strategy/strategy_updater.py | 213 ----------------- freqtrade/strategy/strategyupdater.py | 225 ++++++++++++++++++ 3 files changed, 228 insertions(+), 216 deletions(-) delete mode 100644 freqtrade/strategy/strategy_updater.py create mode 100644 freqtrade/strategy/strategyupdater.py diff --git a/freqtrade/commands/strategy_utils_commands.py b/freqtrade/commands/strategy_utils_commands.py index b9431fecf..13405089f 100644 --- a/freqtrade/commands/strategy_utils_commands.py +++ b/freqtrade/commands/strategy_utils_commands.py @@ -17,7 +17,7 @@ def start_strategy_update(args: Dict[str, Any]) -> None: """ # Import here to avoid loading backtesting module when it's not used - from freqtrade.strategy.strategy_updater import strategy_updater + from freqtrade.strategy.strategyupdater import StrategyUpdater config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) @@ -33,5 +33,5 @@ def start_strategy_update(args: Dict[str, Any]) -> None: for filtered_strategy_obj in filtered_strategy_objs: # Initialize backtesting object - instance_strategy_updater = strategy_updater() - strategy_updater.start(instance_strategy_updater, filtered_strategy_obj) + instance_strategy_updater = StrategyUpdater() + StrategyUpdater.start(instance_strategy_updater, config, filtered_strategy_obj) diff --git a/freqtrade/strategy/strategy_updater.py b/freqtrade/strategy/strategy_updater.py deleted file mode 100644 index fea8edf31..000000000 --- a/freqtrade/strategy/strategy_updater.py +++ /dev/null @@ -1,213 +0,0 @@ -import ast -import os -import shutil - - -class strategy_updater: - name_mapping = { - 'ticker_interval': 'timeframe', - 'buy': 'enter_long', - 'sell': 'exit_long', - 'buy_tag': 'enter_tag', - 'sell_reason': 'exit_reason', - - 'sell_signal': 'exit_signal', - 'custom_sell': 'custom_exit', - 'force_sell': 'force_exit', - 'emergency_sell': 'emergency_exit', - - # Strategy/config settings: - 'use_sell_signal': 'use_exit_signal', - 'sell_profit_only': 'exit_profit_only', - 'sell_profit_offset': 'exit_profit_offset', - 'ignore_roi_if_buy_signal': 'ignore_roi_if_entry_signal', - 'forcebuy_enable': 'force_entry_enable', - } - - function_mapping = { - 'populate_buy_trend': 'populate_entry_trend', - 'populate_sell_trend': 'populate_exit_trend', - 'custom_sell': 'custom_exit', - 'check_buy_timeout': 'check_entry_timeout', - 'check_sell_timeout': 'check_exit_timeout', - # '': '', - } - # order_time_in_force, order_types, unfilledtimeout - otif_ot_unfilledtimeout = { - 'buy': 'entry', - 'sell': 'exit', - } - - # create a dictionary that maps the old column names to the new ones - rename_dict = {'buy': 'enter_long', 'sell': 'exit_long', 'buy_tag': 'enter_tag'} - - def start(self, strategy_obj: dict) -> None: - """ - Run strategy updater - It updates a strategy to v3 with the help of the ast-module - :return: None - """ - - self.cwd = os.getcwd() - self.strategies_backup_folder = f'{os.getcwd()}user_data/strategies_orig_updater' - source_file = strategy_obj['location'] - - # read the file - with open(source_file, 'r') as f: - old_code = f.read() - if not os.path.exists(self.strategies_backup_folder): - os.makedirs(self.strategies_backup_folder) - - # backup original - # => currently no date after the filename, - # could get overridden pretty fast if this is fired twice! - shutil.copy(source_file, f"{self.strategies_backup_folder}/{strategy_obj['location_rel']}") - - # update the code - new_code = strategy_updater.update_code(self, old_code, - strategy_updater.name_mapping, - strategy_updater.function_mapping, - strategy_updater.rename_dict) - - # write the modified code to the destination folder - with open(source_file, 'w') as f: - f.write(new_code) - print(f"conversion of file {source_file} successful.") - - # define the function to update the code - def update_code(self, code, _name_mapping, _function_mapping, _rename_dict): - # parse the code into an AST - tree = ast.parse(code) - - # use the AST to update the code - updated_code = strategy_updater.modify_ast( - tree, - _name_mapping, - _function_mapping, - _rename_dict) - - # return the modified code without executing it - return updated_code - - # function that uses the ast module to update the code - def modify_ast(node, _name_mapping, _function_mapping, _rename_dict): # noqa - # create a visitor that will update the names and functions - class NameUpdater(ast.NodeTransformer): - def generic_visit(self, node): - # traverse the AST recursively by calling the visitor method for each child node - if hasattr(node, "_fields"): - for field_name, field_value in ast.iter_fields(node): - self.check_fields(field_value) - self.check_strategy_and_config_settings(field_value) - # add this check to handle the case where field_value is a slice - if isinstance(field_value, ast.Slice): - self.visit(field_value) - # add this check to handle the case where field_value is a target - if isinstance(field_value, ast.expr_context): - self.visit(field_value) - - def check_fields(self, field_value): - if isinstance(field_value, list): - for item in field_value: - if isinstance(item, ast.AST): - self.visit(item) - - def check_strategy_and_config_settings(self, field_value): - if (isinstance(field_value, ast.AST) and - hasattr(node, "targets") and - isinstance(node.targets, list)): - for target in node.targets: - if (hasattr(target, "id") and - (target.id == "order_time_in_force" or - target.id == "order_types" or - target.id == "unfilledtimeout") and - hasattr(field_value, "keys") and - isinstance(field_value.keys, list)): - for key in field_value.keys: - self.visit(key) - - def visit_Name(self, node): - # if the name is in the mapping, update it - if node.id in _name_mapping: - node.id = _name_mapping[node.id] - return node - - def visit_Import(self, node): - # do not update the names in import statements - return node - - def visit_ImportFrom(self, node): - # do not update the names in import statements - if hasattr(node, "module"): - if node.module == "freqtrade.strategy.hyper": - node.module = "freqtrade.strategy" - return node - - def visit_FunctionDef(self, node): - # if the function name is in the mapping, update it - if node.name in _function_mapping: - node.name = _function_mapping[node.name] - return self.generic_visit(node) - - def visit_Attribute(self, node): - # if the attribute name is 'nr_of_successful_buys', - # update it to 'nr_of_successful_entries' - if isinstance(node.value, ast.Name) and \ - node.value.id == 'trades' and \ - node.attr == 'nr_of_successful_buys': - node.attr = 'nr_of_successful_entries' - return self.generic_visit(node) - - def visit_ClassDef(self, node): - # check if the class is derived from IStrategy - if any(isinstance(base, ast.Name) and - base.id == 'IStrategy' for base in node.bases): - # check if the INTERFACE_VERSION variable exists - has_interface_version = any( - isinstance(child, ast.Assign) and - isinstance(child.targets[0], ast.Name) and - child.targets[0].id == 'INTERFACE_VERSION' - for child in node.body - ) - - # if the INTERFACE_VERSION variable does not exist, add it as the first child - if not has_interface_version: - node.body.insert(0, ast.parse('INTERFACE_VERSION = 3').body[0]) - # otherwise, update its value to 3 - else: - for child in node.body: - if isinstance(child, ast.Assign) and \ - isinstance(child.targets[0], ast.Name) and \ - child.targets[0].id == 'INTERFACE_VERSION': - child.value = ast.parse('3').body[0].value - return self.generic_visit(node) - - def visit_Subscript(self, node): - if isinstance(node.slice, ast.Constant): - if node.slice.value in strategy_updater.rename_dict: - # Replace the slice attributes with the values from rename_dict - node.slice.value = strategy_updater.rename_dict[node.slice.value] - if hasattr(node.slice, "elts"): - for elt in node.slice.elts: - if isinstance(elt, ast.Constant) and \ - elt.value in strategy_updater.rename_dict: - elt.value = strategy_updater.rename_dict[elt.value] - return node - - def visit_Constant(self, node): - # do not update the names in import statements - if node.value in \ - strategy_updater.otif_ot_unfilledtimeout: - node.value = \ - strategy_updater.otif_ot_unfilledtimeout[node.value] - return node - - # use the visitor to update the names and functions in the AST - NameUpdater().visit(node) - - # first fix the comments so it understands "\n" properly inside multi line comments. - ast.fix_missing_locations(node) - ast.increment_lineno(node, n=1) - - # generate the new code from the updated AST - return ast.unparse(node) diff --git a/freqtrade/strategy/strategyupdater.py b/freqtrade/strategy/strategyupdater.py new file mode 100644 index 000000000..b62bc7822 --- /dev/null +++ b/freqtrade/strategy/strategyupdater.py @@ -0,0 +1,225 @@ +import ast +import os +import shutil +from pathlib import Path + +import astor + + +class StrategyUpdater: + name_mapping = { + 'ticker_interval': 'timeframe', + 'buy': 'enter_long', + 'sell': 'exit_long', + 'buy_tag': 'enter_tag', + 'sell_reason': 'exit_reason', + + 'sell_signal': 'exit_signal', + 'custom_sell': 'custom_exit', + 'force_sell': 'force_exit', + 'emergency_sell': 'emergency_exit', + + # Strategy/config settings: + 'use_sell_signal': 'use_exit_signal', + 'sell_profit_only': 'exit_profit_only', + 'sell_profit_offset': 'exit_profit_offset', + 'ignore_roi_if_buy_signal': 'ignore_roi_if_entry_signal', + 'forcebuy_enable': 'force_entry_enable', + } + + function_mapping = { + 'populate_buy_trend': 'populate_entry_trend', + 'populate_sell_trend': 'populate_exit_trend', + 'custom_sell': 'custom_exit', + 'check_buy_timeout': 'check_entry_timeout', + 'check_sell_timeout': 'check_exit_timeout', + # '': '', + } + # order_time_in_force, order_types, unfilledtimeout + otif_ot_unfilledtimeout = { + 'buy': 'entry', + 'sell': 'exit', + } + + # create a dictionary that maps the old column names to the new ones + rename_dict = {'buy': 'enter_long', 'sell': 'exit_long', 'buy_tag': 'enter_tag'} + + def start(self, config, strategy_obj: dict) -> None: + """ + Run strategy updater + It updates a strategy to v3 with the help of the ast-module + :return: None + """ + + source_file = strategy_obj['location'] + strategies_backup_folder = Path.joinpath(config['user_data_dir'], "strategies_orig_updater") + target_file = Path.joinpath(strategies_backup_folder, strategy_obj['location_rel']) + + # read the file + with open(source_file, 'r') as f: + old_code = f.read() + if not os.path.exists(strategies_backup_folder): + os.makedirs(strategies_backup_folder) + + # backup original + # => currently no date after the filename, + # could get overridden pretty fast if this is fired twice! + # The folder is always the same and the file name too (currently). + shutil.copy(source_file, target_file) + + # update the code + new_code = StrategyUpdater.update_code(self, old_code) + + # write the modified code to the destination folder + with open(source_file, 'w') as f: + f.write(new_code) + print(f"conversion of file {source_file} successful.") + + # define the function to update the code + def update_code(self, code): + # parse the code into an AST + tree = ast.parse(code) + + # use the AST to update the code + updated_code = self.modify_ast( + tree) + + # return the modified code without executing it + return updated_code + + # function that uses the ast module to update the code + def modify_ast(self, tree): # noqa + # use the visitor to update the names and functions in the AST + NameUpdater().visit(tree) + + # first fix the comments, so it understands "\n" properly inside multi line comments. + ast.fix_missing_locations(tree) + ast.increment_lineno(tree, n=1) + + # generate the new code from the updated AST + return astor.to_source(tree) + + +# Here we go through each respective node, slice, elt, key ... to replace outdated entries. +class NameUpdater(ast.NodeTransformer): + def generic_visit(self, node): + # traverse the AST recursively by calling the visitor method for each child node + if hasattr(node, "_fields"): + for field_name, field_value in ast.iter_fields(node): + self.visit(field_value) + self.generic_visit(field_value) + self.check_fields(field_value) + self.check_strategy_and_config_settings(node, field_value) + # add this check to handle the case where field_value is a slice + if isinstance(field_value, ast.Slice): + self.visit(field_value) + # add this check to handle the case where field_value is a target + if isinstance(field_value, ast.expr_context): + self.visit(field_value) + + def check_fields(self, field_value): + if isinstance(field_value, list): + for item in field_value: + if isinstance(item, ast.AST): + self.visit(item) + + def check_strategy_and_config_settings(self, node, field_value): + if (isinstance(field_value, ast.AST) and + hasattr(node, "targets") and + isinstance(node.targets, list)): + for target in node.targets: + if (hasattr(target, "id") and + hasattr(field_value, "keys") and + isinstance(field_value.keys, list)): + if (target.id == "order_time_in_force" or + target.id == "order_types" or + target.id == "unfilledtimeout"): + for key in field_value.keys: + self.visit(key) + # Check if the target is a Subscript object with a "value" attribute + if isinstance(target, ast.Subscript) and hasattr(target.value, "attr"): + if target.value.attr == "loc": + self.visit(target) + + def visit_Name(self, node): + # if the name is in the mapping, update it + if node.id in StrategyUpdater.name_mapping: + node.id = StrategyUpdater.name_mapping[node.id] + return node + + def visit_Import(self, node): + # do not update the names in import statements + return node + + def visit_ImportFrom(self, node): + # do not update the names in import statements + if hasattr(node, "module"): + if node.module == "freqtrade.strategy.hyper": + node.module = "freqtrade.strategy" + return node + + def visit_FunctionDef(self, node): + # if the function name is in the mapping, update it + if node.name in StrategyUpdater.function_mapping: + node.name = StrategyUpdater.function_mapping[node.name] + return self.generic_visit(node) + + def visit_Attribute(self, node): + # if the attribute name is 'nr_of_successful_buys', + # update it to 'nr_of_successful_entries' + if isinstance(node.value, ast.Name) and \ + node.value.id == 'trades' and \ + node.attr == 'nr_of_successful_buys': + node.attr = 'nr_of_successful_entries' + return self.generic_visit(node) + + def visit_ClassDef(self, node): + # check if the class is derived from IStrategy + if any(isinstance(base, ast.Name) and + base.id == 'IStrategy' for base in node.bases): + # check if the INTERFACE_VERSION variable exists + has_interface_version = any( + isinstance(child, ast.Assign) and + isinstance(child.targets[0], ast.Name) and + child.targets[0].id == 'INTERFACE_VERSION' + for child in node.body + ) + + # if the INTERFACE_VERSION variable does not exist, add it as the first child + if not has_interface_version: + node.body.insert(0, ast.parse('INTERFACE_VERSION = 3').body[0]) + # otherwise, update its value to 3 + else: + for child in node.body: + if isinstance(child, ast.Assign) and \ + isinstance(child.targets[0], ast.Name) and \ + child.targets[0].id == 'INTERFACE_VERSION': + child.value = ast.parse('3').body[0].value + return self.generic_visit(node) + + def visit_Subscript(self, node): + if isinstance(node.slice, ast.Constant): + if node.slice.value in StrategyUpdater.rename_dict: + # Replace the slice attributes with the values from rename_dict + node.slice.value = StrategyUpdater.rename_dict[node.slice.value] + if hasattr(node.slice, "elts"): + self.visit_slice_elts(node.slice.elts) + if hasattr(node.slice.value, "elts"): + self.visit_slice_elts(node.slice.value.elts) + return node + + # elts can have elts (technically recursively) + def visit_slice_elts(self, elts): + for elt in elts: + if isinstance(elt, ast.Constant) and elt.value in StrategyUpdater.rename_dict: + elt.value = StrategyUpdater.rename_dict[elt.value] + elif hasattr(elt, "elts"): + self.visit_slice_elts(elt.elts) + + def visit_Constant(self, node): + # do not update the names in import statements + if node.value in \ + StrategyUpdater.otif_ot_unfilledtimeout: + node.value = \ + StrategyUpdater.otif_ot_unfilledtimeout[node.value] + return node From 82218d01f48051e2a43ad097a7453f3a1da533ac Mon Sep 17 00:00:00 2001 From: hippocritical Date: Fri, 30 Dec 2022 21:48:06 +0100 Subject: [PATCH 003/360] sped up the function generic_visit that now skips unnecessary fields added mentioning of skipped class names since they could not be found --- freqtrade/commands/strategy_utils_commands.py | 5 +++-- freqtrade/strategy/strategyupdater.py | 8 ++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/freqtrade/commands/strategy_utils_commands.py b/freqtrade/commands/strategy_utils_commands.py index 13405089f..cf7ba5e13 100644 --- a/freqtrade/commands/strategy_utils_commands.py +++ b/freqtrade/commands/strategy_utils_commands.py @@ -25,11 +25,12 @@ def start_strategy_update(args: Dict[str, Any]) -> None: config, enum_failed=True, recursive=config.get('recursive_strategy_search', False)) filtered_strategy_objs = [] - for args_strategy in args['strategy_list']: - for strategy_obj in strategy_objs: + for strategy_obj in strategy_objs: + for args_strategy in args['strategy_list']: if strategy_obj['name'] == args_strategy and strategy_obj not in filtered_strategy_objs: filtered_strategy_objs.append(strategy_obj) break + print(f"strategy {strategy_obj['name']} could not be loaded or found and is skipped.") for filtered_strategy_obj in filtered_strategy_objs: # Initialize backtesting object diff --git a/freqtrade/strategy/strategyupdater.py b/freqtrade/strategy/strategyupdater.py index b62bc7822..74bb4454c 100644 --- a/freqtrade/strategy/strategyupdater.py +++ b/freqtrade/strategy/strategyupdater.py @@ -52,6 +52,7 @@ class StrategyUpdater: """ source_file = strategy_obj['location'] + print(f"started conversion of {source_file}") strategies_backup_folder = Path.joinpath(config['user_data_dir'], "strategies_orig_updater") target_file = Path.joinpath(strategies_backup_folder, strategy_obj['location_rel']) @@ -106,6 +107,8 @@ class NameUpdater(ast.NodeTransformer): # traverse the AST recursively by calling the visitor method for each child node if hasattr(node, "_fields"): for field_name, field_value in ast.iter_fields(node): + if not isinstance(field_value, ast.AST): + continue # to avoid unnecessary loops self.visit(field_value) self.generic_visit(field_value) self.check_fields(field_value) @@ -204,8 +207,9 @@ class NameUpdater(ast.NodeTransformer): node.slice.value = StrategyUpdater.rename_dict[node.slice.value] if hasattr(node.slice, "elts"): self.visit_slice_elts(node.slice.elts) - if hasattr(node.slice.value, "elts"): - self.visit_slice_elts(node.slice.value.elts) + if hasattr(node.slice, "value"): + if hasattr(node.slice.value, "elts"): + self.visit_slice_elts(node.slice.value.elts) return node # elts can have elts (technically recursively) From a51e44eea304e548c74c190aed6a6adb8f81d0ed Mon Sep 17 00:00:00 2001 From: hippocritical Date: Sun, 1 Jan 2023 12:37:15 +0100 Subject: [PATCH 004/360] Adding tests --- freqtrade/commands/strategy_utils_commands.py | 10 +++- freqtrade/strategy/strategyupdater.py | 35 +++++------- tests/test_strategy_updater.py | 56 +++++++++++++++++++ 3 files changed, 76 insertions(+), 25 deletions(-) create mode 100644 tests/test_strategy_updater.py diff --git a/freqtrade/commands/strategy_utils_commands.py b/freqtrade/commands/strategy_utils_commands.py index cf7ba5e13..1f3c27e0d 100644 --- a/freqtrade/commands/strategy_utils_commands.py +++ b/freqtrade/commands/strategy_utils_commands.py @@ -25,12 +25,16 @@ def start_strategy_update(args: Dict[str, Any]) -> None: config, enum_failed=True, recursive=config.get('recursive_strategy_search', False)) filtered_strategy_objs = [] - for strategy_obj in strategy_objs: - for args_strategy in args['strategy_list']: + for args_strategy in args['strategy_list']: + found = False + for strategy_obj in strategy_objs: if strategy_obj['name'] == args_strategy and strategy_obj not in filtered_strategy_objs: filtered_strategy_objs.append(strategy_obj) + found = True break - print(f"strategy {strategy_obj['name']} could not be loaded or found and is skipped.") + + if not found: + print(f"strategy {strategy_obj['name']} could not be loaded or found and is skipped.") for filtered_strategy_obj in filtered_strategy_objs: # Initialize backtesting object diff --git a/freqtrade/strategy/strategyupdater.py b/freqtrade/strategy/strategyupdater.py index 74bb4454c..396d57a8a 100644 --- a/freqtrade/strategy/strategyupdater.py +++ b/freqtrade/strategy/strategyupdater.py @@ -70,7 +70,6 @@ class StrategyUpdater: # update the code new_code = StrategyUpdater.update_code(self, old_code) - # write the modified code to the destination folder with open(source_file, 'w') as f: f.write(new_code) @@ -82,8 +81,7 @@ class StrategyUpdater: tree = ast.parse(code) # use the AST to update the code - updated_code = self.modify_ast( - tree) + updated_code = self.modify_ast(self, tree) # return the modified code without executing it return updated_code @@ -107,18 +105,8 @@ class NameUpdater(ast.NodeTransformer): # traverse the AST recursively by calling the visitor method for each child node if hasattr(node, "_fields"): for field_name, field_value in ast.iter_fields(node): - if not isinstance(field_value, ast.AST): - continue # to avoid unnecessary loops - self.visit(field_value) - self.generic_visit(field_value) - self.check_fields(field_value) self.check_strategy_and_config_settings(node, field_value) - # add this check to handle the case where field_value is a slice - if isinstance(field_value, ast.Slice): - self.visit(field_value) - # add this check to handle the case where field_value is a target - if isinstance(field_value, ast.expr_context): - self.visit(field_value) + self.check_fields(field_value) def check_fields(self, field_value): if isinstance(field_value, list): @@ -139,10 +127,6 @@ class NameUpdater(ast.NodeTransformer): target.id == "unfilledtimeout"): for key in field_value.keys: self.visit(key) - # Check if the target is a Subscript object with a "value" attribute - if isinstance(target, ast.Subscript) and hasattr(target.value, "attr"): - if target.value.attr == "loc": - self.visit(target) def visit_Name(self, node): # if the name is in the mapping, update it @@ -154,11 +138,14 @@ class NameUpdater(ast.NodeTransformer): # do not update the names in import statements return node + # This function is currently never successfully triggered + # since freqtrade currently only allows valid code to be processed. + # The module .hyper does not anymore exist and by that fails to even + # reach this function to be updated currently. def visit_ImportFrom(self, node): - # do not update the names in import statements - if hasattr(node, "module"): - if node.module == "freqtrade.strategy.hyper": - node.module = "freqtrade.strategy" + # if hasattr(node, "module"): + # if node.module == "freqtrade.strategy.hyper": + # node.module = "freqtrade.strategy" return node def visit_FunctionDef(self, node): @@ -210,6 +197,10 @@ class NameUpdater(ast.NodeTransformer): if hasattr(node.slice, "value"): if hasattr(node.slice.value, "elts"): self.visit_slice_elts(node.slice.value.elts) + # Check if the target is a Subscript object with a "value" attribute + # if isinstance(target, ast.Subscript) and hasattr(target.value, "attr"): + # if target.value.attr == "loc": + # self.visit(target) return node # elts can have elts (technically recursively) diff --git a/tests/test_strategy_updater.py b/tests/test_strategy_updater.py new file mode 100644 index 000000000..6997abdce --- /dev/null +++ b/tests/test_strategy_updater.py @@ -0,0 +1,56 @@ +# pragma pylint: disable=missing-docstring, protected-access, invalid-name + +from freqtrade.strategy.strategyupdater import StrategyUpdater + + +def test_strategy_updater(default_conf, caplog) -> None: + modified_code1 = StrategyUpdater.update_code(StrategyUpdater, """ +class testClass(IStrategy): + def populate_buy_trend(): + pass + def populate_sell_trend(): + pass + def check_buy_timeout(): + pass + def check_sell_timeout(): + pass + def custom_sell(): + pass +""") + + modified_code2 = StrategyUpdater.update_code(StrategyUpdater, """ +buy_some_parameter = IntParameter(space='buy') +sell_some_parameter = IntParameter(space='sell') +ticker_interval = '15m' +""") + modified_code3 = StrategyUpdater.update_code(StrategyUpdater, """ +use_sell_signal = True +sell_profit_only = True +sell_profit_offset = True +ignore_roi_if_buy_signal = True +forcebuy_enable = True +""") + modified_code4 = StrategyUpdater.update_code(StrategyUpdater, """ +dataframe.loc[reduce(lambda x, y: x & y, conditions), 'buy'] = 1 +dataframe.loc[reduce(lambda x, y: x & y, conditions), 'sell'] = 1 +""") + assert "populate_entry_trend" in modified_code1 + assert "populate_exit_trend" in modified_code1 + assert "check_entry_timeout" in modified_code1 + assert "check_exit_timeout" in modified_code1 + assert "custom_exit" in modified_code1 + assert "INTERFACE_VERSION = 3" in modified_code1 + + assert "timeframe" in modified_code2 + # check for not editing hyperopt spaces + assert "space='buy'" in modified_code2 + assert "space='sell'" in modified_code2 + + assert "use_exit_signal" in modified_code3 + assert "exit_profit_only" in modified_code3 + assert "exit_profit_offset" in modified_code3 + assert "ignore_roi_if_entry_signal" in modified_code3 + assert "force_entry_enable" in modified_code3 + + assert "enter_long" in modified_code4 + assert "exit_long" in modified_code4 From 762dd4f024907ea9f9ddd0ac303eaf9f77e6dec4 Mon Sep 17 00:00:00 2001 From: hippocritical Date: Sun, 1 Jan 2023 18:57:38 +0100 Subject: [PATCH 005/360] Adding tests added more code inside NameUpdater to grab more variables. --- freqtrade/strategy/strategyupdater.py | 40 +++++++++++++++++++++------ tests/test_strategy_updater.py | 11 +++++++- 2 files changed, 42 insertions(+), 9 deletions(-) diff --git a/freqtrade/strategy/strategyupdater.py b/freqtrade/strategy/strategyupdater.py index 396d57a8a..5b4bb8be0 100644 --- a/freqtrade/strategy/strategyupdater.py +++ b/freqtrade/strategy/strategyupdater.py @@ -128,6 +128,15 @@ class NameUpdater(ast.NodeTransformer): for key in field_value.keys: self.visit(key) + def check_args(self, node): + if isinstance(node.args, ast.arguments): + self.check_args(node.args) + if hasattr(node, "args"): + if isinstance(node.args, list): + for arg in node.args: + arg.arg = StrategyUpdater.name_mapping[arg.arg] + return node + def visit_Name(self, node): # if the name is in the mapping, update it if node.id in StrategyUpdater.name_mapping: @@ -152,6 +161,8 @@ class NameUpdater(ast.NodeTransformer): # if the function name is in the mapping, update it if node.name in StrategyUpdater.function_mapping: node.name = StrategyUpdater.function_mapping[node.name] + if hasattr(node, "args"): + self.check_args(node) return self.generic_visit(node) def visit_Attribute(self, node): @@ -193,10 +204,10 @@ class NameUpdater(ast.NodeTransformer): # Replace the slice attributes with the values from rename_dict node.slice.value = StrategyUpdater.rename_dict[node.slice.value] if hasattr(node.slice, "elts"): - self.visit_slice_elts(node.slice.elts) + self.visit_elts(node.slice.elts) if hasattr(node.slice, "value"): if hasattr(node.slice.value, "elts"): - self.visit_slice_elts(node.slice.value.elts) + self.visit_elts(node.slice.value.elts) # Check if the target is a Subscript object with a "value" attribute # if isinstance(target, ast.Subscript) and hasattr(target.value, "attr"): # if target.value.attr == "loc": @@ -204,12 +215,25 @@ class NameUpdater(ast.NodeTransformer): return node # elts can have elts (technically recursively) - def visit_slice_elts(self, elts): - for elt in elts: - if isinstance(elt, ast.Constant) and elt.value in StrategyUpdater.rename_dict: - elt.value = StrategyUpdater.rename_dict[elt.value] - elif hasattr(elt, "elts"): - self.visit_slice_elts(elt.elts) + def visit_elts(self, elts): + if isinstance(elts, list): + for elt in elts: + self.visit_elt(elt) + else: + self.visit_elt(elts) + + # sub function again needed since the structure itself is highly flexible ... + def visit_elt(self, elt): + if isinstance(elt, ast.Constant) and elt.value in StrategyUpdater.rename_dict: + elt.value = StrategyUpdater.rename_dict[elt.value] + if hasattr(elt, "elts"): + self.visit_elts(elt.elts) + if hasattr(elt, "args"): + if isinstance(elt.args, ast.arguments): + self.visit_elts(elt.args) + else: + for arg in elt.args: + self.visit_elts(arg) def visit_Constant(self, node): # do not update the names in import statements diff --git a/tests/test_strategy_updater.py b/tests/test_strategy_updater.py index 6997abdce..cf18fcc25 100644 --- a/tests/test_strategy_updater.py +++ b/tests/test_strategy_updater.py @@ -4,6 +4,11 @@ from freqtrade.strategy.strategyupdater import StrategyUpdater def test_strategy_updater(default_conf, caplog) -> None: + modified_code5 = StrategyUpdater.update_code(StrategyUpdater, """ +def confirm_trade_exit(sell_reason: str): + pass +""") + modified_code1 = StrategyUpdater.update_code(StrategyUpdater, """ class testClass(IStrategy): def populate_buy_trend(): @@ -31,9 +36,10 @@ ignore_roi_if_buy_signal = True forcebuy_enable = True """) modified_code4 = StrategyUpdater.update_code(StrategyUpdater, """ -dataframe.loc[reduce(lambda x, y: x & y, conditions), 'buy'] = 1 +dataframe.loc[reduce(lambda x, y: x & y, conditions), ["buy", "buy_tag"]] = (1, "buy_signal_1") dataframe.loc[reduce(lambda x, y: x & y, conditions), 'sell'] = 1 """) + assert "populate_entry_trend" in modified_code1 assert "populate_exit_trend" in modified_code1 assert "check_entry_timeout" in modified_code1 @@ -54,3 +60,6 @@ dataframe.loc[reduce(lambda x, y: x & y, conditions), 'sell'] = 1 assert "enter_long" in modified_code4 assert "exit_long" in modified_code4 + assert "enter_tag" in modified_code4 + + assert "exit_reason" in modified_code5 From 66f7c913570b29be29d180caa1e50ff75f2438b2 Mon Sep 17 00:00:00 2001 From: hippocritical Date: Sun, 1 Jan 2023 22:03:45 +0100 Subject: [PATCH 006/360] Adding tests added more code inside NameUpdater to grab more variables. --- freqtrade/strategy/strategyupdater.py | 18 +++++++++- requirements.txt | 20 +++++++++++ tests/test_strategy_updater.py | 50 +++++++++++++++++++++------ 3 files changed, 77 insertions(+), 11 deletions(-) diff --git a/freqtrade/strategy/strategyupdater.py b/freqtrade/strategy/strategyupdater.py index 5b4bb8be0..1a0423076 100644 --- a/freqtrade/strategy/strategyupdater.py +++ b/freqtrade/strategy/strategyupdater.py @@ -107,12 +107,16 @@ class NameUpdater(ast.NodeTransformer): for field_name, field_value in ast.iter_fields(node): self.check_strategy_and_config_settings(node, field_value) self.check_fields(field_value) + for child in ast.iter_child_nodes(node): + self.generic_visit(child) def check_fields(self, field_value): if isinstance(field_value, list): for item in field_value: - if isinstance(item, ast.AST): + if isinstance(item, ast.AST) or isinstance(item, ast.If): self.visit(item) + if isinstance(field_value, ast.Name): + self.visit_Name(field_value) def check_strategy_and_config_settings(self, node, field_value): if (isinstance(field_value, ast.AST) and @@ -157,6 +161,11 @@ class NameUpdater(ast.NodeTransformer): # node.module = "freqtrade.strategy" return node + def visit_If(self, node: ast.If): + for child in ast.iter_child_nodes(node): + self.visit(child) + return self.generic_visit(node) + def visit_FunctionDef(self, node): # if the function name is in the mapping, update it if node.name in StrategyUpdater.function_mapping: @@ -165,6 +174,13 @@ class NameUpdater(ast.NodeTransformer): self.check_args(node) return self.generic_visit(node) + def visit_Assign(self, node): + if hasattr(node, "targets") and isinstance(node.targets, list): + for target in node.targets: + if hasattr(target, "id") and target.id in StrategyUpdater.name_mapping: + target.id = StrategyUpdater.name_mapping[target.id] + return node + def visit_Attribute(self, node): # if the attribute name is 'nr_of_successful_buys', # update it to 'nr_of_successful_entries' diff --git a/requirements.txt b/requirements.txt index 90bc4f702..3229ec3c4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -57,3 +57,23 @@ schedule==1.1.0 #WS Messages websockets==10.4 janus==1.0.0 + +pytest~=7.2.0 +freqtrade~=2022.12.dev0 +filelock~=3.8.2 +plotly~=5.11.0 +setuptools~=65.6.3 +starlette~=0.22.0 +gym~=0.21.0 +torch~=1.13.1 +scikit-learn~=1.1.3 +scipy~=1.9.3 +xgboost~=1.7.2 +catboost~=1.1.1 +lightgbm~=3.3.3 +astor~=0.8.1 +ta~=0.10.2 +finta~=1.3 +tapy~=1.9.1 +matplotlib~=3.6.2 +PyYAML~=6.0 diff --git a/tests/test_strategy_updater.py b/tests/test_strategy_updater.py index cf18fcc25..5736e5c76 100644 --- a/tests/test_strategy_updater.py +++ b/tests/test_strategy_updater.py @@ -4,11 +4,11 @@ from freqtrade.strategy.strategyupdater import StrategyUpdater def test_strategy_updater(default_conf, caplog) -> None: - modified_code5 = StrategyUpdater.update_code(StrategyUpdater, """ -def confirm_trade_exit(sell_reason: str): - pass + modified_code2 = StrategyUpdater.update_code(StrategyUpdater, """ +ticker_interval = '15m' +buy_some_parameter = IntParameter(space='buy') +sell_some_parameter = IntParameter(space='sell') """) - modified_code1 = StrategyUpdater.update_code(StrategyUpdater, """ class testClass(IStrategy): def populate_buy_trend(): @@ -21,12 +21,6 @@ class testClass(IStrategy): pass def custom_sell(): pass -""") - - modified_code2 = StrategyUpdater.update_code(StrategyUpdater, """ -buy_some_parameter = IntParameter(space='buy') -sell_some_parameter = IntParameter(space='sell') -ticker_interval = '15m' """) modified_code3 = StrategyUpdater.update_code(StrategyUpdater, """ use_sell_signal = True @@ -38,6 +32,32 @@ forcebuy_enable = True modified_code4 = StrategyUpdater.update_code(StrategyUpdater, """ dataframe.loc[reduce(lambda x, y: x & y, conditions), ["buy", "buy_tag"]] = (1, "buy_signal_1") dataframe.loc[reduce(lambda x, y: x & y, conditions), 'sell'] = 1 +""") + modified_code5 = StrategyUpdater.update_code(StrategyUpdater, """ +def confirm_trade_exit(sell_reason: str): + pass + """) + modified_code6 = StrategyUpdater.update_code(StrategyUpdater, """ +order_time_in_force = { + 'buy': 'gtc', + 'sell': 'ioc' +} +order_types = { + 'buy': 'limit', + 'sell': 'market', + 'stoploss': 'market', + 'stoploss_on_exchange': False +} +unfilledtimeout = { + 'buy': 1, + 'sell': 2 +} +""") + + modified_code7 = StrategyUpdater.update_code(StrategyUpdater, """ +def confirm_trade_exit(sell_reason): + if (sell_reason == 'stop_loss'): + pass """) assert "populate_entry_trend" in modified_code1 @@ -63,3 +83,13 @@ dataframe.loc[reduce(lambda x, y: x & y, conditions), 'sell'] = 1 assert "enter_tag" in modified_code4 assert "exit_reason" in modified_code5 + + assert "'entry': 'gtc'" in modified_code6 + assert "'exit': 'ioc'" in modified_code6 + assert "'entry': 'limit'" in modified_code6 + assert "'exit': 'market'" in modified_code6 + assert "'entry': 1" in modified_code6 + assert "'exit': 2" in modified_code6 + + assert "exit_reason" in modified_code7 + assert "exit_reason == 'stop_loss'" in modified_code7 From e89609dc3a9fc32003243785c9a2e173a4ebd09d Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 2 Jan 2023 08:51:54 +0100 Subject: [PATCH 007/360] Fix crash due to invalid parameter --- freqtrade/strategy/strategyupdater.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/strategy/strategyupdater.py b/freqtrade/strategy/strategyupdater.py index 1a0423076..ab757e0a9 100644 --- a/freqtrade/strategy/strategyupdater.py +++ b/freqtrade/strategy/strategyupdater.py @@ -81,7 +81,7 @@ class StrategyUpdater: tree = ast.parse(code) # use the AST to update the code - updated_code = self.modify_ast(self, tree) + updated_code = self.modify_ast(tree) # return the modified code without executing it return updated_code From a712c5d42c567751be0949a2202f48d138547c25 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 2 Jan 2023 08:44:00 +0100 Subject: [PATCH 008/360] Improve if formatting --- freqtrade/strategy/strategyupdater.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/freqtrade/strategy/strategyupdater.py b/freqtrade/strategy/strategyupdater.py index ab757e0a9..a0547932c 100644 --- a/freqtrade/strategy/strategyupdater.py +++ b/freqtrade/strategy/strategyupdater.py @@ -184,9 +184,11 @@ class NameUpdater(ast.NodeTransformer): def visit_Attribute(self, node): # if the attribute name is 'nr_of_successful_buys', # update it to 'nr_of_successful_entries' - if isinstance(node.value, ast.Name) and \ - node.value.id == 'trades' and \ - node.attr == 'nr_of_successful_buys': + if ( + isinstance(node.value, ast.Name) + and node.value.id == 'trades' + and node.attr == 'nr_of_successful_buys' + ): node.attr = 'nr_of_successful_entries' return self.generic_visit(node) @@ -208,9 +210,11 @@ class NameUpdater(ast.NodeTransformer): # otherwise, update its value to 3 else: for child in node.body: - if isinstance(child, ast.Assign) and \ - isinstance(child.targets[0], ast.Name) and \ - child.targets[0].id == 'INTERFACE_VERSION': + if ( + isinstance(child, ast.Assign) + and isinstance(child.targets[0], ast.Name) + and child.targets[0].id == 'INTERFACE_VERSION' + ): child.value = ast.parse('3').body[0].value return self.generic_visit(node) @@ -253,8 +257,6 @@ class NameUpdater(ast.NodeTransformer): def visit_Constant(self, node): # do not update the names in import statements - if node.value in \ - StrategyUpdater.otif_ot_unfilledtimeout: - node.value = \ - StrategyUpdater.otif_ot_unfilledtimeout[node.value] + if node.value in StrategyUpdater.otif_ot_unfilledtimeout: + node.value = StrategyUpdater.otif_ot_unfilledtimeout[node.value] return node From df25dbc048594870d6e5c9356575b47326d8ddef Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 2 Jan 2023 08:52:18 +0100 Subject: [PATCH 009/360] Don't require a configuration for strategy-updater --- freqtrade/commands/arguments.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index 9990ad230..a3cdc378a 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -111,7 +111,8 @@ ARGS_ANALYZE_ENTRIES_EXITS = ["exportfilename", "analysis_groups", "enter_reason NO_CONF_REQURIED = ["convert-data", "convert-trade-data", "download-data", "list-timeframes", "list-markets", "list-pairs", "list-strategies", "list-freqaimodels", "list-data", "hyperopt-list", "hyperopt-show", "backtest-filter", - "plot-dataframe", "plot-profit", "show-trades", "trades-to-ohlcv"] + "plot-dataframe", "plot-profit", "show-trades", "trades-to-ohlcv", + "strategy-updater"] NO_CONF_ALLOWED = ["create-userdir", "list-exchanges", "new-strategy"] From 61d7129d7ce65edcbbdd75f7047d47ad75051cce Mon Sep 17 00:00:00 2001 From: hippocritical <41228167+hippocritical@users.noreply.github.com> Date: Mon, 2 Jan 2023 16:51:05 +0100 Subject: [PATCH 010/360] Update freqtrade/commands/strategy_utils_commands.py Co-authored-by: Matthias --- freqtrade/commands/strategy_utils_commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/commands/strategy_utils_commands.py b/freqtrade/commands/strategy_utils_commands.py index 1f3c27e0d..0325f411c 100644 --- a/freqtrade/commands/strategy_utils_commands.py +++ b/freqtrade/commands/strategy_utils_commands.py @@ -39,4 +39,4 @@ def start_strategy_update(args: Dict[str, Any]) -> None: for filtered_strategy_obj in filtered_strategy_objs: # Initialize backtesting object instance_strategy_updater = StrategyUpdater() - StrategyUpdater.start(instance_strategy_updater, config, filtered_strategy_obj) + self.start(config, filtered_strategy_obj) From 0817e1698f654a13fd6f01c2519dee5f1cc19f99 Mon Sep 17 00:00:00 2001 From: hippocritical Date: Mon, 2 Jan 2023 20:45:56 +0100 Subject: [PATCH 011/360] requirements thinned out again StrategyResolver.search_all_objects(enum_failed) set to False since we got no use in True shortened update_code call added modified_code8 test which currently still fails. (and thereby is commented out) --- freqtrade/commands/strategy_utils_commands.py | 2 +- freqtrade/strategy/strategyupdater.py | 8 +++++--- requirements.txt | 18 ------------------ tests/test_strategy_updater.py | 11 +++++++++++ 4 files changed, 17 insertions(+), 22 deletions(-) diff --git a/freqtrade/commands/strategy_utils_commands.py b/freqtrade/commands/strategy_utils_commands.py index 1f3c27e0d..5d8ede9a2 100644 --- a/freqtrade/commands/strategy_utils_commands.py +++ b/freqtrade/commands/strategy_utils_commands.py @@ -22,7 +22,7 @@ def start_strategy_update(args: Dict[str, Any]) -> None: config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) strategy_objs = StrategyResolver.search_all_objects( - config, enum_failed=True, recursive=config.get('recursive_strategy_search', False)) + config, enum_failed=False, recursive=config.get('recursive_strategy_search', False)) filtered_strategy_objs = [] for args_strategy in args['strategy_list']: diff --git a/freqtrade/strategy/strategyupdater.py b/freqtrade/strategy/strategyupdater.py index 1a0423076..7f3a1feff 100644 --- a/freqtrade/strategy/strategyupdater.py +++ b/freqtrade/strategy/strategyupdater.py @@ -69,7 +69,7 @@ class StrategyUpdater: shutil.copy(source_file, target_file) # update the code - new_code = StrategyUpdater.update_code(self, old_code) + new_code = self.update_code(old_code) # write the modified code to the destination folder with open(source_file, 'w') as f: f.write(new_code) @@ -113,7 +113,8 @@ class NameUpdater(ast.NodeTransformer): def check_fields(self, field_value): if isinstance(field_value, list): for item in field_value: - if isinstance(item, ast.AST) or isinstance(item, ast.If): + if (isinstance(item, ast.AST) or isinstance(item, ast.If) or + isinstance(item, ast.Expr)): self.visit(item) if isinstance(field_value, ast.Name): self.visit_Name(field_value) @@ -138,7 +139,8 @@ class NameUpdater(ast.NodeTransformer): if hasattr(node, "args"): if isinstance(node.args, list): for arg in node.args: - arg.arg = StrategyUpdater.name_mapping[arg.arg] + if arg.arg in StrategyUpdater.name_mapping: + arg.arg = StrategyUpdater.name_mapping[arg.arg] return node def visit_Name(self, node): diff --git a/requirements.txt b/requirements.txt index 3229ec3c4..da1db316b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -58,22 +58,4 @@ schedule==1.1.0 websockets==10.4 janus==1.0.0 -pytest~=7.2.0 -freqtrade~=2022.12.dev0 -filelock~=3.8.2 -plotly~=5.11.0 -setuptools~=65.6.3 -starlette~=0.22.0 -gym~=0.21.0 -torch~=1.13.1 -scikit-learn~=1.1.3 -scipy~=1.9.3 -xgboost~=1.7.2 -catboost~=1.1.1 -lightgbm~=3.3.3 astor~=0.8.1 -ta~=0.10.2 -finta~=1.3 -tapy~=1.9.1 -matplotlib~=3.6.2 -PyYAML~=6.0 diff --git a/tests/test_strategy_updater.py b/tests/test_strategy_updater.py index 5736e5c76..682c715fe 100644 --- a/tests/test_strategy_updater.py +++ b/tests/test_strategy_updater.py @@ -59,6 +59,11 @@ def confirm_trade_exit(sell_reason): if (sell_reason == 'stop_loss'): pass """) + # modified_code8 = StrategyUpdater.update_code(StrategyUpdater, """ + # sell_reason == 'sell_signal' + # sell_reason == 'force_sell' + # sell_reason == 'emergency_sell' + # """) assert "populate_entry_trend" in modified_code1 assert "populate_exit_trend" in modified_code1 @@ -93,3 +98,9 @@ def confirm_trade_exit(sell_reason): assert "exit_reason" in modified_code7 assert "exit_reason == 'stop_loss'" in modified_code7 + + # those tests currently don't work, next in line. + # assert "exit_signal" in modified_code8 + # assert "exit_reason" in modified_code8 + # assert "force_exit" in modified_code8 + # assert "emergency_exit" in modified_code8 From 71ec32ac9e8360a87b9aeec32327f2824af24f0f Mon Sep 17 00:00:00 2001 From: hippocritical Date: Mon, 2 Jan 2023 23:35:51 +0100 Subject: [PATCH 012/360] removed prints for strategy could not be loaded changed back to ast, astor is not really needed. --- freqtrade/commands/strategy_utils_commands.py | 8 +------- freqtrade/strategy/strategyupdater.py | 18 ++++++++---------- 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/freqtrade/commands/strategy_utils_commands.py b/freqtrade/commands/strategy_utils_commands.py index 5aeac2266..4a7eacda0 100644 --- a/freqtrade/commands/strategy_utils_commands.py +++ b/freqtrade/commands/strategy_utils_commands.py @@ -26,17 +26,11 @@ def start_strategy_update(args: Dict[str, Any]) -> None: filtered_strategy_objs = [] for args_strategy in args['strategy_list']: - found = False for strategy_obj in strategy_objs: if strategy_obj['name'] == args_strategy and strategy_obj not in filtered_strategy_objs: filtered_strategy_objs.append(strategy_obj) - found = True break - if not found: - print(f"strategy {strategy_obj['name']} could not be loaded or found and is skipped.") - for filtered_strategy_obj in filtered_strategy_objs: - # Initialize backtesting object instance_strategy_updater = StrategyUpdater() - self.start(config, filtered_strategy_obj) + instance_strategy_updater.start(config, filtered_strategy_obj) diff --git a/freqtrade/strategy/strategyupdater.py b/freqtrade/strategy/strategyupdater.py index 8a55de1b6..d12d3eaaa 100644 --- a/freqtrade/strategy/strategyupdater.py +++ b/freqtrade/strategy/strategyupdater.py @@ -3,8 +3,6 @@ import os import shutil from pathlib import Path -import astor - class StrategyUpdater: name_mapping = { @@ -59,7 +57,7 @@ class StrategyUpdater: # read the file with open(source_file, 'r') as f: old_code = f.read() - if not os.path.exists(strategies_backup_folder): + if not strategies_backup_folder.is_dir(): os.makedirs(strategies_backup_folder) # backup original @@ -96,7 +94,7 @@ class StrategyUpdater: ast.increment_lineno(tree, n=1) # generate the new code from the updated AST - return astor.to_source(tree) + return ast.dump(tree) # Here we go through each respective node, slice, elt, key ... to replace outdated entries. @@ -187,9 +185,9 @@ class NameUpdater(ast.NodeTransformer): # if the attribute name is 'nr_of_successful_buys', # update it to 'nr_of_successful_entries' if ( - isinstance(node.value, ast.Name) - and node.value.id == 'trades' - and node.attr == 'nr_of_successful_buys' + isinstance(node.value, ast.Name) + and node.value.id == 'trades' + and node.attr == 'nr_of_successful_buys' ): node.attr = 'nr_of_successful_entries' return self.generic_visit(node) @@ -213,9 +211,9 @@ class NameUpdater(ast.NodeTransformer): else: for child in node.body: if ( - isinstance(child, ast.Assign) - and isinstance(child.targets[0], ast.Name) - and child.targets[0].id == 'INTERFACE_VERSION' + isinstance(child, ast.Assign) + and isinstance(child.targets[0], ast.Name) + and child.targets[0].id == 'INTERFACE_VERSION' ): child.value = ast.parse('3').body[0].value return self.generic_visit(node) From dc256684686f41c46527a88cdcc75496bd165612 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Wed, 4 Jan 2023 11:41:06 +0100 Subject: [PATCH 013/360] handle data gaps between FreqAI and DP better --- freqtrade/freqai/data_drawer.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqai/data_drawer.py b/freqtrade/freqai/data_drawer.py index 848fb20eb..d8e08ed2d 100644 --- a/freqtrade/freqai/data_drawer.py +++ b/freqtrade/freqai/data_drawer.py @@ -645,12 +645,20 @@ class FreqaiDataDrawer: + 1 ) except IndexError: + index = -1 + if history_data[pair][tf].iloc[-1]['date'] < df_dp['date'].iloc[0]: + index = 0 + else: + index = -1 logger.warning( - f"Unable to update pair history for {pair}. " - "If this does not resolve itself after 1 additional candle, " - "please report the error to #freqai discord channel" + f"No common dates in historical data and dataprovider for {pair}. " + f"Appending dataprovider to historical data (full? {not bool(index)})" + "but please be aware that there is likely a gap in the historical " + "data.\n" + f"Historical data ends at {history_data[pair][tf].iloc[-1]['date']} " + f"while dataprovider starts at {df_dp['date'].iloc[0]} and" + f"ends at {df_dp['date'].iloc[0]}." ) - return history_data[pair][tf] = pd.concat( [ From 3cbe51c3caf50070bbe74e09d23c6161893d3cb7 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Wed, 4 Jan 2023 13:58:25 +0100 Subject: [PATCH 014/360] remove duplicated line --- freqtrade/freqai/data_drawer.py | 1 - 1 file changed, 1 deletion(-) diff --git a/freqtrade/freqai/data_drawer.py b/freqtrade/freqai/data_drawer.py index d8e08ed2d..1317ba6d3 100644 --- a/freqtrade/freqai/data_drawer.py +++ b/freqtrade/freqai/data_drawer.py @@ -645,7 +645,6 @@ class FreqaiDataDrawer: + 1 ) except IndexError: - index = -1 if history_data[pair][tf].iloc[-1]['date'] < df_dp['date'].iloc[0]: index = 0 else: From ed55296d202d725b5d2d5e95e60273a8d21dcf1a Mon Sep 17 00:00:00 2001 From: hippocritical Date: Wed, 4 Jan 2023 23:49:33 +0100 Subject: [PATCH 015/360] removed prints for strategy could not be loaded Changed logic to contain much less if conditions currently still missing: Webhook terminology, Telegram notification settings, Strategy/Config settings --- freqtrade/strategy/strategyupdater.py | 124 ++++++++++++-------------- tests/test_strategy_updater.py | 31 ++++--- 2 files changed, 73 insertions(+), 82 deletions(-) diff --git a/freqtrade/strategy/strategyupdater.py b/freqtrade/strategy/strategyupdater.py index d12d3eaaa..ad14bb903 100644 --- a/freqtrade/strategy/strategyupdater.py +++ b/freqtrade/strategy/strategyupdater.py @@ -3,6 +3,8 @@ import os import shutil from pathlib import Path +import astor + class StrategyUpdater: name_mapping = { @@ -79,7 +81,7 @@ class StrategyUpdater: tree = ast.parse(code) # use the AST to update the code - updated_code = self.modify_ast(tree) + updated_code = self.modify_ast(self, tree) # return the modified code without executing it return updated_code @@ -94,67 +96,67 @@ class StrategyUpdater: ast.increment_lineno(tree, n=1) # generate the new code from the updated AST - return ast.dump(tree) + return astor.to_source(tree) # Here we go through each respective node, slice, elt, key ... to replace outdated entries. class NameUpdater(ast.NodeTransformer): def generic_visit(self, node): - # traverse the AST recursively by calling the visitor method for each child node - if hasattr(node, "_fields"): - for field_name, field_value in ast.iter_fields(node): - self.check_strategy_and_config_settings(node, field_value) - self.check_fields(field_value) - for child in ast.iter_child_nodes(node): - self.generic_visit(child) - def check_fields(self, field_value): - if isinstance(field_value, list): - for item in field_value: - if (isinstance(item, ast.AST) or isinstance(item, ast.If) or - isinstance(item, ast.Expr)): - self.visit(item) - if isinstance(field_value, ast.Name): - self.visit_Name(field_value) + # space is not yet transferred from buy/sell to entry/exit and thereby has to be skipped. + if isinstance(node, ast.keyword): + if node.arg == "space": + return node - def check_strategy_and_config_settings(self, node, field_value): - if (isinstance(field_value, ast.AST) and - hasattr(node, "targets") and - isinstance(node.targets, list)): - for target in node.targets: - if (hasattr(target, "id") and - hasattr(field_value, "keys") and - isinstance(field_value.keys, list)): - if (target.id == "order_time_in_force" or - target.id == "order_types" or - target.id == "unfilledtimeout"): - for key in field_value.keys: - self.visit(key) + # from here on this is the original function. + for field, old_value in ast.iter_fields(node): + if isinstance(old_value, list): + new_values = [] + for value in old_value: + if isinstance(value, ast.AST): + value = self.visit(value) + if value is None: + continue + elif not isinstance(value, ast.AST): + new_values.extend(value) + continue + new_values.append(value) + old_value[:] = new_values + elif isinstance(old_value, ast.AST): + new_node = self.visit(old_value) + if new_node is None: + delattr(node, field) + else: + setattr(node, field, new_node) + return node - def check_args(self, node): - if isinstance(node.args, ast.arguments): - self.check_args(node.args) - if hasattr(node, "args"): - if isinstance(node.args, list): - for arg in node.args: - if arg.arg in StrategyUpdater.name_mapping: - arg.arg = StrategyUpdater.name_mapping[arg.arg] + def visit_Expr(self, node): + node.value.left.id = self.check_dict(StrategyUpdater.name_mapping, node.value.left.id) + self.visit(node.value) + return node + + # Renames an element if contained inside a dictionary. + @staticmethod + def check_dict(current_dict: dict, element: str): + if element in current_dict: + element = current_dict[element] + return element + + def visit_arguments(self, node): + if isinstance(node.args, list): + for arg in node.args: + arg.arg = self.check_dict(StrategyUpdater.name_mapping, arg.arg) return node def visit_Name(self, node): # if the name is in the mapping, update it - if node.id in StrategyUpdater.name_mapping: - node.id = StrategyUpdater.name_mapping[node.id] + node.id = self.check_dict(StrategyUpdater.name_mapping, node.id) return node def visit_Import(self, node): # do not update the names in import statements return node - # This function is currently never successfully triggered - # since freqtrade currently only allows valid code to be processed. - # The module .hyper does not anymore exist and by that fails to even - # reach this function to be updated currently. def visit_ImportFrom(self, node): # if hasattr(node, "module"): # if node.module == "freqtrade.strategy.hyper": @@ -164,33 +166,21 @@ class NameUpdater(ast.NodeTransformer): def visit_If(self, node: ast.If): for child in ast.iter_child_nodes(node): self.visit(child) - return self.generic_visit(node) + return node def visit_FunctionDef(self, node): - # if the function name is in the mapping, update it - if node.name in StrategyUpdater.function_mapping: - node.name = StrategyUpdater.function_mapping[node.name] - if hasattr(node, "args"): - self.check_args(node) - return self.generic_visit(node) - - def visit_Assign(self, node): - if hasattr(node, "targets") and isinstance(node.targets, list): - for target in node.targets: - if hasattr(target, "id") and target.id in StrategyUpdater.name_mapping: - target.id = StrategyUpdater.name_mapping[target.id] + node.name = self.check_dict(StrategyUpdater.function_mapping, node.name) + self.generic_visit(node) return node def visit_Attribute(self, node): - # if the attribute name is 'nr_of_successful_buys', - # update it to 'nr_of_successful_entries' if ( isinstance(node.value, ast.Name) and node.value.id == 'trades' and node.attr == 'nr_of_successful_buys' ): node.attr = 'nr_of_successful_entries' - return self.generic_visit(node) + return node def visit_ClassDef(self, node): # check if the class is derived from IStrategy @@ -216,7 +206,8 @@ class NameUpdater(ast.NodeTransformer): and child.targets[0].id == 'INTERFACE_VERSION' ): child.value = ast.parse('3').body[0].value - return self.generic_visit(node) + self.generic_visit(node) + return node def visit_Subscript(self, node): if isinstance(node.slice, ast.Constant): @@ -228,10 +219,6 @@ class NameUpdater(ast.NodeTransformer): if hasattr(node.slice, "value"): if hasattr(node.slice.value, "elts"): self.visit_elts(node.slice.value.elts) - # Check if the target is a Subscript object with a "value" attribute - # if isinstance(target, ast.Subscript) and hasattr(target.value, "attr"): - # if target.value.attr == "loc": - # self.visit(target) return node # elts can have elts (technically recursively) @@ -241,6 +228,7 @@ class NameUpdater(ast.NodeTransformer): self.visit_elt(elt) else: self.visit_elt(elts) + return elts # sub function again needed since the structure itself is highly flexible ... def visit_elt(self, elt): @@ -254,9 +242,9 @@ class NameUpdater(ast.NodeTransformer): else: for arg in elt.args: self.visit_elts(arg) + return elt def visit_Constant(self, node): - # do not update the names in import statements - if node.value in StrategyUpdater.otif_ot_unfilledtimeout: - node.value = StrategyUpdater.otif_ot_unfilledtimeout[node.value] + node.value = self.check_dict(StrategyUpdater.otif_ot_unfilledtimeout, node.value) + node.value = self.check_dict(StrategyUpdater.name_mapping, node.value) return node diff --git a/tests/test_strategy_updater.py b/tests/test_strategy_updater.py index 682c715fe..3ece2b3d8 100644 --- a/tests/test_strategy_updater.py +++ b/tests/test_strategy_updater.py @@ -4,11 +4,6 @@ from freqtrade.strategy.strategyupdater import StrategyUpdater def test_strategy_updater(default_conf, caplog) -> None: - modified_code2 = StrategyUpdater.update_code(StrategyUpdater, """ -ticker_interval = '15m' -buy_some_parameter = IntParameter(space='buy') -sell_some_parameter = IntParameter(space='sell') -""") modified_code1 = StrategyUpdater.update_code(StrategyUpdater, """ class testClass(IStrategy): def populate_buy_trend(): @@ -21,6 +16,11 @@ class testClass(IStrategy): pass def custom_sell(): pass +""") + modified_code2 = StrategyUpdater.update_code(StrategyUpdater, """ +ticker_interval = '15m' +buy_some_parameter = IntParameter(space='buy') +sell_some_parameter = IntParameter(space='sell') """) modified_code3 = StrategyUpdater.update_code(StrategyUpdater, """ use_sell_signal = True @@ -59,11 +59,14 @@ def confirm_trade_exit(sell_reason): if (sell_reason == 'stop_loss'): pass """) - # modified_code8 = StrategyUpdater.update_code(StrategyUpdater, """ - # sell_reason == 'sell_signal' - # sell_reason == 'force_sell' - # sell_reason == 'emergency_sell' - # """) + modified_code8 = StrategyUpdater.update_code(StrategyUpdater, """ +sell_reason == 'sell_signal' +sell_reason == 'force_sell' +sell_reason == 'emergency_sell' +""") + + # currently still missing: + # Webhook terminology, Telegram notification settings, Strategy/Config settings assert "populate_entry_trend" in modified_code1 assert "populate_exit_trend" in modified_code1 @@ -100,7 +103,7 @@ def confirm_trade_exit(sell_reason): assert "exit_reason == 'stop_loss'" in modified_code7 # those tests currently don't work, next in line. - # assert "exit_signal" in modified_code8 - # assert "exit_reason" in modified_code8 - # assert "force_exit" in modified_code8 - # assert "emergency_exit" in modified_code8 + assert "exit_signal" in modified_code8 + assert "exit_reason" in modified_code8 + assert "force_exit" in modified_code8 + assert "emergency_exit" in modified_code8 From 4435c4fd0d1de2e6b57375051d8007ca7435c5ac Mon Sep 17 00:00:00 2001 From: hippocritical Date: Thu, 5 Jan 2023 22:56:06 +0100 Subject: [PATCH 016/360] removed prints for strategy could not be loaded Changed logic to contain much less if conditions currently still missing: Webhook terminology, Telegram notification settings, Strategy/Config settings --- freqtrade/commands/strategy_utils_commands.py | 4 ++++ freqtrade/strategy/strategyupdater.py | 10 +++++----- tests/test_strategy_updater.py | 17 +++++++++-------- 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/freqtrade/commands/strategy_utils_commands.py b/freqtrade/commands/strategy_utils_commands.py index 4a7eacda0..75b8a9cc0 100644 --- a/freqtrade/commands/strategy_utils_commands.py +++ b/freqtrade/commands/strategy_utils_commands.py @@ -1,4 +1,5 @@ import logging +import time from typing import Any, Dict from freqtrade.configuration import setup_utils_configuration @@ -33,4 +34,7 @@ def start_strategy_update(args: Dict[str, Any]) -> None: for filtered_strategy_obj in filtered_strategy_objs: instance_strategy_updater = StrategyUpdater() + start = time.perf_counter() instance_strategy_updater.start(config, filtered_strategy_obj) + elapsed = time.perf_counter() - start + print(f"Conversion of {filtered_strategy_obj['name']} took {elapsed:.1f} seconds.") diff --git a/freqtrade/strategy/strategyupdater.py b/freqtrade/strategy/strategyupdater.py index ad14bb903..e26dd5e79 100644 --- a/freqtrade/strategy/strategyupdater.py +++ b/freqtrade/strategy/strategyupdater.py @@ -52,7 +52,6 @@ class StrategyUpdater: """ source_file = strategy_obj['location'] - print(f"started conversion of {source_file}") strategies_backup_folder = Path.joinpath(config['user_data_dir'], "strategies_orig_updater") target_file = Path.joinpath(strategies_backup_folder, strategy_obj['location_rel']) @@ -73,7 +72,6 @@ class StrategyUpdater: # write the modified code to the destination folder with open(source_file, 'w') as f: f.write(new_code) - print(f"conversion of file {source_file} successful.") # define the function to update the code def update_code(self, code): @@ -81,7 +79,7 @@ class StrategyUpdater: tree = ast.parse(code) # use the AST to update the code - updated_code = self.modify_ast(self, tree) + updated_code = self.modify_ast(tree) # return the modified code without executing it return updated_code @@ -96,6 +94,7 @@ class StrategyUpdater: ast.increment_lineno(tree, n=1) # generate the new code from the updated AST + # without indent {} parameters would just be written straight one after the other. return astor.to_source(tree) @@ -131,8 +130,9 @@ class NameUpdater(ast.NodeTransformer): return node def visit_Expr(self, node): - node.value.left.id = self.check_dict(StrategyUpdater.name_mapping, node.value.left.id) - self.visit(node.value) + if hasattr(node.value, "left") and hasattr(node.value.left, "id"): + node.value.left.id = self.check_dict(StrategyUpdater.name_mapping, node.value.left.id) + self.visit(node.value) return node # Renames an element if contained inside a dictionary. diff --git a/tests/test_strategy_updater.py b/tests/test_strategy_updater.py index 3ece2b3d8..a00340427 100644 --- a/tests/test_strategy_updater.py +++ b/tests/test_strategy_updater.py @@ -4,7 +4,8 @@ from freqtrade.strategy.strategyupdater import StrategyUpdater def test_strategy_updater(default_conf, caplog) -> None: - modified_code1 = StrategyUpdater.update_code(StrategyUpdater, """ + instance_strategy_updater = StrategyUpdater() + modified_code1 = instance_strategy_updater.update_code(""" class testClass(IStrategy): def populate_buy_trend(): pass @@ -17,27 +18,27 @@ class testClass(IStrategy): def custom_sell(): pass """) - modified_code2 = StrategyUpdater.update_code(StrategyUpdater, """ + modified_code2 = instance_strategy_updater.update_code(""" ticker_interval = '15m' buy_some_parameter = IntParameter(space='buy') sell_some_parameter = IntParameter(space='sell') """) - modified_code3 = StrategyUpdater.update_code(StrategyUpdater, """ + modified_code3 = instance_strategy_updater.update_code(""" use_sell_signal = True sell_profit_only = True sell_profit_offset = True ignore_roi_if_buy_signal = True forcebuy_enable = True """) - modified_code4 = StrategyUpdater.update_code(StrategyUpdater, """ + modified_code4 = instance_strategy_updater.update_code(""" dataframe.loc[reduce(lambda x, y: x & y, conditions), ["buy", "buy_tag"]] = (1, "buy_signal_1") dataframe.loc[reduce(lambda x, y: x & y, conditions), 'sell'] = 1 """) - modified_code5 = StrategyUpdater.update_code(StrategyUpdater, """ + modified_code5 = instance_strategy_updater.update_code(""" def confirm_trade_exit(sell_reason: str): pass """) - modified_code6 = StrategyUpdater.update_code(StrategyUpdater, """ + modified_code6 = instance_strategy_updater.update_code(""" order_time_in_force = { 'buy': 'gtc', 'sell': 'ioc' @@ -54,12 +55,12 @@ unfilledtimeout = { } """) - modified_code7 = StrategyUpdater.update_code(StrategyUpdater, """ + modified_code7 = instance_strategy_updater.update_code(""" def confirm_trade_exit(sell_reason): if (sell_reason == 'stop_loss'): pass """) - modified_code8 = StrategyUpdater.update_code(StrategyUpdater, """ + modified_code8 = instance_strategy_updater.update_code(""" sell_reason == 'sell_signal' sell_reason == 'force_sell' sell_reason == 'emergency_sell' From a3cc001f1b38264ef6be1863378e709de102e325 Mon Sep 17 00:00:00 2001 From: Rahul Date: Sat, 11 Feb 2023 18:31:25 -0500 Subject: [PATCH 017/360] initial commit --- freqtrade/enums/marketstatetype.py | 26 ++++++++++++++++++++++++++ freqtrade/rpc/telegram.py | 20 ++++++++++++++++++-- freqtrade/strategy/interface.py | 5 ++++- 3 files changed, 48 insertions(+), 3 deletions(-) create mode 100644 freqtrade/enums/marketstatetype.py diff --git a/freqtrade/enums/marketstatetype.py b/freqtrade/enums/marketstatetype.py new file mode 100644 index 000000000..5f3f219d8 --- /dev/null +++ b/freqtrade/enums/marketstatetype.py @@ -0,0 +1,26 @@ +from enum import Enum + + +class MarketDirection(Enum): + """ + Enum for various market directions. + """ + LONG = "long" + SHORT = "short" + EVEN = "even" + NONE = '' + + @staticmethod + def string_to_enum(label : str) -> str: + match label: + case "long": + return MarketDirection.LONG + case "short": + return MarketDirection.SHORT + case "even": + return MarketDirection.EVEN + case 'none': + return MarketDirection.NONE + case _: + return None + diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index fbd675d02..750f0dd5f 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -25,7 +25,7 @@ from telegram.utils.helpers import escape_markdown from freqtrade.__init__ import __version__ from freqtrade.constants import DUST_PER_COIN, Config -from freqtrade.enums import RPCMessageType, SignalDirection, TradingMode +from freqtrade.enums import MarketDirection, RPCMessageType, SignalDirection, TradingMode from freqtrade.exceptions import OperationalException from freqtrade.misc import chunks, plural, round_coin_value from freqtrade.persistence import Trade @@ -129,7 +129,7 @@ class Telegram(RPCHandler): r'/weekly$', r'/weekly \d+$', r'/monthly$', r'/monthly \d+$', r'/forcebuy$', r'/forcelong$', r'/forceshort$', r'/forcesell$', r'/forceexit$', - r'/edge$', r'/health$', r'/help$', r'/version$' + r'/edge$', r'/health$', r'/help$', r'/version$', r'/marketdir$' ] # Create keys for generation valid_keys_print = [k.replace('$', '') for k in valid_keys] @@ -197,6 +197,7 @@ class Telegram(RPCHandler): CommandHandler('health', self._health), CommandHandler('help', self._help), CommandHandler('version', self._version), + CommandHandler('marketdir', self._changemarketdir) ] callbacks = [ CallbackQueryHandler(self._status_table, pattern='update_status_table'), @@ -1677,3 +1678,18 @@ class Telegram(RPCHandler): 'TelegramError: %s! Giving up on that message.', telegram_err.message ) + + @authorized_only + def _changemarketdir(self, update: Update, context: CallbackContext) -> None: + """ + Handler for /marketdir. + Updates the bot's market_direction + :param bot: telegram bot + :param update: message update + :return: None + """ + if context.args and len(context.args) == 1: + market_dir = MarketDirection.string_to_enum(context.args[0]) + if market_dir: + self._rpc._freqtrade.strategy.market_direction = market_dir + diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 70d656199..96f8681e9 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -12,7 +12,7 @@ from pandas import DataFrame from freqtrade.constants import Config, IntOrInf, ListPairsWithTimeframes from freqtrade.data.dataprovider import DataProvider -from freqtrade.enums import (CandleType, ExitCheckTuple, ExitType, RunMode, SignalDirection, +from freqtrade.enums import (CandleType, ExitCheckTuple, ExitType, MarketDirection, RunMode, SignalDirection, SignalTagType, SignalType, TradingMode) from freqtrade.exceptions import OperationalException, StrategyError from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date, timeframe_to_seconds @@ -122,6 +122,9 @@ class IStrategy(ABC, HyperStrategyMixin): # Definition of plot_config. See plotting documentation for more details. plot_config: Dict = {} + # A self set parameter that represents the market direction. filled from configuration + market_direction: MarketDirection = MarketDirection.NONE + def __init__(self, config: Config) -> None: self.config = config # Dict to determine if analysis is necessary From b73089deb82d15ea2db1780f9d1ebea709ce3daa Mon Sep 17 00:00:00 2001 From: Rahul Date: Thu, 16 Feb 2023 17:51:50 -0500 Subject: [PATCH 018/360] fixed a test --- freqtrade/enums/__init__.py | 1 + tests/rpc/test_rpc_telegram.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/enums/__init__.py b/freqtrade/enums/__init__.py index 8ef53e12d..160ebc052 100644 --- a/freqtrade/enums/__init__.py +++ b/freqtrade/enums/__init__.py @@ -12,3 +12,4 @@ from freqtrade.enums.runmode import NON_UTIL_MODES, OPTIMIZE_MODES, TRADING_MODE from freqtrade.enums.signaltype import SignalDirection, SignalTagType, SignalType from freqtrade.enums.state import State from freqtrade.enums.tradingmode import TradingMode +from freqtrade.enums.marketstatetype import MarketDirection diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 5e3c2bd18..050d7b7c0 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -106,7 +106,7 @@ def test_telegram_init(default_conf, mocker, caplog) -> None: "['reload_config', 'reload_conf'], ['show_config', 'show_conf'], " "['stopbuy', 'stopentry'], ['whitelist'], ['blacklist'], " "['blacklist_delete', 'bl_delete'], " - "['logs'], ['edge'], ['health'], ['help'], ['version']" + "['logs'], ['edge'], ['health'], ['help'], ['version'], ['marketdir']" "]") assert log_has(message_str, caplog) From 06edc5c04491b15b2b43c0b9f0900cbf91958e8d Mon Sep 17 00:00:00 2001 From: hippocritical Date: Fri, 17 Feb 2023 21:01:09 +0100 Subject: [PATCH 019/360] changed to ast_comments, added tests for comments. --- freqtrade/commands/strategy_utils_commands.py | 43 ++++++++++----- freqtrade/strategy/strategyupdater.py | 54 ++++++++++--------- tests/test_strategy_updater.py | 20 +++++++ 3 files changed, 78 insertions(+), 39 deletions(-) diff --git a/freqtrade/commands/strategy_utils_commands.py b/freqtrade/commands/strategy_utils_commands.py index 75b8a9cc0..dc94f2b67 100644 --- a/freqtrade/commands/strategy_utils_commands.py +++ b/freqtrade/commands/strategy_utils_commands.py @@ -1,10 +1,12 @@ import logging +import os import time from typing import Any, Dict from freqtrade.configuration import setup_utils_configuration from freqtrade.enums import RunMode from freqtrade.resolvers import StrategyResolver +from freqtrade.strategy.strategyupdater import StrategyUpdater logger = logging.getLogger(__name__) @@ -17,24 +19,37 @@ def start_strategy_update(args: Dict[str, Any]) -> None: :return: None """ - # Import here to avoid loading backtesting module when it's not used - from freqtrade.strategy.strategyupdater import StrategyUpdater - config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) strategy_objs = StrategyResolver.search_all_objects( config, enum_failed=False, recursive=config.get('recursive_strategy_search', False)) filtered_strategy_objs = [] - for args_strategy in args['strategy_list']: - for strategy_obj in strategy_objs: - if strategy_obj['name'] == args_strategy and strategy_obj not in filtered_strategy_objs: - filtered_strategy_objs.append(strategy_obj) - break + if hasattr(args, "strategy_list"): + for args_strategy in args['strategy_list']: + for strategy_obj in strategy_objs: + if (strategy_obj['name'] == args_strategy + and strategy_obj not in filtered_strategy_objs): + filtered_strategy_objs.append(strategy_obj) + break - for filtered_strategy_obj in filtered_strategy_objs: - instance_strategy_updater = StrategyUpdater() - start = time.perf_counter() - instance_strategy_updater.start(config, filtered_strategy_obj) - elapsed = time.perf_counter() - start - print(f"Conversion of {filtered_strategy_obj['name']} took {elapsed:.1f} seconds.") + for filtered_strategy_obj in filtered_strategy_objs: + start_conversion(filtered_strategy_obj, config) + else: + processed_locations = set() + for strategy_obj in strategy_objs: + if strategy_obj['location'] not in processed_locations: + processed_locations.add(strategy_obj['location']) + start_conversion(strategy_obj, config) + + +def start_conversion(strategy_obj, config): + # try: + print(f"Conversion of {os.path.basename(strategy_obj['location'])} started.") + instance_strategy_updater = StrategyUpdater() + start = time.perf_counter() + instance_strategy_updater.start(config, strategy_obj) + elapsed = time.perf_counter() - start + print(f"Conversion of {os.path.basename(strategy_obj['location'])} took {elapsed:.1f} seconds.") + # except: + # pass diff --git a/freqtrade/strategy/strategyupdater.py b/freqtrade/strategy/strategyupdater.py index e26dd5e79..db19d4fba 100644 --- a/freqtrade/strategy/strategyupdater.py +++ b/freqtrade/strategy/strategyupdater.py @@ -1,9 +1,8 @@ -import ast import os import shutil from pathlib import Path -import astor +import ast_comments class StrategyUpdater: @@ -76,7 +75,7 @@ class StrategyUpdater: # define the function to update the code def update_code(self, code): # parse the code into an AST - tree = ast.parse(code) + tree = ast_comments.parse(code) # use the AST to update the code updated_code = self.modify_ast(tree) @@ -90,38 +89,43 @@ class StrategyUpdater: NameUpdater().visit(tree) # first fix the comments, so it understands "\n" properly inside multi line comments. - ast.fix_missing_locations(tree) - ast.increment_lineno(tree, n=1) + ast_comments.fix_missing_locations(tree) + ast_comments.increment_lineno(tree, n=1) # generate the new code from the updated AST # without indent {} parameters would just be written straight one after the other. - return astor.to_source(tree) + + # ast_comments would be amazing since this is the only solution that carries over comments, + # but it does currently not have an unparse function, hopefully in the future ... ! + # return ast_comments.unparse(tree) + + return ast_comments.unparse(tree) # Here we go through each respective node, slice, elt, key ... to replace outdated entries. -class NameUpdater(ast.NodeTransformer): +class NameUpdater(ast_comments.NodeTransformer): def generic_visit(self, node): # space is not yet transferred from buy/sell to entry/exit and thereby has to be skipped. - if isinstance(node, ast.keyword): + if isinstance(node, ast_comments.keyword): if node.arg == "space": return node # from here on this is the original function. - for field, old_value in ast.iter_fields(node): + for field, old_value in ast_comments.iter_fields(node): if isinstance(old_value, list): new_values = [] for value in old_value: - if isinstance(value, ast.AST): + if isinstance(value, ast_comments.AST): value = self.visit(value) if value is None: continue - elif not isinstance(value, ast.AST): + elif not isinstance(value, ast_comments.AST): new_values.extend(value) continue new_values.append(value) old_value[:] = new_values - elif isinstance(old_value, ast.AST): + elif isinstance(old_value, ast_comments.AST): new_node = self.visit(old_value) if new_node is None: delattr(node, field) @@ -163,8 +167,8 @@ class NameUpdater(ast.NodeTransformer): # node.module = "freqtrade.strategy" return node - def visit_If(self, node: ast.If): - for child in ast.iter_child_nodes(node): + def visit_If(self, node: ast_comments.If): + for child in ast_comments.iter_child_nodes(node): self.visit(child) return node @@ -175,7 +179,7 @@ class NameUpdater(ast.NodeTransformer): def visit_Attribute(self, node): if ( - isinstance(node.value, ast.Name) + isinstance(node.value, ast_comments.Name) and node.value.id == 'trades' and node.attr == 'nr_of_successful_buys' ): @@ -184,33 +188,33 @@ class NameUpdater(ast.NodeTransformer): def visit_ClassDef(self, node): # check if the class is derived from IStrategy - if any(isinstance(base, ast.Name) and + if any(isinstance(base, ast_comments.Name) and base.id == 'IStrategy' for base in node.bases): # check if the INTERFACE_VERSION variable exists has_interface_version = any( - isinstance(child, ast.Assign) and - isinstance(child.targets[0], ast.Name) and + isinstance(child, ast_comments.Assign) and + isinstance(child.targets[0], ast_comments.Name) and child.targets[0].id == 'INTERFACE_VERSION' for child in node.body ) # if the INTERFACE_VERSION variable does not exist, add it as the first child if not has_interface_version: - node.body.insert(0, ast.parse('INTERFACE_VERSION = 3').body[0]) + node.body.insert(0, ast_comments.parse('INTERFACE_VERSION = 3').body[0]) # otherwise, update its value to 3 else: for child in node.body: if ( - isinstance(child, ast.Assign) - and isinstance(child.targets[0], ast.Name) + isinstance(child, ast_comments.Assign) + and isinstance(child.targets[0], ast_comments.Name) and child.targets[0].id == 'INTERFACE_VERSION' ): - child.value = ast.parse('3').body[0].value + child.value = ast_comments.parse('3').body[0].value self.generic_visit(node) return node def visit_Subscript(self, node): - if isinstance(node.slice, ast.Constant): + if isinstance(node.slice, ast_comments.Constant): if node.slice.value in StrategyUpdater.rename_dict: # Replace the slice attributes with the values from rename_dict node.slice.value = StrategyUpdater.rename_dict[node.slice.value] @@ -232,12 +236,12 @@ class NameUpdater(ast.NodeTransformer): # sub function again needed since the structure itself is highly flexible ... def visit_elt(self, elt): - if isinstance(elt, ast.Constant) and elt.value in StrategyUpdater.rename_dict: + if isinstance(elt, ast_comments.Constant) and elt.value in StrategyUpdater.rename_dict: elt.value = StrategyUpdater.rename_dict[elt.value] if hasattr(elt, "elts"): self.visit_elts(elt.elts) if hasattr(elt, "args"): - if isinstance(elt.args, ast.arguments): + if isinstance(elt.args, ast_comments.arguments): self.visit_elts(elt.args) else: for arg in elt.args: diff --git a/tests/test_strategy_updater.py b/tests/test_strategy_updater.py index a00340427..927c5e99f 100644 --- a/tests/test_strategy_updater.py +++ b/tests/test_strategy_updater.py @@ -65,7 +65,23 @@ sell_reason == 'sell_signal' sell_reason == 'force_sell' sell_reason == 'emergency_sell' """) + modified_code9 = instance_strategy_updater.update_code(""" +# This is the 1st comment +import talib.abstract as ta +# This is the 2nd comment +import freqtrade.vendor.qtpylib.indicators as qtpylib + +class someStrategy(IStrategy): + # This is the 3rd comment + # This attribute will be overridden if the config file contains "minimal_roi" + minimal_roi = { + "0": 0.50 + } + + # This is the 4th comment + stoploss = -0.1 +""") # currently still missing: # Webhook terminology, Telegram notification settings, Strategy/Config settings @@ -108,3 +124,7 @@ sell_reason == 'emergency_sell' assert "exit_reason" in modified_code8 assert "force_exit" in modified_code8 assert "emergency_exit" in modified_code8 + + assert "This is the 1st comment" in modified_code9 + assert "This is the 2nd comment" in modified_code9 + assert "This is the 3rd comment" in modified_code9 From bcef00edeef1ea1e5611aef6d6d39eb31448195f Mon Sep 17 00:00:00 2001 From: hippocritical Date: Fri, 17 Feb 2023 21:04:26 +0100 Subject: [PATCH 020/360] changed to ast_comments, added tests for comments. --- user_data/strategies/.gitkeep | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 user_data/strategies/.gitkeep diff --git a/user_data/strategies/.gitkeep b/user_data/strategies/.gitkeep deleted file mode 100644 index e69de29bb..000000000 From 72af1912caff5862938bd0c71eff4998314d6107 Mon Sep 17 00:00:00 2001 From: Rahul Date: Fri, 17 Feb 2023 22:01:00 +0000 Subject: [PATCH 021/360] added new text --- freqtrade/enums/__init__.py | 2 +- freqtrade/enums/marketstatetype.py | 15 --------------- freqtrade/rpc/telegram.py | 16 ++++++++++++---- freqtrade/strategy/interface.py | 4 ++-- tests/rpc/test_rpc_telegram.py | 14 +++++++++++++- 5 files changed, 28 insertions(+), 23 deletions(-) diff --git a/freqtrade/enums/__init__.py b/freqtrade/enums/__init__.py index 160ebc052..69ef345e8 100644 --- a/freqtrade/enums/__init__.py +++ b/freqtrade/enums/__init__.py @@ -5,6 +5,7 @@ from freqtrade.enums.exitchecktuple import ExitCheckTuple from freqtrade.enums.exittype import ExitType from freqtrade.enums.hyperoptstate import HyperoptState from freqtrade.enums.marginmode import MarginMode +from freqtrade.enums.marketstatetype import MarketDirection from freqtrade.enums.ordertypevalue import OrderTypeValues from freqtrade.enums.pricetype import PriceType from freqtrade.enums.rpcmessagetype import NO_ECHO_MESSAGES, RPCMessageType, RPCRequestType @@ -12,4 +13,3 @@ from freqtrade.enums.runmode import NON_UTIL_MODES, OPTIMIZE_MODES, TRADING_MODE from freqtrade.enums.signaltype import SignalDirection, SignalTagType, SignalType from freqtrade.enums.state import State from freqtrade.enums.tradingmode import TradingMode -from freqtrade.enums.marketstatetype import MarketDirection diff --git a/freqtrade/enums/marketstatetype.py b/freqtrade/enums/marketstatetype.py index 5f3f219d8..8132be74a 100644 --- a/freqtrade/enums/marketstatetype.py +++ b/freqtrade/enums/marketstatetype.py @@ -9,18 +9,3 @@ class MarketDirection(Enum): SHORT = "short" EVEN = "even" NONE = '' - - @staticmethod - def string_to_enum(label : str) -> str: - match label: - case "long": - return MarketDirection.LONG - case "short": - return MarketDirection.SHORT - case "even": - return MarketDirection.EVEN - case 'none': - return MarketDirection.NONE - case _: - return None - diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 750f0dd5f..60a5bcce6 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -1689,7 +1689,15 @@ class Telegram(RPCHandler): :return: None """ if context.args and len(context.args) == 1: - market_dir = MarketDirection.string_to_enum(context.args[0]) - if market_dir: - self._rpc._freqtrade.strategy.market_direction = market_dir - + new_market_dir = context.args[0] + match new_market_dir: + case "long": + self._rpc._freqtrade.strategy.market_direction = MarketDirection.LONG + case "short": + self._rpc._freqtrade.strategy.market_direction = MarketDirection.SHORT + case "even": + self._rpc._freqtrade.strategy.market_direction = MarketDirection.EVEN + case "none": + self._rpc._freqtrade.strategy.market_direction = MarketDirection.NONE + case _: + raise RPCException("Invalid market direction provided") diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index d53f57d17..96b2ac8ce 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -12,8 +12,8 @@ from pandas import DataFrame from freqtrade.constants import Config, IntOrInf, ListPairsWithTimeframes from freqtrade.data.dataprovider import DataProvider -from freqtrade.enums import (CandleType, ExitCheckTuple, ExitType, MarketDirection, RunMode, SignalDirection, - SignalTagType, SignalType, TradingMode) +from freqtrade.enums import (CandleType, ExitCheckTuple, ExitType, MarketDirection, RunMode, + SignalDirection, SignalTagType, SignalType, TradingMode) from freqtrade.exceptions import OperationalException, StrategyError from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date, timeframe_to_seconds from freqtrade.misc import remove_entry_exit_signals diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 050d7b7c0..a63d7f9be 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -20,7 +20,7 @@ from telegram.error import BadRequest, NetworkError, TelegramError from freqtrade import __version__ from freqtrade.constants import CANCEL_REASON from freqtrade.edge import PairInfo -from freqtrade.enums import ExitType, RPCMessageType, RunMode, SignalDirection, State +from freqtrade.enums import ExitType, MarketDirection, RPCMessageType, RunMode, SignalDirection, State from freqtrade.exceptions import OperationalException from freqtrade.freqtradebot import FreqtradeBot from freqtrade.loggers import setup_logging @@ -2394,3 +2394,15 @@ def test__send_msg_keyboard(default_conf, mocker, caplog) -> None: assert log_has("using custom keyboard from config.json: " "[['/daily', '/stats', '/balance', '/profit', '/profit 5'], ['/count', " "'/start', '/reload_config', '/help']]", caplog) + + +def test_change_market_direction(default_conf, mocker, update) -> None: + telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) + assert telegram._rpc._freqtrade.strategy.market_direction == MarketDirection.NONE + context = MagicMock() + context.args = ["long"] + telegram._changemarketdir(update, context) + assert telegram._rpc._freqtrade.strategy.market_direction == MarketDirection.LONG + context = MagicMock() + context.args = ["invalid"] + assert telegram._rpc._freqtrade.strategy.market_direction == MarketDirection.LONG From ade64f25d385d8fffda81b2e6199b7185bdc239a Mon Sep 17 00:00:00 2001 From: Rahul Gudise Date: Fri, 17 Feb 2023 17:08:39 -0500 Subject: [PATCH 022/360] fixed formatting --- tests/rpc/test_rpc_telegram.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index a63d7f9be..01a598734 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -20,7 +20,8 @@ from telegram.error import BadRequest, NetworkError, TelegramError from freqtrade import __version__ from freqtrade.constants import CANCEL_REASON from freqtrade.edge import PairInfo -from freqtrade.enums import ExitType, MarketDirection, RPCMessageType, RunMode, SignalDirection, State +from freqtrade.enums import (ExitType, MarketDirection, RPCMessageType, RunMode, SignalDirection, + State) from freqtrade.exceptions import OperationalException from freqtrade.freqtradebot import FreqtradeBot from freqtrade.loggers import setup_logging From 5fb539190d99746765ad8d68b179d01ba93a11aa Mon Sep 17 00:00:00 2001 From: Rahul Date: Sat, 18 Feb 2023 23:50:02 +0000 Subject: [PATCH 023/360] addressed some issues mentioned in PR --- freqtrade/rpc/rpc.py | 5 ++++- freqtrade/rpc/telegram.py | 25 +++++++++++++------------ 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 83bffb779..10ea04c6c 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -20,7 +20,7 @@ from freqtrade.constants import CANCEL_REASON, DATETIME_PRINT_FORMAT, Config from freqtrade.data.history import load_data from freqtrade.data.metrics import calculate_max_drawdown from freqtrade.enums import (CandleType, ExitCheckTuple, ExitType, SignalDirection, State, - TradingMode) + TradingMode, MarketDirection) from freqtrade.exceptions import ExchangeError, PricingError from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs from freqtrade.loggers import bufferHandler @@ -1205,3 +1205,6 @@ class RPC: 'last_process_loc': last_p.astimezone(tzlocal()).strftime(DATETIME_PRINT_FORMAT), 'last_process_ts': int(last_p.timestamp()), } + + def _update_market_direction(self, direction: MarketDirection): + self._freqtrade.strategy.market_direction = direction \ No newline at end of file diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 60a5bcce6..65ef1bb21 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -129,7 +129,7 @@ class Telegram(RPCHandler): r'/weekly$', r'/weekly \d+$', r'/monthly$', r'/monthly \d+$', r'/forcebuy$', r'/forcelong$', r'/forceshort$', r'/forcesell$', r'/forceexit$', - r'/edge$', r'/health$', r'/help$', r'/version$', r'/marketdir$' + r'/edge$', r'/health$', r'/help$', r'/version$', r'/marketdir \d+$' ] # Create keys for generation valid_keys_print = [k.replace('$', '') for k in valid_keys] @@ -1690,14 +1690,15 @@ class Telegram(RPCHandler): """ if context.args and len(context.args) == 1: new_market_dir = context.args[0] - match new_market_dir: - case "long": - self._rpc._freqtrade.strategy.market_direction = MarketDirection.LONG - case "short": - self._rpc._freqtrade.strategy.market_direction = MarketDirection.SHORT - case "even": - self._rpc._freqtrade.strategy.market_direction = MarketDirection.EVEN - case "none": - self._rpc._freqtrade.strategy.market_direction = MarketDirection.NONE - case _: - raise RPCException("Invalid market direction provided") + if new_market_dir == "long": + self._rpc._update_market_direction(MarketDirection.LONG) + elif new_market_dir == "short": + self._rpc._update_market_direction(MarketDirection.SHORT) + elif new_market_dir == "even": + self._rpc._update_market_direction(MarketDirection.EVEN) + elif new_market_dir == "none": + self._rpc._update_market_direction(MarketDirection.NONE) + else: + raise RPCException("Invalid market direction provided") + else: + raise RPCException("Invalid usage of command /marketdir.") From 8927a92eafc5d30ab106daec8bb43cfdaecd43d7 Mon Sep 17 00:00:00 2001 From: Rahul Date: Sun, 19 Feb 2023 16:11:21 +0000 Subject: [PATCH 024/360] fixed lint issue --- freqtrade/rpc/rpc.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 10ea04c6c..f1e6c15e6 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -19,8 +19,8 @@ from freqtrade.configuration.timerange import TimeRange from freqtrade.constants import CANCEL_REASON, DATETIME_PRINT_FORMAT, Config from freqtrade.data.history import load_data from freqtrade.data.metrics import calculate_max_drawdown -from freqtrade.enums import (CandleType, ExitCheckTuple, ExitType, SignalDirection, State, - TradingMode, MarketDirection) +from freqtrade.enums import (CandleType, ExitCheckTuple, ExitType, MarketDirection, SignalDirection, + State, TradingMode) from freqtrade.exceptions import ExchangeError, PricingError from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs from freqtrade.loggers import bufferHandler @@ -1207,4 +1207,4 @@ class RPC: } def _update_market_direction(self, direction: MarketDirection): - self._freqtrade.strategy.market_direction = direction \ No newline at end of file + self._freqtrade.strategy.market_direction = direction From 3033e274660df0fa1c0eb445b0d03a459eefcdb3 Mon Sep 17 00:00:00 2001 From: Rahul Gudise Date: Mon, 20 Feb 2023 15:53:29 -0500 Subject: [PATCH 025/360] Added documentation for new telegram command --- docs/telegram-usage.md | 9 ++++++++- freqtrade/rpc/telegram.py | 4 +++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index 4626944c5..03bbb4b87 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -152,7 +152,7 @@ You can create your own keyboard in `config.json`: !!! Note "Supported Commands" Only the following commands are allowed. Command arguments are not supported! - `/start`, `/stop`, `/status`, `/status table`, `/trades`, `/profit`, `/performance`, `/daily`, `/stats`, `/count`, `/locks`, `/balance`, `/stopentry`, `/reload_config`, `/show_config`, `/logs`, `/whitelist`, `/blacklist`, `/edge`, `/help`, `/version` + `/start`, `/stop`, `/status`, `/status table`, `/trades`, `/profit`, `/performance`, `/daily`, `/stats`, `/count`, `/locks`, `/balance`, `/stopentry`, `/reload_config`, `/show_config`, `/logs`, `/whitelist`, `/blacklist`, `/edge`, `/help`, `/version`, `/marketdir` ## Telegram commands @@ -179,6 +179,7 @@ official commands. You can ask at any moment for help with `/help`. | `/count` | Displays number of trades used and available | `/locks` | Show currently locked pairs. | `/unlock ` | Remove the lock for this pair (or for this lock id). +| `/marketdir [long | short | even | none]` | Updates the user managed variable that represents the current market direction. | **Modify Trade states** | | `/forceexit | /fx ` | Instantly exits the given trade (Ignoring `minimum_roi`). | `/forceexit all | /fx all` | Instantly exits all open trades (Ignoring `minimum_roi`). @@ -416,3 +417,9 @@ ARDR/ETH 0.366667 0.143059 -0.01 ### /version > **Version:** `0.14.3` + +### /marketdir + +Updates the user managed variable that represents the current market direction. This variable is not set +to any market direction on bot startup and must be set by the user. For example `/marketdir long` +would set the variable to be `long`. diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 65ef1bb21..5f682b436 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -129,7 +129,7 @@ class Telegram(RPCHandler): r'/weekly$', r'/weekly \d+$', r'/monthly$', r'/monthly \d+$', r'/forcebuy$', r'/forcelong$', r'/forceshort$', r'/forcesell$', r'/forceexit$', - r'/edge$', r'/health$', r'/help$', r'/version$', r'/marketdir \d+$' + r'/edge$', r'/health$', r'/help$', r'/version$', r'/marketdir (long|short|even|none)$' ] # Create keys for generation valid_keys_print = [k.replace('$', '') for k in valid_keys] @@ -1495,6 +1495,8 @@ class Telegram(RPCHandler): "*/count:* `Show number of active trades compared to allowed number of trades`\n" "*/edge:* `Shows validated pairs by Edge if it is enabled` \n" "*/health* `Show latest process timestamp - defaults to 1970-01-01 00:00:00` \n" + "*/marketdir [long | short | even | none]:* `Updates the user managed variable" + " that represents the current market direction` \n" "_Statistics_\n" "------------\n" From 2261cbd92e180779f5e234d9fb5713c33b0b1e82 Mon Sep 17 00:00:00 2001 From: Rahul Gudise Date: Mon, 20 Feb 2023 16:22:17 -0500 Subject: [PATCH 026/360] fixed command regex and updated documentation --- docs/telegram-usage.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index 03bbb4b87..a4145df02 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -421,5 +421,5 @@ ARDR/ETH 0.366667 0.143059 -0.01 ### /marketdir Updates the user managed variable that represents the current market direction. This variable is not set -to any market direction on bot startup and must be set by the user. For example `/marketdir long` +to any valid market direction on bot startup and must be set by the user. As an example `/marketdir long` would set the variable to be `long`. From fd4e27d889c4b4d192373e86b4b4a591dab8783e Mon Sep 17 00:00:00 2001 From: robcaulk Date: Tue, 21 Feb 2023 14:22:40 +0100 Subject: [PATCH 027/360] remove populate_any_indicators --- freqtrade/freqai/data_kitchen.py | 139 ++++++------------------ freqtrade/freqai/freqai_interface.py | 32 +----- freqtrade/optimize/backtesting.py | 2 +- tests/freqai/test_freqai_backtesting.py | 4 +- tests/strategy/test_interface.py | 12 -- 5 files changed, 41 insertions(+), 148 deletions(-) diff --git a/freqtrade/freqai/data_kitchen.py b/freqtrade/freqai/data_kitchen.py index 30d2509b5..ba304aca3 100644 --- a/freqtrade/freqai/data_kitchen.py +++ b/freqtrade/freqai/data_kitchen.py @@ -1315,123 +1315,54 @@ class FreqaiDataKitchen: dataframe: DataFrame = dataframe containing populated indicators """ - # this is a hack to check if the user is using the populate_any_indicators function + # check if the user is using the deprecated populate_any_indicators function new_version = inspect.getsource(strategy.populate_any_indicators) == ( inspect.getsource(IStrategy.populate_any_indicators)) - if new_version: - tfs: List[str] = self.freqai_config["feature_parameters"].get("include_timeframes") - pairs: List[str] = self.freqai_config["feature_parameters"].get( - "include_corr_pairlist", []) + if not new_version: + raise OperationalException( + "You are using the `populate_any_indicators()` function" + " which was deprecated on March 1, 2023. Please refer " + "to the strategy migration guide to use the new " + "feature_engineering_* methods: \n" + "https://www.freqtrade.io/en/stable/strategy_migration/#freqai-strategy \n" + "And the feature_engineering_* documentation: \n" + "https://www.freqtrade.io/en/latest/freqai-feature-engineering/" + ) - for tf in tfs: - if tf not in base_dataframes: - base_dataframes[tf] = pd.DataFrame() - for p in pairs: - if p not in corr_dataframes: - corr_dataframes[p] = {} - if tf not in corr_dataframes[p]: - corr_dataframes[p][tf] = pd.DataFrame() - - if not prediction_dataframe.empty: - dataframe = prediction_dataframe.copy() - else: - dataframe = base_dataframes[self.config["timeframe"]].copy() - - corr_pairs: List[str] = self.freqai_config["feature_parameters"].get( - "include_corr_pairlist", []) - dataframe = self.populate_features(dataframe.copy(), pair, strategy, - corr_dataframes, base_dataframes) - metadata = {"pair": pair} - dataframe = strategy.feature_engineering_standard(dataframe.copy(), metadata=metadata) - # ensure corr pairs are always last - for corr_pair in corr_pairs: - if pair == corr_pair: - continue # dont repeat anything from whitelist - if corr_pairs and do_corr_pairs: - dataframe = self.populate_features(dataframe.copy(), corr_pair, strategy, - corr_dataframes, base_dataframes, True) - - dataframe = strategy.set_freqai_targets(dataframe.copy(), metadata=metadata) - - self.get_unique_classes_from_labels(dataframe) - - dataframe = self.remove_special_chars_from_feature_names(dataframe) - - if self.config.get('reduce_df_footprint', False): - dataframe = reduce_dataframe_footprint(dataframe) - - return dataframe - - else: - # the user is using the populate_any_indicators functions which is deprecated - - df = self.use_strategy_to_populate_indicators_old_version( - strategy, corr_dataframes, base_dataframes, pair, - prediction_dataframe, do_corr_pairs) - return df - - def use_strategy_to_populate_indicators_old_version( - self, - strategy: IStrategy, - corr_dataframes: dict = {}, - base_dataframes: dict = {}, - pair: str = "", - prediction_dataframe: DataFrame = pd.DataFrame(), - do_corr_pairs: bool = True, - ) -> DataFrame: - """ - Use the user defined strategy for populating indicators during retrain - :param strategy: IStrategy = user defined strategy object - :param corr_dataframes: dict = dict containing the df pair dataframes - (for user defined timeframes) - :param base_dataframes: dict = dict containing the current pair dataframes - (for user defined timeframes) - :param metadata: dict = strategy furnished pair metadata - :return: - dataframe: DataFrame = dataframe containing populated indicators - """ - - # for prediction dataframe creation, we let dataprovider handle everything in the strategy - # so we create empty dictionaries, which allows us to pass None to - # `populate_any_indicators()`. Signaling we want the dp to give us the live dataframe. tfs: List[str] = self.freqai_config["feature_parameters"].get("include_timeframes") - pairs: List[str] = self.freqai_config["feature_parameters"].get("include_corr_pairlist", []) + pairs: List[str] = self.freqai_config["feature_parameters"].get( + "include_corr_pairlist", []) + + for tf in tfs: + if tf not in base_dataframes: + base_dataframes[tf] = pd.DataFrame() + for p in pairs: + if p not in corr_dataframes: + corr_dataframes[p] = {} + if tf not in corr_dataframes[p]: + corr_dataframes[p][tf] = pd.DataFrame() + if not prediction_dataframe.empty: dataframe = prediction_dataframe.copy() - for tf in tfs: - base_dataframes[tf] = None - for p in pairs: - if p not in corr_dataframes: - corr_dataframes[p] = {} - corr_dataframes[p][tf] = None else: dataframe = base_dataframes[self.config["timeframe"]].copy() - sgi = False - for tf in tfs: - if tf == tfs[-1]: - sgi = True # doing this last allows user to use all tf raw prices in labels - dataframe = strategy.populate_any_indicators( - pair, - dataframe.copy(), - tf, - informative=base_dataframes[tf], - set_generalized_indicators=sgi - ) - + corr_pairs: List[str] = self.freqai_config["feature_parameters"].get( + "include_corr_pairlist", []) + dataframe = self.populate_features(dataframe.copy(), pair, strategy, + corr_dataframes, base_dataframes) + metadata = {"pair": pair} + dataframe = strategy.feature_engineering_standard(dataframe.copy(), metadata=metadata) # ensure corr pairs are always last - for corr_pair in pairs: + for corr_pair in corr_pairs: if pair == corr_pair: continue # dont repeat anything from whitelist - for tf in tfs: - if pairs and do_corr_pairs: - dataframe = strategy.populate_any_indicators( - corr_pair, - dataframe.copy(), - tf, - informative=corr_dataframes[corr_pair][tf] - ) + if corr_pairs and do_corr_pairs: + dataframe = self.populate_features(dataframe.copy(), corr_pair, strategy, + corr_dataframes, base_dataframes, True) + + dataframe = strategy.set_freqai_targets(dataframe.copy(), metadata=metadata) self.get_unique_classes_from_labels(dataframe) diff --git a/freqtrade/freqai/freqai_interface.py b/freqtrade/freqai/freqai_interface.py index c265e42f9..c7b39b4e8 100644 --- a/freqtrade/freqai/freqai_interface.py +++ b/freqtrade/freqai/freqai_interface.py @@ -1,4 +1,3 @@ -import inspect import logging import threading import time @@ -106,8 +105,6 @@ class IFreqaiModel(ABC): self.max_system_threads = max(int(psutil.cpu_count() * 2 - 2), 1) self.can_short = True # overridden in start() with strategy.can_short - self.warned_deprecated_populate_any_indicators = False - record_params(config, self.full_path) def __getstate__(self): @@ -138,9 +135,6 @@ class IFreqaiModel(ABC): self.data_provider = strategy.dp self.can_short = strategy.can_short - # check if the strategy has deprecated populate_any_indicators function - self.check_deprecated_populate_any_indicators(strategy) - if self.live: self.inference_timer('start') self.dk = FreqaiDataKitchen(self.config, self.live, metadata["pair"]) @@ -489,7 +483,7 @@ class IFreqaiModel(ABC): "strategy is furnishing the same features as the pretrained" "model. In case of --strategy-list, please be aware that FreqAI " "requires all strategies to maintain identical " - "populate_any_indicator() functions" + "feature_engineering_* functions" ) def data_cleaning_train(self, dk: FreqaiDataKitchen) -> None: @@ -601,7 +595,7 @@ class IFreqaiModel(ABC): :param strategy: IStrategy = user defined strategy object :param dk: FreqaiDataKitchen = non-persistent data container for current coin/loop :param data_load_timerange: TimeRange = the amount of data to be loaded - for populate_any_indicators + for populating indicators (larger than new_trained_timerange so that new_trained_timerange does not contain any NaNs) """ @@ -806,7 +800,7 @@ class IFreqaiModel(ABC): logger.warning("Couldn't cache corr_pair dataframes for improved performance. " "Consider ensuring that the full coin/stake, e.g. XYZ/USD, " "is included in the column names when you are creating features " - "in `populate_any_indicators()`.") + "in `feature_engineering_*` functions.") self.get_corr_dataframes = not bool(self.corr_dataframes) elif self.corr_dataframes: dataframe = dk.attach_corr_pair_columns( @@ -933,26 +927,6 @@ class IFreqaiModel(ABC): dk.return_dataframe, saved_dataframe, how='left', left_on='date', right_on="date_pred") return dk - def check_deprecated_populate_any_indicators(self, strategy: IStrategy): - """ - Check and warn if the deprecated populate_any_indicators function is used. - :param strategy: strategy object - """ - - if not self.warned_deprecated_populate_any_indicators: - self.warned_deprecated_populate_any_indicators = True - old_version = inspect.getsource(strategy.populate_any_indicators) != ( - inspect.getsource(IStrategy.populate_any_indicators)) - - if old_version: - logger.warning("DEPRECATION WARNING: " - "You are using the deprecated populate_any_indicators function. " - "This function will raise an error on March 1 2023. " - "Please update your strategy by using " - "the new feature_engineering functions. See \n" - "https://www.freqtrade.io/en/latest/freqai-feature-engineering/" - "for details.") - # Following methods which are overridden by user made prediction models. # See freqai/prediction_models/CatboostPredictionModel.py for an example. diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 065a88f40..023be9a1a 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -93,7 +93,7 @@ class Backtesting: if self.config.get('strategy_list'): if self.config.get('freqai', {}).get('enabled', False): logger.warning("Using --strategy-list with FreqAI REQUIRES all strategies " - "to have identical populate_any_indicators.") + "to have identical feature_engineering_* functions.") for strat in list(self.config['strategy_list']): stratconf = deepcopy(self.config) stratconf['strategy'] = strat diff --git a/tests/freqai/test_freqai_backtesting.py b/tests/freqai/test_freqai_backtesting.py index 60963e762..0a8059966 100644 --- a/tests/freqai/test_freqai_backtesting.py +++ b/tests/freqai/test_freqai_backtesting.py @@ -35,8 +35,8 @@ def test_freqai_backtest_start_backtest_list(freqai_conf, mocker, testdatadir, c args = get_args(args) bt_config = setup_optimize_configuration(args, RunMode.BACKTEST) Backtesting(bt_config) - assert log_has_re('Using --strategy-list with FreqAI REQUIRES all strategies to have identical ' - 'populate_any_indicators.', caplog) + assert log_has_re('Using --strategy-list with FreqAI REQUIRES all strategies to have identical', + caplog) Backtesting.cleanup() diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index fe562907a..87075d56d 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -291,18 +291,6 @@ def test_advise_all_indicators(default_conf, testdatadir) -> None: assert len(processed['UNITTEST/BTC']) == 103 -def test_populate_any_indicators(default_conf, testdatadir) -> None: - strategy = StrategyResolver.load_strategy(default_conf) - - timerange = TimeRange.parse_timerange('1510694220-1510700340') - data = load_data(testdatadir, '1m', ['UNITTEST/BTC'], timerange=timerange, - fill_up_missing=True) - processed = strategy.populate_any_indicators('UNITTEST/BTC', data, '5m') - assert processed == data - assert id(processed) == id(data) - assert len(processed['UNITTEST/BTC']) == 103 - - def test_freqai_not_initialized(default_conf) -> None: strategy = StrategyResolver.load_strategy(default_conf) strategy.ft_bot_start() From 986bc63e54f9c6953a6bb2064e0fa892eff01339 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Tue, 21 Feb 2023 21:23:58 +0100 Subject: [PATCH 028/360] raise OperationalException if latest historical data candle is older than earliest dataprovider candle --- freqtrade/freqai/data_drawer.py | 34 +++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/freqtrade/freqai/data_drawer.py b/freqtrade/freqai/data_drawer.py index 1317ba6d3..883b9d94b 100644 --- a/freqtrade/freqai/data_drawer.py +++ b/freqtrade/freqai/data_drawer.py @@ -627,12 +627,12 @@ class FreqaiDataDrawer: for pair in dk.all_pairs: for tf in feat_params.get("include_timeframes"): - + hist_df = history_data[pair][tf] # check if newest candle is already appended df_dp = strategy.dp.get_pair_dataframe(pair, tf) if len(df_dp.index) == 0: continue - if str(history_data[pair][tf].iloc[-1]["date"]) == str( + if str(hist_df.iloc[-1]["date"]) == str( df_dp.iloc[-1:]["date"].iloc[-1] ): continue @@ -640,28 +640,30 @@ class FreqaiDataDrawer: try: index = ( df_dp.loc[ - df_dp["date"] == history_data[pair][tf].iloc[-1]["date"] + df_dp["date"] == hist_df.iloc[-1]["date"] ].index[0] + 1 ) except IndexError: - if history_data[pair][tf].iloc[-1]['date'] < df_dp['date'].iloc[0]: - index = 0 + if hist_df.iloc[-1]['date'] < df_dp['date'].iloc[0]: + raise OperationalException("In memory historical data is older than " + f"oldest DataProvider candle for {pair} on " + f"timeframe {tf}") else: index = -1 - logger.warning( - f"No common dates in historical data and dataprovider for {pair}. " - f"Appending dataprovider to historical data (full? {not bool(index)})" - "but please be aware that there is likely a gap in the historical " - "data.\n" - f"Historical data ends at {history_data[pair][tf].iloc[-1]['date']} " - f"while dataprovider starts at {df_dp['date'].iloc[0]} and" - f"ends at {df_dp['date'].iloc[0]}." - ) + logger.warning( + f"No common dates in historical data and dataprovider for {pair}. " + f"Appending latest dataprovider candle to historical data " + "but please be aware that there is likely a gap in the historical " + "data. \n" + f"Historical data ends at {hist_df.iloc[-1]['date']} " + f"while dataprovider starts at {df_dp['date'].iloc[0]} and" + f"ends at {df_dp['date'].iloc[0]}." + ) - history_data[pair][tf] = pd.concat( + hist_df = pd.concat( [ - history_data[pair][tf], + hist_df, df_dp.iloc[index:], ], ignore_index=True, From 75bc5809a9b18bf1c1b97ce922046fe764be62f6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 22 Feb 2023 20:02:51 +0100 Subject: [PATCH 029/360] Better handle backtest errors --- freqtrade/rpc/api_server/api_backtest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/api_server/api_backtest.py b/freqtrade/rpc/api_server/api_backtest.py index ce71467ca..77de33994 100644 --- a/freqtrade/rpc/api_server/api_backtest.py +++ b/freqtrade/rpc/api_server/api_backtest.py @@ -118,7 +118,7 @@ async def api_start_backtest( # noqa: C901 logger.info("Backtest finished.") - except (OperationalException, DependencyException) as e: + except (Exception, OperationalException, DependencyException) as e: logger.exception(f"Backtesting caused an error: {e}") pass finally: From e6766b9b82805d0673155b7da7fc8eca121ec21c Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 22 Feb 2023 20:22:59 +0100 Subject: [PATCH 030/360] Add bt-error to UI backtest method. --- freqtrade/rpc/api_server/api_backtest.py | 10 ++++++++++ freqtrade/rpc/api_server/webserver.py | 1 + tests/rpc/test_rpc_apiserver.py | 10 ++++++++-- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/api_server/api_backtest.py b/freqtrade/rpc/api_server/api_backtest.py index 77de33994..d9d7a27f1 100644 --- a/freqtrade/rpc/api_server/api_backtest.py +++ b/freqtrade/rpc/api_server/api_backtest.py @@ -29,6 +29,7 @@ router = APIRouter() async def api_start_backtest( # noqa: C901 bt_settings: BacktestRequest, background_tasks: BackgroundTasks, config=Depends(get_config), ws_mode=Depends(is_webserver_mode)): + ApiServer._bt['bt_error'] = None """Start backtesting if not done so already""" if ApiServer._bgtask_running: raise RPCException('Bot Background task already running') @@ -120,6 +121,7 @@ async def api_start_backtest( # noqa: C901 except (Exception, OperationalException, DependencyException) as e: logger.exception(f"Backtesting caused an error: {e}") + ApiServer._bt['bt_error'] = str(e) pass finally: ApiServer._bgtask_running = False @@ -162,6 +164,14 @@ def api_get_backtest(ws_mode=Depends(is_webserver_mode)): "progress": 0, "status_msg": "Backtest not yet executed" } + if ApiServer._bt['bt_error']: + return { + "status": "error", + "running": False, + "step": "", + "progress": 0, + "status_msg": f"Backtest failed with {ApiServer._bt['bt_error']}" + } return { "status": "ended", diff --git a/freqtrade/rpc/api_server/webserver.py b/freqtrade/rpc/api_server/webserver.py index b3ef794d8..b53662451 100644 --- a/freqtrade/rpc/api_server/webserver.py +++ b/freqtrade/rpc/api_server/webserver.py @@ -41,6 +41,7 @@ class ApiServer(RPCHandler): 'data': None, 'timerange': None, 'last_config': {}, + 'bt_error': None, } _has_rpc: bool = False _bgtask_running: bool = False diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 94b210c76..43d9abb78 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -1737,9 +1737,15 @@ def test_api_backtesting(botclient, mocker, fee, caplog, tmpdir): data['stake_amount'] = 101 mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest_one_strategy', - side_effect=DependencyException()) + side_effect=DependencyException('DeadBeef')) rc = client_post(client, f"{BASE_URI}/backtest", data=data) - assert log_has("Backtesting caused an error: ", caplog) + assert log_has("Backtesting caused an error: DeadBeef", caplog) + + rc = client_get(client, f"{BASE_URI}/backtest") + assert_response(rc) + result = rc.json() + assert result['status'] == 'error' + assert 'Backtest failed' in result['status_msg'] # Delete backtesting to avoid leakage since the backtest-object may stick around. rc = client_delete(client, f"{BASE_URI}/backtest") From 2bc9413be11288d0b42c0c354cb2d988eca5af22 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 22 Feb 2023 20:58:24 +0100 Subject: [PATCH 031/360] Fix minor stylistic errors --- freqtrade/__main__.py | 0 freqtrade/commands/analyze_commands.py | 0 freqtrade/commands/hyperopt_commands.py | 0 freqtrade/data/entryexitanalysis.py | 0 freqtrade/optimize/hyperopt_tools.py | 0 freqtrade/vendor/qtpylib/indicators.py | 1 - freqtrade/worker.py | 0 scripts/ws_client.py | 0 tests/data/test_entryexitanalysis.py | 0 tests/exchange/test_ccxt_compat.py | 2 +- tests/strategy/test_interface.py | 4 ++-- 11 files changed, 3 insertions(+), 4 deletions(-) mode change 100644 => 100755 freqtrade/__main__.py mode change 100755 => 100644 freqtrade/commands/analyze_commands.py mode change 100755 => 100644 freqtrade/commands/hyperopt_commands.py mode change 100755 => 100644 freqtrade/data/entryexitanalysis.py mode change 100755 => 100644 freqtrade/optimize/hyperopt_tools.py mode change 100755 => 100644 freqtrade/worker.py mode change 100644 => 100755 scripts/ws_client.py mode change 100755 => 100644 tests/data/test_entryexitanalysis.py diff --git a/freqtrade/__main__.py b/freqtrade/__main__.py old mode 100644 new mode 100755 diff --git a/freqtrade/commands/analyze_commands.py b/freqtrade/commands/analyze_commands.py old mode 100755 new mode 100644 diff --git a/freqtrade/commands/hyperopt_commands.py b/freqtrade/commands/hyperopt_commands.py old mode 100755 new mode 100644 diff --git a/freqtrade/data/entryexitanalysis.py b/freqtrade/data/entryexitanalysis.py old mode 100755 new mode 100644 diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py old mode 100755 new mode 100644 diff --git a/freqtrade/vendor/qtpylib/indicators.py b/freqtrade/vendor/qtpylib/indicators.py index 4f14ae13c..3da4f038d 100644 --- a/freqtrade/vendor/qtpylib/indicators.py +++ b/freqtrade/vendor/qtpylib/indicators.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # -*- coding: utf-8 -*- # # QTPyLib: Quantitative Trading Python Library diff --git a/freqtrade/worker.py b/freqtrade/worker.py old mode 100755 new mode 100644 diff --git a/scripts/ws_client.py b/scripts/ws_client.py old mode 100644 new mode 100755 diff --git a/tests/data/test_entryexitanalysis.py b/tests/data/test_entryexitanalysis.py old mode 100755 new mode 100644 diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index f1d240f9f..bbeb56c6a 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -309,7 +309,7 @@ def exchange(request, exchange_conf): @pytest.fixture(params=EXCHANGES, scope="class") def exchange_futures(request, exchange_conf, class_mocker): - if not EXCHANGES[request.param].get('futures') is True: + if EXCHANGES[request.param].get('futures') is not True: yield None, request.param else: exchange_conf = set_test_proxy( diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index fe562907a..0b30d2059 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -214,12 +214,12 @@ def test_ignore_expired_candle(default_conf): current_time = latest_date + timedelta(seconds=30 + 300) - assert not strategy.ignore_expired_candle( + assert strategy.ignore_expired_candle( latest_date=latest_date, current_time=current_time, timeframe_seconds=300, enter=True - ) is True + ) is not True def test_assert_df_raise(mocker, caplog, ohlcv_history): From 549a0e1c447df9fe75ee834dad58d83ee4cd8bb9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 22 Feb 2023 21:06:07 +0100 Subject: [PATCH 032/360] Add ruff linting - initial configuration --- .github/workflows/ci.yml | 12 ++++++++++++ .pre-commit-config.yaml | 6 ++++++ pyproject.toml | 14 ++++++++++++++ requirements-dev.txt | 1 + 4 files changed, 33 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cfc8ac3b6..4cff9ef7a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -98,6 +98,10 @@ jobs: run: | isort --check . + - name: Ruff - linting + run: | + ruff check . + - name: Mypy run: | mypy freqtrade scripts tests @@ -194,6 +198,10 @@ jobs: run: | isort --check . + - name: Ruff - linting + run: | + ruff check . + - name: Mypy run: | mypy freqtrade scripts @@ -252,6 +260,10 @@ jobs: run: | flake8 + - name: Ruff - linting + run: | + ruff check . + - name: Mypy run: | mypy freqtrade scripts tests diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 57ce81b8c..58f526ce9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -27,6 +27,12 @@ repos: name: isort (python) # stages: [push] + - repo: https://github.com/charliermarsh/ruff-pre-commit + # Ruff version. + rev: 'v0.0.251' + hooks: + - id: ruff + - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.4.0 hooks: diff --git a/pyproject.toml b/pyproject.toml index 82d4ceaf8..7da2a2200 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,3 +56,17 @@ exclude = [ "build_helpers/*.py", ] ignore = ["freqtrade/vendor/**"] + + +[tool.ruff] +line-length = 100 +extend-exclude = [".env"] +extend-select = [ + "TID", + "EXE", + "YTT", + # "DTZ", + # "RSE", + # "TCH", + # "PTH", +] diff --git a/requirements-dev.txt b/requirements-dev.txt index 32b7cfcc5..287cb8ae9 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -9,6 +9,7 @@ coveralls==3.3.1 flake8==6.0.0 flake8-tidy-imports==4.8.0 +ruff==0.0.251 mypy==1.0.1 pre-commit==3.0.4 pytest==7.2.1 From b4ea37d59862ef5769468a0749c5f4e4ee4254da Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 22 Feb 2023 21:08:17 +0100 Subject: [PATCH 033/360] Remove flake8 in favor of ruff --- .github/workflows/ci.yml | 12 ------------ CONTRIBUTING.md | 11 ++++++----- docs/developer.md | 2 +- environment.yml | 3 +-- requirements-dev.txt | 2 -- setup.py | 2 -- 6 files changed, 8 insertions(+), 24 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4cff9ef7a..e00b9040b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -90,10 +90,6 @@ jobs: freqtrade create-userdir --userdir user_data freqtrade hyperopt --datadir tests/testdata -e 6 --strategy SampleStrategy --hyperopt-loss SharpeHyperOptLossDaily --print-all - - name: Flake8 - run: | - flake8 - - name: Sort imports (isort) run: | isort --check . @@ -190,10 +186,6 @@ jobs: freqtrade create-userdir --userdir user_data freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt-loss SharpeHyperOptLossDaily --print-all - - name: Flake8 - run: | - flake8 - - name: Sort imports (isort) run: | isort --check . @@ -256,10 +248,6 @@ jobs: freqtrade create-userdir --userdir user_data freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt-loss SharpeHyperOptLossDaily --print-all - - name: Flake8 - run: | - flake8 - - name: Ruff - linting run: | ruff check . diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b4e0bc024..040aae39c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -45,16 +45,17 @@ pytest tests/test_.py::test_ ### 2. Test if your code is PEP8 compliant -#### Run Flake8 +#### Run Ruff ```bash -flake8 freqtrade tests scripts +ruff . ``` -We receive a lot of code that fails the `flake8` checks. +We receive a lot of code that fails the `ruff` checks. To help with that, we encourage you to install the git pre-commit -hook that will warn you when you try to commit code that fails these checks. -Guide for installing them is [here](http://flake8.pycqa.org/en/latest/user/using-hooks.html). +hook that will warn you when you try to commit code that fails these checks. + +you can manually run pre-commit with `pre-commit run -a`. ##### Additional styles applied diff --git a/docs/developer.md b/docs/developer.md index 0546c20e9..1bc75551f 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -24,7 +24,7 @@ This will spin up a local server (usually on port 8000) so you can see if everyt To configure a development environment, you can either use the provided [DevContainer](#devcontainer-setup), or use the `setup.sh` script and answer "y" when asked "Do you want to install dependencies for dev [y/N]? ". Alternatively (e.g. if your system is not supported by the setup.sh script), follow the manual installation process and run `pip3 install -e .[all]`. -This will install all required tools for development, including `pytest`, `flake8`, `mypy`, and `coveralls`. +This will install all required tools for development, including `pytest`, `ruff`, `mypy`, and `coveralls`. Then install the git hook scripts by running `pre-commit install`, so your changes will be verified locally before committing. This avoids a lot of waiting for CI already, as some basic formatting checks are done locally on your machine. diff --git a/environment.yml b/environment.yml index 5b039e7f7..171e7c3da 100644 --- a/environment.yml +++ b/environment.yml @@ -41,7 +41,6 @@ dependencies: # 2/4 req dev - coveralls - - flake8 - mypy - pytest - pytest-asyncio @@ -70,6 +69,6 @@ dependencies: - tables - pytest-random-order - ccxt - - flake8-tidy-imports + - ruff - -e . # - python-rapidjso diff --git a/requirements-dev.txt b/requirements-dev.txt index 287cb8ae9..c23447694 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -7,8 +7,6 @@ -r docs/requirements-docs.txt coveralls==3.3.1 -flake8==6.0.0 -flake8-tidy-imports==4.8.0 ruff==0.0.251 mypy==1.0.1 pre-commit==3.0.4 diff --git a/setup.py b/setup.py index 30aacc3f2..edd7b243b 100644 --- a/setup.py +++ b/setup.py @@ -32,8 +32,6 @@ hdf5 = [ develop = [ 'coveralls', - 'flake8', - 'flake8-tidy-imports', 'mypy', 'pytest', 'pytest-asyncio', From bf968a9fd878cfdb37fe7b0a5865f82767921cf6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 23 Feb 2023 06:51:03 +0100 Subject: [PATCH 034/360] Use actions as documented --- .github/workflows/ci.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e00b9040b..17c0efd6d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -94,9 +94,9 @@ jobs: run: | isort --check . - - name: Ruff - linting + - name: Run Ruff run: | - ruff check . + ruff check --format=github . - name: Mypy run: | @@ -190,9 +190,9 @@ jobs: run: | isort --check . - - name: Ruff - linting + - name: Run Ruff run: | - ruff check . + ruff check --format=github . - name: Mypy run: | @@ -248,9 +248,9 @@ jobs: freqtrade create-userdir --userdir user_data freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt-loss SharpeHyperOptLossDaily --print-all - - name: Ruff - linting + - name: Run Ruff run: | - ruff check . + ruff check --format=github . - name: Mypy run: | From 6b829d839b93d5a793a7ff70bfea632ce6a05655 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 23 Feb 2023 07:12:54 +0100 Subject: [PATCH 035/360] Improve ruff config --- pyproject.toml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 7da2a2200..8a7750731 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,11 +62,11 @@ ignore = ["freqtrade/vendor/**"] line-length = 100 extend-exclude = [".env"] extend-select = [ - "TID", - "EXE", - "YTT", - # "DTZ", - # "RSE", - # "TCH", - # "PTH", + "TID", # flake8-tidy-imports + # "EXE", # flake8-executable + "YTT", # flake8-2020 + # "DTZ", # flake8-datetimez + # "RSE", # flake8-raise + # "TCH", # flake8-type-checking + # "PTH", # flake8-use-pathlib ] From cb80d7c26f77c82983f1cb4fed000bf36c94bdc8 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Fri, 24 Feb 2023 11:19:54 +0100 Subject: [PATCH 036/360] close the multi_proc env before creating new ones in an attempt to avoid increasing processes --- .../prediction_models/ReinforcementLearner_multiproc.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/freqtrade/freqai/prediction_models/ReinforcementLearner_multiproc.py b/freqtrade/freqai/prediction_models/ReinforcementLearner_multiproc.py index 9ee035c95..b3b8c40e6 100644 --- a/freqtrade/freqai/prediction_models/ReinforcementLearner_multiproc.py +++ b/freqtrade/freqai/prediction_models/ReinforcementLearner_multiproc.py @@ -34,6 +34,11 @@ class ReinforcementLearner_multiproc(ReinforcementLearner): train_df = data_dictionary["train_features"] test_df = data_dictionary["test_features"] + if self.train_env: + self.train_env.close() + if self.eval_env: + self.eval_env.close() + env_info = self.pack_env_dict(dk.pair) env_id = "train_env" From 7bcae7b6659ee7714eb04fbf30bb7f7d85c1356b Mon Sep 17 00:00:00 2001 From: ASU Date: Sat, 25 Feb 2023 00:26:20 +0200 Subject: [PATCH 037/360] removed redundant dependencies from environment.yml --- environment.yml | 76 ++++--------------------------------------------- 1 file changed, 6 insertions(+), 70 deletions(-) diff --git a/environment.yml b/environment.yml index 5b039e7f7..5193947cf 100644 --- a/environment.yml +++ b/environment.yml @@ -3,73 +3,9 @@ channels: - conda-forge # - defaults dependencies: -# 1/4 req main - - python>=3.8,<=3.10 - - numpy - - pandas - - pip - - - py-find-1st - - aiohttp - - SQLAlchemy - - python-telegram-bot<20.0.0 - - arrow - - cachetools - - requests - - urllib3 - - jsonschema - - TA-Lib - - tabulate - - jinja2 - - blosc - - sdnotify - - fastapi - - uvicorn - - pyjwt - - aiofiles - - psutil - - colorama - - questionary - - prompt-toolkit - - schedule - - python-dateutil - - joblib - - pyarrow - - - # ============================ - # 2/4 req dev - - - coveralls - - flake8 - - mypy - - pytest - - pytest-asyncio - - pytest-cov - - pytest-mock - - isort - - nbconvert - - # ============================ - # 3/4 req hyperopt - - - scipy - - scikit-learn - - filelock - - scikit-optimize - - progressbar2 - # ============================ - # 4/4 req plot - - - plotly - - jupyter - - - pip: - - pycoingecko - # - py_find_1st - - tables - - pytest-random-order - - ccxt - - flake8-tidy-imports - - -e . - # - python-rapidjso + - python>=3.8,<=3.10 + - pip + - libta-lib=0.4.0 # this is required for ta-lib from requirements.txt + - pip: + - -r requirements.txt + - -e . From 563742f13ce25e1399734d830d77ecba5d609772 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 Feb 2023 13:34:19 +0100 Subject: [PATCH 038/360] Fix enum behavior for python 3.11 closes #8221 closes #8217 --- freqtrade/enums/candletype.py | 3 +++ freqtrade/enums/rpcmessagetype.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/freqtrade/enums/candletype.py b/freqtrade/enums/candletype.py index 9d05ff6d7..dcb9f1448 100644 --- a/freqtrade/enums/candletype.py +++ b/freqtrade/enums/candletype.py @@ -13,6 +13,9 @@ class CandleType(str, Enum): FUNDING_RATE = "funding_rate" # BORROW_RATE = "borrow_rate" # * unimplemented + def __str__(self): + return f"{self.name.lower()}" + @staticmethod def from_string(value: str) -> 'CandleType': if not value: diff --git a/freqtrade/enums/rpcmessagetype.py b/freqtrade/enums/rpcmessagetype.py index 2453d16d9..404c75401 100644 --- a/freqtrade/enums/rpcmessagetype.py +++ b/freqtrade/enums/rpcmessagetype.py @@ -37,5 +37,8 @@ class RPCRequestType(str, Enum): WHITELIST = 'whitelist' ANALYZED_DF = 'analyzed_df' + def __str__(self): + return self.value + NO_ECHO_MESSAGES = (RPCMessageType.ANALYZED_DF, RPCMessageType.WHITELIST, RPCMessageType.NEW_CANDLE) From be352ae01499f794ced15af4cfec905f773c4407 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 Feb 2023 13:46:14 +0100 Subject: [PATCH 039/360] Update more enums --- freqtrade/enums/signaltype.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/freqtrade/enums/signaltype.py b/freqtrade/enums/signaltype.py index f706fd4dc..b5af1f1b2 100644 --- a/freqtrade/enums/signaltype.py +++ b/freqtrade/enums/signaltype.py @@ -10,6 +10,9 @@ class SignalType(Enum): ENTER_SHORT = "enter_short" EXIT_SHORT = "exit_short" + def __str__(self): + return f"{self.name.lower()}" + class SignalTagType(Enum): """ @@ -18,7 +21,13 @@ class SignalTagType(Enum): ENTER_TAG = "enter_tag" EXIT_TAG = "exit_tag" + def __str__(self): + return f"{self.name.lower()}" + class SignalDirection(str, Enum): LONG = 'long' SHORT = 'short' + + def __str__(self): + return f"{self.name.lower()}" From ff3aa7c1a996e99191b2e315e93bf3300ff91e6b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 Feb 2023 16:10:24 +0100 Subject: [PATCH 040/360] Bump Version to 2023.3.dev --- freqtrade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index db339bea3..6ba045adf 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ Freqtrade bot """ -__version__ = '2023.2.dev' +__version__ = '2023.3.dev' if 'dev' in __version__: from pathlib import Path From c8a4a773ee192a7eb495da728181dc738f29ef69 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 24 Feb 2023 18:15:27 +0100 Subject: [PATCH 041/360] Fix _pairs_last_refresh_time storing the wrong date Depending on the drop_incomplete settings, this can lead to implicit bugs --- freqtrade/exchange/exchange.py | 7 +++++-- tests/exchange/test_exchange.py | 10 +++++----- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 0cac411c7..cdbda1506 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1961,7 +1961,8 @@ class Exchange: cache: bool, drop_incomplete: bool) -> DataFrame: # keeping last candle time as last refreshed time of the pair if ticks and cache: - self._pairs_last_refresh_time[(pair, timeframe, c_type)] = ticks[-1][0] // 1000 + idx = -2 if drop_incomplete and len(ticks) > 1 else -1 + self._pairs_last_refresh_time[(pair, timeframe, c_type)] = ticks[idx][0] // 1000 # keeping parsed dataframe in cache ohlcv_df = ohlcv_to_dataframe(ticks, timeframe, pair=pair, fill_missing=True, drop_incomplete=drop_incomplete) @@ -2034,7 +2035,9 @@ class Exchange: # Timeframe in seconds interval_in_sec = timeframe_to_seconds(timeframe) plr = self._pairs_last_refresh_time.get((pair, timeframe, candle_type), 0) + interval_in_sec - return plr < arrow.utcnow().int_timestamp + # current,active candle open date + now = int(timeframe_to_prev_date(timeframe).timestamp()) + return plr < now @retrier_async async def _async_get_candle_history( diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 13613df37..df878dbd3 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2215,7 +2215,7 @@ def test_refresh_latest_ohlcv_cache(mocker, default_conf, candle_type, time_mach assert len(res[pair1]) == 99 assert len(res[pair2]) == 99 assert exchange._klines - assert exchange._pairs_last_refresh_time[pair1] == ohlcv[-1][0] // 1000 + assert exchange._pairs_last_refresh_time[pair1] == ohlcv[-2][0] // 1000 exchange._api_async.fetch_ohlcv.reset_mock() # Returned from cache @@ -2224,7 +2224,7 @@ def test_refresh_latest_ohlcv_cache(mocker, default_conf, candle_type, time_mach assert len(res) == 2 assert len(res[pair1]) == 99 assert len(res[pair2]) == 99 - assert exchange._pairs_last_refresh_time[pair1] == ohlcv[-1][0] // 1000 + assert exchange._pairs_last_refresh_time[pair1] == ohlcv[-2][0] // 1000 # Move time 1 candle further but result didn't change yet time_machine.move_to(start + timedelta(hours=101)) @@ -2234,7 +2234,7 @@ def test_refresh_latest_ohlcv_cache(mocker, default_conf, candle_type, time_mach assert len(res[pair1]) == 99 assert len(res[pair2]) == 99 assert res[pair2].at[0, 'open'] - assert exchange._pairs_last_refresh_time[pair1] == ohlcv[-1][0] // 1000 + assert exchange._pairs_last_refresh_time[pair1] == ohlcv[-2][0] // 1000 refresh_pior = exchange._pairs_last_refresh_time[pair1] # New candle on exchange - return 100 candles - but skip one candle so we actually get 2 candles @@ -2252,8 +2252,8 @@ def test_refresh_latest_ohlcv_cache(mocker, default_conf, candle_type, time_mach assert res[pair2].at[0, 'open'] assert refresh_pior != exchange._pairs_last_refresh_time[pair1] - assert exchange._pairs_last_refresh_time[pair1] == ohlcv[-1][0] // 1000 - assert exchange._pairs_last_refresh_time[pair2] == ohlcv[-1][0] // 1000 + assert exchange._pairs_last_refresh_time[pair1] == ohlcv[-2][0] // 1000 + assert exchange._pairs_last_refresh_time[pair2] == ohlcv[-2][0] // 1000 exchange._api_async.fetch_ohlcv.reset_mock() # Retry same call - from cache From d014e4590e2a5bac5c246b3b1d71ae74c1bff6b6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 Feb 2023 17:08:02 +0100 Subject: [PATCH 042/360] use Path.open() instead of open --- freqtrade/configuration/load_config.py | 2 +- freqtrade/data/entryexitanalysis.py | 6 +++--- freqtrade/exchange/binance.py | 2 +- freqtrade/misc.py | 8 ++++---- freqtrade/optimize/backtest_caching.py | 2 +- freqtrade/plugins/pairlist/RemotePairList.py | 2 +- tests/data/test_history.py | 2 +- tests/optimize/test_optimize_reports.py | 4 ++-- tests/test_configuration.py | 4 ++-- tests/test_misc.py | 2 +- 10 files changed, 17 insertions(+), 17 deletions(-) diff --git a/freqtrade/configuration/load_config.py b/freqtrade/configuration/load_config.py index a1a77815a..57424468d 100644 --- a/freqtrade/configuration/load_config.py +++ b/freqtrade/configuration/load_config.py @@ -58,7 +58,7 @@ def load_config_file(path: str) -> Dict[str, Any]: """ try: # Read config from stdin if requested in the options - with open(path) if path != '-' else sys.stdin as file: + with Path(path).open() if path != '-' else sys.stdin as file: config = rapidjson.load(file, parse_mode=CONFIG_PARSE_MODE) except FileNotFoundError: raise OperationalException( diff --git a/freqtrade/data/entryexitanalysis.py b/freqtrade/data/entryexitanalysis.py index b2679bcea..5d67655cd 100644 --- a/freqtrade/data/entryexitanalysis.py +++ b/freqtrade/data/entryexitanalysis.py @@ -24,9 +24,9 @@ def _load_signal_candles(backtest_dir: Path): scpf = Path(backtest_dir.parent / f"{backtest_dir.stem}_signals.pkl") try: - scp = open(scpf, "rb") - signal_candles = joblib.load(scp) - logger.info(f"Loaded signal candles: {str(scpf)}") + with scpf.open("rb") as scp: + signal_candles = joblib.load(scp) + logger.info(f"Loaded signal candles: {str(scpf)}") except Exception as e: logger.error("Cannot load signal candles from pickled results: ", e) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 740d6e8a0..9580bc690 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -195,7 +195,7 @@ class Binance(Exchange): leverage_tiers_path = ( Path(__file__).parent / 'binance_leverage_tiers.json' ) - with open(leverage_tiers_path) as json_file: + with leverage_tiers_path.open() as json_file: return json_load(json_file) else: try: diff --git a/freqtrade/misc.py b/freqtrade/misc.py index 9d9cf38d7..87cea54c0 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -81,7 +81,7 @@ def file_dump_json(filename: Path, data: Any, is_zip: bool = False, log: bool = else: if log: logger.info(f'dumping json to "{filename}"') - with open(filename, 'w') as fp: + with filename.open('w') as fp: rapidjson.dump(data, fp, default=str, number_mode=rapidjson.NM_NATIVE) logger.debug(f'done json to "{filename}"') @@ -98,7 +98,7 @@ def file_dump_joblib(filename: Path, data: Any, log: bool = True) -> None: if log: logger.info(f'dumping joblib to "{filename}"') - with open(filename, 'wb') as fp: + with filename.open('wb') as fp: joblib.dump(data, fp) logger.debug(f'done joblib dump to "{filename}"') @@ -112,7 +112,7 @@ def json_load(datafile: IO) -> Any: return rapidjson.load(datafile, number_mode=rapidjson.NM_NATIVE) -def file_load_json(file): +def file_load_json(file: Path): if file.suffix != ".gz": gzipfile = file.with_suffix(file.suffix + '.gz') @@ -125,7 +125,7 @@ def file_load_json(file): pairdata = json_load(datafile) elif file.is_file(): logger.debug(f"Loading historical data from file {file}") - with open(file) as datafile: + with file.open() as datafile: pairdata = json_load(datafile) else: return None diff --git a/freqtrade/optimize/backtest_caching.py b/freqtrade/optimize/backtest_caching.py index d9d270072..f34bbffef 100644 --- a/freqtrade/optimize/backtest_caching.py +++ b/freqtrade/optimize/backtest_caching.py @@ -29,7 +29,7 @@ def get_strategy_run_id(strategy) -> str: # Include _ft_params_from_file - so changing parameter files cause cache eviction digest.update(rapidjson.dumps( strategy._ft_params_from_file, default=str, number_mode=rapidjson.NM_NAN).encode('utf-8')) - with open(strategy.__file__, 'rb') as fp: + with Path(strategy.__file__).open('rb') as fp: digest.update(fp.read()) return digest.hexdigest().lower() diff --git a/freqtrade/plugins/pairlist/RemotePairList.py b/freqtrade/plugins/pairlist/RemotePairList.py index b54be1fa7..764c16f1a 100644 --- a/freqtrade/plugins/pairlist/RemotePairList.py +++ b/freqtrade/plugins/pairlist/RemotePairList.py @@ -157,7 +157,7 @@ class RemotePairList(IPairList): file_path = Path(filename) if file_path.exists(): - with open(filename) as json_file: + with file_path.open() as json_file: # Load the JSON data into a dictionary jsonparse = json.load(json_file) diff --git a/tests/data/test_history.py b/tests/data/test_history.py index 7d313c446..5cd7327fd 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -191,7 +191,7 @@ def test_load_cached_data_for_updating(mocker, testdatadir) -> None: test_data = None test_filename = testdatadir.joinpath('UNITTEST_BTC-1m.json') - with open(test_filename, "rt") as file: + with test_filename.open("rt") as file: test_data = json.load(file) test_data_df = ohlcv_to_dataframe(test_data, '1m', 'UNITTEST/BTC', diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index 549202284..f71e6c492 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -255,7 +255,7 @@ def test_write_read_backtest_candles(tmpdir): # test directory exporting stored_file = store_backtest_signal_candles(Path(tmpdir), candle_dict, '2022_01_01_15_05_13') - scp = open(stored_file, "rb") + scp = stored_file.open("rb") pickled_signal_candles = joblib.load(scp) scp.close() @@ -269,7 +269,7 @@ def test_write_read_backtest_candles(tmpdir): # test file exporting filename = Path(tmpdir / 'testresult') stored_file = store_backtest_signal_candles(filename, candle_dict, '2022_01_01_15_05_13') - scp = open(stored_file, "rb") + scp = stored_file.open("rb") pickled_signal_candles = joblib.load(scp) scp.close() diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 4a94a3c2e..357e16702 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -59,7 +59,7 @@ def test_load_config_incorrect_stake_amount(default_conf) -> None: def test_load_config_file(default_conf, mocker, caplog) -> None: del default_conf['user_data_dir'] default_conf['datadir'] = str(default_conf['datadir']) - file_mock = mocker.patch('freqtrade.configuration.load_config.open', mocker.mock_open( + file_mock = mocker.patch('freqtrade.configuration.load_config.Path.open', mocker.mock_open( read_data=json.dumps(default_conf) )) @@ -272,7 +272,7 @@ def test_load_config_max_open_trades_minus_one(default_conf, mocker, caplog) -> def test_load_config_file_exception(mocker) -> None: mocker.patch( - 'freqtrade.configuration.configuration.open', + 'freqtrade.configuration.configuration.Path.open', MagicMock(side_effect=FileNotFoundError('File not found')) ) diff --git a/tests/test_misc.py b/tests/test_misc.py index 596c7bd51..6b4343ab2 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -46,7 +46,7 @@ def test_shorten_date() -> None: def test_file_dump_json(mocker) -> None: - file_open = mocker.patch('freqtrade.misc.open', MagicMock()) + file_open = mocker.patch('freqtrade.misc.Path.open', MagicMock()) json_dump = mocker.patch('rapidjson.dump', MagicMock()) file_dump_json(Path('somefile'), [1, 2, 3]) assert file_open.call_count == 1 From 26315b6bc25c75e6d2d0cc78be9e82fdaa177538 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 Feb 2023 17:17:05 +0100 Subject: [PATCH 043/360] add PTH ruff selection --- freqtrade/freqai/data_drawer.py | 32 ++++++++++++++++---------------- freqtrade/freqai/utils.py | 2 +- pyproject.toml | 2 +- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/freqtrade/freqai/data_drawer.py b/freqtrade/freqai/data_drawer.py index c90bb23fc..03e5ba56e 100644 --- a/freqtrade/freqai/data_drawer.py +++ b/freqtrade/freqai/data_drawer.py @@ -126,7 +126,7 @@ class FreqaiDataDrawer: """ exists = self.global_metadata_path.is_file() if exists: - with open(self.global_metadata_path, "r") as fp: + with self.global_metadata_path.open("r") as fp: metatada_dict = rapidjson.load(fp, number_mode=rapidjson.NM_NATIVE) return metatada_dict return {} @@ -139,7 +139,7 @@ class FreqaiDataDrawer: """ exists = self.pair_dictionary_path.is_file() if exists: - with open(self.pair_dictionary_path, "r") as fp: + with self.pair_dictionary_path.open("r") as fp: self.pair_dict = rapidjson.load(fp, number_mode=rapidjson.NM_NATIVE) else: logger.info("Could not find existing datadrawer, starting from scratch") @@ -152,7 +152,7 @@ class FreqaiDataDrawer: if self.freqai_info.get('write_metrics_to_disk', False): exists = self.metric_tracker_path.is_file() if exists: - with open(self.metric_tracker_path, "r") as fp: + with self.metric_tracker_path.open("r") as fp: self.metric_tracker = rapidjson.load(fp, number_mode=rapidjson.NM_NATIVE) logger.info("Loading existing metric tracker from disk.") else: @@ -166,7 +166,7 @@ class FreqaiDataDrawer: exists = self.historic_predictions_path.is_file() if exists: try: - with open(self.historic_predictions_path, "rb") as fp: + with self.historic_predictions_path.open("rb") as fp: self.historic_predictions = cloudpickle.load(fp) logger.info( f"Found existing historic predictions at {self.full_path}, but beware " @@ -176,7 +176,7 @@ class FreqaiDataDrawer: except EOFError: logger.warning( 'Historical prediction file was corrupted. Trying to load backup file.') - with open(self.historic_predictions_bkp_path, "rb") as fp: + with self.historic_predictions_bkp_path.open("rb") as fp: self.historic_predictions = cloudpickle.load(fp) logger.warning('FreqAI successfully loaded the backup historical predictions file.') @@ -189,7 +189,7 @@ class FreqaiDataDrawer: """ Save historic predictions pickle to disk """ - with open(self.historic_predictions_path, "wb") as fp: + with self.historic_predictions_path.open("wb") as fp: cloudpickle.dump(self.historic_predictions, fp, protocol=cloudpickle.DEFAULT_PROTOCOL) # create a backup @@ -200,16 +200,16 @@ class FreqaiDataDrawer: Save metric tracker of all pair metrics collected. """ with self.save_lock: - with open(self.metric_tracker_path, 'w') as fp: + with self.metric_tracker_path.open('w') as fp: rapidjson.dump(self.metric_tracker, fp, default=self.np_encoder, number_mode=rapidjson.NM_NATIVE) - def save_drawer_to_disk(self): + def save_drawer_to_disk(self) -> None: """ Save data drawer full of all pair model metadata in present model folder. """ with self.save_lock: - with open(self.pair_dictionary_path, 'w') as fp: + with self.pair_dictionary_path.open('w') as fp: rapidjson.dump(self.pair_dict, fp, default=self.np_encoder, number_mode=rapidjson.NM_NATIVE) @@ -218,7 +218,7 @@ class FreqaiDataDrawer: Save global metadata json to disk """ with self.save_lock: - with open(self.global_metadata_path, 'w') as fp: + with self.global_metadata_path.open('w') as fp: rapidjson.dump(metadata, fp, default=self.np_encoder, number_mode=rapidjson.NM_NATIVE) @@ -424,7 +424,7 @@ class FreqaiDataDrawer: dk.data["training_features_list"] = list(dk.data_dictionary["train_features"].columns) dk.data["label_list"] = dk.label_list - with open(save_path / f"{dk.model_filename}_metadata.json", "w") as fp: + with (save_path / f"{dk.model_filename}_metadata.json").open("w") as fp: rapidjson.dump(dk.data, fp, default=self.np_encoder, number_mode=rapidjson.NM_NATIVE) return @@ -457,7 +457,7 @@ class FreqaiDataDrawer: dk.data["training_features_list"] = dk.training_features_list dk.data["label_list"] = dk.label_list # store the metadata - with open(save_path / f"{dk.model_filename}_metadata.json", "w") as fp: + with (save_path / f"{dk.model_filename}_metadata.json").open("w") as fp: rapidjson.dump(dk.data, fp, default=self.np_encoder, number_mode=rapidjson.NM_NATIVE) # save the train data to file so we can check preds for area of applicability later @@ -471,7 +471,7 @@ class FreqaiDataDrawer: if self.freqai_info["feature_parameters"].get("principal_component_analysis"): cloudpickle.dump( - dk.pca, open(dk.data_path / f"{dk.model_filename}_pca_object.pkl", "wb") + dk.pca, (dk.data_path / f"{dk.model_filename}_pca_object.pkl").open("wb") ) self.model_dictionary[coin] = model @@ -491,7 +491,7 @@ class FreqaiDataDrawer: Load only metadata into datakitchen to increase performance during presaved backtesting (prediction file loading). """ - with open(dk.data_path / f"{dk.model_filename}_metadata.json", "r") as fp: + with (dk.data_path / f"{dk.model_filename}_metadata.json").open("r") as fp: dk.data = rapidjson.load(fp, number_mode=rapidjson.NM_NATIVE) dk.training_features_list = dk.data["training_features_list"] dk.label_list = dk.data["label_list"] @@ -514,7 +514,7 @@ class FreqaiDataDrawer: dk.data = self.meta_data_dictionary[coin]["meta_data"] dk.data_dictionary["train_features"] = self.meta_data_dictionary[coin]["train_df"] else: - with open(dk.data_path / f"{dk.model_filename}_metadata.json", "r") as fp: + with (dk.data_path / f"{dk.model_filename}_metadata.json").open("r") as fp: dk.data = rapidjson.load(fp, number_mode=rapidjson.NM_NATIVE) dk.data_dictionary["train_features"] = pd.read_pickle( @@ -552,7 +552,7 @@ class FreqaiDataDrawer: if self.config["freqai"]["feature_parameters"]["principal_component_analysis"]: dk.pca = cloudpickle.load( - open(dk.data_path / f"{dk.model_filename}_pca_object.pkl", "rb") + (dk.data_path / f"{dk.model_filename}_pca_object.pkl").open("rb") ) return model diff --git a/freqtrade/freqai/utils.py b/freqtrade/freqai/utils.py index 806e3ca15..2ba49ac40 100644 --- a/freqtrade/freqai/utils.py +++ b/freqtrade/freqai/utils.py @@ -211,7 +211,7 @@ def record_params(config: Dict[str, Any], full_path: Path) -> None: "pairs": config.get('exchange', {}).get('pair_whitelist') } - with open(params_record_path, "w") as handle: + with params_record_path.open("w") as handle: rapidjson.dump( run_params, handle, diff --git a/pyproject.toml b/pyproject.toml index 8a7750731..f53802dc6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,5 +68,5 @@ extend-select = [ # "DTZ", # flake8-datetimez # "RSE", # flake8-raise # "TCH", # flake8-type-checking - # "PTH", # flake8-use-pathlib + "PTH", # flake8-use-pathlib ] From 32ce819889c776192e12c975a4e3c507ab572843 Mon Sep 17 00:00:00 2001 From: ASU Date: Sat, 25 Feb 2023 18:23:07 +0200 Subject: [PATCH 044/360] Removed environment.yml and updated documentation --- docs/installation.md | 18 +++++++----------- environment.yml | 11 ----------- 2 files changed, 7 insertions(+), 22 deletions(-) delete mode 100644 environment.yml diff --git a/docs/installation.md b/docs/installation.md index 1c0aed7ba..6e8488b9f 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -290,10 +290,8 @@ cd freqtrade #### Freqtrade install: Conda Environment -Prepare conda-freqtrade environment, using file `environment.yml`, which exist in main freqtrade directory - ```bash -conda env create -n freqtrade-conda -f environment.yml +conda create --name freqtrade python=3.10 ``` !!! Note "Creating Conda Environment" @@ -302,12 +300,9 @@ conda env create -n freqtrade-conda -f environment.yml ```bash # choose your own packages conda env create -n [name of the environment] [python version] [packages] - - # point to file with packages - conda env create -n [name of the environment] -f [file] ``` -#### Enter/exit freqtrade-conda environment +#### Enter/exit freqtrade environment To check available environments, type @@ -319,7 +314,7 @@ Enter installed environment ```bash # enter conda environment -conda activate freqtrade-conda +conda activate freqtrade # exit conda environment - don't do it now conda deactivate @@ -329,6 +324,7 @@ Install last python dependencies with pip ```bash python3 -m pip install --upgrade pip +python3 -m pip install -r requirements.txt python3 -m pip install -e . ``` @@ -336,7 +332,7 @@ Patch conda libta-lib (Linux only) ```bash # Ensure that the environment is active! -conda activate freqtrade-conda +conda activate freqtrade cd build_helpers bash install_ta-lib.sh ${CONDA_PREFIX} nosudo @@ -355,8 +351,8 @@ conda env list # activate base environment conda activate -# activate freqtrade-conda environment -conda activate freqtrade-conda +# activate freqtrade environment +conda activate freqtrade #deactivate any conda environments conda deactivate diff --git a/environment.yml b/environment.yml deleted file mode 100644 index 5193947cf..000000000 --- a/environment.yml +++ /dev/null @@ -1,11 +0,0 @@ -name: freqtrade -channels: - - conda-forge -# - defaults -dependencies: - - python>=3.8,<=3.10 - - pip - - libta-lib=0.4.0 # this is required for ta-lib from requirements.txt - - pip: - - -r requirements.txt - - -e . From 84d905a648040783d49d7293db1662605977cfa9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 Feb 2023 17:39:18 +0100 Subject: [PATCH 045/360] Fix missed test --- tests/test_configuration.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 357e16702..aab868bec 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -73,7 +73,8 @@ def test_load_config_file_error(default_conf, mocker, caplog) -> None: default_conf['datadir'] = str(default_conf['datadir']) filedata = json.dumps(default_conf).replace( '"stake_amount": 0.001,', '"stake_amount": .001,') - mocker.patch('freqtrade.configuration.load_config.open', mocker.mock_open(read_data=filedata)) + mocker.patch('freqtrade.configuration.load_config.Path.open', + mocker.mock_open(read_data=filedata)) mocker.patch.object(Path, "read_text", MagicMock(return_value=filedata)) with pytest.raises(OperationalException, match=r".*Please verify the following segment.*"): From 305eda74e2e8ccff87b727cc6c034b4b5ded19b0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 Feb 2023 20:50:26 +0100 Subject: [PATCH 046/360] Enable Complexity for ruff --- freqtrade/commands/data_commands.py | 22 +++++++++++++--------- pyproject.toml | 7 +++++++ 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/freqtrade/commands/data_commands.py b/freqtrade/commands/data_commands.py index 2cd736b3e..1e74e1036 100644 --- a/freqtrade/commands/data_commands.py +++ b/freqtrade/commands/data_commands.py @@ -5,7 +5,7 @@ from datetime import datetime, timedelta from typing import Any, Dict, List from freqtrade.configuration import TimeRange, setup_utils_configuration -from freqtrade.constants import DATETIME_PRINT_FORMAT +from freqtrade.constants import DATETIME_PRINT_FORMAT, Config from freqtrade.data.converter import convert_ohlcv_format, convert_trades_format from freqtrade.data.history import (convert_trades_to_ohlcv, refresh_backtest_ohlcv_data, refresh_backtest_trades_data) @@ -20,15 +20,24 @@ from freqtrade.util.binance_mig import migrate_binance_futures_data logger = logging.getLogger(__name__) +def _data_download_sanity(config: Config) -> None: + if 'days' in config and 'timerange' in config: + raise OperationalException("--days and --timerange are mutually exclusive. " + "You can only specify one or the other.") + + if 'pairs' not in config: + raise OperationalException( + "Downloading data requires a list of pairs. " + "Please check the documentation on how to configure this.") + + def start_download_data(args: Dict[str, Any]) -> None: """ Download data (former download_backtest_data.py script) """ config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE) - if 'days' in config and 'timerange' in config: - raise OperationalException("--days and --timerange are mutually exclusive. " - "You can only specify one or the other.") + _data_download_sanity(config) timerange = TimeRange() if 'days' in config: time_since = (datetime.now() - timedelta(days=config['days'])).strftime("%Y%m%d") @@ -40,11 +49,6 @@ def start_download_data(args: Dict[str, Any]) -> None: # Remove stake-currency to skip checks which are not relevant for datadownload config['stake_currency'] = '' - if 'pairs' not in config: - raise OperationalException( - "Downloading data requires a list of pairs. " - "Please check the documentation on how to configure this.") - pairs_not_available: List[str] = [] # Init exchange diff --git a/pyproject.toml b/pyproject.toml index 8a7750731..698a621b4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,7 +61,11 @@ ignore = ["freqtrade/vendor/**"] [tool.ruff] line-length = 100 extend-exclude = [".env"] +target-version = "py38" extend-select = [ + "C90", # mccabe + # "N", # pep8-naming + # "UP", # pyupgrade "TID", # flake8-tidy-imports # "EXE", # flake8-executable "YTT", # flake8-2020 @@ -70,3 +74,6 @@ extend-select = [ # "TCH", # flake8-type-checking # "PTH", # flake8-use-pathlib ] + +[tool.ruff.mccabe] +max-complexity = 12 From e88bb4e05ca7cdd17656fe6aa118631c828e501d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 26 Feb 2023 15:09:25 +0100 Subject: [PATCH 047/360] Revert small change - otherwise the data is never updated. --- freqtrade/freqai/data_drawer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqai/data_drawer.py b/freqtrade/freqai/data_drawer.py index 883b9d94b..356100aea 100644 --- a/freqtrade/freqai/data_drawer.py +++ b/freqtrade/freqai/data_drawer.py @@ -661,7 +661,7 @@ class FreqaiDataDrawer: f"ends at {df_dp['date'].iloc[0]}." ) - hist_df = pd.concat( + history_data[pair][tf] = pd.concat( [ hist_df, df_dp.iloc[index:], From 79dc972e5a41e8b60733f30dd164e74702e7b4c2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 26 Feb 2023 15:25:54 +0100 Subject: [PATCH 048/360] Add explicit test for kucoin --- tests/exchange/test_kucoin.py | 42 +++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/tests/exchange/test_kucoin.py b/tests/exchange/test_kucoin.py index 65c855b7a..0a57d728c 100644 --- a/tests/exchange/test_kucoin.py +++ b/tests/exchange/test_kucoin.py @@ -125,3 +125,45 @@ def test_stoploss_adjust_kucoin(mocker, default_conf): # Test with invalid order case order['stopPrice'] = None assert exchange.stoploss_adjust(1501, order, 'sell') + + +@pytest.mark.parametrize("side", ["buy", "sell"]) +@pytest.mark.parametrize("ordertype,rate", [ + ("market", None), + ("market", 200), + ("limit", 200), + ("stop_loss_limit", 200) +]) +def test_kucoin_create_order(default_conf, mocker, side, ordertype, rate): + api_mock = MagicMock() + order_id = 'test_prod_{}_{}'.format(side, randint(0, 10 ** 6)) + api_mock.create_order = MagicMock(return_value={ + 'id': order_id, + 'info': { + 'foo': 'bar' + }, + 'symbol': 'XRP/USDT', + 'amount': 1 + }) + default_conf['dry_run'] = False + mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y) + mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id='kucoin') + exchange._set_leverage = MagicMock() + exchange.set_margin_mode = MagicMock() + + order = exchange.create_order( + pair='XRP/USDT', + ordertype=ordertype, + side=side, + amount=1, + rate=rate, + leverage=1.0 + ) + + assert 'id' in order + assert 'info' in order + assert order['id'] == order_id + assert order['amount'] == 1 + # Status must be faked to open for kucoin. + assert order['status'] == 'open' From 27676f4aa291b9433be89a322ab7c926ddbe2912 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 26 Feb 2023 15:30:37 +0100 Subject: [PATCH 049/360] Add explicit bybit test --- tests/exchange/test_exchange.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index df878dbd3..828bba16f 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -27,7 +27,7 @@ from tests.conftest import (generate_test_data_raw, get_mock_coro, get_patched_e # Make sure to always keep one exchange here which is NOT subclassed!! -EXCHANGES = ['bittrex', 'binance', 'kraken', 'gate'] +EXCHANGES = ['bittrex', 'binance', 'kraken', 'gate', 'bybit'] get_entry_rate_data = [ ('other', 20, 19, 10, 0.0, 20), # Full ask side @@ -5015,7 +5015,7 @@ def test_get_max_leverage_futures(default_conf, mocker, leverage_tiers): exchange.get_max_leverage("BTC/USDT:USDT", 1000000000.01) -@pytest.mark.parametrize("exchange_name", ['bittrex', 'binance', 'kraken', 'gate', 'okx']) +@pytest.mark.parametrize("exchange_name", ['bittrex', 'binance', 'kraken', 'gate', 'okx', 'bybit']) def test__get_params(mocker, default_conf, exchange_name): api_mock = MagicMock() mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True) @@ -5036,6 +5036,9 @@ def test__get_params(mocker, default_conf, exchange_name): params2['tdMode'] = 'isolated' params2['posSide'] = 'net' + if exchange_name == 'bybit': + params2['position_idx'] = 0 + assert exchange._get_params( side="buy", ordertype='market', From 6f7ab97fc363daa90a9c1767498a80192f69edef Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 26 Feb 2023 15:41:42 +0100 Subject: [PATCH 050/360] Improve bybit test coverage --- tests/exchange/test_bybit.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/exchange/test_bybit.py b/tests/exchange/test_bybit.py index 7c8324bf6..d0d5114a1 100644 --- a/tests/exchange/test_bybit.py +++ b/tests/exchange/test_bybit.py @@ -1,3 +1,4 @@ +from datetime import datetime, timezone from unittest.mock import MagicMock from freqtrade.enums.marginmode import MarginMode @@ -55,3 +56,19 @@ async def test_bybit_fetch_funding_rate(default_conf, mocker): kwargs = api_mock.fetch_funding_rate_history.call_args_list[0][1] assert kwargs['params'] == {'until': since_ms_end} assert kwargs['since'] == since_ms + + +def test_bybit_get_funding_fees(default_conf, mocker): + now = datetime.now(timezone.utc) + exchange = get_patched_exchange(mocker, default_conf, id='bybit') + exchange._fetch_and_calculate_funding_fees = MagicMock() + exchange.get_funding_fees('BTC/USDT:USDT', 1, False, now) + assert exchange._fetch_and_calculate_funding_fees.call_count == 0 + + default_conf['trading_mode'] = 'futures' + default_conf['margin_mode'] = 'isolated' + exchange = get_patched_exchange(mocker, default_conf, id='bybit') + exchange._fetch_and_calculate_funding_fees = MagicMock() + exchange.get_funding_fees('BTC/USDT:USDT', 1, False, now) + + assert exchange._fetch_and_calculate_funding_fees.call_count == 1 From 5b0bc5bbc54d7122f677a8ff9b615238e7940622 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 26 Feb 2023 16:11:47 +0100 Subject: [PATCH 051/360] Don't "fix" dry-run kucoin orders closes #8229 --- freqtrade/exchange/kucoin.py | 5 +++-- tests/exchange/test_exchange.py | 11 ++++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/freqtrade/exchange/kucoin.py b/freqtrade/exchange/kucoin.py index 797d9fbd2..20e558513 100644 --- a/freqtrade/exchange/kucoin.py +++ b/freqtrade/exchange/kucoin.py @@ -64,6 +64,7 @@ class Kucoin(Exchange): # ccxt returns status = 'closed' at the moment - which is information ccxt invented. # Since we rely on status heavily, we must set it to 'open' here. # ref: https://github.com/ccxt/ccxt/pull/16674, (https://github.com/ccxt/ccxt/pull/16553) - res['type'] = ordertype - res['status'] = 'open' + if not self._config['dry_run']: + res['type'] = ordertype + res['status'] = 'open' return res diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 828bba16f..7ccd32155 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -27,7 +27,7 @@ from tests.conftest import (generate_test_data_raw, get_mock_coro, get_patched_e # Make sure to always keep one exchange here which is NOT subclassed!! -EXCHANGES = ['bittrex', 'binance', 'kraken', 'gate', 'bybit'] +EXCHANGES = ['bittrex', 'binance', 'kraken', 'gate', 'kucoin', 'bybit'] get_entry_rate_data = [ ('other', 20, 19, 10, 0.0, 20), # Full ask side @@ -1269,7 +1269,7 @@ def test_create_dry_run_order_limit_fill(default_conf, mocker, side, price, fill fetch_l2_order_book=order_book_l2_usd, ) - order = exchange.create_dry_run_order( + order = exchange.create_order( pair='LTC/USDT', ordertype='limit', side=side, @@ -1332,7 +1332,7 @@ def test_create_dry_run_order_market_fill(default_conf, mocker, side, rate, amou fetch_l2_order_book=order_book_l2_usd, ) - order = exchange.create_dry_run_order( + order = exchange.create_order( pair='LTC/USDT', ordertype='market', side=side, @@ -1425,9 +1425,10 @@ def test_create_order(default_conf, mocker, side, ordertype, rate, marketprice, assert order['amount'] == 0.01 -def test_buy_dry_run(default_conf, mocker): +@pytest.mark.parametrize("exchange_name", EXCHANGES) +def test_buy_dry_run(default_conf, mocker, exchange_name): default_conf['dry_run'] = True - exchange = get_patched_exchange(mocker, default_conf) + exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) order = exchange.create_order(pair='ETH/BTC', ordertype='limit', side="buy", amount=1, rate=200, leverage=1.0, From 533f97f0801065d6c82da5b71815f40bffe66fa0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Feb 2023 03:56:50 +0000 Subject: [PATCH 052/360] Bump pydantic from 1.10.4 to 1.10.5 Bumps [pydantic](https://github.com/pydantic/pydantic) from 1.10.4 to 1.10.5. - [Release notes](https://github.com/pydantic/pydantic/releases) - [Changelog](https://github.com/pydantic/pydantic/blob/v1.10.5/HISTORY.md) - [Commits](https://github.com/pydantic/pydantic/compare/v1.10.4...v1.10.5) --- updated-dependencies: - dependency-name: pydantic 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 855aa664d..da990721f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -35,7 +35,7 @@ sdnotify==0.3.2 # API Server fastapi==0.92.0 -pydantic==1.10.4 +pydantic==1.10.5 uvicorn==0.20.0 pyjwt==2.6.0 aiofiles==23.1.0 From cc78054b8c70f2de6b5ca96008f2773c61139787 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Feb 2023 03:56:57 +0000 Subject: [PATCH 053/360] Bump types-python-dateutil from 2.8.19.6 to 2.8.19.8 Bumps [types-python-dateutil](https://github.com/python/typeshed) from 2.8.19.6 to 2.8.19.8. - [Release notes](https://github.com/python/typeshed/releases) - [Commits](https://github.com/python/typeshed/commits) --- updated-dependencies: - dependency-name: types-python-dateutil dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index c23447694..97b5f3e7e 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -29,4 +29,4 @@ types-cachetools==5.3.0.0 types-filelock==3.2.7 types-requests==2.28.11.13 types-tabulate==0.9.0.0 -types-python-dateutil==2.8.19.6 +types-python-dateutil==2.8.19.8 From 7add902bc7c47067b3aacba8e7071ef9c5855095 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Feb 2023 03:57:04 +0000 Subject: [PATCH 054/360] Bump pre-commit from 3.0.4 to 3.1.0 Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 3.0.4 to 3.1.0. - [Release notes](https://github.com/pre-commit/pre-commit/releases) - [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md) - [Commits](https://github.com/pre-commit/pre-commit/compare/v3.0.4...v3.1.0) --- updated-dependencies: - dependency-name: pre-commit dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index c23447694..4a5a959f2 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -9,7 +9,7 @@ coveralls==3.3.1 ruff==0.0.251 mypy==1.0.1 -pre-commit==3.0.4 +pre-commit==3.1.0 pytest==7.2.1 pytest-asyncio==0.20.3 pytest-cov==4.0.0 From 2a7f86bfb4834d83f73d8ac0abae65a5fcd9310a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Feb 2023 03:57:22 +0000 Subject: [PATCH 055/360] Bump ruff from 0.0.251 to 0.0.252 Bumps [ruff](https://github.com/charliermarsh/ruff) from 0.0.251 to 0.0.252. - [Release notes](https://github.com/charliermarsh/ruff/releases) - [Changelog](https://github.com/charliermarsh/ruff/blob/main/BREAKING_CHANGES.md) - [Commits](https://github.com/charliermarsh/ruff/compare/v0.0.251...v0.0.252) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index c23447694..a0d362f55 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -7,7 +7,7 @@ -r docs/requirements-docs.txt coveralls==3.3.1 -ruff==0.0.251 +ruff==0.0.252 mypy==1.0.1 pre-commit==3.0.4 pytest==7.2.1 From a4423778d552e843d922fa5d285b7af693ab1999 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Feb 2023 03:58:01 +0000 Subject: [PATCH 056/360] Bump mkdocs-material from 9.0.13 to 9.0.15 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 9.0.13 to 9.0.15. - [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/9.0.13...9.0.15) --- 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 f5e671e88..065411018 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,6 +1,6 @@ markdown==3.3.7 mkdocs==1.4.2 -mkdocs-material==9.0.13 +mkdocs-material==9.0.15 mdx_truly_sane_lists==1.3 pymdown-extensions==9.9.2 jinja2==3.1.2 From 05f3884722404b71b499c18a99fca72fa3419b67 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 27 Feb 2023 06:25:13 +0100 Subject: [PATCH 057/360] bump pre-commit dateutil --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 58f526ce9..0936fff74 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: - types-filelock==3.2.7 - types-requests==2.28.11.13 - types-tabulate==0.9.0.0 - - types-python-dateutil==2.8.19.6 + - types-python-dateutil==2.8.19.8 # stages: [push] - repo: https://github.com/pycqa/isort From e83eefb71d1197891c982cf466929082a28f8b0f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Feb 2023 06:10:08 +0000 Subject: [PATCH 058/360] Bump types-requests from 2.28.11.13 to 2.28.11.15 Bumps [types-requests](https://github.com/python/typeshed) from 2.28.11.13 to 2.28.11.15. - [Release notes](https://github.com/python/typeshed/releases) - [Commits](https://github.com/python/typeshed/commits) --- updated-dependencies: - dependency-name: types-requests dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index ad9d303cf..19130ec8e 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -27,6 +27,6 @@ nbconvert==7.2.9 # mypy types types-cachetools==5.3.0.0 types-filelock==3.2.7 -types-requests==2.28.11.13 +types-requests==2.28.11.15 types-tabulate==0.9.0.0 types-python-dateutil==2.8.19.8 From 48b21d00d2afb8705a3c78259d39c007c35365a5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 27 Feb 2023 07:12:12 +0100 Subject: [PATCH 059/360] bump pre-commit requests --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0936fff74..436d7bc09 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: additional_dependencies: - types-cachetools==5.3.0.0 - types-filelock==3.2.7 - - types-requests==2.28.11.13 + - types-requests==2.28.11.15 - types-tabulate==0.9.0.0 - types-python-dateutil==2.8.19.8 # stages: [push] From 201522f1b170de855cbb33d9b7d21060a31ea31f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Feb 2023 07:10:51 +0000 Subject: [PATCH 060/360] Bump types-tabulate from 0.9.0.0 to 0.9.0.1 Bumps [types-tabulate](https://github.com/python/typeshed) from 0.9.0.0 to 0.9.0.1. - [Release notes](https://github.com/python/typeshed/releases) - [Commits](https://github.com/python/typeshed/commits) --- updated-dependencies: - dependency-name: types-tabulate dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 19130ec8e..f45daaabd 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -28,5 +28,5 @@ nbconvert==7.2.9 types-cachetools==5.3.0.0 types-filelock==3.2.7 types-requests==2.28.11.15 -types-tabulate==0.9.0.0 +types-tabulate==0.9.0.1 types-python-dateutil==2.8.19.8 From 81bc515e5ddac9f11d8d280b8b3a81290c0d5142 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 27 Feb 2023 10:00:41 +0100 Subject: [PATCH 061/360] Bump tabulate types for pre-commit --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 436d7bc09..8711a65dc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,7 +16,7 @@ repos: - types-cachetools==5.3.0.0 - types-filelock==3.2.7 - types-requests==2.28.11.15 - - types-tabulate==0.9.0.0 + - types-tabulate==0.9.0.1 - types-python-dateutil==2.8.19.8 # stages: [push] From 1d5608d627966d4fae47ba11b5aff416f5961f83 Mon Sep 17 00:00:00 2001 From: ASU Date: Mon, 27 Feb 2023 12:14:38 +0200 Subject: [PATCH 062/360] Fix last_process related bug in RPC.health --- freqtrade/freqtradebot.py | 2 +- freqtrade/rpc/api_server/api_schemas.py | 4 ++-- freqtrade/rpc/api_server/api_v1.py | 2 +- freqtrade/rpc/rpc.py | 17 ++++++++++++----- freqtrade/rpc/telegram.py | 2 +- tests/rpc/test_rpc.py | 6 +++--- tests/rpc/test_rpc_apiserver.py | 4 ++-- 7 files changed, 22 insertions(+), 15 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 82be6f3b5..6529037e8 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -127,7 +127,7 @@ class FreqtradeBot(LoggingMixin): for minutes in [0, 15, 30, 45]: t = str(time(time_slot, minutes, 2)) self._schedule.every().day.at(t).do(update) - self.last_process = datetime(1970, 1, 1, tzinfo=timezone.utc) + self.last_process: Optional[datetime] = None self.strategy.ft_bot_start() # Initialize protections AFTER bot start - otherwise parameters are not loaded. diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 58f6ad583..b9595a3dd 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -456,5 +456,5 @@ class SysInfo(BaseModel): class Health(BaseModel): - last_process: datetime - last_process_ts: int + last_process: Optional[datetime] + last_process_ts: Optional[int] diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index 73bdde86b..f6bab3624 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -346,4 +346,4 @@ def sysinfo(): @router.get('/health', response_model=Health, tags=['info']) def health(rpc: RPC = Depends(get_rpc)): - return rpc._health() + return rpc.health() diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 83bffb779..08bf8d5c8 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -89,7 +89,7 @@ class RPC: # Bind _fiat_converter if needed _fiat_converter: Optional[CryptoToFiatConverter] = None - def __init__(self, freqtrade) -> None: + def __init__(self, freqtrade: "FreqtradeBot") -> None: """ Initializes all enabled rpc modules :param freqtrade: Instance of a freqtrade bot @@ -1198,10 +1198,17 @@ class RPC: "ram_pct": psutil.virtual_memory().percent } - def _health(self) -> Dict[str, Union[str, int]]: + def health(self) -> Dict[str, Optional[Union[str, int]]]: last_p = self._freqtrade.last_process + if last_p is None: + return { + "last_process": None, + "last_process_loc": None, + "last_process_ts": None, + } + return { - 'last_process': str(last_p), - 'last_process_loc': last_p.astimezone(tzlocal()).strftime(DATETIME_PRINT_FORMAT), - 'last_process_ts': int(last_p.timestamp()), + "last_process": str(last_p), + "last_process_loc": last_p.astimezone(tzlocal()).strftime(DATETIME_PRINT_FORMAT), + "last_process_ts": int(last_p.timestamp()), } diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index fbd675d02..09032f10d 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -1527,7 +1527,7 @@ class Telegram(RPCHandler): Handler for /health Shows the last process timestamp """ - health = self._rpc._health() + health = self._rpc.health() message = f"Last process: `{health['last_process_loc']}`" self._send_msg(message) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 31e19ce3f..d9b7c764a 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -1252,6 +1252,6 @@ def test_rpc_health(mocker, default_conf) -> None: freqtradebot = get_patched_freqtradebot(mocker, default_conf) rpc = RPC(freqtradebot) - result = rpc._health() - assert result['last_process'] == '1970-01-01 00:00:00+00:00' - assert result['last_process_ts'] == 0 + result = rpc.health() + assert result['last_process'] is None + assert result['last_process_ts'] is None diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 43d9abb78..9c6f33046 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -1801,8 +1801,8 @@ def test_health(botclient): assert_response(rc) ret = rc.json() - assert ret['last_process_ts'] == 0 - assert ret['last_process'] == '1970-01-01T00:00:00+00:00' + assert ret["last_process_ts"] is None + assert ret["last_process"] is None def test_api_ws_subscribe(botclient, mocker): From bcd416c83d7f6abe7adb99f0653dbe1c8bcef5dc Mon Sep 17 00:00:00 2001 From: ASU Date: Mon, 27 Feb 2023 16:18:24 +0200 Subject: [PATCH 063/360] Removed unresolved FreqTrade typehint --- freqtrade/rpc/rpc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 08bf8d5c8..949ace595 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -89,7 +89,7 @@ class RPC: # Bind _fiat_converter if needed _fiat_converter: Optional[CryptoToFiatConverter] = None - def __init__(self, freqtrade: "FreqtradeBot") -> None: + def __init__(self, freqtrade) -> None: """ Initializes all enabled rpc modules :param freqtrade: Instance of a freqtrade bot From 02c831a4e7c0316e46f911eca2beb5917c982ce7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 27 Feb 2023 18:04:21 +0100 Subject: [PATCH 064/360] Improve Note wording closes #8235 --- docs/strategy-customization.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 462f20402..3519a80cd 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -954,12 +954,14 @@ In some situations it may be confusing to deal with stops relative to current ra ## Additional data (Wallets) -The strategy provides access to the `Wallets` object. This contains the current balances on the exchange. +The strategy provides access to the `wallets` object. This contains the current balances on the exchange. -!!! Note - Wallets is not available during backtesting / hyperopt. +!!! Note "Backtesting / Hyperopt" + Wallets behaves differently depending on the function it's called. + Within `populate_*()` methods, it'll return the full wallet as configured. + Within [callbacks](strategy-callbacks.md), you'll get the wallet state corresponding to the actual simulated wallet at that point in the simulation process. -Please always check if `Wallets` is available to avoid failures during backtesting. +Please always check if `wallets` is available to avoid failures during backtesting. ``` python if self.wallets: From 87fe4108a252ca31cbb0200a452ef283a41e8c35 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 27 Feb 2023 18:24:19 +0100 Subject: [PATCH 065/360] Fix order numeration to also work with stoploss on exchange --- freqtrade/rpc/telegram.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index fbd675d02..d5c76bdb7 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -469,26 +469,27 @@ class Telegram(RPCHandler): lines_detail: List[str] = [] if len(filled_orders) > 0: first_avg = filled_orders[0]["safe_price"] - - for x, order in enumerate(filled_orders): + order_nr = 0 + for order in filled_orders: lines: List[str] = [] if order['is_open'] is True: continue + order_nr += 1 wording = 'Entry' if order['ft_is_entry'] else 'Exit' cur_entry_datetime = arrow.get(order["order_filled_date"]) cur_entry_amount = order["filled"] or order["amount"] cur_entry_average = order["safe_price"] lines.append(" ") - if x == 0: - lines.append(f"*{wording} #{x+1}:*") + if order_nr == 1: + lines.append(f"*{wording} #{order_nr}:*") lines.append( f"*Amount:* {cur_entry_amount} ({order['cost']:.8f} {quote_currency})") lines.append(f"*Average Price:* {cur_entry_average}") else: sumA = 0 sumB = 0 - for y in range(x): + for y in range(order_nr): amount = filled_orders[y]["filled"] or filled_orders[y]["amount"] sumA += amount * filled_orders[y]["safe_price"] sumB += amount @@ -499,7 +500,7 @@ class Telegram(RPCHandler): if prev_avg_price: minus_on_entry = (cur_entry_average - prev_avg_price) / prev_avg_price - lines.append(f"*{wording} #{x+1}:* at {minus_on_entry:.2%} avg profit") + lines.append(f"*{wording} #{order_nr}:* at {minus_on_entry:.2%} avg profit") if is_open: lines.append("({})".format(cur_entry_datetime .humanize(granularity=["day", "hour", "minute"]))) @@ -518,6 +519,7 @@ class Telegram(RPCHandler): # lines.append( # f"({days}d {hours}h {minutes}m {seconds}s from previous {wording.lower()})") lines_detail.append("\n".join(lines)) + return lines_detail @authorized_only From e482feed7dea7d017c8ffc4023e6a449bf4eb1ea Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 27 Feb 2023 19:40:02 +0100 Subject: [PATCH 066/360] Further improve behavior for telegram /status with stop on exchange --- freqtrade/rpc/telegram.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index d5c76bdb7..de4741f6b 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -487,13 +487,17 @@ class Telegram(RPCHandler): f"*Amount:* {cur_entry_amount} ({order['cost']:.8f} {quote_currency})") lines.append(f"*Average Price:* {cur_entry_average}") else: - sumA = 0 - sumB = 0 + sum_stake = 0 + sum_amount = 0 for y in range(order_nr): - amount = filled_orders[y]["filled"] or filled_orders[y]["amount"] - sumA += amount * filled_orders[y]["safe_price"] - sumB += amount - prev_avg_price = sumA / sumB + loc_order = filled_orders[y] + if loc_order['is_open'] is True: + # Skip open orders (e.g. stop orders) + continue + amount = loc_order["filled"] or loc_order["amount"] + sum_stake += amount * loc_order["safe_price"] + sum_amount += amount + prev_avg_price = sum_stake / sum_amount # TODO: This calculation ignores fees. price_to_1st_entry = ((cur_entry_average - first_avg) / first_avg) minus_on_entry = 0 From e5c68661feb16b9c43daf7bd25e76c3121d668b8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 27 Feb 2023 19:57:28 +0100 Subject: [PATCH 067/360] Simplify code line wrapping --- freqtrade/freqtradebot.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 82be6f3b5..7cea4d329 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1275,8 +1275,7 @@ class FreqtradeBot(LoggingMixin): if order['side'] == trade.entry_side: self.handle_cancel_enter(trade, order, reason) else: - canceled = self.handle_cancel_exit( - trade, order, reason) + canceled = self.handle_cancel_exit(trade, order, reason) canceled_count = trade.get_exit_order_count() max_timeouts = self.config.get('unfilledtimeout', {}).get('exit_timeout_count', 0) if canceled and max_timeouts > 0 and canceled_count >= max_timeouts: From 75d1dd2793a887b38102469b6be1708fbb42e40f Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 27 Feb 2023 20:28:55 +0100 Subject: [PATCH 068/360] Properly round Stake currencies in telegram message --- freqtrade/rpc/telegram.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index de4741f6b..cb7d4515f 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -508,8 +508,8 @@ class Telegram(RPCHandler): if is_open: lines.append("({})".format(cur_entry_datetime .humanize(granularity=["day", "hour", "minute"]))) - lines.append( - f"*Amount:* {cur_entry_amount} ({order['cost']:.8f} {quote_currency})") + lines.append(f"*Amount:* {cur_entry_amount} " + f"({round_coin_value(order['cost'], quote_currency)})") lines.append(f"*Average {wording} Price:* {cur_entry_average} " f"({price_to_1st_entry:.2%} from 1st entry rate)") lines.append(f"*Order filled:* {order['order_filled_date']}") @@ -560,13 +560,14 @@ class Telegram(RPCHandler): r['open_date_hum'] = arrow.get(r['open_date']).humanize() r['num_entries'] = len([o for o in r['orders'] if o['ft_is_entry']]) r['exit_reason'] = r.get('exit_reason', "") + r['rounded_stake_amount'] = round_coin_value(r['stake_amount'], r['quote_currency']) lines = [ "*Trade ID:* `{trade_id}`" + (" `(since {open_date_hum})`" if r['is_open'] else ""), "*Current Pair:* {pair}", "*Direction:* " + ("`Short`" if r.get('is_short') else "`Long`"), "*Leverage:* `{leverage}`" if r.get('leverage') else "", - "*Amount:* `{amount} ({stake_amount} {quote_currency})`", + "*Amount:* `{amount} ({rounded_stake_amount})`", "*Enter Tag:* `{enter_tag}`" if r['enter_tag'] else "", "*Exit Reason:* `{exit_reason}`" if r['exit_reason'] else "", ] From 46b987042b194b53edf71c2b4270babd86669ed5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 27 Feb 2023 20:31:02 +0100 Subject: [PATCH 069/360] Include realized_profit in api output --- freqtrade/rpc/api_server/api_schemas.py | 1 + tests/rpc/test_rpc_apiserver.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 58f6ad583..0831b390f 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -244,6 +244,7 @@ class TradeSchema(BaseModel): profit_pct: Optional[float] profit_abs: Optional[float] profit_fiat: Optional[float] + realized_profit: float exit_reason: Optional[str] exit_order_status: Optional[str] stop_loss_abs: Optional[float] diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 43d9abb78..03e8ca837 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -1015,6 +1015,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets, is_short, 'profit_pct': ANY, 'profit_abs': ANY, 'profit_fiat': ANY, + 'realized_profit': 0.0, 'current_rate': current_rate, 'open_date': ANY, 'open_timestamp': ANY, @@ -1244,6 +1245,7 @@ def test_api_force_entry(botclient, mocker, fee, endpoint): 'profit_pct': None, 'profit_abs': None, 'profit_fiat': None, + 'realized_profit': 0.0, 'fee_close': 0.0025, 'fee_close_cost': None, 'fee_close_currency': None, From 65d1598a90f4a395f3b1929d3e8887875bdff5ef Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 27 Feb 2023 20:50:25 +0100 Subject: [PATCH 070/360] Show absolute profit in /status command --- freqtrade/rpc/telegram.py | 3 ++- tests/rpc/test_rpc_telegram.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index cb7d4515f..34b54e047 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -561,6 +561,7 @@ class Telegram(RPCHandler): r['num_entries'] = len([o for o in r['orders'] if o['ft_is_entry']]) r['exit_reason'] = r.get('exit_reason', "") r['rounded_stake_amount'] = round_coin_value(r['stake_amount'], r['quote_currency']) + r['rounded_profit_abs'] = round_coin_value(r['profit_abs'], r['quote_currency']) lines = [ "*Trade ID:* `{trade_id}`" + (" `(since {open_date_hum})`" if r['is_open'] else ""), @@ -583,7 +584,7 @@ class Telegram(RPCHandler): "*Close Date:* `{close_date}`" if r['close_date'] else "", "*Current Rate:* `{current_rate:.8f}`" if r['is_open'] else "", ("*Current Profit:* " if r['is_open'] else "*Close Profit: *") - + "`{profit_ratio:.2%}`", + + "`{profit_ratio:.2%}` `({rounded_profit_abs})`", ]) if r['is_open']: diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 855062af0..974cc45f7 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -202,6 +202,7 @@ def test_telegram_status(default_conf, update, mocker) -> None: 'close_profit_ratio': None, 'profit': -0.0059, 'profit_ratio': -0.0059, + 'profit_abs': -0.225, 'initial_stop_loss_abs': 1.098e-05, 'stop_loss_abs': 1.099e-05, 'exit_order_status': None, From 39331b59ed011d40f1d314624ee30f88cda6fdd0 Mon Sep 17 00:00:00 2001 From: Rahul Date: Mon, 27 Feb 2023 22:51:22 +0000 Subject: [PATCH 071/360] Fixed issues raised in PR --- docs/telegram-usage.md | 14 +++++++--- freqtrade/enums/marketstatetype.py | 6 ++++- freqtrade/rpc/rpc.py | 5 +++- freqtrade/rpc/telegram.py | 42 ++++++++++++++++++++---------- 4 files changed, 47 insertions(+), 20 deletions(-) diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index a4145df02..dfab3754c 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -179,7 +179,7 @@ official commands. You can ask at any moment for help with `/help`. | `/count` | Displays number of trades used and available | `/locks` | Show currently locked pairs. | `/unlock ` | Remove the lock for this pair (or for this lock id). -| `/marketdir [long | short | even | none]` | Updates the user managed variable that represents the current market direction. +| `/marketdir [long | short | even | none]` | Updates the user managed variable that represents the current market direction. If no direction is provided, the currently set direction will be displayed. | **Modify Trade states** | | `/forceexit | /fx ` | Instantly exits the given trade (Ignoring `minimum_roi`). | `/forceexit all | /fx all` | Instantly exits all open trades (Ignoring `minimum_roi`). @@ -420,6 +420,12 @@ ARDR/ETH 0.366667 0.143059 -0.01 ### /marketdir -Updates the user managed variable that represents the current market direction. This variable is not set -to any valid market direction on bot startup and must be set by the user. As an example `/marketdir long` -would set the variable to be `long`. +If a market direction is provided the command updates the user managed variable that represents the current market direction. +This variable is not set to any valid market direction on bot startup and must be set by the user. The example below is for `/marketdir long`: +``` +Successfully updated marketdirection from none to long. +``` +If no market direction is provided the command outputs the currently set market directions. The example below is for `/marketdir`: +``` +Currently set marketdirection: even +``` diff --git a/freqtrade/enums/marketstatetype.py b/freqtrade/enums/marketstatetype.py index 8132be74a..5cede32c2 100644 --- a/freqtrade/enums/marketstatetype.py +++ b/freqtrade/enums/marketstatetype.py @@ -8,4 +8,8 @@ class MarketDirection(Enum): LONG = "long" SHORT = "short" EVEN = "even" - NONE = '' + NONE = "none" + + def __str__(self): + # convert to string + return self.value diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index f1e6c15e6..d2e66cfff 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -1206,5 +1206,8 @@ class RPC: 'last_process_ts': int(last_p.timestamp()), } - def _update_market_direction(self, direction: MarketDirection): + def _update_market_direction(self, direction: MarketDirection) -> None: self._freqtrade.strategy.market_direction = direction + + def _get_market_direction(self) -> MarketDirection: + return self._freqtrade.strategy.market_direction diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 5f682b436..050dc3f31 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -129,7 +129,8 @@ class Telegram(RPCHandler): r'/weekly$', r'/weekly \d+$', r'/monthly$', r'/monthly \d+$', r'/forcebuy$', r'/forcelong$', r'/forceshort$', r'/forcesell$', r'/forceexit$', - r'/edge$', r'/health$', r'/help$', r'/version$', r'/marketdir (long|short|even|none)$' + r'/edge$', r'/health$', r'/help$', r'/version$', r'/marketdir (long|short|even|none)$', + r'/marketdir$' ] # Create keys for generation valid_keys_print = [k.replace('$', '') for k in valid_keys] @@ -1495,8 +1496,9 @@ class Telegram(RPCHandler): "*/count:* `Show number of active trades compared to allowed number of trades`\n" "*/edge:* `Shows validated pairs by Edge if it is enabled` \n" "*/health* `Show latest process timestamp - defaults to 1970-01-01 00:00:00` \n" - "*/marketdir [long | short | even | none]:* `Updates the user managed variable" - " that represents the current market direction` \n" + "*/marketdir [long | short | even | none]:* `Updates the user managed variable " + "that represents the current market direction. If no direction is provided `" + "`the currently set market direction will be output.` \n" "_Statistics_\n" "------------\n" @@ -1691,16 +1693,28 @@ class Telegram(RPCHandler): :return: None """ if context.args and len(context.args) == 1: - new_market_dir = context.args[0] - if new_market_dir == "long": - self._rpc._update_market_direction(MarketDirection.LONG) - elif new_market_dir == "short": - self._rpc._update_market_direction(MarketDirection.SHORT) - elif new_market_dir == "even": - self._rpc._update_market_direction(MarketDirection.EVEN) - elif new_market_dir == "none": - self._rpc._update_market_direction(MarketDirection.NONE) + new_market_dir_arg = context.args[0] + old_market_dir = self._rpc._get_market_direction() + new_market_dir = None + if new_market_dir_arg == "long": + new_market_dir = MarketDirection.LONG + elif new_market_dir_arg == "short": + new_market_dir = MarketDirection.SHORT + elif new_market_dir_arg == "even": + new_market_dir = MarketDirection.EVEN + elif new_market_dir_arg == "none": + new_market_dir = MarketDirection.NONE + + if new_market_dir is not None: + self._rpc._update_market_direction(new_market_dir) + self._send_msg("Successfully updated market direction" + f" from *{old_market_dir}* to *{new_market_dir}*.") else: - raise RPCException("Invalid market direction provided") + raise RPCException("Invalid market direction provided. \n" + "Valid market directions: *long, short, even, none*") + elif context.args is not None and len(context.args) == 0: + old_market_dir = self._rpc._get_market_direction() + self._send_msg(f"Currently set market direction: *{old_market_dir}*") else: - raise RPCException("Invalid usage of command /marketdir.") + raise RPCException("Invalid usage of command /marketdir. \n" + "Usage: */marketdir [short | long | even | none]*") From 0899e5cb8337adde4de58e0b80a578c4af0ed497 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 28 Feb 2023 06:41:18 +0100 Subject: [PATCH 072/360] Improve documentation wording --- docs/telegram-usage.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index dfab3754c..653d31ee6 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -422,10 +422,18 @@ ARDR/ETH 0.366667 0.143059 -0.01 If a market direction is provided the command updates the user managed variable that represents the current market direction. This variable is not set to any valid market direction on bot startup and must be set by the user. The example below is for `/marketdir long`: + ``` Successfully updated marketdirection from none to long. ``` + If no market direction is provided the command outputs the currently set market directions. The example below is for `/marketdir`: + ``` Currently set marketdirection: even ``` + +You can use the market direction in your strategy via `self.market_direction`. + +!!! Warning "Bot restarts" + Please note that the market direction is not persisted, and will be reset after a bot restart/reload. From a75e9f193f134a400868d2e74bd64fabb50214f4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 28 Feb 2023 05:52:21 +0000 Subject: [PATCH 073/360] Bump ruff from 0.0.252 to 0.0.253 Bumps [ruff](https://github.com/charliermarsh/ruff) from 0.0.252 to 0.0.253. - [Release notes](https://github.com/charliermarsh/ruff/releases) - [Changelog](https://github.com/charliermarsh/ruff/blob/main/BREAKING_CHANGES.md) - [Commits](https://github.com/charliermarsh/ruff/compare/v0.0.252...v0.0.253) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index f45daaabd..6754591a6 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -7,7 +7,7 @@ -r docs/requirements-docs.txt coveralls==3.3.1 -ruff==0.0.252 +ruff==0.0.253 mypy==1.0.1 pre-commit==3.1.0 pytest==7.2.1 From 6e45e998ac657c76f0ea399d01d664e8828dc3f2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 28 Feb 2023 05:52:29 +0000 Subject: [PATCH 074/360] Bump pre-commit from 3.1.0 to 3.1.1 Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 3.1.0 to 3.1.1. - [Release notes](https://github.com/pre-commit/pre-commit/releases) - [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md) - [Commits](https://github.com/pre-commit/pre-commit/compare/v3.1.0...v3.1.1) --- updated-dependencies: - dependency-name: pre-commit dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index f45daaabd..a187ceedc 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -9,7 +9,7 @@ coveralls==3.3.1 ruff==0.0.252 mypy==1.0.1 -pre-commit==3.1.0 +pre-commit==3.1.1 pytest==7.2.1 pytest-asyncio==0.20.3 pytest-cov==4.0.0 From 78e7ab92d8f833876c880e75667301884239fc78 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 28 Feb 2023 05:52:35 +0000 Subject: [PATCH 075/360] Bump plotly from 5.13.0 to 5.13.1 Bumps [plotly](https://github.com/plotly/plotly.py) from 5.13.0 to 5.13.1. - [Release notes](https://github.com/plotly/plotly.py/releases) - [Changelog](https://github.com/plotly/plotly.py/blob/master/CHANGELOG.md) - [Commits](https://github.com/plotly/plotly.py/compare/v5.13.0...v5.13.1) --- updated-dependencies: - dependency-name: plotly dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-plot.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-plot.txt b/requirements-plot.txt index b97d42fb6..ad7bade95 100644 --- a/requirements-plot.txt +++ b/requirements-plot.txt @@ -1,4 +1,4 @@ # Include all requirements to run the bot. -r requirements.txt -plotly==5.13.0 +plotly==5.13.1 From 1b4c831469b2d4a72ec0e29ea94ef7c2f10a1ffb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 28 Feb 2023 05:52:40 +0000 Subject: [PATCH 076/360] Bump prompt-toolkit from 3.0.36 to 3.0.37 Bumps [prompt-toolkit](https://github.com/prompt-toolkit/python-prompt-toolkit) from 3.0.36 to 3.0.37. - [Release notes](https://github.com/prompt-toolkit/python-prompt-toolkit/releases) - [Changelog](https://github.com/prompt-toolkit/python-prompt-toolkit/blob/master/CHANGELOG) - [Commits](https://github.com/prompt-toolkit/python-prompt-toolkit/compare/3.0.36...3.0.37) --- updated-dependencies: - dependency-name: prompt-toolkit dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index da990721f..903fc90a7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -45,7 +45,7 @@ psutil==5.9.4 colorama==0.4.6 # Building config files interactively questionary==1.10.0 -prompt-toolkit==3.0.36 +prompt-toolkit==3.0.37 # Extensions to datetime library python-dateutil==2.8.2 From adf5b7f233db199dca4014a5ded6a9900681be9f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 28 Feb 2023 05:52:48 +0000 Subject: [PATCH 077/360] Bump ccxt from 2.8.17 to 2.8.54 Bumps [ccxt](https://github.com/ccxt/ccxt) from 2.8.17 to 2.8.54. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/CHANGELOG.md) - [Commits](https://github.com/ccxt/ccxt/compare/2.8.17...2.8.54) --- 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 da990721f..9ed7e8943 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.24.2 pandas==1.5.3 pandas-ta==0.3.14b -ccxt==2.8.17 +ccxt==2.8.54 cryptography==39.0.1 aiohttp==3.8.4 SQLAlchemy==1.4.46 From fed5d87cfd72ddd2d54f553e63f17d3a10ad7247 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 28 Feb 2023 05:52:55 +0000 Subject: [PATCH 078/360] Bump xgboost from 1.7.3 to 1.7.4 Bumps [xgboost](https://github.com/dmlc/xgboost) from 1.7.3 to 1.7.4. - [Release notes](https://github.com/dmlc/xgboost/releases) - [Changelog](https://github.com/dmlc/xgboost/blob/master/NEWS.md) - [Commits](https://github.com/dmlc/xgboost/compare/v1.7.3...v1.7.4) --- updated-dependencies: - dependency-name: xgboost dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-freqai.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-freqai.txt b/requirements-freqai.txt index cf5bc4c0b..5b27ecf95 100644 --- a/requirements-freqai.txt +++ b/requirements-freqai.txt @@ -7,5 +7,5 @@ scikit-learn==1.1.3 joblib==1.2.0 catboost==1.1.1; platform_machine != 'aarch64' lightgbm==3.3.5 -xgboost==1.7.3 +xgboost==1.7.4 tensorboard==2.12.0 From 594757d27dad985067380e6aec72f3fbdec65bed Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 28 Feb 2023 05:52:58 +0000 Subject: [PATCH 079/360] Bump types-python-dateutil from 2.8.19.8 to 2.8.19.9 Bumps [types-python-dateutil](https://github.com/python/typeshed) from 2.8.19.8 to 2.8.19.9. - [Release notes](https://github.com/python/typeshed/releases) - [Commits](https://github.com/python/typeshed/commits) --- updated-dependencies: - dependency-name: types-python-dateutil dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index f45daaabd..711ac68f3 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -29,4 +29,4 @@ types-cachetools==5.3.0.0 types-filelock==3.2.7 types-requests==2.28.11.15 types-tabulate==0.9.0.1 -types-python-dateutil==2.8.19.8 +types-python-dateutil==2.8.19.9 From deca5479f0ddd16d2c2ee16f338fd14c2d2af46f Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 28 Feb 2023 10:05:38 +0100 Subject: [PATCH 080/360] pre-commit dateutil-types --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8711a65dc..a0ff2e6d5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: - types-filelock==3.2.7 - types-requests==2.28.11.15 - types-tabulate==0.9.0.1 - - types-python-dateutil==2.8.19.8 + - types-python-dateutil==2.8.19.9 # stages: [push] - repo: https://github.com/pycqa/isort From 5a3f23f00caede88cb9b1ede2245a1d8bd6a5654 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 28 Feb 2023 09:26:32 +0000 Subject: [PATCH 081/360] Bump types-cachetools from 5.3.0.0 to 5.3.0.4 Bumps [types-cachetools](https://github.com/python/typeshed) from 5.3.0.0 to 5.3.0.4. - [Release notes](https://github.com/python/typeshed/releases) - [Commits](https://github.com/python/typeshed/commits) --- updated-dependencies: - dependency-name: types-cachetools dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 3fee2dc65..2ba004f8d 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -25,7 +25,7 @@ httpx==0.23.3 nbconvert==7.2.9 # mypy types -types-cachetools==5.3.0.0 +types-cachetools==5.3.0.4 types-filelock==3.2.7 types-requests==2.28.11.15 types-tabulate==0.9.0.1 From 9a5b0908940d58af2cfeb6a6b913c948a5ff95c0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 28 Feb 2023 11:23:11 +0100 Subject: [PATCH 082/360] pre-commit cachetools --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a0ff2e6d5..05f4df92b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,7 +13,7 @@ repos: - id: mypy exclude: build_helpers additional_dependencies: - - types-cachetools==5.3.0.0 + - types-cachetools==5.3.0.4 - types-filelock==3.2.7 - types-requests==2.28.11.15 - types-tabulate==0.9.0.1 From 262f03bc928abed50ab8d26c8ab886a11fde8bf9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 28 Feb 2023 17:25:42 +0100 Subject: [PATCH 083/360] Add backtest warning for market_direction feature --- docs/telegram-usage.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index 653d31ee6..1b7502184 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -437,3 +437,7 @@ You can use the market direction in your strategy via `self.market_direction`. !!! Warning "Bot restarts" Please note that the market direction is not persisted, and will be reset after a bot restart/reload. + +!!! Danger "Backtesting" + As this value/variable is intended to be changed manually in dry/live trading. + Strategies using `market_direction` will probably not produce reliable, reproducible results (changes to this variable will not be reflected for backtesting). Use at your own risk. From bebee15d100257f19f86532e718e9a2d4353cf1a Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 28 Feb 2023 18:09:52 +0100 Subject: [PATCH 084/360] Improve TradeSchema readability --- freqtrade/rpc/api_server/api_schemas.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index e5b038d90..af64019d5 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -228,25 +228,32 @@ class TradeSchema(BaseModel): fee_close: Optional[float] fee_close_cost: Optional[float] fee_close_currency: Optional[str] + open_date: str open_timestamp: int open_rate: float open_rate_requested: Optional[float] open_trade_value: float + close_date: Optional[str] close_timestamp: Optional[int] close_rate: Optional[float] close_rate_requested: Optional[float] + close_profit: Optional[float] close_profit_pct: Optional[float] close_profit_abs: Optional[float] + profit_ratio: Optional[float] profit_pct: Optional[float] profit_abs: Optional[float] profit_fiat: Optional[float] + realized_profit: float + exit_reason: Optional[str] exit_order_status: Optional[str] + stop_loss_abs: Optional[float] stop_loss_ratio: Optional[float] stop_loss_pct: Optional[float] @@ -256,6 +263,7 @@ class TradeSchema(BaseModel): initial_stop_loss_abs: Optional[float] initial_stop_loss_ratio: Optional[float] initial_stop_loss_pct: Optional[float] + min_rate: Optional[float] max_rate: Optional[float] open_order_id: Optional[str] From 0707e70183e8f534f5683ce827eab8067cc0d831 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 28 Feb 2023 18:15:58 +0100 Subject: [PATCH 085/360] Remove deprecated current_profit from api responses --- freqtrade/rpc/api_server/api_schemas.py | 3 --- freqtrade/rpc/rpc.py | 3 --- tests/rpc/test_rpc.py | 11 +---------- tests/rpc/test_rpc_apiserver.py | 3 --- 4 files changed, 1 insertion(+), 19 deletions(-) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index af64019d5..bb30678ab 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -282,9 +282,6 @@ class OpenTradeSchema(TradeSchema): stoploss_current_dist_ratio: Optional[float] stoploss_entry_dist: Optional[float] stoploss_entry_dist_ratio: Optional[float] - current_profit: float - current_profit_abs: float - current_profit_pct: float current_rate: float open_order: Optional[str] diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index ba05d3d7f..807efb4fc 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -210,9 +210,6 @@ class RPC: trade_dict.update(dict( close_profit=trade.close_profit if not trade.is_open else None, current_rate=current_rate, - current_profit=current_profit, # Deprecated - current_profit_pct=round(current_profit * 100, 2), # Deprecated - current_profit_abs=current_profit_abs, # Deprecated profit_ratio=current_profit, profit_pct=round(current_profit * 100, 2), profit_abs=current_profit_abs, diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index d9b7c764a..e179ec9da 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -68,9 +68,6 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'close_profit': None, 'close_profit_pct': None, 'close_profit_abs': None, - 'current_profit': -0.00408133, - 'current_profit_pct': -0.41, - 'current_profit_abs': -4.09e-06, 'profit_ratio': -0.00408133, 'profit_pct': -0.41, 'profit_abs': -4.09e-06, @@ -134,9 +131,6 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'profit_ratio': 0.0, 'profit_pct': 0.0, 'profit_abs': 0.0, - 'current_profit': 0.0, - 'current_profit_pct': 0.0, - 'current_profit_abs': 0.0, 'stop_loss_abs': 0.0, 'stop_loss_pct': None, 'stop_loss_ratio': None, @@ -187,7 +181,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: mocker.patch('freqtrade.exchange.Exchange.get_rate', MagicMock(side_effect=ExchangeError("Pair 'ETH/BTC' not available"))) results = rpc._rpc_trade_status() - assert isnan(results[0]['current_profit']) + assert isnan(results[0]['profit_ratio']) assert isnan(results[0]['current_rate']) response_norate = deepcopy(gen_response) # Update elements that are NaN when no rate is available. @@ -198,9 +192,6 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'profit_ratio': ANY, 'profit_pct': ANY, 'profit_abs': ANY, - 'current_profit_abs': ANY, - 'current_profit': ANY, - 'current_profit_pct': ANY, 'current_rate': ANY, }) assert results[0] == response_norate diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 8892995c7..3697fac05 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -1008,9 +1008,6 @@ def test_api_status(botclient, mocker, ticker, fee, markets, is_short, 'close_profit_pct': None, 'close_profit_abs': None, 'close_rate': None, - 'current_profit': ANY, - 'current_profit_pct': ANY, - 'current_profit_abs': ANY, 'profit_ratio': ANY, 'profit_pct': ANY, 'profit_abs': ANY, From 3706d28125bff39e8cdf48b07f46dff92ad52913 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 28 Feb 2023 18:17:29 +0100 Subject: [PATCH 086/360] use pytest.approx in favor of "prec_satoshi" ... --- tests/rpc/test_rpc.py | 31 +++++++++++-------------------- 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index e179ec9da..d2ea1ffde 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -19,15 +19,6 @@ from tests.conftest import (create_mock_trades, create_mock_trades_usdt, get_pat patch_get_signal) -# Functions for recurrent object patching -def prec_satoshi(a, b) -> float: - """ - :return: True if A and B differs less than one satoshi. - """ - return abs(a - b) < 0.00000001 - - -# Unit tests def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: gen_response = { 'trade_id': 1, @@ -544,8 +535,8 @@ def test_rpc_balance_handle(default_conf, mocker, tickers): rpc._fiat_converter = CryptoToFiatConverter() result = rpc._rpc_balance(default_conf['stake_currency'], default_conf['fiat_display_currency']) - assert prec_satoshi(result['total'], 30.30909624) - assert prec_satoshi(result['value'], 454636.44360691) + assert pytest.approx(result['total']) == 30.30909624 + assert pytest.approx(result['value']) == 454636.44360691 assert tickers.call_count == 1 assert tickers.call_args_list[0][1]['cached'] is True assert 'USD' == result['symbol'] @@ -866,17 +857,17 @@ def test_enter_tag_performance_handle_2(mocker, default_conf, markets, fee): assert len(res) == 2 assert res[0]['enter_tag'] == 'TEST1' assert res[0]['count'] == 1 - assert prec_satoshi(res[0]['profit_pct'], 0.5) + assert pytest.approx(res[0]['profit_pct']) == 0.5 assert res[1]['enter_tag'] == 'Other' assert res[1]['count'] == 1 - assert prec_satoshi(res[1]['profit_pct'], 1.0) + assert pytest.approx(res[1]['profit_pct']) == 1.0 # Test for a specific pair res = rpc._rpc_enter_tag_performance('ETC/BTC') assert len(res) == 1 assert res[0]['count'] == 1 assert res[0]['enter_tag'] == 'TEST1' - assert prec_satoshi(res[0]['profit_pct'], 0.5) + assert pytest.approx(res[0]['profit_pct']) == 0.5 def test_exit_reason_performance_handle(default_conf_usdt, ticker, fee, mocker) -> None: @@ -922,17 +913,17 @@ def test_exit_reason_performance_handle_2(mocker, default_conf, markets, fee): assert len(res) == 2 assert res[0]['exit_reason'] == 'sell_signal' assert res[0]['count'] == 1 - assert prec_satoshi(res[0]['profit_pct'], 0.5) + assert pytest.approx(res[0]['profit_pct']) == 0.5 assert res[1]['exit_reason'] == 'roi' assert res[1]['count'] == 1 - assert prec_satoshi(res[1]['profit_pct'], 1.0) + assert pytest.approx(res[1]['profit_pct']) == 1.0 # Test for a specific pair res = rpc._rpc_exit_reason_performance('ETC/BTC') assert len(res) == 1 assert res[0]['count'] == 1 assert res[0]['exit_reason'] == 'sell_signal' - assert prec_satoshi(res[0]['profit_pct'], 0.5) + assert pytest.approx(res[0]['profit_pct']) == 0.5 def test_mix_tag_performance_handle(default_conf, ticker, fee, mocker) -> None: @@ -975,10 +966,10 @@ def test_mix_tag_performance_handle_2(mocker, default_conf, markets, fee): assert len(res) == 2 assert res[0]['mix_tag'] == 'TEST1 sell_signal' assert res[0]['count'] == 1 - assert prec_satoshi(res[0]['profit_pct'], 0.5) + assert pytest.approx(res[0]['profit_pct']) == 0.5 assert res[1]['mix_tag'] == 'Other roi' assert res[1]['count'] == 1 - assert prec_satoshi(res[1]['profit_pct'], 1.0) + assert pytest.approx(res[1]['profit_pct']) == 1.0 # Test for a specific pair res = rpc._rpc_mix_tag_performance('ETC/BTC') @@ -986,7 +977,7 @@ def test_mix_tag_performance_handle_2(mocker, default_conf, markets, fee): assert len(res) == 1 assert res[0]['count'] == 1 assert res[0]['mix_tag'] == 'TEST1 sell_signal' - assert prec_satoshi(res[0]['profit_pct'], 0.5) + assert pytest.approx(res[0]['profit_pct']) == 0.5 def test_rpc_count(mocker, default_conf, ticker, fee) -> None: From 2f1c5cf1439ba6f562dc46d33ec8ab169572344b Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 28 Feb 2023 18:22:17 +0100 Subject: [PATCH 087/360] Remove pointless pylint rules --- tests/rpc/test_rpc.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index d2ea1ffde..8e3eabd66 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -1,6 +1,3 @@ -# pragma pylint: disable=missing-docstring, C0103 -# pragma pylint: disable=invalid-sequence-index, invalid-name, too-many-arguments - from copy import deepcopy from datetime import datetime, timedelta, timezone from unittest.mock import ANY, MagicMock, PropertyMock @@ -535,7 +532,7 @@ def test_rpc_balance_handle(default_conf, mocker, tickers): rpc._fiat_converter = CryptoToFiatConverter() result = rpc._rpc_balance(default_conf['stake_currency'], default_conf['fiat_display_currency']) - assert pytest.approx(result['total']) == 30.30909624 + assert pytest.approx(result['total']) == 30.30909624 assert pytest.approx(result['value']) == 454636.44360691 assert tickers.call_count == 1 assert tickers.call_args_list[0][1]['cached'] is True From 386915378b11e14d1aad49bc834700eda1c5cb6c Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 28 Feb 2023 19:54:47 +0100 Subject: [PATCH 088/360] Improve /status message (show Total profit) --- docs/telegram-usage.md | 2 +- freqtrade/rpc/rpc.py | 9 +++++++++ freqtrade/rpc/telegram.py | 17 +++++++++++------ tests/rpc/test_rpc.py | 4 ++++ tests/rpc/test_rpc_telegram.py | 2 ++ 5 files changed, 27 insertions(+), 7 deletions(-) diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index 1b7502184..dc0ab0976 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -243,7 +243,7 @@ Enter Tag is configurable via Strategy. > **Enter Tag:** Awesome Long Signal > **Open Rate:** `0.00007489` > **Current Rate:** `0.00007489` -> **Current Profit:** `12.95%` +> **Unrealized Profit:** `12.95%` > **Stoploss:** `0.00007389 (-0.02%)` ### /status table diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 807efb4fc..230079cad 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -169,6 +169,7 @@ class RPC: for trade in trades: order: Optional[Order] = None current_profit_fiat: Optional[float] = None + combined_profit_fiat: Optional[float] = None if trade.open_order_id: order = trade.select_order_by_order_id(trade.open_order_id) # calculate profit and send message to user @@ -190,6 +191,7 @@ class RPC: current_rate = trade.close_rate current_profit = trade.close_profit current_profit_abs = trade.close_profit_abs + combined_profit_abs = trade.realized_profit + current_profit_abs # Calculate fiat profit if not isnan(current_profit_abs) and self._fiat_converter: @@ -198,6 +200,11 @@ class RPC: self._freqtrade.config['stake_currency'], self._freqtrade.config['fiat_display_currency'] ) + combined_profit_fiat = self._fiat_converter.convert_amount( + combined_profit_abs, + self._freqtrade.config['stake_currency'], + self._freqtrade.config['fiat_display_currency'] + ) # Calculate guaranteed profit (in case of trailing stop) stoploss_entry_dist = trade.calc_profit(trade.stop_loss) @@ -215,6 +222,8 @@ class RPC: profit_abs=current_profit_abs, profit_fiat=current_profit_fiat, + combined_profit_abs=combined_profit_abs, + combined_profit_fiat=combined_profit_fiat, stoploss_current_dist=stoploss_current_dist, stoploss_current_dist_ratio=round(stoploss_current_dist_ratio, 8), stoploss_current_dist_pct=round(stoploss_current_dist_ratio * 100, 2), diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index b961e6fd7..e7bc2a6d7 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -562,15 +562,18 @@ class Telegram(RPCHandler): r['open_date_hum'] = arrow.get(r['open_date']).humanize() r['num_entries'] = len([o for o in r['orders'] if o['ft_is_entry']]) r['exit_reason'] = r.get('exit_reason', "") - r['rounded_stake_amount'] = round_coin_value(r['stake_amount'], r['quote_currency']) - r['rounded_profit_abs'] = round_coin_value(r['profit_abs'], r['quote_currency']) + r['stake_amount_r'] = round_coin_value(r['stake_amount'], r['quote_currency']) + r['profit_abs_r'] = round_coin_value(r['profit_abs'], r['quote_currency']) + r['realized_profit_r'] = round_coin_value(r['realized_profit'], r['quote_currency']) + r['combined_profit_abs_r'] = round_coin_value( + r['combined_profit_abs'], r['quote_currency']) lines = [ "*Trade ID:* `{trade_id}`" + (" `(since {open_date_hum})`" if r['is_open'] else ""), "*Current Pair:* {pair}", "*Direction:* " + ("`Short`" if r.get('is_short') else "`Long`"), "*Leverage:* `{leverage}`" if r.get('leverage') else "", - "*Amount:* `{amount} ({rounded_stake_amount})`", + "*Amount:* `{amount} ({stake_amount_r})`", "*Enter Tag:* `{enter_tag}`" if r['enter_tag'] else "", "*Exit Reason:* `{exit_reason}`" if r['exit_reason'] else "", ] @@ -585,13 +588,15 @@ class Telegram(RPCHandler): "*Open Date:* `{open_date}`", "*Close Date:* `{close_date}`" if r['close_date'] else "", "*Current Rate:* `{current_rate:.8f}`" if r['is_open'] else "", - ("*Current Profit:* " if r['is_open'] else "*Close Profit: *") - + "`{profit_ratio:.2%}` `({rounded_profit_abs})`", + ("*Unrealized Profit:* " if r['is_open'] else "*Close Profit: *") + + "`{profit_ratio:.2%}` `({profit_abs_r})`", ]) if r['is_open']: if r.get('realized_profit'): - lines.append("*Realized Profit:* `{realized_profit:.8f}`") + lines.append("*Realized Profit:* `{realized_profit_r}`") + lines.append("*Total Profit:* `{combined_profit_abs_r}` ") + if (r['stop_loss_abs'] != r['initial_stop_loss_abs'] and r['initial_stop_loss_ratio'] is not None): # Adding initial stoploss only if it is different from stoploss diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 8e3eabd66..734eb7cf1 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -76,6 +76,8 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'stoploss_entry_dist_ratio': -0.10376381, 'open_order': None, 'realized_profit': 0.0, + 'combined_profit_abs': -4.09e-06, + 'combined_profit_fiat': ANY, 'exchange': 'binance', 'leverage': 1.0, 'interest_rate': 0.0, @@ -119,6 +121,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'profit_ratio': 0.0, 'profit_pct': 0.0, 'profit_abs': 0.0, + 'combined_profit_abs': 0.0, 'stop_loss_abs': 0.0, 'stop_loss_pct': None, 'stop_loss_ratio': None, @@ -180,6 +183,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'profit_ratio': ANY, 'profit_pct': ANY, 'profit_abs': ANY, + 'combined_profit_abs': ANY, 'current_rate': ANY, }) assert results[0] == response_norate diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index dd58c53a3..2a60f0b42 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -204,6 +204,8 @@ def test_telegram_status(default_conf, update, mocker) -> None: 'profit': -0.0059, 'profit_ratio': -0.0059, 'profit_abs': -0.225, + 'realized_profit': 0.0, + 'combined_profit_abs': -0.225, 'initial_stop_loss_abs': 1.098e-05, 'stop_loss_abs': 1.099e-05, 'exit_order_status': None, From f822f1795a7f98faa9c29b118f122700320dbe53 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 28 Feb 2023 19:54:56 +0100 Subject: [PATCH 089/360] Reduce `/status` verbosity --- freqtrade/rpc/telegram.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index e7bc2a6d7..18e412bbb 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -571,8 +571,8 @@ class Telegram(RPCHandler): "*Trade ID:* `{trade_id}`" + (" `(since {open_date_hum})`" if r['is_open'] else ""), "*Current Pair:* {pair}", - "*Direction:* " + ("`Short`" if r.get('is_short') else "`Long`"), - "*Leverage:* `{leverage}`" if r.get('leverage') else "", + f"*Direction:* {'`Short`' if r.get('is_short') else '`Long`'}" + + " ` ({leverage}x)`" if r.get('leverage') else "", "*Amount:* `{amount} ({stake_amount_r})`", "*Enter Tag:* `{enter_tag}`" if r['enter_tag'] else "", "*Exit Reason:* `{exit_reason}`" if r['exit_reason'] else "", From dd10dec73d6f025b40bee247ab5f5aa9901720d4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 28 Feb 2023 20:31:02 +0100 Subject: [PATCH 090/360] Improve variable wording --- freqtrade/rpc/api_server/api_schemas.py | 3 +++ freqtrade/rpc/rpc.py | 12 ++++++------ freqtrade/rpc/telegram.py | 6 +++--- tests/rpc/test_rpc.py | 8 ++++---- tests/rpc/test_rpc_apiserver.py | 2 ++ tests/rpc/test_rpc_telegram.py | 2 +- 6 files changed, 19 insertions(+), 14 deletions(-) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index bb30678ab..562c9aa7d 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -283,6 +283,9 @@ class OpenTradeSchema(TradeSchema): stoploss_entry_dist: Optional[float] stoploss_entry_dist_ratio: Optional[float] current_rate: float + total_profit_abs: float + total_profit_fiat: Optional[float] + open_order: Optional[str] diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 230079cad..82f892101 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -169,7 +169,7 @@ class RPC: for trade in trades: order: Optional[Order] = None current_profit_fiat: Optional[float] = None - combined_profit_fiat: Optional[float] = None + total_profit_fiat: Optional[float] = None if trade.open_order_id: order = trade.select_order_by_order_id(trade.open_order_id) # calculate profit and send message to user @@ -191,7 +191,7 @@ class RPC: current_rate = trade.close_rate current_profit = trade.close_profit current_profit_abs = trade.close_profit_abs - combined_profit_abs = trade.realized_profit + current_profit_abs + total_profit_abs = trade.realized_profit + current_profit_abs # Calculate fiat profit if not isnan(current_profit_abs) and self._fiat_converter: @@ -200,8 +200,8 @@ class RPC: self._freqtrade.config['stake_currency'], self._freqtrade.config['fiat_display_currency'] ) - combined_profit_fiat = self._fiat_converter.convert_amount( - combined_profit_abs, + total_profit_fiat = self._fiat_converter.convert_amount( + total_profit_abs, self._freqtrade.config['stake_currency'], self._freqtrade.config['fiat_display_currency'] ) @@ -222,8 +222,8 @@ class RPC: profit_abs=current_profit_abs, profit_fiat=current_profit_fiat, - combined_profit_abs=combined_profit_abs, - combined_profit_fiat=combined_profit_fiat, + total_profit_abs=total_profit_abs, + total_profit_fiat=total_profit_fiat, stoploss_current_dist=stoploss_current_dist, stoploss_current_dist_ratio=round(stoploss_current_dist_ratio, 8), stoploss_current_dist_pct=round(stoploss_current_dist_ratio * 100, 2), diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 18e412bbb..7bbeea2a2 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -565,8 +565,8 @@ class Telegram(RPCHandler): r['stake_amount_r'] = round_coin_value(r['stake_amount'], r['quote_currency']) r['profit_abs_r'] = round_coin_value(r['profit_abs'], r['quote_currency']) r['realized_profit_r'] = round_coin_value(r['realized_profit'], r['quote_currency']) - r['combined_profit_abs_r'] = round_coin_value( - r['combined_profit_abs'], r['quote_currency']) + r['total_profit_abs_r'] = round_coin_value( + r['total_profit_abs'], r['quote_currency']) lines = [ "*Trade ID:* `{trade_id}`" + (" `(since {open_date_hum})`" if r['is_open'] else ""), @@ -595,7 +595,7 @@ class Telegram(RPCHandler): if r['is_open']: if r.get('realized_profit'): lines.append("*Realized Profit:* `{realized_profit_r}`") - lines.append("*Total Profit:* `{combined_profit_abs_r}` ") + lines.append("*Total Profit:* `{total_profit_abs_r}` ") if (r['stop_loss_abs'] != r['initial_stop_loss_abs'] and r['initial_stop_loss_ratio'] is not None): diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 734eb7cf1..3eb391edd 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -76,8 +76,8 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'stoploss_entry_dist_ratio': -0.10376381, 'open_order': None, 'realized_profit': 0.0, - 'combined_profit_abs': -4.09e-06, - 'combined_profit_fiat': ANY, + 'total_profit_abs': -4.09e-06, + 'total_profit_fiat': ANY, 'exchange': 'binance', 'leverage': 1.0, 'interest_rate': 0.0, @@ -121,7 +121,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'profit_ratio': 0.0, 'profit_pct': 0.0, 'profit_abs': 0.0, - 'combined_profit_abs': 0.0, + 'total_profit_abs': 0.0, 'stop_loss_abs': 0.0, 'stop_loss_pct': None, 'stop_loss_ratio': None, @@ -183,7 +183,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'profit_ratio': ANY, 'profit_pct': ANY, 'profit_abs': ANY, - 'combined_profit_abs': ANY, + 'total_profit_abs': ANY, 'current_rate': ANY, }) assert results[0] == response_norate diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 3697fac05..67156da45 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -1012,6 +1012,8 @@ def test_api_status(botclient, mocker, ticker, fee, markets, is_short, 'profit_pct': ANY, 'profit_abs': ANY, 'profit_fiat': ANY, + 'total_profit_abs': ANY, + 'total_profit_fiat': ANY, 'realized_profit': 0.0, 'current_rate': current_rate, 'open_date': ANY, diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 2a60f0b42..65e676860 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -205,7 +205,7 @@ def test_telegram_status(default_conf, update, mocker) -> None: 'profit_ratio': -0.0059, 'profit_abs': -0.225, 'realized_profit': 0.0, - 'combined_profit_abs': -0.225, + 'total_profit_abs': -0.225, 'initial_stop_loss_abs': 1.098e-05, 'stop_loss_abs': 1.099e-05, 'exit_order_status': None, From d1b2e38ae971800c9e73cb9e4f0eb35758108d0f Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 28 Feb 2023 20:39:17 +0100 Subject: [PATCH 091/360] if a stoploss order exists, always allow canceling that --- freqtrade/freqtradebot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 94bfc1b72..633e9dc71 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -841,7 +841,7 @@ class FreqtradeBot(LoggingMixin): def cancel_stoploss_on_exchange(self, trade: Trade) -> Trade: # First cancelling stoploss on exchange ... - if self.strategy.order_types.get('stoploss_on_exchange') and trade.stoploss_order_id: + if trade.stoploss_order_id: try: logger.info(f"Canceling stoploss on exchange for {trade}") co = self.exchange.cancel_stoploss_order_with_result( From 8b51f5f5638eecf5f7d6f847767537335f0def06 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 Feb 2023 15:12:29 +0100 Subject: [PATCH 092/360] Lowercase exchange ID --- tests/exchange/test_exchange.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 7ccd32155..bdffa0d29 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2341,7 +2341,7 @@ async def test__async_kucoin_get_candle_history(default_conf, mocker, caplog): "kucoin GET https://openapi-v2.kucoin.com/api/v1/market/candles?" "symbol=ETH-BTC&type=5min&startAt=1640268735&endAt=1640418735" "429 Too Many Requests" '{"code":"429000","msg":"Too Many Requests"}')) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id="KuCoin") + exchange = get_patched_exchange(mocker, default_conf, api_mock, id="kucoin") mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value='KuCoin')) msg = "Kucoin 429 error, avoid triggering DDosProtection backoff delay" From 78e5ec13bb17696433549754333006b845cbb297 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 Feb 2023 14:21:44 +0100 Subject: [PATCH 093/360] Use absolute path for generic mocks --- tests/conftest.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index c74b1f0f1..c0a3661a0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -145,22 +145,23 @@ def patch_exchange( mock_markets=True, mock_supported_modes=True ) -> None: - mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock(return_value={})) - mocker.patch('freqtrade.exchange.Exchange.validate_config', MagicMock()) - mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) - mocker.patch('freqtrade.exchange.Exchange.id', PropertyMock(return_value=id)) - mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value=id.title())) - mocker.patch('freqtrade.exchange.Exchange.precisionMode', PropertyMock(return_value=2)) + mocker.patch('freqtrade.exchange.exchange.Exchange._load_async_markets', + MagicMock(return_value={})) + mocker.patch('freqtrade.exchange.exchange.Exchange.validate_config', MagicMock()) + mocker.patch('freqtrade.exchange.exchange.Exchange.validate_timeframes', MagicMock()) + mocker.patch('freqtrade.exchange.exchange.Exchange.id', PropertyMock(return_value=id)) + mocker.patch('freqtrade.exchange.exchange.Exchange.name', PropertyMock(return_value=id.title())) + mocker.patch('freqtrade.exchange.exchange.Exchange.precisionMode', PropertyMock(return_value=2)) if mock_markets: if isinstance(mock_markets, bool): mock_markets = get_markets() - mocker.patch('freqtrade.exchange.Exchange.markets', + mocker.patch('freqtrade.exchange.exchange.Exchange.markets', PropertyMock(return_value=mock_markets)) if mock_supported_modes: mocker.patch( - f'freqtrade.exchange.{id.capitalize()}._supported_trading_mode_margin_pairs', + f'freqtrade.exchange.{id}.{id.capitalize()}._supported_trading_mode_margin_pairs', PropertyMock(return_value=[ (TradingMode.MARGIN, MarginMode.CROSS), (TradingMode.MARGIN, MarginMode.ISOLATED), @@ -170,10 +171,11 @@ def patch_exchange( ) if api_mock: - mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch('freqtrade.exchange.exchange.Exchange._init_ccxt', + MagicMock(return_value=api_mock)) else: - mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock()) - mocker.patch('freqtrade.exchange.Exchange.timeframes', PropertyMock( + mocker.patch('freqtrade.exchange.exchange.Exchange._init_ccxt', MagicMock()) + mocker.patch('freqtrade.exchange.exchange.Exchange.timeframes', PropertyMock( return_value=['5m', '15m', '1h', '1d'])) From bcdf4e0fe837621d2d50ca4c7ecb7bc18c054427 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 Feb 2023 20:26:04 +0100 Subject: [PATCH 094/360] Use variable for exchange mocks to shorten lines --- tests/commands/test_commands.py | 40 +- tests/conftest.py | 24 +- tests/data/test_dataprovider.py | 8 +- tests/data/test_history.py | 35 +- tests/edge/test_edge.py | 12 +- tests/exchange/test_binance.py | 12 +- tests/exchange/test_bitpanda.py | 4 +- tests/exchange/test_ccxt_compat.py | 9 +- tests/exchange/test_exchange.py | 436 ++++++++---------- tests/exchange/test_gate.py | 18 +- tests/exchange/test_huobi.py | 10 +- tests/exchange/test_kraken.py | 18 +- tests/exchange/test_kucoin.py | 10 +- tests/freqai/test_freqai_interface.py | 4 +- tests/optimize/test_backtest_detail.py | 8 +- tests/optimize/test_backtesting.py | 122 ++--- .../test_backtesting_adjust_position.py | 16 +- tests/optimize/test_edge_cli.py | 6 +- tests/optimize/test_hyperopt.py | 20 +- tests/plugins/test_pairlist.py | 44 +- tests/rpc/test_rpc.py | 35 +- tests/rpc/test_rpc_apiserver.py | 18 +- tests/rpc/test_rpc_telegram.py | 26 +- tests/test_freqtradebot.py | 302 ++++++------ tests/test_integration.py | 27 +- tests/test_worker.py | 4 +- 26 files changed, 582 insertions(+), 686 deletions(-) diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 55ffaccb0..d2ce287e9 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -24,7 +24,7 @@ from freqtrade.enums import RunMode from freqtrade.exceptions import OperationalException from freqtrade.persistence.models import init_db from freqtrade.persistence.pairlock_middleware import PairLocks -from tests.conftest import (CURRENT_TEST_STRATEGY, create_mock_trades, get_args, log_has, +from tests.conftest import (CURRENT_TEST_STRATEGY, EXMS, create_mock_trades, get_args, log_has, log_has_re, patch_exchange, patched_configuration_load_config_file) from tests.conftest_trades import MOCK_TRADE_COUNT @@ -454,7 +454,7 @@ def test_list_markets(mocker, markets_static, capsys): assert re.search(r"^BLK/BTC$", captured.out, re.MULTILINE) assert re.search(r"^LTC/USD$", captured.out, re.MULTILINE) - mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(side_effect=ValueError)) + mocker.patch(f'{EXMS}.markets', PropertyMock(side_effect=ValueError)) # Test --one-column args = [ "list-markets", @@ -643,9 +643,7 @@ def test_download_data_keyboardInterrupt(mocker, markets): dl_mock = mocker.patch('freqtrade.commands.data_commands.refresh_backtest_ohlcv_data', MagicMock(side_effect=KeyboardInterrupt)) patch_exchange(mocker) - mocker.patch( - 'freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets) - ) + mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets)) args = [ "download-data", "--exchange", "binance", @@ -664,9 +662,7 @@ def test_download_data_timerange(mocker, markets): dl_mock = mocker.patch('freqtrade.commands.data_commands.refresh_backtest_ohlcv_data', MagicMock(return_value=["ETH/BTC", "XRP/BTC"])) patch_exchange(mocker) - mocker.patch( - 'freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets) - ) + mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets)) args = [ "download-data", "--exchange", "binance", @@ -715,9 +711,7 @@ def test_download_data_no_markets(mocker, caplog): dl_mock = mocker.patch('freqtrade.commands.data_commands.refresh_backtest_ohlcv_data', MagicMock(return_value=["ETH/BTC", "XRP/BTC"])) patch_exchange(mocker, id='binance') - mocker.patch( - 'freqtrade.exchange.Exchange.markets', PropertyMock(return_value={}) - ) + mocker.patch(f'{EXMS}.markets', PropertyMock(return_value={})) args = [ "download-data", "--exchange", "binance", @@ -733,9 +727,7 @@ def test_download_data_no_exchange(mocker, caplog): mocker.patch('freqtrade.commands.data_commands.refresh_backtest_ohlcv_data', MagicMock(return_value=["ETH/BTC", "XRP/BTC"])) patch_exchange(mocker) - mocker.patch( - 'freqtrade.exchange.Exchange.markets', PropertyMock(return_value={}) - ) + mocker.patch(f'{EXMS}.markets', PropertyMock(return_value={})) args = [ "download-data", ] @@ -751,9 +743,7 @@ def test_download_data_no_pairs(mocker): mocker.patch('freqtrade.commands.data_commands.refresh_backtest_ohlcv_data', MagicMock(return_value=["ETH/BTC", "XRP/BTC"])) patch_exchange(mocker) - mocker.patch( - 'freqtrade.exchange.Exchange.markets', PropertyMock(return_value={}) - ) + mocker.patch(f'{EXMS}.markets', PropertyMock(return_value={})) args = [ "download-data", "--exchange", @@ -771,9 +761,7 @@ def test_download_data_all_pairs(mocker, markets): dl_mock = mocker.patch('freqtrade.commands.data_commands.refresh_backtest_ohlcv_data', MagicMock(return_value=["ETH/BTC", "XRP/BTC"])) patch_exchange(mocker) - mocker.patch( - 'freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets) - ) + mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets)) args = [ "download-data", "--exchange", @@ -810,9 +798,7 @@ def test_download_data_trades(mocker, caplog): convert_mock = mocker.patch('freqtrade.commands.data_commands.convert_trades_to_ohlcv', MagicMock(return_value=[])) patch_exchange(mocker) - mocker.patch( - 'freqtrade.exchange.Exchange.markets', PropertyMock(return_value={}) - ) + mocker.patch(f'{EXMS}.markets', PropertyMock(return_value={})) args = [ "download-data", "--exchange", "kraken", @@ -843,9 +829,7 @@ def test_download_data_trades(mocker, caplog): def test_download_data_data_invalid(mocker): patch_exchange(mocker, id="kraken") - mocker.patch( - 'freqtrade.exchange.Exchange.markets', PropertyMock(return_value={}) - ) + mocker.patch(f'{EXMS}.markets', PropertyMock(return_value={})) args = [ "download-data", "--exchange", "kraken", @@ -862,9 +846,7 @@ def test_start_convert_trades(mocker, caplog): convert_mock = mocker.patch('freqtrade.commands.data_commands.convert_trades_to_ohlcv', MagicMock(return_value=[])) patch_exchange(mocker) - mocker.patch( - 'freqtrade.exchange.Exchange.markets', PropertyMock(return_value={}) - ) + mocker.patch(f'{EXMS}.markets', PropertyMock(return_value={})) args = [ "trades-to-ohlcv", "--exchange", "kraken", diff --git a/tests/conftest.py b/tests/conftest.py index c0a3661a0..3c10de4ec 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -40,6 +40,7 @@ np.seterr(all='raise') CURRENT_TEST_STRATEGY = 'StrategyTestV3' TRADE_SIDES = ('long', 'short') +EXMS = 'freqtrade.exchange.exchange.Exchange' def pytest_addoption(parser): @@ -145,19 +146,17 @@ def patch_exchange( mock_markets=True, mock_supported_modes=True ) -> None: - mocker.patch('freqtrade.exchange.exchange.Exchange._load_async_markets', - MagicMock(return_value={})) - mocker.patch('freqtrade.exchange.exchange.Exchange.validate_config', MagicMock()) - mocker.patch('freqtrade.exchange.exchange.Exchange.validate_timeframes', MagicMock()) - mocker.patch('freqtrade.exchange.exchange.Exchange.id', PropertyMock(return_value=id)) - mocker.patch('freqtrade.exchange.exchange.Exchange.name', PropertyMock(return_value=id.title())) - mocker.patch('freqtrade.exchange.exchange.Exchange.precisionMode', PropertyMock(return_value=2)) + mocker.patch(f'{EXMS}._load_async_markets', return_value={}) + mocker.patch(f'{EXMS}.validate_config', MagicMock()) + mocker.patch(f'{EXMS}.validate_timeframes', MagicMock()) + mocker.patch(f'{EXMS}.id', PropertyMock(return_value=id)) + mocker.patch(f'{EXMS}.name', PropertyMock(return_value=id.title())) + mocker.patch(f'{EXMS}.precisionMode', PropertyMock(return_value=2)) if mock_markets: if isinstance(mock_markets, bool): mock_markets = get_markets() - mocker.patch('freqtrade.exchange.exchange.Exchange.markets', - PropertyMock(return_value=mock_markets)) + mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=mock_markets)) if mock_supported_modes: mocker.patch( @@ -171,11 +170,10 @@ def patch_exchange( ) if api_mock: - mocker.patch('freqtrade.exchange.exchange.Exchange._init_ccxt', - MagicMock(return_value=api_mock)) + mocker.patch(f'{EXMS}._init_ccxt', return_value=api_mock) else: - mocker.patch('freqtrade.exchange.exchange.Exchange._init_ccxt', MagicMock()) - mocker.patch('freqtrade.exchange.exchange.Exchange.timeframes', PropertyMock( + mocker.patch(f'{EXMS}._init_ccxt', MagicMock()) + mocker.patch(f'{EXMS}.timeframes', PropertyMock( return_value=['5m', '15m', '1h', '1d'])) diff --git a/tests/data/test_dataprovider.py b/tests/data/test_dataprovider.py index c6b1dcc5a..ff748e976 100644 --- a/tests/data/test_dataprovider.py +++ b/tests/data/test_dataprovider.py @@ -8,7 +8,7 @@ from freqtrade.data.dataprovider import DataProvider from freqtrade.enums import CandleType, RunMode from freqtrade.exceptions import ExchangeError, OperationalException from freqtrade.plugins.pairlistmanager import PairListManager -from tests.conftest import generate_test_data, get_patched_exchange +from tests.conftest import EXMS, generate_test_data, get_patched_exchange @pytest.mark.parametrize('candle_type', [ @@ -223,7 +223,7 @@ def test_emit_df(mocker, default_conf, ohlcv_history): def test_refresh(mocker, default_conf): refresh_mock = MagicMock() - mocker.patch("freqtrade.exchange.Exchange.refresh_latest_ohlcv", refresh_mock) + mocker.patch(f"{EXMS}.refresh_latest_ohlcv", refresh_mock) exchange = get_patched_exchange(mocker, default_conf, id="binance") timeframe = default_conf["timeframe"] @@ -281,7 +281,7 @@ def test_market(mocker, default_conf, markets): def test_ticker(mocker, default_conf, tickers): ticker_mock = MagicMock(return_value=tickers()['ETH/BTC']) - mocker.patch("freqtrade.exchange.Exchange.fetch_ticker", ticker_mock) + mocker.patch(f"{EXMS}.fetch_ticker", ticker_mock) exchange = get_patched_exchange(mocker, default_conf) dp = DataProvider(default_conf, exchange) res = dp.ticker('ETH/BTC') @@ -290,7 +290,7 @@ def test_ticker(mocker, default_conf, tickers): assert res['symbol'] == 'ETH/BTC' ticker_mock = MagicMock(side_effect=ExchangeError('Pair not found')) - mocker.patch("freqtrade.exchange.Exchange.fetch_ticker", ticker_mock) + mocker.patch(f"{EXMS}.fetch_ticker", ticker_mock) exchange = get_patched_exchange(mocker, default_conf) dp = DataProvider(default_conf, exchange) res = dp.ticker('UNITTEST/BTC') diff --git a/tests/data/test_history.py b/tests/data/test_history.py index 5cd7327fd..c967f0c89 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -26,7 +26,7 @@ from freqtrade.enums import CandleType from freqtrade.exchange import timeframe_to_minutes from freqtrade.misc import file_dump_json from freqtrade.resolvers import StrategyResolver -from tests.conftest import (CURRENT_TEST_STRATEGY, get_patched_exchange, log_has, log_has_re, +from tests.conftest import (CURRENT_TEST_STRATEGY, EXMS, get_patched_exchange, log_has, log_has_re, patch_exchange) @@ -66,7 +66,7 @@ def test_load_data_7min_timeframe(caplog, testdatadir) -> None: def test_load_data_1min_timeframe(ohlcv_history, mocker, caplog, testdatadir) -> None: - mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=ohlcv_history) + mocker.patch(f'{EXMS}.get_historic_ohlcv', return_value=ohlcv_history) file = testdatadir / 'UNITTEST_BTC-1m.json' load_data(datadir=testdatadir, timeframe='1m', pairs=['UNITTEST/BTC']) assert file.is_file() @@ -77,7 +77,7 @@ def test_load_data_1min_timeframe(ohlcv_history, mocker, caplog, testdatadir) -> def test_load_data_mark(ohlcv_history, mocker, caplog, testdatadir) -> None: - mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=ohlcv_history) + mocker.patch(f'{EXMS}.get_historic_ohlcv', return_value=ohlcv_history) file = testdatadir / 'futures/UNITTEST_USDT_USDT-1h-mark.json' load_data(datadir=testdatadir, timeframe='1h', pairs=['UNITTEST/BTC'], candle_type='mark') assert file.is_file() @@ -109,7 +109,7 @@ def test_load_data_with_new_pair_1min(ohlcv_history_list, mocker, caplog, Test load_pair_history() with 1 min timeframe """ tmpdir1 = Path(tmpdir) - mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=ohlcv_history_list) + mocker.patch(f'{EXMS}.get_historic_ohlcv', return_value=ohlcv_history_list) exchange = get_patched_exchange(mocker, default_conf) file = tmpdir1 / 'MEME_BTC-1m.json' @@ -277,7 +277,7 @@ def test_download_pair_history( subdir, file_tail ) -> None: - mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=ohlcv_history_list) + mocker.patch(f'{EXMS}.get_historic_ohlcv', return_value=ohlcv_history_list) exchange = get_patched_exchange(mocker, default_conf) tmpdir1 = Path(tmpdir) file1_1 = tmpdir1 / f'{subdir}MEME_BTC-1m{file_tail}.json' @@ -328,7 +328,7 @@ def test_download_pair_history2(mocker, default_conf, testdatadir) -> None: json_dump_mock = mocker.patch( 'freqtrade.data.history.jsondatahandler.JsonDataHandler.ohlcv_store', return_value=None) - mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=tick) + mocker.patch(f'{EXMS}.get_historic_ohlcv', return_value=tick) exchange = get_patched_exchange(mocker, default_conf) _download_pair_history(datadir=testdatadir, exchange=exchange, pair="UNITTEST/BTC", timeframe='1m', candle_type='spot') @@ -340,7 +340,7 @@ def test_download_pair_history2(mocker, default_conf, testdatadir) -> None: def test_download_backtesting_data_exception(mocker, caplog, default_conf, tmpdir) -> None: - mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', + mocker.patch(f'{EXMS}.get_historic_ohlcv', side_effect=Exception('File Error')) tmpdir1 = Path(tmpdir) exchange = get_patched_exchange(mocker, default_conf) @@ -506,9 +506,7 @@ def test_refresh_backtest_ohlcv_data( mocker, default_conf, markets, caplog, testdatadir, trademode, callcount): dl_mock = mocker.patch('freqtrade.data.history.history_utils._download_pair_history', MagicMock()) - mocker.patch( - 'freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets) - ) + mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets)) mocker.patch.object(Path, "exists", MagicMock(return_value=True)) mocker.patch.object(Path, "unlink", MagicMock()) @@ -531,9 +529,7 @@ def test_download_data_no_markets(mocker, default_conf, caplog, testdatadir): MagicMock()) ex = get_patched_exchange(mocker, default_conf) - mocker.patch( - 'freqtrade.exchange.Exchange.markets', PropertyMock(return_value={}) - ) + mocker.patch(f'{EXMS}.markets', PropertyMock(return_value={})) timerange = TimeRange.parse_timerange("20190101-20190102") unav_pairs = refresh_backtest_ohlcv_data(exchange=ex, pairs=["BTT/BTC", "LTC/USDT"], timeframes=["1m", "5m"], @@ -551,9 +547,7 @@ def test_download_data_no_markets(mocker, default_conf, caplog, testdatadir): def test_refresh_backtest_trades_data(mocker, default_conf, markets, caplog, testdatadir): dl_mock = mocker.patch('freqtrade.data.history.history_utils._download_trades_history', MagicMock()) - mocker.patch( - 'freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets) - ) + mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets)) mocker.patch.object(Path, "exists", MagicMock(return_value=True)) mocker.patch.object(Path, "unlink", MagicMock()) @@ -577,8 +571,7 @@ def test_download_trades_history(trades_history, mocker, default_conf, testdatad tmpdir) -> None: tmpdir1 = Path(tmpdir) ght_mock = MagicMock(side_effect=lambda pair, *args, **kwargs: (pair, trades_history)) - mocker.patch('freqtrade.exchange.Exchange.get_historic_trades', - ght_mock) + mocker.patch(f'{EXMS}.get_historic_trades', ght_mock) exchange = get_patched_exchange(mocker, default_conf) file1 = tmpdir1 / 'ETH_BTC-trades.json.gz' data_handler = get_datahandler(tmpdir1, data_format='jsongz') @@ -604,8 +597,7 @@ def test_download_trades_history(trades_history, mocker, default_conf, testdatad file1.unlink() - mocker.patch('freqtrade.exchange.Exchange.get_historic_trades', - MagicMock(side_effect=ValueError)) + mocker.patch(f'{EXMS}.get_historic_trades', MagicMock(side_effect=ValueError)) assert not _download_trades_history(data_handler=data_handler, exchange=exchange, pair='ETH/BTC') @@ -615,8 +607,7 @@ def test_download_trades_history(trades_history, mocker, default_conf, testdatad copyfile(testdatadir / file2.name, file2) ght_mock.reset_mock() - mocker.patch('freqtrade.exchange.Exchange.get_historic_trades', - ght_mock) + mocker.patch(f'{EXMS}.get_historic_trades', ght_mock) # Since before first start date since_time = int(trades_history[0][0] // 1000) - 500 timerange = TimeRange('date', None, since_time, 0) diff --git a/tests/edge/test_edge.py b/tests/edge/test_edge.py index e414d7624..be0346b78 100644 --- a/tests/edge/test_edge.py +++ b/tests/edge/test_edge.py @@ -14,7 +14,7 @@ from freqtrade.data.converter import ohlcv_to_dataframe from freqtrade.edge import Edge, PairInfo from freqtrade.enums import ExitType from freqtrade.exceptions import OperationalException -from tests.conftest import get_patched_freqtradebot, log_has +from tests.conftest import EXMS, get_patched_freqtradebot, log_has from tests.optimize import (BTContainer, BTrade, _build_backtest_dataframe, _get_frame_time_from_offset) @@ -261,7 +261,7 @@ def mocked_load_data(datadir, pairs=[], timeframe='0m', def test_edge_process_downloaded_data(mocker, edge_conf): freqtrade = get_patched_freqtradebot(mocker, edge_conf) - mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.001)) + mocker.patch(f'{EXMS}.get_fee', MagicMock(return_value=0.001)) mocker.patch('freqtrade.edge.edge_positioning.refresh_data', MagicMock()) mocker.patch('freqtrade.edge.edge_positioning.load_data', mocked_load_data) edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy) @@ -273,7 +273,7 @@ def test_edge_process_downloaded_data(mocker, edge_conf): def test_edge_process_no_data(mocker, edge_conf, caplog): freqtrade = get_patched_freqtradebot(mocker, edge_conf) - mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.001)) + mocker.patch(f'{EXMS}.get_fee', MagicMock(return_value=0.001)) mocker.patch('freqtrade.edge.edge_positioning.refresh_data', MagicMock()) mocker.patch('freqtrade.edge.edge_positioning.load_data', MagicMock(return_value={})) edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy) @@ -286,7 +286,7 @@ def test_edge_process_no_data(mocker, edge_conf, caplog): def test_edge_process_no_trades(mocker, edge_conf, caplog): freqtrade = get_patched_freqtradebot(mocker, edge_conf) - mocker.patch('freqtrade.exchange.Exchange.get_fee', return_value=0.001) + mocker.patch(f'{EXMS}.get_fee', return_value=0.001) mocker.patch('freqtrade.edge.edge_positioning.refresh_data', ) mocker.patch('freqtrade.edge.edge_positioning.load_data', mocked_load_data) # Return empty @@ -303,7 +303,7 @@ def test_edge_process_no_pairs(mocker, edge_conf, caplog): mocker.patch('freqtrade.freqtradebot.validate_config_consistency') freqtrade = get_patched_freqtradebot(mocker, edge_conf) - fee_mock = mocker.patch('freqtrade.exchange.Exchange.get_fee', return_value=0.001) + fee_mock = mocker.patch(f'{EXMS}.get_fee', return_value=0.001) mocker.patch('freqtrade.edge.edge_positioning.refresh_data') mocker.patch('freqtrade.edge.edge_positioning.load_data', mocked_load_data) # Return empty @@ -319,7 +319,7 @@ def test_edge_process_no_pairs(mocker, edge_conf, caplog): def test_edge_init_error(mocker, edge_conf,): edge_conf['stake_amount'] = 0.5 - mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.001)) + mocker.patch(f'{EXMS}.get_fee', MagicMock(return_value=0.001)) with pytest.raises(OperationalException, match='Edge works only with unlimited stake amount'): get_patched_freqtradebot(mocker, edge_conf) diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index 79d3c0836..616910682 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -7,7 +7,7 @@ import pytest from freqtrade.enums import CandleType, MarginMode, TradingMode from freqtrade.exceptions import DependencyException, InvalidOrderException, OperationalException -from tests.conftest import get_mock_coro, get_patched_exchange, log_has_re +from tests.conftest import EXMS, get_mock_coro, get_patched_exchange, log_has_re from tests.exchange.test_exchange import ccxt_exceptionhandlers @@ -34,8 +34,8 @@ def test_create_stoploss_order_binance(default_conf, mocker, limitratio, expecte default_conf['dry_run'] = False default_conf['margin_mode'] = MarginMode.ISOLATED default_conf['trading_mode'] = trademode - mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y) - mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y) exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') @@ -113,8 +113,8 @@ def test_create_stoploss_order_dry_run_binance(default_conf, mocker): api_mock = MagicMock() order_type = 'stop_loss_limit' default_conf['dry_run'] = True - mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y) - mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y) exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') @@ -600,7 +600,7 @@ def test_get_maintenance_ratio_and_amt_binance( mm_ratio, amt, ): - mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True) + mocker.patch(f'{EXMS}.exchange_has', return_value=True) exchange = get_patched_exchange(mocker, default_conf, id="binance") exchange._leverage_tiers = leverage_tiers (result_ratio, result_amt) = exchange.get_maintenance_ratio_and_amt(pair, nominal_value) diff --git a/tests/exchange/test_bitpanda.py b/tests/exchange/test_bitpanda.py index 4bd168e7e..de44be986 100644 --- a/tests/exchange/test_bitpanda.py +++ b/tests/exchange/test_bitpanda.py @@ -1,7 +1,7 @@ from datetime import datetime from unittest.mock import MagicMock -from tests.conftest import get_patched_exchange +from tests.conftest import EXMS, get_patched_exchange def test_get_trades_for_order(default_conf, mocker): @@ -9,7 +9,7 @@ def test_get_trades_for_order(default_conf, mocker): order_id = 'ABCD-ABCD' since = datetime(2018, 5, 5, 0, 0, 0) default_conf["dry_run"] = False - mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True) + mocker.patch(f'{EXMS}.exchange_has', return_value=True) api_mock = MagicMock() api_mock.fetch_my_trades = MagicMock(return_value=[{'id': 'TTR67E-3PFBD-76IISV', diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index bbeb56c6a..f06a53308 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -17,7 +17,7 @@ from freqtrade.enums import CandleType from freqtrade.exchange import timeframe_to_minutes, timeframe_to_prev_date from freqtrade.exchange.exchange import Exchange, timeframe_to_msecs from freqtrade.resolvers.exchange_resolver import ExchangeResolver -from tests.conftest import get_default_conf_usdt +from tests.conftest import EXMS, get_default_conf_usdt EXCHANGE_FIXTURE_TYPE = Tuple[Exchange, str] @@ -322,13 +322,12 @@ def exchange_futures(request, exchange_conf, class_mocker): class_mocker.patch( 'freqtrade.exchange.binance.Binance.fill_leverage_tiers') - class_mocker.patch('freqtrade.exchange.exchange.Exchange.fetch_trading_fees') + class_mocker.patch(f'{EXMS}.fetch_trading_fees') class_mocker.patch('freqtrade.exchange.okx.Okx.additional_exchange_init') class_mocker.patch('freqtrade.exchange.binance.Binance.additional_exchange_init') class_mocker.patch('freqtrade.exchange.bybit.Bybit.additional_exchange_init') - class_mocker.patch('freqtrade.exchange.exchange.Exchange.load_cached_leverage_tiers', - return_value=None) - class_mocker.patch('freqtrade.exchange.exchange.Exchange.cache_leverage_tiers') + class_mocker.patch(f'{EXMS}.load_cached_leverage_tiers', return_value=None) + class_mocker.patch(f'{EXMS}.cache_leverage_tiers') exchange = ExchangeResolver.load_exchange( request.param, exchange_conf, validate=True, load_leverage_tiers=True) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index bdffa0d29..c9d1b6cab 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -22,8 +22,8 @@ from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, API_RETRY_CO calculate_backoff, remove_credentials) from freqtrade.exchange.exchange import amount_to_contract_precision from freqtrade.resolvers.exchange_resolver import ExchangeResolver -from tests.conftest import (generate_test_data_raw, get_mock_coro, get_patched_exchange, log_has, - log_has_re, num_log_has_re) +from tests.conftest import (EXMS, generate_test_data_raw, get_mock_coro, get_patched_exchange, + log_has, log_has_re, num_log_has_re) # Make sure to always keep one exchange here which is NOT subclassed!! @@ -150,9 +150,9 @@ def test_remove_credentials(default_conf, caplog) -> None: def test_init_ccxt_kwargs(default_conf, mocker, caplog): - mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={})) - mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') - aei_mock = mocker.patch('freqtrade.exchange.Exchange.additional_exchange_init') + mocker.patch(f'{EXMS}._load_markets', MagicMock(return_value={})) + mocker.patch(f'{EXMS}.validate_stakecurrency') + aei_mock = mocker.patch(f'{EXMS}.additional_exchange_init') caplog.set_level(logging.INFO) conf = copy.deepcopy(default_conf) @@ -218,12 +218,12 @@ def test_init_exception(default_conf, mocker): def test_exchange_resolver(default_conf, mocker, caplog): - mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=MagicMock())) - mocker.patch('freqtrade.exchange.Exchange._load_async_markets') - mocker.patch('freqtrade.exchange.Exchange.validate_pairs') - mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') - mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') - mocker.patch('freqtrade.exchange.Exchange.validate_pricing') + mocker.patch(f'{EXMS}._init_ccxt', MagicMock(return_value=MagicMock())) + mocker.patch(f'{EXMS}._load_async_markets') + mocker.patch(f'{EXMS}.validate_pairs') + mocker.patch(f'{EXMS}.validate_timeframes') + mocker.patch(f'{EXMS}.validate_stakecurrency') + mocker.patch(f'{EXMS}.validate_pricing') exchange = ExchangeResolver.load_exchange('zaif', default_conf) assert isinstance(exchange, Exchange) @@ -362,9 +362,8 @@ def test_price_to_precision(price, precision_mode, precision, expected): def test_price_get_one_pip(default_conf, mocker, price, precision_mode, precision, expected): markets = PropertyMock(return_value={'ETH/BTC': {'precision': {'price': precision}}}) exchange = get_patched_exchange(mocker, default_conf, id="binance") - mocker.patch('freqtrade.exchange.Exchange.markets', markets) - mocker.patch('freqtrade.exchange.Exchange.precisionMode', - PropertyMock(return_value=precision_mode)) + mocker.patch(f'{EXMS}.markets', markets) + mocker.patch(f'{EXMS}.precisionMode', PropertyMock(return_value=precision_mode)) pair = 'ETH/BTC' assert pytest.approx(exchange.price_get_one_pip(pair, price)) == expected @@ -376,10 +375,7 @@ def test__get_stake_amount_limit(mocker, default_conf) -> None: markets = {'ETH/BTC': {'symbol': 'ETH/BTC'}} # no pair found - mocker.patch( - 'freqtrade.exchange.Exchange.markets', - PropertyMock(return_value=markets) - ) + mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets)) with pytest.raises(ValueError, match=r'.*get market information.*'): exchange.get_min_pair_stake_amount('BNB/BTC', 1, stoploss) @@ -388,10 +384,7 @@ def test__get_stake_amount_limit(mocker, default_conf) -> None: 'cost': {'min': None, 'max': None}, 'amount': {'min': None, 'max': None}, } - mocker.patch( - 'freqtrade.exchange.Exchange.markets', - PropertyMock(return_value=markets) - ) + mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets)) result = exchange.get_min_pair_stake_amount('ETH/BTC', 1, stoploss) assert result is None result = exchange.get_max_pair_stake_amount('ETH/BTC', 1) @@ -402,10 +395,7 @@ def test__get_stake_amount_limit(mocker, default_conf) -> None: 'cost': {'min': 2, 'max': 10000}, 'amount': {'min': None, 'max': None}, } - mocker.patch( - 'freqtrade.exchange.Exchange.markets', - PropertyMock(return_value=markets) - ) + mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets)) # min result = exchange.get_min_pair_stake_amount('ETH/BTC', 1, stoploss) expected_result = 2 * (1 + 0.05) / (1 - abs(stoploss)) @@ -422,10 +412,7 @@ def test__get_stake_amount_limit(mocker, default_conf) -> None: 'cost': {'min': None, 'max': None}, 'amount': {'min': 2, 'max': 10000}, } - mocker.patch( - 'freqtrade.exchange.Exchange.markets', - PropertyMock(return_value=markets) - ) + mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets)) result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) expected_result = 2 * 2 * (1 + 0.05) / (1 - abs(stoploss)) assert pytest.approx(result) == expected_result @@ -441,10 +428,7 @@ def test__get_stake_amount_limit(mocker, default_conf) -> None: 'cost': {'min': 2, 'max': None}, 'amount': {'min': 2, 'max': None}, } - mocker.patch( - 'freqtrade.exchange.Exchange.markets', - PropertyMock(return_value=markets) - ) + mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets)) result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) expected_result = max(2, 2 * 2) * (1 + 0.05) / (1 - abs(stoploss)) assert pytest.approx(result) == expected_result @@ -457,10 +441,7 @@ def test__get_stake_amount_limit(mocker, default_conf) -> None: 'cost': {'min': 8, 'max': 10000}, 'amount': {'min': 2, 'max': 500}, } - mocker.patch( - 'freqtrade.exchange.Exchange.markets', - PropertyMock(return_value=markets) - ) + mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets)) result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) expected_result = max(8, 2 * 2) * (1 + 0.05) / (1 - abs(stoploss)) assert pytest.approx(result) == expected_result @@ -496,10 +477,7 @@ def test__get_stake_amount_limit(mocker, default_conf) -> None: default_conf['trading_mode'] = 'futures' default_conf['margin_mode'] = 'isolated' exchange = get_patched_exchange(mocker, default_conf, id="binance") - mocker.patch( - 'freqtrade.exchange.Exchange.markets', - PropertyMock(return_value=markets) - ) + mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets)) # Contract size 0.01 result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -1) @@ -509,10 +487,7 @@ def test__get_stake_amount_limit(mocker, default_conf) -> None: assert result == 10 markets["ETH/BTC"]["contractSize"] = '10' - mocker.patch( - 'freqtrade.exchange.Exchange.markets', - PropertyMock(return_value=markets) - ) + mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets)) # With Leverage, Contract size 10 result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -1, 12.0) assert pytest.approx(result) == (expected_result / 12) * 10.0 @@ -531,10 +506,7 @@ def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None: 'cost': {'min': 0.0001, 'max': 4000}, 'amount': {'min': 0.001, 'max': 10000}, } - mocker.patch( - 'freqtrade.exchange.Exchange.markets', - PropertyMock(return_value=markets) - ) + mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets)) result = exchange.get_min_pair_stake_amount('ETH/BTC', 0.020405, stoploss) expected_result = max(0.0001, 0.001 * 0.020405) * (1 + 0.05) / (1 - abs(stoploss)) assert round(result, 8) == round(expected_result, 8) @@ -592,12 +564,12 @@ def test_set_sandbox_exception(default_conf, mocker): def test__load_async_markets(default_conf, mocker, caplog): - mocker.patch('freqtrade.exchange.Exchange._init_ccxt') - mocker.patch('freqtrade.exchange.Exchange.validate_pairs') - mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') - mocker.patch('freqtrade.exchange.Exchange._load_markets') - mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') - mocker.patch('freqtrade.exchange.Exchange.validate_pricing') + mocker.patch(f'{EXMS}._init_ccxt') + mocker.patch(f'{EXMS}.validate_pairs') + mocker.patch(f'{EXMS}.validate_timeframes') + mocker.patch(f'{EXMS}._load_markets') + mocker.patch(f'{EXMS}.validate_stakecurrency') + mocker.patch(f'{EXMS}.validate_pricing') exchange = Exchange(default_conf) exchange._api_async.load_markets = get_mock_coro(None) exchange._load_async_markets() @@ -614,19 +586,19 @@ def test__load_markets(default_conf, mocker, caplog): caplog.set_level(logging.INFO) api_mock = MagicMock() api_mock.load_markets = MagicMock(side_effect=ccxt.BaseError("SomeError")) - mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) - mocker.patch('freqtrade.exchange.Exchange.validate_pairs') - mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') - mocker.patch('freqtrade.exchange.Exchange._load_async_markets') - mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') - mocker.patch('freqtrade.exchange.Exchange.validate_pricing') + mocker.patch(f'{EXMS}._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch(f'{EXMS}.validate_pairs') + mocker.patch(f'{EXMS}.validate_timeframes') + mocker.patch(f'{EXMS}._load_async_markets') + mocker.patch(f'{EXMS}.validate_stakecurrency') + mocker.patch(f'{EXMS}.validate_pricing') Exchange(default_conf) assert log_has('Unable to initialize markets.', caplog) expected_return = {'ETH/BTC': 'available'} api_mock = MagicMock() api_mock.load_markets = MagicMock(return_value=expected_return) - mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch(f'{EXMS}._init_ccxt', MagicMock(return_value=api_mock)) default_conf['exchange']['pair_whitelist'] = ['ETH/BTC'] ex = Exchange(default_conf) @@ -684,11 +656,11 @@ def test_validate_stakecurrency(default_conf, stake_currency, mocker, caplog): 'ETH/BTC': {'quote': 'BTC'}, 'LTC/BTC': {'quote': 'BTC'}, 'XRP/ETH': {'quote': 'ETH'}, 'NEO/USDT': {'quote': 'USDT'}, }) - mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) - mocker.patch('freqtrade.exchange.Exchange.validate_pairs') - mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') - mocker.patch('freqtrade.exchange.Exchange._load_async_markets') - mocker.patch('freqtrade.exchange.Exchange.validate_pricing') + mocker.patch(f'{EXMS}._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch(f'{EXMS}.validate_pairs') + mocker.patch(f'{EXMS}.validate_timeframes') + mocker.patch(f'{EXMS}._load_async_markets') + mocker.patch(f'{EXMS}.validate_pricing') Exchange(default_conf) @@ -699,17 +671,17 @@ def test_validate_stakecurrency_error(default_conf, mocker, caplog): 'ETH/BTC': {'quote': 'BTC'}, 'LTC/BTC': {'quote': 'BTC'}, 'XRP/ETH': {'quote': 'ETH'}, 'NEO/USDT': {'quote': 'USDT'}, }) - mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) - mocker.patch('freqtrade.exchange.Exchange.validate_pairs') - mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') - mocker.patch('freqtrade.exchange.Exchange._load_async_markets') + mocker.patch(f'{EXMS}._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch(f'{EXMS}.validate_pairs') + mocker.patch(f'{EXMS}.validate_timeframes') + mocker.patch(f'{EXMS}._load_async_markets') with pytest.raises(OperationalException, match=r'XRP is not available as stake on .*' 'Available currencies are: BTC, ETH, USDT'): Exchange(default_conf) type(api_mock).load_markets = MagicMock(side_effect=ccxt.NetworkError('No connection.')) - mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch(f'{EXMS}._init_ccxt', MagicMock(return_value=api_mock)) with pytest.raises(OperationalException, match=r'Could not load markets, therefore cannot start\. Please.*'): @@ -757,11 +729,11 @@ def test_validate_pairs(default_conf, mocker): # test exchange.validate_pairs d id_mock = PropertyMock(return_value='test_exchange') type(api_mock).id = id_mock - mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) - mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') - mocker.patch('freqtrade.exchange.Exchange._load_async_markets') - mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') - mocker.patch('freqtrade.exchange.Exchange.validate_pricing') + mocker.patch(f'{EXMS}._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch(f'{EXMS}.validate_timeframes') + mocker.patch(f'{EXMS}._load_async_markets') + mocker.patch(f'{EXMS}.validate_stakecurrency') + mocker.patch(f'{EXMS}.validate_pricing') Exchange(default_conf) @@ -770,10 +742,10 @@ def test_validate_pairs_not_available(default_conf, mocker): type(api_mock).markets = PropertyMock(return_value={ 'XRP/BTC': {'inactive': True, 'base': 'XRP', 'quote': 'BTC'} }) - mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) - mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') - mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') - mocker.patch('freqtrade.exchange.Exchange._load_async_markets') + mocker.patch(f'{EXMS}._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch(f'{EXMS}.validate_timeframes') + mocker.patch(f'{EXMS}.validate_stakecurrency') + mocker.patch(f'{EXMS}._load_async_markets') with pytest.raises(OperationalException, match=r'not available'): Exchange(default_conf) @@ -782,19 +754,19 @@ def test_validate_pairs_not_available(default_conf, mocker): def test_validate_pairs_exception(default_conf, mocker, caplog): caplog.set_level(logging.INFO) api_mock = MagicMock() - mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value='Binance')) + mocker.patch(f'{EXMS}.name', PropertyMock(return_value='Binance')) type(api_mock).markets = PropertyMock(return_value={}) - mocker.patch('freqtrade.exchange.Exchange._init_ccxt', api_mock) - mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') - mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') - mocker.patch('freqtrade.exchange.Exchange.validate_pricing') - mocker.patch('freqtrade.exchange.Exchange._load_async_markets') + mocker.patch(f'{EXMS}._init_ccxt', api_mock) + mocker.patch(f'{EXMS}.validate_timeframes') + mocker.patch(f'{EXMS}.validate_stakecurrency') + mocker.patch(f'{EXMS}.validate_pricing') + mocker.patch(f'{EXMS}._load_async_markets') with pytest.raises(OperationalException, match=r'Pair ETH/BTC is not available on Binance'): Exchange(default_conf) - mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value={})) + mocker.patch(f'{EXMS}.markets', PropertyMock(return_value={})) Exchange(default_conf) assert log_has('Unable to validate pairs (assuming they are correct).', caplog) @@ -806,11 +778,11 @@ def test_validate_pairs_restricted(default_conf, mocker, caplog): 'XRP/BTC': {'quote': 'BTC', 'info': {'prohibitedIn': ['US']}}, 'NEO/BTC': {'quote': 'BTC', 'info': 'TestString'}, # info can also be a string ... }) - mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) - mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') - mocker.patch('freqtrade.exchange.Exchange._load_async_markets') - mocker.patch('freqtrade.exchange.Exchange.validate_pricing') - mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') + mocker.patch(f'{EXMS}._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch(f'{EXMS}.validate_timeframes') + mocker.patch(f'{EXMS}._load_async_markets') + mocker.patch(f'{EXMS}.validate_pricing') + mocker.patch(f'{EXMS}.validate_stakecurrency') Exchange(default_conf) assert log_has("Pair XRP/BTC is restricted for some users on this exchange." @@ -825,11 +797,11 @@ def test_validate_pairs_stakecompatibility(default_conf, mocker, caplog): 'XRP/BTC': {'quote': 'BTC'}, 'NEO/BTC': {'quote': 'BTC'}, 'HELLO-WORLD': {'quote': 'BTC'}, }) - mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) - mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') - mocker.patch('freqtrade.exchange.Exchange._load_async_markets') - mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') - mocker.patch('freqtrade.exchange.Exchange.validate_pricing') + mocker.patch(f'{EXMS}._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch(f'{EXMS}.validate_timeframes') + mocker.patch(f'{EXMS}._load_async_markets') + mocker.patch(f'{EXMS}.validate_stakecurrency') + mocker.patch(f'{EXMS}.validate_pricing') Exchange(default_conf) @@ -842,11 +814,11 @@ def test_validate_pairs_stakecompatibility_downloaddata(default_conf, mocker, ca 'XRP/BTC': {'quote': 'BTC'}, 'NEO/BTC': {'quote': 'BTC'}, 'HELLO-WORLD': {'quote': 'BTC'}, }) - mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) - mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') - mocker.patch('freqtrade.exchange.Exchange._load_async_markets') - mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') - mocker.patch('freqtrade.exchange.Exchange.validate_pricing') + mocker.patch(f'{EXMS}._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch(f'{EXMS}.validate_timeframes') + mocker.patch(f'{EXMS}._load_async_markets') + mocker.patch(f'{EXMS}.validate_stakecurrency') + mocker.patch(f'{EXMS}.validate_pricing') Exchange(default_conf) assert type(api_mock).load_markets.call_count == 1 @@ -860,10 +832,10 @@ def test_validate_pairs_stakecompatibility_fail(default_conf, mocker, caplog): 'XRP/BTC': {'quote': 'BTC'}, 'NEO/BTC': {'quote': 'BTC'}, 'HELLO-WORLD': {'quote': 'USDT'}, }) - mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) - mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') - mocker.patch('freqtrade.exchange.Exchange._load_async_markets') - mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') + mocker.patch(f'{EXMS}._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch(f'{EXMS}.validate_timeframes') + mocker.patch(f'{EXMS}._load_async_markets') + mocker.patch(f'{EXMS}.validate_stakecurrency') with pytest.raises(OperationalException, match=r"Stake-currency 'BTC' not compatible with.*"): Exchange(default_conf) @@ -883,11 +855,11 @@ def test_validate_timeframes(default_conf, mocker, timeframe): '1h': '1h'}) type(api_mock).timeframes = timeframes - mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) - mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={})) - mocker.patch('freqtrade.exchange.Exchange.validate_pairs') - mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') - mocker.patch('freqtrade.exchange.Exchange.validate_pricing') + mocker.patch(f'{EXMS}._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch(f'{EXMS}._load_markets', MagicMock(return_value={})) + mocker.patch(f'{EXMS}.validate_pairs') + mocker.patch(f'{EXMS}.validate_stakecurrency') + mocker.patch(f'{EXMS}.validate_pricing') Exchange(default_conf) @@ -903,9 +875,9 @@ def test_validate_timeframes_failed(default_conf, mocker): '1h': '1h'}) type(api_mock).timeframes = timeframes - mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) - mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={})) - mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) + mocker.patch(f'{EXMS}._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch(f'{EXMS}._load_markets', MagicMock(return_value={})) + mocker.patch(f'{EXMS}.validate_pairs', MagicMock()) with pytest.raises(OperationalException, match=r"Invalid timeframe '3m'. This exchange supports.*"): Exchange(default_conf) @@ -925,10 +897,10 @@ def test_validate_timeframes_emulated_ohlcv_1(default_conf, mocker): # delete timeframes so magicmock does not autocreate it del api_mock.timeframes - mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) - mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={})) - mocker.patch('freqtrade.exchange.Exchange.validate_pairs') - mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') + mocker.patch(f'{EXMS}._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch(f'{EXMS}._load_markets', MagicMock(return_value={})) + mocker.patch(f'{EXMS}.validate_pairs') + mocker.patch(f'{EXMS}.validate_stakecurrency') with pytest.raises(OperationalException, match=r'The ccxt library does not provide the list of timeframes ' r'for the exchange .* and this exchange ' @@ -945,11 +917,11 @@ def test_validate_timeframes_emulated_ohlcvi_2(default_conf, mocker): # delete timeframes so magicmock does not autocreate it del api_mock.timeframes - mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) - mocker.patch('freqtrade.exchange.Exchange._load_markets', + mocker.patch(f'{EXMS}._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch(f'{EXMS}._load_markets', MagicMock(return_value={'timeframes': None})) - mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) - mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') + mocker.patch(f'{EXMS}.validate_pairs', MagicMock()) + mocker.patch(f'{EXMS}.validate_stakecurrency') with pytest.raises(OperationalException, match=r'The ccxt library does not provide the list of timeframes ' r'for the exchange .* and this exchange ' @@ -969,12 +941,12 @@ def test_validate_timeframes_not_in_config(default_conf, mocker): '1h': '1h'}) type(api_mock).timeframes = timeframes - mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) - mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={})) - mocker.patch('freqtrade.exchange.Exchange.validate_pairs') - mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') - mocker.patch('freqtrade.exchange.Exchange.validate_pricing') - mocker.patch('freqtrade.exchange.Exchange.validate_required_startup_candles') + mocker.patch(f'{EXMS}._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch(f'{EXMS}._load_markets', MagicMock(return_value={})) + mocker.patch(f'{EXMS}.validate_pairs') + mocker.patch(f'{EXMS}.validate_stakecurrency') + mocker.patch(f'{EXMS}.validate_pricing') + mocker.patch(f'{EXMS}.validate_required_startup_candles') Exchange(default_conf) @@ -985,13 +957,13 @@ def test_validate_pricing(default_conf, mocker): 'fetchTicker': True, } type(api_mock).has = PropertyMock(return_value=has) - mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) - mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={})) - mocker.patch('freqtrade.exchange.exchange.Exchange.validate_trading_mode_and_margin_mode') - mocker.patch('freqtrade.exchange.Exchange.validate_pairs') - mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') - mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') - mocker.patch('freqtrade.exchange.Exchange.name', 'Binance') + mocker.patch(f'{EXMS}._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch(f'{EXMS}._load_markets', MagicMock(return_value={})) + mocker.patch(f'{EXMS}.validate_trading_mode_and_margin_mode') + mocker.patch(f'{EXMS}.validate_pairs') + mocker.patch(f'{EXMS}.validate_timeframes') + mocker.patch(f'{EXMS}.validate_stakecurrency') + mocker.patch(f'{EXMS}.name', 'Binance') ExchangeResolver.load_exchange('binance', default_conf) has.update({'fetchTicker': False}) with pytest.raises(OperationalException, match="Ticker pricing not available for .*"): @@ -1020,13 +992,13 @@ def test_validate_ordertypes(default_conf, mocker): api_mock = MagicMock() type(api_mock).has = PropertyMock(return_value={'createMarketOrder': True}) - mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) - mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={})) - mocker.patch('freqtrade.exchange.Exchange.validate_pairs') - mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') - mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') - mocker.patch('freqtrade.exchange.Exchange.validate_pricing') - mocker.patch('freqtrade.exchange.Exchange.name', 'Bittrex') + mocker.patch(f'{EXMS}._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch(f'{EXMS}._load_markets', MagicMock(return_value={})) + mocker.patch(f'{EXMS}.validate_pairs') + mocker.patch(f'{EXMS}.validate_timeframes') + mocker.patch(f'{EXMS}.validate_stakecurrency') + mocker.patch(f'{EXMS}.validate_pricing') + mocker.patch(f'{EXMS}.name', 'Bittrex') default_conf['order_types'] = { 'entry': 'limit', @@ -1037,7 +1009,7 @@ def test_validate_ordertypes(default_conf, mocker): Exchange(default_conf) type(api_mock).has = PropertyMock(return_value={'createMarketOrder': False}) - mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch(f'{EXMS}._init_ccxt', MagicMock(return_value=api_mock)) default_conf['order_types'] = { 'entry': 'limit', @@ -1080,12 +1052,12 @@ def test_validate_ordertypes_stop_advanced(default_conf, mocker, exchange_name, default_conf['trading_mode'] = TradingMode.FUTURES default_conf['margin_mode'] = MarginMode.ISOLATED type(api_mock).has = PropertyMock(return_value={'createMarketOrder': True}) - mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) - mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={})) - mocker.patch('freqtrade.exchange.Exchange.validate_pairs') - mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') - mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') - mocker.patch('freqtrade.exchange.Exchange.validate_pricing') + mocker.patch(f'{EXMS}._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch(f'{EXMS}._load_markets', MagicMock(return_value={})) + mocker.patch(f'{EXMS}.validate_pairs') + mocker.patch(f'{EXMS}.validate_timeframes') + mocker.patch(f'{EXMS}.validate_stakecurrency') + mocker.patch(f'{EXMS}.validate_pricing') default_conf['order_types'] = { 'entry': 'limit', 'exit': 'limit', @@ -1103,12 +1075,12 @@ def test_validate_ordertypes_stop_advanced(default_conf, mocker, exchange_name, def test_validate_order_types_not_in_config(default_conf, mocker): api_mock = MagicMock() - mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) - mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={})) - mocker.patch('freqtrade.exchange.Exchange.validate_pairs') - mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') - mocker.patch('freqtrade.exchange.Exchange.validate_pricing') - mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') + mocker.patch(f'{EXMS}._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch(f'{EXMS}._load_markets', MagicMock(return_value={})) + mocker.patch(f'{EXMS}.validate_pairs') + mocker.patch(f'{EXMS}.validate_timeframes') + mocker.patch(f'{EXMS}.validate_pricing') + mocker.patch(f'{EXMS}.validate_stakecurrency') conf = copy.deepcopy(default_conf) Exchange(conf) @@ -1116,14 +1088,14 @@ def test_validate_order_types_not_in_config(default_conf, mocker): def test_validate_required_startup_candles(default_conf, mocker, caplog): api_mock = MagicMock() - mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value='Binance')) + mocker.patch(f'{EXMS}.name', PropertyMock(return_value='Binance')) - mocker.patch('freqtrade.exchange.Exchange._init_ccxt', api_mock) - mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') - mocker.patch('freqtrade.exchange.Exchange._load_async_markets') - mocker.patch('freqtrade.exchange.Exchange.validate_pairs') - mocker.patch('freqtrade.exchange.Exchange.validate_pricing') - mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') + mocker.patch(f'{EXMS}._init_ccxt', api_mock) + mocker.patch(f'{EXMS}.validate_timeframes') + mocker.patch(f'{EXMS}._load_async_markets') + mocker.patch(f'{EXMS}.validate_pairs') + mocker.patch(f'{EXMS}.validate_pricing') + mocker.patch(f'{EXMS}.validate_stakecurrency') default_conf['startup_candle_count'] = 20 ex = Exchange(default_conf) @@ -1220,11 +1192,10 @@ def test_create_dry_run_order_fees( fee, ): mocker.patch( - 'freqtrade.exchange.Exchange.get_fee', - side_effect=lambda symbol, taker_or_maker: 2.0 if taker_or_maker == 'taker' else 1.0 + f'{EXMS}.get_fee', + side_effect=lambda symbol, taker_or_maker: 2.0 if taker_or_maker == 'taker' else 1.0 ) - mocker.patch('freqtrade.exchange.Exchange._dry_is_price_crossed', - return_value=price_side == 'other') + mocker.patch(f'{EXMS}._dry_is_price_crossed', return_value=price_side == 'other') exchange = get_patched_exchange(mocker, default_conf) order = exchange.create_dry_run_order( @@ -1241,8 +1212,7 @@ def test_create_dry_run_order_fees( else: assert order['fee'] is None - mocker.patch('freqtrade.exchange.Exchange._dry_is_price_crossed', - return_value=price_side != 'other') + mocker.patch(f'{EXMS}._dry_is_price_crossed', return_value=price_side != 'other') order1 = exchange.fetch_dry_run_order(order['id']) assert order1['fee']['rate'] == fee @@ -1303,8 +1273,7 @@ def test_create_dry_run_order_limit_fill(default_conf, mocker, side, price, fill order_book_l2_usd.reset_mock() # Empty orderbook test - mocker.patch('freqtrade.exchange.Exchange.fetch_l2_order_book', - return_value={'asks': [], 'bids': []}) + mocker.patch(f'{EXMS}.fetch_l2_order_book', return_value={'asks': [], 'bids': []}) exchange._dry_run_open_orders[order['id']]['status'] = 'open' order_closed = exchange.fetch_dry_run_order(order['id']) @@ -1372,8 +1341,8 @@ def test_create_order(default_conf, mocker, side, ordertype, rate, marketprice, }) default_conf['dry_run'] = False default_conf['margin_mode'] = 'isolated' - mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y) - mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange._set_leverage = MagicMock() exchange.set_margin_mode = MagicMock() @@ -1452,8 +1421,8 @@ def test_buy_prod(default_conf, mocker, exchange_name): } }) default_conf['dry_run'] = False - mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y) - mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) order = exchange.create_order(pair='ETH/BTC', ordertype=order_type, side="buy", @@ -1536,8 +1505,8 @@ def test_buy_considers_time_in_force(default_conf, mocker, exchange_name): } }) default_conf['dry_run'] = False - mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y) - mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) order_type = 'limit' @@ -1602,8 +1571,8 @@ def test_sell_prod(default_conf, mocker, exchange_name): }) default_conf['dry_run'] = False - mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y) - mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) order = exchange.create_order(pair='ETH/BTC', ordertype=order_type, @@ -1675,8 +1644,8 @@ def test_sell_considers_time_in_force(default_conf, mocker, exchange_name): }) api_mock.options = {} default_conf['dry_run'] = False - mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y) - mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) order_type = 'limit' @@ -1742,7 +1711,7 @@ def test_get_balances_prod(default_conf, mocker, exchange_name): @pytest.mark.parametrize("exchange_name", EXCHANGES) def test_fetch_positions(default_conf, mocker, exchange_name): - mocker.patch('freqtrade.exchange.Exchange.validate_trading_mode_and_margin_mode') + mocker.patch(f'{EXMS}.validate_trading_mode_and_margin_mode') api_mock = MagicMock() api_mock.fetch_positions = MagicMock(return_value=[ {'symbol': 'ETH/USDT:USDT', 'leverage': 5}, @@ -1798,7 +1767,7 @@ def test_fetch_trading_fees(default_conf, mocker): default_conf['trading_mode'] = TradingMode.FUTURES default_conf['margin_mode'] = MarginMode.ISOLATED api_mock.fetch_trading_fees = MagicMock(return_value=tick) - mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True) + mocker.patch(f'{EXMS}.exchange_has', return_value=True) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) assert '1INCH/USDT:USDT' in exchange._trading_fees @@ -1813,7 +1782,7 @@ def test_fetch_trading_fees(default_conf, mocker): api_mock.fetch_trading_fees = MagicMock(return_value={}) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.fetch_trading_fees() - mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True) + mocker.patch(f'{EXMS}.exchange_has', return_value=True) assert exchange.fetch_trading_fees() == {} @@ -1833,7 +1802,7 @@ def test_fetch_bids_asks(default_conf, mocker): } exchange_name = 'binance' api_mock.fetch_bids_asks = MagicMock(return_value=tick) - mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True) + mocker.patch(f'{EXMS}.exchange_has', return_value=True) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) # retrieve original ticker bidsasks = exchange.fetch_bids_asks() @@ -1866,7 +1835,7 @@ def test_fetch_bids_asks(default_conf, mocker): api_mock.fetch_bids_asks = MagicMock(return_value={}) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.fetch_bids_asks() - mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True) + mocker.patch(f'{EXMS}.exchange_has', return_value=True) assert exchange.fetch_bids_asks() == {} @@ -1885,7 +1854,7 @@ def test_get_tickers(default_conf, mocker, exchange_name): 'last': 41, } } - mocker.patch('freqtrade.exchange.exchange.Exchange.exchange_has', return_value=True) + mocker.patch(f'{EXMS}.exchange_has', return_value=True) api_mock.fetch_tickers = MagicMock(return_value=tick) api_mock.fetch_bids_asks = MagicMock(return_value={}) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) @@ -1928,7 +1897,7 @@ def test_get_tickers(default_conf, mocker, exchange_name): api_mock.fetch_bids_asks.reset_mock() default_conf['trading_mode'] = TradingMode.FUTURES default_conf['margin_mode'] = MarginMode.ISOLATED - mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True) + mocker.patch(f'{EXMS}.exchange_has', return_value=True) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.get_tickers() @@ -1937,7 +1906,7 @@ def test_get_tickers(default_conf, mocker, exchange_name): api_mock.fetch_tickers.reset_mock() api_mock.fetch_bids_asks.reset_mock() - mocker.patch('freqtrade.exchange.exchange.Exchange.exchange_has', return_value=False) + mocker.patch(f'{EXMS}.exchange_has', return_value=False) assert exchange.get_tickers() == {} @@ -2191,7 +2160,7 @@ def test_refresh_latest_ohlcv_cache(mocker, default_conf, candle_type, time_mach time_machine.move_to(start + timedelta(hours=99, minutes=30)) exchange = get_patched_exchange(mocker, default_conf) - mocker.patch("freqtrade.exchange.Exchange.ohlcv_candle_limit", return_value=100) + mocker.patch(f"{EXMS}.ohlcv_candle_limit", return_value=100) assert exchange._startup_candle_count == 0 exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv) @@ -2241,7 +2210,7 @@ def test_refresh_latest_ohlcv_cache(mocker, default_conf, candle_type, time_mach # New candle on exchange - return 100 candles - but skip one candle so we actually get 2 candles # in one go new_startdate = (start + timedelta(hours=2)).strftime('%Y-%m-%d %H:%M') - # mocker.patch("freqtrade.exchange.Exchange.ohlcv_candle_limit", return_value=100) + # mocker.patch(f"{EXMS}.ohlcv_candle_limit", return_value=100) ohlcv = generate_test_data_raw('1h', 100, new_startdate) exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv) res = exchange.refresh_latest_ohlcv(pairs) @@ -2342,7 +2311,7 @@ async def test__async_kucoin_get_candle_history(default_conf, mocker, caplog): "symbol=ETH-BTC&type=5min&startAt=1640268735&endAt=1640418735" "429 Too Many Requests" '{"code":"429000","msg":"Too Many Requests"}')) exchange = get_patched_exchange(mocker, default_conf, api_mock, id="kucoin") - mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value='KuCoin')) + mocker.patch(f'{EXMS}.name', PropertyMock(return_value='KuCoin')) msg = "Kucoin 429 error, avoid triggering DDosProtection backoff delay" assert not num_log_has_re(msg, caplog) @@ -2500,8 +2469,7 @@ def test_get_entry_rate(mocker, default_conf, caplog, side, ask, bid, default_conf['entry_pricing']['price_last_balance'] = last_ab default_conf['entry_pricing']['price_side'] = side exchange = get_patched_exchange(mocker, default_conf) - mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', - return_value={'ask': ask, 'last': last, 'bid': bid}) + mocker.patch(f'{EXMS}.fetch_ticker', return_value={'ask': ask, 'last': last, 'bid': bid}) assert exchange.get_rate('ETH/BTC', side="entry", is_short=False, refresh=True) == expected assert not log_has("Using cached entry rate for ETH/BTC.", caplog) @@ -2522,8 +2490,7 @@ def test_get_exit_rate(default_conf, mocker, caplog, side, bid, ask, default_conf['exit_pricing']['price_side'] = side if last_ab is not None: default_conf['exit_pricing']['price_last_balance'] = last_ab - mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', - return_value={'ask': ask, 'bid': bid, 'last': last}) + mocker.patch(f'{EXMS}.fetch_ticker', return_value={'ask': ask, 'bid': bid, 'last': last}) pair = "ETH/BTC" # Test regular mode @@ -2556,8 +2523,7 @@ def test_get_ticker_rate_error(mocker, entry, default_conf, caplog, side, is_sho default_conf['exit_pricing']['price_side'] = side default_conf['exit_pricing']['price_last_balance'] = last_ab exchange = get_patched_exchange(mocker, default_conf) - mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', - return_value={'ask': ask, 'last': last, 'bid': bid}) + mocker.patch(f'{EXMS}.fetch_ticker', return_value={'ask': ask, 'last': last, 'bid': bid}) with pytest.raises(PricingError): exchange.get_rate('ETH/BTC', refresh=True, side=entry, is_short=is_short) @@ -2581,7 +2547,7 @@ def test_get_exit_rate_orderbook( default_conf['exit_pricing']['use_order_book'] = True default_conf['exit_pricing']['order_book_top'] = 1 pair = "ETH/BTC" - mocker.patch('freqtrade.exchange.Exchange.fetch_l2_order_book', order_book_l2) + mocker.patch(f'{EXMS}.fetch_l2_order_book', order_book_l2) exchange = get_patched_exchange(mocker, default_conf) rate = exchange.get_rate(pair, refresh=True, side="exit", is_short=is_short) assert not log_has("Using cached exit rate for ETH/BTC.", caplog) @@ -2599,8 +2565,7 @@ def test_get_exit_rate_orderbook_exception(default_conf, mocker, caplog): default_conf['exit_pricing']['order_book_top'] = 1 pair = "ETH/BTC" # Test What happens if the exchange returns an empty orderbook. - mocker.patch('freqtrade.exchange.Exchange.fetch_l2_order_book', - return_value={'bids': [[]], 'asks': [[]]}) + mocker.patch(f'{EXMS}.fetch_l2_order_book', return_value={'bids': [[]], 'asks': [[]]}) exchange = get_patched_exchange(mocker, default_conf) with pytest.raises(PricingError): exchange.get_rate(pair, refresh=True, side="exit", is_short=False) @@ -2614,8 +2579,7 @@ def test_get_exit_rate_exception(default_conf, mocker, is_short): # Ticker on one side can be empty in certain circumstances. default_conf['exit_pricing']['price_side'] = 'ask' pair = "ETH/BTC" - mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', - return_value={'ask': None, 'bid': 0.12, 'last': None}) + mocker.patch(f'{EXMS}.fetch_ticker', return_value={'ask': None, 'bid': 0.12, 'last': None}) exchange = get_patched_exchange(mocker, default_conf) with pytest.raises(PricingError, match=r"Exit-Rate for ETH/BTC was empty."): exchange.get_rate(pair, refresh=True, side="exit", is_short=is_short) @@ -2623,8 +2587,7 @@ def test_get_exit_rate_exception(default_conf, mocker, is_short): exchange._config['exit_pricing']['price_side'] = 'bid' assert exchange.get_rate(pair, refresh=True, side="exit", is_short=is_short) == 0.12 # Reverse sides - mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', - return_value={'ask': 0.13, 'bid': None, 'last': None}) + mocker.patch(f'{EXMS}.fetch_ticker', return_value={'ask': 0.13, 'bid': None, 'last': None}) with pytest.raises(PricingError, match=r"Exit-Rate for ETH/BTC was empty."): exchange.get_rate(pair, refresh=True, side="exit", is_short=is_short) @@ -2990,7 +2953,7 @@ async def test__async_get_trade_history_time_empty(default_conf, mocker, caplog, @pytest.mark.parametrize("exchange_name", EXCHANGES) def test_get_historic_trades(default_conf, mocker, caplog, exchange_name, trades_history): - mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True) + mocker.patch(f'{EXMS}.exchange_has', return_value=True) exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) pair = 'ETH/BTC' @@ -3012,7 +2975,7 @@ def test_get_historic_trades(default_conf, mocker, caplog, exchange_name, trades @pytest.mark.parametrize("exchange_name", EXCHANGES) def test_get_historic_trades_notsupported(default_conf, mocker, caplog, exchange_name, trades_history): - mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=False) + mocker.patch(f'{EXMS}.exchange_has', return_value=False) exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) pair = 'ETH/BTC' @@ -3028,7 +2991,7 @@ def test_get_historic_trades_notsupported(default_conf, mocker, caplog, exchange def test_cancel_order_dry_run(default_conf, mocker, exchange_name): default_conf['dry_run'] = True exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) - mocker.patch('freqtrade.exchange.Exchange._dry_is_price_crossed', return_value=True) + mocker.patch(f'{EXMS}._dry_is_price_crossed', return_value=True) assert exchange.cancel_order(order_id='123', pair='TKN/BTC') == {} assert exchange.cancel_stoploss_order(order_id='123', pair='TKN/BTC') == {} @@ -3156,24 +3119,24 @@ def test_cancel_stoploss_order(default_conf, mocker, exchange_name): @pytest.mark.parametrize("exchange_name", EXCHANGES) def test_cancel_stoploss_order_with_result(default_conf, mocker, exchange_name): default_conf['dry_run'] = False - mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', return_value={'for': 123}) + mocker.patch(f'{EXMS}.fetch_stoploss_order', return_value={'for': 123}) mocker.patch('freqtrade.exchange.Gate.fetch_stoploss_order', return_value={'for': 123}) exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) res = {'fee': {}, 'status': 'canceled', 'amount': 1234} - mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order', return_value=res) + mocker.patch(f'{EXMS}.cancel_stoploss_order', return_value=res) mocker.patch('freqtrade.exchange.Gate.cancel_stoploss_order', return_value=res) co = exchange.cancel_stoploss_order_with_result(order_id='_', pair='TKN/BTC', amount=555) assert co == res - mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order', return_value='canceled') + mocker.patch(f'{EXMS}.cancel_stoploss_order', return_value='canceled') mocker.patch('freqtrade.exchange.Gate.cancel_stoploss_order', return_value='canceled') # Fall back to fetch_stoploss_order co = exchange.cancel_stoploss_order_with_result(order_id='_', pair='TKN/BTC', amount=555) assert co == {'for': 123} exc = InvalidOrderException("") - mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', side_effect=exc) + mocker.patch(f'{EXMS}.fetch_stoploss_order', side_effect=exc) mocker.patch('freqtrade.exchange.Gate.fetch_stoploss_order', side_effect=exc) co = exchange.cancel_stoploss_order_with_result(order_id='_', pair='TKN/BTC', amount=555) assert co['amount'] == 555 @@ -3181,7 +3144,7 @@ def test_cancel_stoploss_order_with_result(default_conf, mocker, exchange_name): with pytest.raises(InvalidOrderException): exc = InvalidOrderException("Did not find order") - mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order', side_effect=exc) + mocker.patch(f'{EXMS}.cancel_stoploss_order', side_effect=exc) mocker.patch('freqtrade.exchange.Gate.cancel_stoploss_order', side_effect=exc) exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) exchange.cancel_stoploss_order_with_result(order_id='_', pair='TKN/BTC', amount=123) @@ -3315,7 +3278,7 @@ def test_get_trades_for_order(default_conf, mocker, exchange_name, trading_mode, default_conf["dry_run"] = False default_conf["trading_mode"] = trading_mode default_conf["margin_mode"] = 'isolated' - mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True) + mocker.patch(f'{EXMS}.exchange_has', return_value=True) api_mock = MagicMock() api_mock.fetch_my_trades = MagicMock(return_value=[{'id': 'TTR67E-3PFBD-76IISV', @@ -3358,7 +3321,7 @@ def test_get_trades_for_order(default_conf, mocker, exchange_name, trading_mode, 'get_trades_for_order', 'fetch_my_trades', order_id=order_id, pair='ETH/USDT:USDT', since=since) - mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=False)) + mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=False)) assert exchange.get_trades_for_order(order_id, 'ETH/USDT:USDT', since) == [] @@ -3550,7 +3513,7 @@ def test_get_markets(default_conf, mocker, markets_static, def test_get_markets_error(default_conf, mocker): ex = get_patched_exchange(mocker, default_conf) - mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=None)) + mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=None)) with pytest.raises(OperationalException, match="Markets were not loaded."): ex.get_markets('LTC', 'USDT', True, False) @@ -3695,7 +3658,7 @@ def test_market_is_tradable( quote, spot, margin, futures, trademode, add_dict, exchange, expected_result ) -> None: default_conf['trading_mode'] = trademode - mocker.patch('freqtrade.exchange.exchange.Exchange.validate_trading_mode_and_margin_mode') + mocker.patch(f'{EXMS}.validate_trading_mode_and_margin_mode') ex = get_patched_exchange(mocker, default_conf, id=exchange) market = { 'symbol': market_symbol, @@ -3740,7 +3703,7 @@ def test_order_has_fee(order, expected) -> None: (0.34, 'USDT', 0.01)), ]) def test_extract_cost_curr_rate(mocker, default_conf, order, expected) -> None: - mocker.patch('freqtrade.exchange.Exchange.calculate_fee_rate', MagicMock(return_value=0.01)) + mocker.patch(f'{EXMS}.calculate_fee_rate', MagicMock(return_value=0.01)) ex = get_patched_exchange(mocker, default_conf) assert ex.extract_cost_curr_rate(order['fee'], order['symbol'], cost=20, amount=1) == expected @@ -3785,7 +3748,7 @@ def test_extract_cost_curr_rate(mocker, default_conf, order, expected) -> None: 'fee': {'currency': None, 'cost': 0.005}}, None, None), ]) def test_calculate_fee_rate(mocker, default_conf, order, expected, unknown_fee_rate) -> None: - mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', return_value={'last': 0.081}) + mocker.patch(f'{EXMS}.fetch_ticker', return_value={'last': 0.081}) if unknown_fee_rate: default_conf['exchange']['unknown_fee_rate'] = unknown_fee_rate @@ -3857,7 +3820,7 @@ def test__get_funding_fees_from_exchange(default_conf, mocker, exchange_name): ]) type(api_mock).has = PropertyMock(return_value={'fetchFundingHistory': True}) - # mocker.patch('freqtrade.exchange.Exchange.get_funding_fees', lambda pair, since: y) + # mocker.patch(f'{EXMS}.get_funding_fees', lambda pair, since: y) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) date_time = datetime.strptime("2021-09-01T00:00:01.000Z", '%Y-%m-%dT%H:%M:%S.%fZ') unix_time = int(date_time.timestamp()) @@ -4257,8 +4220,7 @@ def test__fetch_and_calculate_funding_fees( type(api_mock).has = PropertyMock(return_value={'fetchFundingRateHistory': True}) ex = get_patched_exchange(mocker, default_conf, api_mock, id=exchange) - mocker.patch('freqtrade.exchange.Exchange.timeframes', PropertyMock( - return_value=['1h', '4h', '8h'])) + mocker.patch(f'{EXMS}.timeframes', PropertyMock(return_value=['1h', '4h', '8h'])) funding_fees = ex._fetch_and_calculate_funding_fees( pair='ADA/USDT', amount=amount, is_short=True, open_date=d1, close_date=d2) assert pytest.approx(funding_fees) == expected_fees @@ -4268,7 +4230,7 @@ def test__fetch_and_calculate_funding_fees( assert pytest.approx(funding_fees) == -expected_fees # Return empty "refresh_latest" - mocker.patch("freqtrade.exchange.Exchange.refresh_latest_ohlcv", return_value={}) + mocker.patch(f"{EXMS}.refresh_latest_ohlcv", return_value={}) ex = get_patched_exchange(mocker, default_conf, api_mock, id=exchange) with pytest.raises(ExchangeError, match="Could not find funding rates."): ex._fetch_and_calculate_funding_fees( @@ -4294,7 +4256,7 @@ def test__fetch_and_calculate_funding_fees_datetime_called( return_value=funding_rate_history_octohourly) type(api_mock).has = PropertyMock(return_value={'fetchOHLCV': True}) type(api_mock).has = PropertyMock(return_value={'fetchFundingRateHistory': True}) - mocker.patch('freqtrade.exchange.Exchange.timeframes', PropertyMock(return_value=['4h', '8h'])) + mocker.patch(f'{EXMS}.timeframes', PropertyMock(return_value=['4h', '8h'])) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange) d1 = datetime.strptime("2021-09-01 00:00:00 +0000", '%Y-%m-%d %H:%M:%S %z') @@ -4317,7 +4279,7 @@ def test__get_contract_size(mocker, default_conf, pair, expected_size, trading_m default_conf['trading_mode'] = trading_mode default_conf['margin_mode'] = 'isolated' exchange = get_patched_exchange(mocker, default_conf, api_mock) - mocker.patch('freqtrade.exchange.Exchange.markets', { + mocker.patch(f'{EXMS}.markets', { 'LTC/USD': { 'symbol': 'LTC/USD', 'contractSize': None, @@ -4353,7 +4315,7 @@ def test__order_contracts_to_amount( api_mock = MagicMock() default_conf['trading_mode'] = trading_mode default_conf['margin_mode'] = 'isolated' - mocker.patch('freqtrade.exchange.Exchange.markets', markets) + mocker.patch(f'{EXMS}.markets', markets) exchange = get_patched_exchange(mocker, default_conf, api_mock) orders = [ @@ -4475,7 +4437,7 @@ def test__trades_contracts_to_amount( api_mock = MagicMock() default_conf['trading_mode'] = trading_mode default_conf['margin_mode'] = 'isolated' - mocker.patch('freqtrade.exchange.Exchange.markets', markets) + mocker.patch(f'{EXMS}.markets', markets) exchange = get_patched_exchange(mocker, default_conf, api_mock) trades = [ @@ -4511,7 +4473,7 @@ def test__amount_to_contracts( default_conf['trading_mode'] = 'spot' default_conf['margin_mode'] = 'isolated' exchange = get_patched_exchange(mocker, default_conf, api_mock) - mocker.patch('freqtrade.exchange.Exchange.markets', { + mocker.patch(f'{EXMS}.markets', { 'LTC/USD': { 'symbol': 'LTC/USD', 'contractSize': None, @@ -4769,7 +4731,7 @@ def test_get_max_pair_stake_amount( }, } - mocker.patch('freqtrade.exchange.Exchange.markets', markets) + mocker.patch(f'{EXMS}.markets', markets) assert exchange.get_max_pair_stake_amount('XRP/USDT:USDT', 2.0) == 20000 assert exchange.get_max_pair_stake_amount('XRP/USDT:USDT', 2.0, 5) == 4000 assert exchange.get_max_pair_stake_amount('LTC/USDT:USDT', 2.0) == float('inf') @@ -4779,7 +4741,7 @@ def test_get_max_pair_stake_amount( default_conf['trading_mode'] = 'spot' exchange = get_patched_exchange(mocker, default_conf, api_mock) - mocker.patch('freqtrade.exchange.Exchange.markets', markets) + mocker.patch(f'{EXMS}.markets', markets) assert exchange.get_max_pair_stake_amount('BTC/USDT', 2.0) == 20000 assert exchange.get_max_pair_stake_amount('ADA/USDT', 2.0) == 500 @@ -4790,7 +4752,7 @@ def test_load_leverage_tiers(mocker, default_conf, leverage_tiers, exchange_name api_mock.fetch_leverage_tiers = MagicMock() type(api_mock).has = PropertyMock(return_value={'fetchLeverageTiers': True}) default_conf['dry_run'] = False - mocker.patch('freqtrade.exchange.exchange.Exchange.validate_trading_mode_and_margin_mode') + mocker.patch(f'{EXMS}.validate_trading_mode_and_margin_mode') api_mock.fetch_leverage_tiers = MagicMock(return_value={ 'ADA/USDT:USDT': [ @@ -4943,7 +4905,7 @@ def test_get_maintenance_ratio_and_amt_exceptions(mocker, default_conf, leverage api_mock = MagicMock() default_conf['trading_mode'] = 'futures' default_conf['margin_mode'] = 'isolated' - mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True) + mocker.patch(f'{EXMS}.exchange_has', return_value=True) exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange._leverage_tiers = leverage_tiers @@ -4980,7 +4942,7 @@ def test_get_maintenance_ratio_and_amt( api_mock = MagicMock() default_conf['trading_mode'] = 'futures' default_conf['margin_mode'] = 'isolated' - mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True) + mocker.patch(f'{EXMS}.exchange_has', return_value=True) exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange._leverage_tiers = leverage_tiers exchange.get_maintenance_ratio_and_amt(pair, value) == (mmr, maintAmt) @@ -5019,7 +4981,7 @@ def test_get_max_leverage_futures(default_conf, mocker, leverage_tiers): @pytest.mark.parametrize("exchange_name", ['bittrex', 'binance', 'kraken', 'gate', 'okx', 'bybit']) def test__get_params(mocker, default_conf, exchange_name): api_mock = MagicMock() - mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True) + mocker.patch(f'{EXMS}.exchange_has', return_value=True) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange._params = {'test': True} @@ -5324,8 +5286,8 @@ def test_stoploss_contract_size(mocker, default_conf, contract_size, order_amoun 'symbol': 'ETH/BTC', }) default_conf['dry_run'] = False - mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y) - mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y) exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange.get_contract_size = MagicMock(return_value=contract_size) diff --git a/tests/exchange/test_gate.py b/tests/exchange/test_gate.py index f777dd7d0..db7591a40 100644 --- a/tests/exchange/test_gate.py +++ b/tests/exchange/test_gate.py @@ -7,18 +7,18 @@ from freqtrade.enums import MarginMode, TradingMode from freqtrade.exceptions import OperationalException from freqtrade.exchange import Gate from freqtrade.resolvers.exchange_resolver import ExchangeResolver -from tests.conftest import get_patched_exchange +from tests.conftest import EXMS, get_patched_exchange def test_validate_order_types_gate(default_conf, mocker): default_conf['exchange']['name'] = 'gate' - mocker.patch('freqtrade.exchange.Exchange._init_ccxt') - mocker.patch('freqtrade.exchange.Exchange._load_markets', return_value={}) - mocker.patch('freqtrade.exchange.Exchange.validate_pairs') - mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') - mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') - mocker.patch('freqtrade.exchange.Exchange.validate_pricing') - mocker.patch('freqtrade.exchange.Exchange.name', 'Gate') + mocker.patch(f'{EXMS}._init_ccxt') + mocker.patch(f'{EXMS}._load_markets', return_value={}) + mocker.patch(f'{EXMS}.validate_pairs') + mocker.patch(f'{EXMS}.validate_timeframes') + mocker.patch(f'{EXMS}.validate_stakecurrency') + mocker.patch(f'{EXMS}.validate_pricing') + mocker.patch(f'{EXMS}.name', 'Gate') exch = ExchangeResolver.load_exchange('gate', default_conf, True) assert isinstance(exch, Gate) @@ -105,7 +105,7 @@ def test_stoploss_adjust_gate(mocker, default_conf, sl1, sl2, sl3, side): ('maker', 0.0, 0.0), ]) def test_fetch_my_trades_gate(mocker, default_conf, takerormaker, rate, cost): - mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True) + mocker.patch(f'{EXMS}.exchange_has', return_value=True) tick = {'ETH/USDT:USDT': { 'info': {'user_id': '', 'taker_fee': '0.0018', diff --git a/tests/exchange/test_huobi.py b/tests/exchange/test_huobi.py index e5fa986c3..5e4fd7316 100644 --- a/tests/exchange/test_huobi.py +++ b/tests/exchange/test_huobi.py @@ -5,7 +5,7 @@ import ccxt import pytest from freqtrade.exceptions import DependencyException, InvalidOrderException, OperationalException -from tests.conftest import get_patched_exchange +from tests.conftest import EXMS, get_patched_exchange from tests.exchange.test_exchange import ccxt_exceptionhandlers @@ -26,8 +26,8 @@ def test_create_stoploss_order_huobi(default_conf, mocker, limitratio, expected, } }) default_conf['dry_run'] = False - mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y) - mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y) exchange = get_patched_exchange(mocker, default_conf, api_mock, 'huobi') @@ -79,8 +79,8 @@ def test_create_stoploss_order_dry_run_huobi(default_conf, mocker): api_mock = MagicMock() order_type = 'stop-limit' default_conf['dry_run'] = True - mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y) - mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y) exchange = get_patched_exchange(mocker, default_conf, api_mock, 'huobi') diff --git a/tests/exchange/test_kraken.py b/tests/exchange/test_kraken.py index 3a183de93..40a5a5b38 100644 --- a/tests/exchange/test_kraken.py +++ b/tests/exchange/test_kraken.py @@ -5,7 +5,7 @@ import ccxt import pytest from freqtrade.exceptions import DependencyException, InvalidOrderException -from tests.conftest import get_patched_exchange +from tests.conftest import EXMS, get_patched_exchange from tests.exchange.test_exchange import ccxt_exceptionhandlers @@ -28,8 +28,8 @@ def test_buy_kraken_trading_agreement(default_conf, mocker): }) default_conf['dry_run'] = False - mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y) - mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y) exchange = get_patched_exchange(mocker, default_conf, api_mock, id="kraken") order = exchange.create_order( @@ -68,8 +68,8 @@ def test_sell_kraken_trading_agreement(default_conf, mocker): }) default_conf['dry_run'] = False - mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y) - mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y) exchange = get_patched_exchange(mocker, default_conf, api_mock, id="kraken") order = exchange.create_order(pair='ETH/BTC', ordertype=order_type, @@ -191,8 +191,8 @@ def test_create_stoploss_order_kraken(default_conf, mocker, ordertype, side, adj }) default_conf['dry_run'] = False - mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y) - mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y) exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken') @@ -262,8 +262,8 @@ def test_create_stoploss_order_kraken(default_conf, mocker, ordertype, side, adj def test_create_stoploss_order_dry_run_kraken(default_conf, mocker, side): api_mock = MagicMock() default_conf['dry_run'] = True - mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y) - mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y) exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken') diff --git a/tests/exchange/test_kucoin.py b/tests/exchange/test_kucoin.py index 0a57d728c..d2cb23091 100644 --- a/tests/exchange/test_kucoin.py +++ b/tests/exchange/test_kucoin.py @@ -5,7 +5,7 @@ import ccxt import pytest from freqtrade.exceptions import DependencyException, InvalidOrderException, OperationalException -from tests.conftest import get_patched_exchange +from tests.conftest import EXMS, get_patched_exchange from tests.exchange.test_exchange import ccxt_exceptionhandlers @@ -26,8 +26,8 @@ def test_create_stoploss_order_kucoin(default_conf, mocker, limitratio, expected } }) default_conf['dry_run'] = False - mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y) - mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y) exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kucoin') if order_type == 'limit': @@ -87,8 +87,8 @@ def test_stoploss_order_dry_run_kucoin(default_conf, mocker): api_mock = MagicMock() order_type = 'market' default_conf['dry_run'] = True - mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y) - mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y) exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kucoin') diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index 5565bbed2..cdfc943af 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -13,7 +13,7 @@ from freqtrade.freqai.utils import download_all_data_for_training, get_required_ from freqtrade.optimize.backtesting import Backtesting from freqtrade.persistence import Trade from freqtrade.plugins.pairlistmanager import PairListManager -from tests.conftest import create_mock_trades, get_patched_exchange, log_has_re +from tests.conftest import EXMS, create_mock_trades, get_patched_exchange, log_has_re from tests.freqai.conftest import get_patched_freqai_strategy, make_rl_config @@ -520,7 +520,7 @@ def test_get_state_info(mocker, freqai_conf, dp_exists, caplog, tickers): strategy = get_patched_freqai_strategy(mocker, freqai_conf) exchange = get_patched_exchange(mocker, freqai_conf) ticker_mock = MagicMock(return_value=tickers()['ETH/BTC']) - mocker.patch("freqtrade.exchange.Exchange.fetch_ticker", ticker_mock) + mocker.patch(f"{EXMS}.fetch_ticker", ticker_mock) strategy.dp = DataProvider(freqai_conf, exchange) if not dp_exists: diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index 4e78fc139..90efc40b1 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -8,7 +8,7 @@ from freqtrade.data.history import get_timerange from freqtrade.enums import ExitType from freqtrade.optimize.backtesting import Backtesting from freqtrade.persistence.trade_model import LocalTrade -from tests.conftest import patch_exchange +from tests.conftest import EXMS, patch_exchange from tests.optimize import (BTContainer, BTrade, _build_backtest_dataframe, _get_frame_time_from_offset, tests_timeframe) @@ -921,9 +921,9 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data: BTContainer) default_conf["use_exit_signal"] = data.use_exit_signal default_conf["max_open_trades"] = 10 - mocker.patch("freqtrade.exchange.Exchange.get_fee", return_value=0.0) - mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) - mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf')) + mocker.patch(f"{EXMS}.get_fee", return_value=0.0) + mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001) + mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf')) mocker.patch("freqtrade.exchange.Binance.get_max_leverage", return_value=100) patch_exchange(mocker) frame = _build_backtest_dataframe(data.data) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index e407b4173..8dee45b6d 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -26,8 +26,8 @@ from freqtrade.optimize.backtest_caching import get_strategy_run_id from freqtrade.optimize.backtesting import Backtesting from freqtrade.persistence import LocalTrade, Trade from freqtrade.resolvers import StrategyResolver -from tests.conftest import (CURRENT_TEST_STRATEGY, get_args, log_has, log_has_re, patch_exchange, - patched_configuration_load_config_file) +from tests.conftest import (CURRENT_TEST_STRATEGY, EXMS, get_args, log_has, log_has_re, + patch_exchange, patched_configuration_load_config_file) ORDER_TYPES = [ @@ -245,7 +245,7 @@ def test_setup_optimize_configuration_stake_amount(mocker, default_conf, caplog) def test_start(mocker, fee, default_conf, caplog) -> None: start_mock = MagicMock() - mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) + mocker.patch(f'{EXMS}.get_fee', fee) patch_exchange(mocker) mocker.patch('freqtrade.optimize.backtesting.Backtesting.start', start_mock) patched_configuration_load_config_file(mocker, default_conf) @@ -269,7 +269,7 @@ def test_backtesting_init(mocker, default_conf, order_types) -> None: """ default_conf["order_types"] = order_types patch_exchange(mocker) - get_fee = mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.5)) + get_fee = mocker.patch(f'{EXMS}.get_fee', MagicMock(return_value=0.5)) backtesting = Backtesting(default_conf) backtesting._set_strategy(backtesting.strategylist[0]) assert backtesting.config == default_conf @@ -290,7 +290,7 @@ def test_backtesting_init_no_timeframe(mocker, default_conf, caplog) -> None: default_conf['strategy_list'] = [CURRENT_TEST_STRATEGY, 'HyperoptableStrategy'] - mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.5)) + mocker.patch(f'{EXMS}.get_fee', MagicMock(return_value=0.5)) with pytest.raises(OperationalException, match=r"Timeframe needs to be set in either configuration"): Backtesting(default_conf) @@ -300,7 +300,7 @@ def test_data_with_fee(default_conf, mocker) -> None: patch_exchange(mocker) default_conf['fee'] = 0.1234 - fee_mock = mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.5)) + fee_mock = mocker.patch(f'{EXMS}.get_fee', MagicMock(return_value=0.5)) backtesting = Backtesting(default_conf) backtesting._set_strategy(backtesting.strategylist[0]) assert backtesting.fee == 0.1234 @@ -404,7 +404,7 @@ def test_backtesting_start_no_data(default_conf, mocker, caplog, testdatadir) -> def test_backtesting_no_pair_left(default_conf, mocker, caplog, testdatadir) -> None: - mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) + mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=True)) mocker.patch('freqtrade.data.history.history_utils.load_pair_history', MagicMock(return_value=pd.DataFrame())) mocker.patch('freqtrade.data.history.get_timerange', get_timerange) @@ -436,9 +436,9 @@ def test_backtesting_no_pair_left(default_conf, mocker, caplog, testdatadir) -> def test_backtesting_pairlist_list(default_conf, mocker, caplog, testdatadir, tickers) -> None: - mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) - mocker.patch('freqtrade.exchange.Exchange.get_tickers', tickers) - mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=True)) + mocker.patch(f'{EXMS}.get_tickers', tickers) + mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y) mocker.patch('freqtrade.data.history.get_timerange', get_timerange) patch_exchange(mocker) mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest') @@ -474,9 +474,9 @@ def test_backtesting_pairlist_list(default_conf, mocker, caplog, testdatadir, ti def test_backtest__enter_trade(default_conf, fee, mocker) -> None: default_conf['use_exit_signal'] = False - mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) - mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) - mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf')) + mocker.patch(f'{EXMS}.get_fee', fee) + mocker.patch(f'{EXMS}.get_min_pair_stake_amount', return_value=0.00001) + mocker.patch(f'{EXMS}.get_max_pair_stake_amount', return_value=float('inf')) patch_exchange(mocker) default_conf['stake_amount'] = 'unlimited' default_conf['max_open_trades'] = 2 @@ -525,7 +525,7 @@ def test_backtest__enter_trade(default_conf, fee, mocker) -> None: assert trade.stake_amount == 495 assert trade.is_short is True - mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=300.0) + mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=300.0) trade = backtesting._enter_trade(pair, row=row, direction='long') assert trade assert trade.stake_amount == 300.0 @@ -533,10 +533,10 @@ def test_backtest__enter_trade(default_conf, fee, mocker) -> None: def test_backtest__enter_trade_futures(default_conf_usdt, fee, mocker) -> None: default_conf_usdt['use_exit_signal'] = False - mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) - mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) - mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf')) - mocker.patch("freqtrade.exchange.Exchange.get_max_leverage", return_value=100) + mocker.patch(f'{EXMS}.get_fee', fee) + mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001) + mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf')) + mocker.patch(f"{EXMS}.get_max_leverage", return_value=100) mocker.patch("freqtrade.optimize.backtesting.price_to_precision", lambda p, *args: p) patch_exchange(mocker) default_conf_usdt['stake_amount'] = 300 @@ -564,7 +564,7 @@ def test_backtest__enter_trade_futures(default_conf_usdt, fee, mocker) -> None: ] backtesting.strategy.leverage = MagicMock(return_value=5.0) - mocker.patch("freqtrade.exchange.Exchange.get_maintenance_ratio_and_amt", + mocker.patch(f"{EXMS}.get_maintenance_ratio_and_amt", return_value=(0.01, 0.01)) # leverage = 5 @@ -601,7 +601,7 @@ def test_backtest__enter_trade_futures(default_conf_usdt, fee, mocker) -> None: assert pytest.approx(trade.liquidation_price) == 0.11787191 # Stake-amount too high! - mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=600.0) + mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=600.0) trade = backtesting._enter_trade(pair, row=row, direction='long') assert trade is None @@ -616,9 +616,9 @@ def test_backtest__enter_trade_futures(default_conf_usdt, fee, mocker) -> None: def test_backtest__check_trade_exit(default_conf, fee, mocker) -> None: default_conf['use_exit_signal'] = False - mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) - mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) - mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf')) + mocker.patch(f'{EXMS}.get_fee', fee) + mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001) + mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf')) patch_exchange(mocker) default_conf['timeframe_detail'] = '1m' default_conf['max_open_trades'] = 2 @@ -681,9 +681,9 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None: default_conf['use_exit_signal'] = False default_conf['max_open_trades'] = 10 - mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) - mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) - mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf')) + mocker.patch(f'{EXMS}.get_fee', fee) + mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001) + mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf')) patch_exchange(mocker) backtesting = Backtesting(default_conf) backtesting._set_strategy(backtesting.strategylist[0]) @@ -766,9 +766,9 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None: @pytest.mark.parametrize('use_detail', [True, False]) def test_backtest_one_detail(default_conf_usdt, fee, mocker, testdatadir, use_detail) -> None: default_conf_usdt['use_exit_signal'] = False - mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) - mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) - mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf')) + mocker.patch(f'{EXMS}.get_fee', fee) + mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001) + mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf')) if use_detail: default_conf_usdt['timeframe_detail'] = '1m' patch_exchange(mocker) @@ -854,12 +854,12 @@ def test_backtest_one_detail_futures( default_conf_usdt['margin_mode'] = 'isolated' default_conf_usdt['candle_type_def'] = CandleType.FUTURES - mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) - mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) - mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf')) + mocker.patch(f'{EXMS}.get_fee', fee) + mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001) + mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf')) mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist', PropertyMock(return_value=['XRP/USDT:USDT'])) - mocker.patch("freqtrade.exchange.Exchange.get_maintenance_ratio_and_amt", + mocker.patch(f"{EXMS}.get_maintenance_ratio_and_amt", return_value=(0.01, 0.01)) default_conf_usdt['timeframe'] = '1h' if use_detail: @@ -945,12 +945,12 @@ def test_backtest_one_detail_futures_funding_fees( default_conf_usdt['minimal_roi'] = {'0': 1} default_conf_usdt['dry_run_wallet'] = 100000 - mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) - mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) - mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf')) + mocker.patch(f'{EXMS}.get_fee', fee) + mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001) + mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf')) mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist', PropertyMock(return_value=['XRP/USDT:USDT'])) - mocker.patch("freqtrade.exchange.Exchange.get_maintenance_ratio_and_amt", + mocker.patch(f"{EXMS}.get_maintenance_ratio_and_amt", return_value=(0.01, 0.01)) default_conf_usdt['timeframe'] = '1h' if use_detail: @@ -1010,9 +1010,9 @@ def test_backtest_timedout_entry_orders(default_conf, fee, mocker, testdatadir) default_conf['startup_candle_count'] = 0 # Cancel unfilled order after 4 minutes on 5m timeframe. default_conf["unfilledtimeout"] = {"entry": 4} - mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) - mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) - mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf')) + mocker.patch(f'{EXMS}.get_fee', fee) + mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001) + mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf')) patch_exchange(mocker) default_conf['max_open_trades'] = 1 backtesting = Backtesting(default_conf) @@ -1035,9 +1035,9 @@ def test_backtest_timedout_entry_orders(default_conf, fee, mocker, testdatadir) def test_backtest_1min_timeframe(default_conf, fee, mocker, testdatadir) -> None: default_conf['use_exit_signal'] = False default_conf['max_open_trades'] = 1 - mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) - mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) - mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf')) + mocker.patch(f'{EXMS}.get_fee', fee) + mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001) + mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf')) patch_exchange(mocker) backtesting = Backtesting(default_conf) backtesting._set_strategy(backtesting.strategylist[0]) @@ -1061,9 +1061,9 @@ def test_backtest_trim_no_data_left(default_conf, fee, mocker, testdatadir) -> N default_conf['use_exit_signal'] = False default_conf['max_open_trades'] = 10 - mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) - mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) - mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf')) + mocker.patch(f'{EXMS}.get_fee', fee) + mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001) + mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf')) patch_exchange(mocker) backtesting = Backtesting(default_conf) backtesting._set_strategy(backtesting.strategylist[0]) @@ -1105,9 +1105,9 @@ def test_processed(default_conf, mocker, testdatadir) -> None: def test_backtest_dataprovider_analyzed_df(default_conf, fee, mocker, testdatadir) -> None: default_conf['use_exit_signal'] = False default_conf['max_open_trades'] = 10 - mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) - mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) - mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=100000) + mocker.patch(f'{EXMS}.get_fee', fee) + mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001) + mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=100000) patch_exchange(mocker) backtesting = Backtesting(default_conf) backtesting._set_strategy(backtesting.strategylist[0]) @@ -1155,9 +1155,9 @@ def test_backtest_pricecontours_protections(default_conf, fee, mocker, testdatad default_conf['enable_protections'] = True default_conf['timeframe'] = '1m' default_conf['max_open_trades'] = 1 - mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) - mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) - mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf')) + mocker.patch(f'{EXMS}.get_fee', fee) + mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001) + mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf')) tests = [ ['sine', 9], ['raise', 10], @@ -1203,9 +1203,9 @@ def test_backtest_pricecontours(default_conf, fee, mocker, testdatadir, default_conf['protections'] = protections default_conf['enable_protections'] = True - mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) - mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf')) - mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) + mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001) + mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf')) + mocker.patch(f'{EXMS}.get_fee', fee) # While entry-signals are unrealistic, running backtesting # over and over again should not cause different results @@ -1262,9 +1262,9 @@ def test_backtest_only_sell(mocker, default_conf, testdatadir): def test_backtest_alternate_buy_sell(default_conf, fee, mocker, testdatadir): - mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) - mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf')) - mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) + mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001) + mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf')) + mocker.patch(f'{EXMS}.get_fee', fee) default_conf['max_open_trades'] = 10 backtest_conf = _make_backtest_conf(mocker, conf=default_conf, pair='UNITTEST/BTC', datadir=testdatadir) @@ -1310,9 +1310,9 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir) dataframe['exit_short'] = 0 return dataframe - mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) - mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf')) - mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) + mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001) + mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf')) + mocker.patch(f'{EXMS}.get_fee', fee) patch_exchange(mocker) pairs = ['ADA/BTC', 'DASH/BTC', 'ETH/BTC', 'LTC/BTC', 'NXT/BTC'] diff --git a/tests/optimize/test_backtesting_adjust_position.py b/tests/optimize/test_backtesting_adjust_position.py index 23b5eb93b..9fc726bd1 100644 --- a/tests/optimize/test_backtesting_adjust_position.py +++ b/tests/optimize/test_backtesting_adjust_position.py @@ -12,17 +12,17 @@ from freqtrade.data import history from freqtrade.data.history import get_timerange from freqtrade.enums import ExitType from freqtrade.optimize.backtesting import Backtesting -from tests.conftest import patch_exchange +from tests.conftest import EXMS, patch_exchange def test_backtest_position_adjustment(default_conf, fee, mocker, testdatadir) -> None: default_conf['use_exit_signal'] = False default_conf['max_open_trades'] = 10 - mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) + mocker.patch(f'{EXMS}.get_fee', fee) mocker.patch('freqtrade.optimize.backtesting.amount_to_contract_precision', lambda x, *args, **kwargs: round(x, 8)) - mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) - mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf')) + mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001) + mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf')) patch_exchange(mocker) default_conf.update({ "stake_amount": 100.0, @@ -99,10 +99,10 @@ def test_backtest_position_adjustment(default_conf, fee, mocker, testdatadir) -> ]) def test_backtest_position_adjustment_detailed(default_conf, fee, mocker, leverage) -> None: default_conf['use_exit_signal'] = False - mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) - mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=10) - mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf')) - mocker.patch("freqtrade.exchange.Exchange.get_max_leverage", return_value=10) + mocker.patch(f'{EXMS}.get_fee', fee) + mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=10) + mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf')) + mocker.patch(f"{EXMS}.get_max_leverage", return_value=10) patch_exchange(mocker) default_conf.update({ diff --git a/tests/optimize/test_edge_cli.py b/tests/optimize/test_edge_cli.py index 8241a5362..64172bf1c 100644 --- a/tests/optimize/test_edge_cli.py +++ b/tests/optimize/test_edge_cli.py @@ -6,7 +6,7 @@ from unittest.mock import MagicMock from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_edge from freqtrade.enums import RunMode from freqtrade.optimize.edge_cli import EdgeCli -from tests.conftest import (CURRENT_TEST_STRATEGY, get_args, log_has, patch_exchange, +from tests.conftest import (CURRENT_TEST_STRATEGY, EXMS, get_args, log_has, patch_exchange, patched_configuration_load_config_file) @@ -71,7 +71,7 @@ def test_setup_edge_configuration_with_arguments(mocker, edge_conf, caplog) -> N def test_start(mocker, fee, edge_conf, caplog) -> None: start_mock = MagicMock() - mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) + mocker.patch(f'{EXMS}.get_fee', fee) patch_exchange(mocker) mocker.patch('freqtrade.optimize.edge_cli.EdgeCli.start', start_mock) patched_configuration_load_config_file(mocker, edge_conf) @@ -101,7 +101,7 @@ def test_edge_init_fee(mocker, edge_conf) -> None: patch_exchange(mocker) edge_conf['fee'] = 0.1234 edge_conf['stake_amount'] = 20 - fee_mock = mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.5)) + fee_mock = mocker.patch(f'{EXMS}.get_fee', return_value=0.5) edge_cli = EdgeCli(edge_conf) assert edge_cli.edge.fee == 0.1234 assert fee_mock.call_count == 0 diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 36ceaeab2..998798580 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -20,7 +20,7 @@ from freqtrade.optimize.hyperopt_tools import HyperoptTools from freqtrade.optimize.optimize_reports import generate_strategy_stats from freqtrade.optimize.space import SKDecimal from freqtrade.strategy import IntParameter -from tests.conftest import (CURRENT_TEST_STRATEGY, get_args, get_markets, log_has, log_has_re, +from tests.conftest import (CURRENT_TEST_STRATEGY, EXMS, get_args, get_markets, log_has, log_has_re, patch_exchange, patched_configuration_load_config_file) @@ -859,7 +859,7 @@ def test_simplified_interface_failed(mocker, hyperopt_conf, space) -> None: def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir, fee) -> None: patch_exchange(mocker) - mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) + mocker.patch(f'{EXMS}.get_fee', fee) (Path(tmpdir) / 'hyperopt_results').mkdir(parents=True) # No hyperopt needed hyperopt_conf.update({ @@ -897,10 +897,10 @@ def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir, fee) -> None: def test_in_strategy_auto_hyperopt_with_parallel(mocker, hyperopt_conf, tmpdir, fee) -> None: - mocker.patch('freqtrade.exchange.Exchange.validate_config', MagicMock()) - mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) - mocker.patch('freqtrade.exchange.Exchange._load_markets') - mocker.patch('freqtrade.exchange.Exchange.markets', + mocker.patch(f'{EXMS}.validate_config', MagicMock()) + mocker.patch(f'{EXMS}.get_fee', fee) + mocker.patch(f'{EXMS}._load_markets') + mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=get_markets())) (Path(tmpdir) / 'hyperopt_results').mkdir(parents=True) # No hyperopt needed @@ -938,7 +938,7 @@ def test_in_strategy_auto_hyperopt_with_parallel(mocker, hyperopt_conf, tmpdir, def test_in_strategy_auto_hyperopt_per_epoch(mocker, hyperopt_conf, tmpdir, fee) -> None: patch_exchange(mocker) - mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) + mocker.patch(f'{EXMS}.get_fee', fee) (Path(tmpdir) / 'hyperopt_results').mkdir(parents=True) hyperopt_conf.update({ @@ -996,7 +996,7 @@ def test_stake_amount_unlimited_max_open_trades(mocker, hyperopt_conf, tmpdir, f # This test is to ensure that unlimited max_open_trades are ignored for the backtesting # if we have an unlimited stake amount patch_exchange(mocker) - mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) + mocker.patch(f'{EXMS}.get_fee', fee) (Path(tmpdir) / 'hyperopt_results').mkdir(parents=True) hyperopt_conf.update({ 'strategy': 'HyperoptableStrategy', @@ -1024,7 +1024,7 @@ def test_max_open_trades_dump(mocker, hyperopt_conf, tmpdir, fee, capsys) -> Non # This test is to ensure that after hyperopting, max_open_trades is never # saved as inf in the output json params patch_exchange(mocker) - mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) + mocker.patch(f'{EXMS}.get_fee', fee) (Path(tmpdir) / 'hyperopt_results').mkdir(parents=True) hyperopt_conf.update({ 'strategy': 'HyperoptableStrategy', @@ -1070,7 +1070,7 @@ def test_max_open_trades_consistency(mocker, hyperopt_conf, tmpdir, fee) -> None # This test is to ensure that max_open_trades is the same across all functions needing it # after it has been changed from the hyperopt patch_exchange(mocker) - mocker.patch('freqtrade.exchange.Exchange.get_fee', return_value=0) + mocker.patch(f'{EXMS}.get_fee', return_value=0) (Path(tmpdir) / 'hyperopt_results').mkdir(parents=True) hyperopt_conf.update({ diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index 739c3a7ac..2a7d4ccec 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -18,8 +18,8 @@ from freqtrade.persistence import Trade from freqtrade.plugins.pairlist.pairlist_helpers import dynamic_expand_pairlist, expand_pairlist from freqtrade.plugins.pairlistmanager import PairListManager from freqtrade.resolvers import PairListResolver -from tests.conftest import (create_mock_trades_usdt, get_patched_exchange, get_patched_freqtradebot, - log_has, log_has_re, num_log_has) +from tests.conftest import (EXMS, create_mock_trades_usdt, get_patched_exchange, + get_patched_freqtradebot, log_has, log_has_re, num_log_has) # Exclude RemotePairList from tests. @@ -139,7 +139,7 @@ def test_log_cached(mocker, static_pl_conf, markets, tickers): def test_load_pairlist_noexist(mocker, markets, default_conf): freqtrade = get_patched_freqtradebot(mocker, default_conf) - mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets)) + mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets)) plm = PairListManager(freqtrade.exchange, default_conf, MagicMock()) with pytest.raises(OperationalException, match=r"Impossible to load Pairlist 'NonexistingPairList'. " @@ -150,7 +150,7 @@ def test_load_pairlist_noexist(mocker, markets, default_conf): def test_load_pairlist_verify_multi(mocker, markets_static, default_conf): freqtrade = get_patched_freqtradebot(mocker, default_conf) - mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets_static)) + mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets_static)) plm = PairListManager(freqtrade.exchange, default_conf, MagicMock()) # Call different versions one after the other, should always consider what was passed in # and have no side-effects (therefore the same check multiple times) @@ -166,7 +166,7 @@ def test_refresh_market_pair_not_in_whitelist(mocker, markets, static_pl_conf): freqtrade = get_patched_freqtradebot(mocker, static_pl_conf) - mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets)) + mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets)) freqtrade.pairlists.refresh_pairlist() # List ordered by BaseVolume whitelist = ['ETH/BTC', 'TKN/BTC'] @@ -324,7 +324,7 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): exchange_has=MagicMock(return_value=True), ) freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) - mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets_empty)) + mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets_empty)) # argument: use the whitelist dynamically by exchange-volume whitelist = [] @@ -523,7 +523,7 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t ('HOT/BTC', '1d', CandleType.SPOT): ohlcv_history_high_vola, } - mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) + mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=True)) freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) mocker.patch.multiple('freqtrade.exchange.Exchange', @@ -649,7 +649,7 @@ def test_VolumePairList_range(mocker, whitelist_conf, shitcoinmarkets, tickers, ('HOT/BTC', '1d', CandleType.SPOT): ohlcv_history_high_volume, } - mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) + mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=True)) if volumefilter_result == 'default_refresh_too_short': with pytest.raises(OperationalException, @@ -702,7 +702,7 @@ def test_PrecisionFilter_error(mocker, whitelist_conf) -> None: whitelist_conf['pairlists'] = [{"method": "StaticPairList"}, {"method": "PrecisionFilter"}] del whitelist_conf['stoploss'] - mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) + mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=True)) with pytest.raises(OperationalException, match=r"PrecisionFilter can only work with stoploss defined\..*"): @@ -713,7 +713,7 @@ def test_PerformanceFilter_error(mocker, whitelist_conf, caplog) -> None: whitelist_conf['pairlists'] = [{"method": "StaticPairList"}, {"method": "PerformanceFilter"}] if hasattr(Trade, 'query'): del Trade.query - mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) + mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=True)) exchange = get_patched_exchange(mocker, whitelist_conf) pm = PairListManager(exchange, whitelist_conf, MagicMock()) pm.refresh_pairlist() @@ -755,7 +755,7 @@ def test_PerformanceFilter_lookback(mocker, default_conf_usdt, fee, caplog) -> N {"method": "StaticPairList"}, {"method": "PerformanceFilter", "minutes": 60, "min_profit": 0.01} ] - mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) + mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=True)) exchange = get_patched_exchange(mocker, default_conf_usdt) pm = PairListManager(exchange, default_conf_usdt) pm.refresh_pairlist() @@ -781,7 +781,7 @@ def test_PerformanceFilter_keep_mid_order(mocker, default_conf_usdt, fee, caplog {"method": "StaticPairList", "allow_inactive": True}, {"method": "PerformanceFilter", "minutes": 60, } ] - mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True) + mocker.patch(f'{EXMS}.exchange_has', return_value=True) exchange = get_patched_exchange(mocker, default_conf_usdt) pm = PairListManager(exchange, default_conf_usdt) pm.refresh_pairlist() @@ -881,7 +881,7 @@ def test__whitelist_for_active_markets(mocker, whitelist_conf, markets, pairlist def test__whitelist_for_active_markets_empty(mocker, whitelist_conf, pairlist, tickers): whitelist_conf['pairlists'][0]['method'] = pairlist - mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True) + mocker.patch(f'{EXMS}.exchange_has', return_value=True) freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) mocker.patch.multiple('freqtrade.exchange.Exchange', @@ -897,7 +897,7 @@ def test__whitelist_for_active_markets_empty(mocker, whitelist_conf, pairlist, t def test_volumepairlist_invalid_sortvalue(mocker, whitelist_conf): whitelist_conf['pairlists'][0].update({"sort_key": "asdf"}) - mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) + mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=True)) with pytest.raises(OperationalException, match=r"key asdf not in .*"): get_patched_freqtradebot(mocker, whitelist_conf) @@ -1000,14 +1000,14 @@ def test_agefilter_caching(mocker, markets, whitelist_conf_agefilter, tickers, o ('LTC/BTC', '1d', CandleType.SPOT): ohlcv_history, ('XRP/BTC', '1d', CandleType.SPOT): ohlcv_history.iloc[[0]], } - mocker.patch('freqtrade.exchange.Exchange.refresh_latest_ohlcv', return_value=ohlcv_data) + mocker.patch(f'{EXMS}.refresh_latest_ohlcv', return_value=ohlcv_data) freqtrade.pairlists.refresh_pairlist() assert len(freqtrade.pairlists.whitelist) == 3 assert freqtrade.exchange.refresh_latest_ohlcv.call_count == 1 # Move to next day t.move_to("2021-09-02 01:00:00 +00:00") - mocker.patch('freqtrade.exchange.Exchange.refresh_latest_ohlcv', return_value=ohlcv_data) + mocker.patch(f'{EXMS}.refresh_latest_ohlcv', return_value=ohlcv_data) freqtrade.pairlists.refresh_pairlist() assert len(freqtrade.pairlists.whitelist) == 3 assert freqtrade.exchange.refresh_latest_ohlcv.call_count == 1 @@ -1021,7 +1021,7 @@ def test_agefilter_caching(mocker, markets, whitelist_conf_agefilter, tickers, o ('LTC/BTC', '1d', CandleType.SPOT): ohlcv_history, ('XRP/BTC', '1d', CandleType.SPOT): ohlcv_history, } - mocker.patch('freqtrade.exchange.Exchange.refresh_latest_ohlcv', return_value=ohlcv_data) + mocker.patch(f'{EXMS}.refresh_latest_ohlcv', return_value=ohlcv_data) freqtrade.pairlists.refresh_pairlist() assert len(freqtrade.pairlists.whitelist) == 4 # Called once (only for XRP/BTC) @@ -1033,7 +1033,7 @@ def test_OffsetFilter_error(mocker, whitelist_conf) -> None: [{"method": "StaticPairList"}, {"method": "OffsetFilter", "offset": -1}] ) - mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) + mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=True)) with pytest.raises(OperationalException, match=r'OffsetFilter requires offset to be >= 0'): @@ -1214,7 +1214,7 @@ def test_pricefilter_desc(mocker, whitelist_conf, markets, pairlistconfig, def test_pairlistmanager_no_pairlist(mocker, whitelist_conf): - mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) + mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=True)) whitelist_conf['pairlists'] = [] @@ -1266,7 +1266,7 @@ def test_performance_filter(mocker, whitelist_conf, pairlists, pair_allowlist, o allowlist_conf['pairlists'] = pairlists allowlist_conf['exchange']['pair_whitelist'] = pair_allowlist - mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) + mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=True)) freqtrade = get_patched_freqtradebot(mocker, allowlist_conf) mocker.patch.multiple('freqtrade.exchange.Exchange', @@ -1371,7 +1371,7 @@ def test_expand_pairlist_keep_invalid(wildcardlist, pairs, expected): def test_ProducerPairlist_no_emc(mocker, whitelist_conf): - mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) + mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=True)) whitelist_conf['pairlists'] = [ { @@ -1388,7 +1388,7 @@ def test_ProducerPairlist_no_emc(mocker, whitelist_conf): def test_ProducerPairlist(mocker, whitelist_conf, markets): - mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) + mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=True)) mocker.patch.multiple('freqtrade.exchange.Exchange', markets=PropertyMock(return_value=markets), exchange_has=MagicMock(return_value=True), diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 3eb391edd..40d14212f 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -12,8 +12,8 @@ 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, create_mock_trades_usdt, get_patched_freqtradebot, - patch_get_signal) +from tests.conftest import (EXMS, create_mock_trades, create_mock_trades_usdt, + get_patched_freqtradebot, patch_get_signal) def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: @@ -169,7 +169,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: response = deepcopy(gen_response) assert results[0] == response - mocker.patch('freqtrade.exchange.Exchange.get_rate', + mocker.patch(f'{EXMS}.get_rate', MagicMock(side_effect=ExchangeError("Pair 'ETH/BTC' not available"))) results = rpc._rpc_trade_status() assert isnan(results[0]['profit_ratio']) @@ -209,7 +209,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None: freqtradebot.state = State.RUNNING with pytest.raises(RPCException, match=r'.*no active trade*'): rpc._rpc_status_table(default_conf['stake_currency'], 'USD') - mocker.patch('freqtrade.exchange.Exchange._dry_is_price_crossed', return_value=False) + mocker.patch(f'{EXMS}._dry_is_price_crossed', return_value=False) freqtradebot.enter_positions() result, headers, fiat_profit_sum = rpc._rpc_status_table(default_conf['stake_currency'], 'USD') @@ -220,7 +220,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None: assert '0.00' == result[0][3] assert isnan(fiat_profit_sum) - mocker.patch('freqtrade.exchange.Exchange._dry_is_price_crossed', return_value=True) + mocker.patch(f'{EXMS}._dry_is_price_crossed', return_value=True) freqtradebot.process() result, headers, fiat_profit_sum = rpc._rpc_status_table(default_conf['stake_currency'], 'USD') @@ -231,7 +231,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None: assert '-0.41%' == result[0][3] assert isnan(fiat_profit_sum) - # Test with fiatconvert + # Test with fiat convert rpc._fiat_converter = CryptoToFiatConverter() result, headers, fiat_profit_sum = rpc._rpc_status_table(default_conf['stake_currency'], 'USD') assert "Since" in headers @@ -251,7 +251,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None: # 3 on top of the initial one. assert result[0][4] == '1/4' - mocker.patch('freqtrade.exchange.Exchange.get_rate', + mocker.patch(f'{EXMS}.get_rate', MagicMock(side_effect=ExchangeError("Pair 'ETH/BTC' not available"))) result, headers, fiat_profit_sum = rpc._rpc_status_table(default_conf['stake_currency'], 'USD') assert 'instantly' == result[0][2] @@ -367,15 +367,13 @@ def test_rpc_delete_trade(mocker, default_conf, fee, markets, caplog, is_short): assert stoploss_mock.call_count == 1 assert res['cancel_order_count'] == 2 - stoploss_mock = mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order', - side_effect=InvalidOrderException) + stoploss_mock = mocker.patch(f'{EXMS}.cancel_stoploss_order', side_effect=InvalidOrderException) res = rpc._rpc_delete('3') assert stoploss_mock.call_count == 1 stoploss_mock.reset_mock() - cancel_mock = mocker.patch('freqtrade.exchange.Exchange.cancel_order', - side_effect=InvalidOrderException) + cancel_mock = mocker.patch(f'{EXMS}.cancel_order', side_effect=InvalidOrderException) res = rpc._rpc_delete('4') assert cancel_mock.call_count == 1 @@ -423,7 +421,7 @@ def test_rpc_trade_statistics(default_conf_usdt, ticker, fee, mocker) -> None: assert stats['best_rate'] == 10.0 # Test non-available pair - mocker.patch('freqtrade.exchange.Exchange.get_rate', + mocker.patch(f'{EXMS}.get_rate', MagicMock(side_effect=ExchangeError("Pair 'XRP/USDT' not available"))) stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency) assert stats['trade_count'] == 7 @@ -708,15 +706,14 @@ def test_rpc_force_exit(default_conf, ticker, fee, mocker) -> None: freqtradebot.state = State.RUNNING assert cancel_order_mock.call_count == 0 - mocker.patch( - 'freqtrade.exchange.Exchange._dry_is_price_crossed', MagicMock(return_value=False)) + mocker.patch(f'{EXMS}._dry_is_price_crossed', MagicMock(return_value=False)) freqtradebot.enter_positions() # make an limit-buy open trade trade = Trade.query.filter(Trade.id == '3').first() filled_amount = trade.amount / 2 # Fetch order - it's open first, and closed after cancel_order is called. mocker.patch( - 'freqtrade.exchange.Exchange.fetch_order', + f'{EXMS}.fetch_order', side_effect=[{ 'id': trade.orders[0].order_id, 'status': 'open', @@ -738,7 +735,7 @@ def test_rpc_force_exit(default_conf, ticker, fee, mocker) -> None: assert pytest.approx(trade.amount) == filled_amount mocker.patch( - 'freqtrade.exchange.Exchange.fetch_order', + f'{EXMS}.fetch_order', return_value={ 'status': 'open', 'type': 'limit', @@ -752,7 +749,7 @@ def test_rpc_force_exit(default_conf, ticker, fee, mocker) -> None: amount = trade.amount # make an limit-buy open trade, if there is no 'filled', don't sell it mocker.patch( - 'freqtrade.exchange.Exchange.fetch_order', + f'{EXMS}.fetch_order', return_value={ 'status': 'open', 'type': 'limit', @@ -770,7 +767,7 @@ def test_rpc_force_exit(default_conf, ticker, fee, mocker) -> None: # make an limit-sell open trade mocker.patch( - 'freqtrade.exchange.Exchange.fetch_order', + f'{EXMS}.fetch_order', return_value={ 'status': 'open', 'type': 'limit', @@ -1138,7 +1135,7 @@ def test_rpc_whitelist_dynamic(mocker, default_conf) -> None: default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 4, }] - mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) + mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=True)) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) freqtradebot = get_patched_freqtradebot(mocker, default_conf) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 67156da45..46cfd07d7 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -24,7 +24,7 @@ from freqtrade.rpc import RPC from freqtrade.rpc.api_server import ApiServer from freqtrade.rpc.api_server.api_auth import create_token, get_user_from_token from freqtrade.rpc.api_server.uvicorn_threaded import UvicornServer -from tests.conftest import (CURRENT_TEST_STRATEGY, create_mock_trades, get_mock_coro, +from tests.conftest import (CURRENT_TEST_STRATEGY, EXMS, create_mock_trades, get_mock_coro, get_patched_freqtradebot, log_has, log_has_re, patch_get_signal) @@ -473,9 +473,9 @@ def test_api_balance(botclient, mocker, rpc_balance, tickers): ftbot, client = botclient ftbot.config['dry_run'] = False - mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=rpc_balance) - mocker.patch('freqtrade.exchange.Exchange.get_tickers', tickers) - mocker.patch('freqtrade.exchange.Exchange.get_valid_pair_combination', + mocker.patch(f'{EXMS}.get_balances', return_value=rpc_balance) + mocker.patch(f'{EXMS}.get_tickers', tickers) + mocker.patch(f'{EXMS}.get_valid_pair_combination', side_effect=lambda a, b: f"{a}/{b}") ftbot.wallets.update() @@ -731,15 +731,13 @@ def test_api_delete_open_order(botclient, mocker, fee, markets, ticker, is_short assert_response(rc, 502) assert 'No open order for trade_id' in rc.json()['error'] trade = Trade.get_trades([Trade.id == 6]).first() - mocker.patch('freqtrade.exchange.Exchange.fetch_order', - side_effect=ExchangeError) + mocker.patch(f'{EXMS}.fetch_order', side_effect=ExchangeError) rc = client_delete(client, f"{BASE_URI}/trades/6/open-order") assert_response(rc, 502) assert 'Order not found.' in rc.json()['error'] trade = Trade.get_trades([Trade.id == 6]).first() - mocker.patch('freqtrade.exchange.Exchange.fetch_order', - return_value=trade.orders[-1].to_ccxt_object()) + mocker.patch(f'{EXMS}.fetch_order', return_value=trade.orders[-1].to_ccxt_object()) rc = client_delete(client, f"{BASE_URI}/trades/6/open-order") assert_response(rc) @@ -1068,7 +1066,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets, is_short, 'orders': [ANY], } - mocker.patch('freqtrade.exchange.Exchange.get_rate', + mocker.patch(f'{EXMS}.get_rate', MagicMock(side_effect=ExchangeError("Pair 'ETH/BTC' not available"))) rc = client_get(client, f"{BASE_URI}/status") @@ -1631,7 +1629,7 @@ def test_sysinfo(botclient): def test_api_backtesting(botclient, mocker, fee, caplog, tmpdir): ftbot, client = botclient - mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) + mocker.patch(f'{EXMS}.get_fee', fee) rc = client_get(client, f"{BASE_URI}/backtest") # Backtest prevented in default mode diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 65e676860..84ca1914e 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -30,9 +30,9 @@ from freqtrade.persistence.models import Order from freqtrade.rpc import RPC from freqtrade.rpc.rpc import RPCException from freqtrade.rpc.telegram import Telegram, authorized_only -from tests.conftest import (CURRENT_TEST_STRATEGY, create_mock_trades, create_mock_trades_usdt, - get_patched_freqtradebot, log_has, log_has_re, patch_exchange, - patch_get_signal, patch_whitelist) +from tests.conftest import (CURRENT_TEST_STRATEGY, EXMS, create_mock_trades, + create_mock_trades_usdt, get_patched_freqtradebot, log_has, log_has_re, + patch_exchange, patch_get_signal, patch_whitelist) class DummyCls(Telegram): @@ -706,7 +706,7 @@ def test_profit_handle(default_conf_usdt, update, ticker_usdt, ticker_sell_up, f msg_mock.reset_mock() # Update the ticker with a market going up - mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', ticker_sell_up) + mocker.patch(f'{EXMS}.fetch_ticker', ticker_sell_up) # Simulate fulfilled LIMIT_SELL order for trade oobj = Order.parse_from_ccxt_object( limit_sell_order_usdt, limit_sell_order_usdt['symbol'], 'sell') @@ -764,10 +764,9 @@ def test_telegram_stats(default_conf, update, ticker, fee, mocker, is_short) -> def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance, tickers) -> None: default_conf['dry_run'] = False - mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=rpc_balance) - mocker.patch('freqtrade.exchange.Exchange.get_tickers', tickers) - mocker.patch('freqtrade.exchange.Exchange.get_valid_pair_combination', - side_effect=lambda a, b: f"{a}/{b}") + mocker.patch(f'{EXMS}.get_balances', return_value=rpc_balance) + mocker.patch(f'{EXMS}.get_tickers', tickers) + mocker.patch(f'{EXMS}.get_valid_pair_combination', side_effect=lambda a, b: f"{a}/{b}") telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot) @@ -790,7 +789,7 @@ def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance, tick def test_balance_handle_empty_response(default_conf, update, mocker) -> None: default_conf['dry_run'] = False - mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value={}) + mocker.patch(f'{EXMS}.get_balances', return_value={}) telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot) @@ -803,7 +802,7 @@ def test_balance_handle_empty_response(default_conf, update, mocker) -> None: def test_balance_handle_empty_response_dry(default_conf, update, mocker) -> None: - mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value={}) + mocker.patch(f'{EXMS}.get_balances', return_value={}) telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot) @@ -949,7 +948,7 @@ def test_telegram_forceexit_handle(default_conf, update, ticker, fee, assert trade # Increase the price and sell it - mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', ticker_sell_up) + mocker.patch(f'{EXMS}.fetch_ticker', ticker_sell_up) # /forceexit 1 context = MagicMock() @@ -1492,7 +1491,7 @@ def test_whitelist_static(default_conf, update, mocker) -> None: def test_whitelist_dynamic(default_conf, update, mocker) -> None: - mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) + mocker.patch(f'{EXMS}.exchange_has', return_value=True) default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 4 }] @@ -1707,8 +1706,7 @@ def test_telegram_delete_open_order(mocker, update, default_conf, fee, is_short, msg_mock.reset_mock() trade = Trade.get_trades([Trade.id == 6]).first() - mocker.patch('freqtrade.exchange.Exchange.fetch_order', - return_value=trade.orders[-1].to_ccxt_object()) + mocker.patch(f'{EXMS}.fetch_order', return_value=trade.orders[-1].to_ccxt_object()) context = MagicMock() context.args = [6] telegram._cancel_open_order(update=update, context=context) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index dc4539401..a80937e1a 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -22,9 +22,10 @@ from freqtrade.persistence import Order, PairLocks, Trade from freqtrade.persistence.models import PairLock from freqtrade.plugins.protections.iprotection import ProtectionReturn from freqtrade.worker import Worker -from tests.conftest import (create_mock_trades, create_mock_trades_usdt, get_patched_freqtradebot, - get_patched_worker, log_has, log_has_re, patch_edge, patch_exchange, - patch_get_signal, patch_wallet, patch_whitelist) +from tests.conftest import (EXMS, create_mock_trades, create_mock_trades_usdt, + get_patched_freqtradebot, get_patched_worker, log_has, log_has_re, + patch_edge, patch_exchange, patch_get_signal, patch_wallet, + patch_whitelist) from tests.conftest_trades import (MOCK_TRADE_COUNT, entry_side, exit_side, mock_order_1, mock_order_2, mock_order_2_sell, mock_order_3, mock_order_3_sell, mock_order_4, mock_order_5_stoploss, mock_order_6_sell) @@ -46,7 +47,7 @@ def patch_RPCManager(mocker) -> MagicMock: def test_freqtradebot_state(mocker, default_conf_usdt, markets) -> None: - mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets)) + mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets)) freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) assert freqtrade.state is State.RUNNING @@ -862,8 +863,7 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order, order['cost'] = 300 order['id'] = '444' - mocker.patch('freqtrade.exchange.Exchange.create_order', - MagicMock(return_value=order)) + mocker.patch(f'{EXMS}.create_order', MagicMock(return_value=order)) assert freqtrade.execute_entry(pair, stake_amount, is_short=is_short) trade = Trade.query.all()[2] trade.is_short = is_short @@ -881,8 +881,7 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order, order['average'] = 0.5 order['cost'] = 10.0 order['id'] = '555' - mocker.patch('freqtrade.exchange.Exchange.create_order', - MagicMock(return_value=order)) + mocker.patch(f'{EXMS}.create_order', MagicMock(return_value=order)) assert freqtrade.execute_entry(pair, stake_amount) trade = Trade.query.all()[3] trade.is_short = is_short @@ -919,19 +918,18 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order, order['average'] = 0.5 order['cost'] = 0.0 order['id'] = '66' - mocker.patch('freqtrade.exchange.Exchange.create_order', - MagicMock(return_value=order)) + mocker.patch(f'{EXMS}.create_order', MagicMock(return_value=order)) assert not freqtrade.execute_entry(pair, stake_amount) assert freqtrade.strategy.leverage.call_count == 0 if trading_mode == 'spot' else 2 # Fail to get price... - mocker.patch('freqtrade.exchange.Exchange.get_rate', MagicMock(return_value=0.0)) + mocker.patch(f'{EXMS}.get_rate', MagicMock(return_value=0.0)) with pytest.raises(PricingError, match="Could not determine entry price."): freqtrade.execute_entry(pair, stake_amount, is_short=is_short) # In case of custom entry price - mocker.patch('freqtrade.exchange.Exchange.get_rate', return_value=0.50) + mocker.patch(f'{EXMS}.get_rate', return_value=0.50) order['status'] = 'open' order['id'] = '5566' freqtrade.strategy.custom_entry_price = lambda **kwargs: 0.508 @@ -1066,11 +1064,11 @@ def test_add_stoploss_on_exchange(mocker, default_conf_usdt, limit_order, is_sho patch_exchange(mocker) order = limit_order[entry_side(is_short)] mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_trade', MagicMock(return_value=True)) - mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=order) - mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[]) + mocker.patch(f'{EXMS}.fetch_order', return_value=order) + mocker.patch(f'{EXMS}.get_trades_for_order', return_value=[]) stoploss = MagicMock(return_value={'id': 13434334}) - mocker.patch('freqtrade.exchange.Binance.create_stoploss', stoploss) + mocker.patch('freqtrade.exchange.binance.Binance.create_stoploss', stoploss) freqtrade = FreqtradeBot(default_conf_usdt) freqtrade.strategy.order_types['stoploss_on_exchange'] = True @@ -1135,7 +1133,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_ trade.stoploss_order_id = "100" hanging_stoploss_order = MagicMock(return_value={'status': 'open'}) - mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', hanging_stoploss_order) + mocker.patch(f'{EXMS}.fetch_stoploss_order', hanging_stoploss_order) assert freqtrade.handle_stoploss_on_exchange(trade) is False assert trade.stoploss_order_id == "100" @@ -1148,7 +1146,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_ trade.stoploss_order_id = "100" canceled_stoploss_order = MagicMock(return_value={'status': 'canceled'}) - mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', canceled_stoploss_order) + mocker.patch(f'{EXMS}.fetch_stoploss_order', canceled_stoploss_order) stoploss.reset_mock() assert freqtrade.handle_stoploss_on_exchange(trade) is False @@ -1183,17 +1181,14 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_ 'average': 2, 'amount': enter_order['amount'], }) - mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', stoploss_order_hit) + mocker.patch(f'{EXMS}.fetch_stoploss_order', stoploss_order_hit) assert freqtrade.handle_stoploss_on_exchange(trade) is True assert log_has_re(r'STOP_LOSS_LIMIT is hit for Trade\(id=1, .*\)\.', caplog) assert trade.stoploss_order_id is None assert trade.is_open is False caplog.clear() - mocker.patch( - 'freqtrade.exchange.Exchange.create_stoploss', - side_effect=ExchangeError() - ) + mocker.patch(f'{EXMS}.create_stoploss', side_effect=ExchangeError()) trade.is_open = True freqtrade.handle_stoploss_on_exchange(trade) assert log_has('Unable to place a stoploss order on exchange.', caplog) @@ -1203,9 +1198,8 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_ # It should try to add stoploss order trade.stoploss_order_id = 100 stoploss.reset_mock() - mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', - side_effect=InvalidOrderException()) - mocker.patch('freqtrade.exchange.Exchange.create_stoploss', stoploss) + mocker.patch(f'{EXMS}.fetch_stoploss_order', side_effect=InvalidOrderException()) + mocker.patch(f'{EXMS}.create_stoploss', stoploss) freqtrade.handle_stoploss_on_exchange(trade) assert stoploss.call_count == 1 @@ -1214,8 +1208,8 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_ trade.stoploss_order_id = None trade.is_open = False stoploss.reset_mock() - mocker.patch('freqtrade.exchange.Exchange.fetch_order') - mocker.patch('freqtrade.exchange.Exchange.create_stoploss', stoploss) + mocker.patch(f'{EXMS}.fetch_order') + mocker.patch(f'{EXMS}.create_stoploss', stoploss) assert freqtrade.handle_stoploss_on_exchange(trade) is False assert stoploss.call_count == 0 @@ -1237,10 +1231,10 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_ freqtrade.config['trailing_stop'] = True stoploss = MagicMock(side_effect=InvalidOrderException()) - mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order_with_result', + mocker.patch(f'{EXMS}.cancel_stoploss_order_with_result', side_effect=InvalidOrderException()) - mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', stoploss_order_cancelled) - mocker.patch('freqtrade.exchange.Exchange.create_stoploss', stoploss) + mocker.patch(f'{EXMS}.fetch_stoploss_order', stoploss_order_cancelled) + mocker.patch(f'{EXMS}.create_stoploss', stoploss) assert freqtrade.handle_stoploss_on_exchange(trade) is False assert trade.stoploss_order_id is None assert trade.is_open is False @@ -1269,7 +1263,7 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf_usdt, fee, caplog, get_fee=fee, ) mocker.patch.multiple( - 'freqtrade.exchange.Binance', + 'freqtrade.exchange.binance.Binance', fetch_stoploss_order=MagicMock(return_value={'status': 'canceled', 'id': 100}), create_stoploss=MagicMock(side_effect=ExchangeError()), ) @@ -1313,7 +1307,7 @@ def test_create_stoploss_order_invalid_order( get_fee=fee, ) mocker.patch.multiple( - 'freqtrade.exchange.Binance', + 'freqtrade.exchange.binance.Binance', fetch_order=MagicMock(return_value={'status': 'canceled'}), create_stoploss=MagicMock(side_effect=InvalidOrderException()), ) @@ -1366,7 +1360,7 @@ def test_create_stoploss_order_insufficient_funds( fetch_order=MagicMock(return_value={'status': 'canceled'}), ) mocker.patch.multiple( - 'freqtrade.exchange.Binance', + 'freqtrade.exchange.binance.Binance', create_stoploss=MagicMock(side_effect=InsufficientFundsError()), ) patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) @@ -1416,7 +1410,7 @@ def test_handle_stoploss_on_exchange_trailing( get_fee=fee, ) mocker.patch.multiple( - 'freqtrade.exchange.Binance', + 'freqtrade.exchange.binance.Binance', create_stoploss=stoploss, stoploss_adjust=MagicMock(return_value=True), ) @@ -1459,7 +1453,7 @@ def test_handle_stoploss_on_exchange_trailing( } }) - mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order', stoploss_order_hanging) + mocker.patch('freqtrade.exchange.binance.Binance.fetch_stoploss_order', stoploss_order_hanging) # stoploss initially at 5% assert freqtrade.handle_trade(trade) is False @@ -1467,7 +1461,7 @@ def test_handle_stoploss_on_exchange_trailing( # price jumped 2x mocker.patch( - 'freqtrade.exchange.Exchange.fetch_ticker', + f'{EXMS}.fetch_ticker', MagicMock(return_value={ 'bid': bid[0], 'ask': ask[0], @@ -1477,8 +1471,8 @@ def test_handle_stoploss_on_exchange_trailing( cancel_order_mock = MagicMock() stoploss_order_mock = MagicMock(return_value={'id': 'so1'}) - mocker.patch('freqtrade.exchange.Binance.cancel_stoploss_order', cancel_order_mock) - mocker.patch('freqtrade.exchange.Binance.create_stoploss', stoploss_order_mock) + mocker.patch('freqtrade.exchange.binance.Binance.cancel_stoploss_order', cancel_order_mock) + mocker.patch('freqtrade.exchange.binance.Binance.create_stoploss', stoploss_order_mock) # stoploss should not be updated as the interval is 60 seconds assert freqtrade.handle_trade(trade) is False @@ -1506,7 +1500,7 @@ def test_handle_stoploss_on_exchange_trailing( # price fell below stoploss, so dry-run sells trade. mocker.patch( - 'freqtrade.exchange.Exchange.fetch_ticker', + f'{EXMS}.fetch_ticker', MagicMock(return_value={ 'bid': bid[1], 'ask': ask[1], @@ -1541,7 +1535,7 @@ def test_handle_stoploss_on_exchange_trailing_error( get_fee=fee, ) mocker.patch.multiple( - 'freqtrade.exchange.Binance', + 'freqtrade.exchange.binance.Binance', create_stoploss=stoploss, stoploss_adjust=MagicMock(return_value=True), ) @@ -1579,9 +1573,9 @@ def test_handle_stoploss_on_exchange_trailing_error( 'stopPrice': '0.1' } } - mocker.patch('freqtrade.exchange.Binance.cancel_stoploss_order', + mocker.patch('freqtrade.exchange.binance.Binance.cancel_stoploss_order', side_effect=InvalidOrderException()) - mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order', + mocker.patch('freqtrade.exchange.binance.Binance.fetch_stoploss_order', return_value=stoploss_order_hanging) freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging) assert log_has_re(r"Could not cancel stoploss order abcd for pair ETH/USDT.*", caplog) @@ -1610,7 +1604,7 @@ def test_stoploss_on_exchange_price_rounding( stoploss_mock = MagicMock(return_value={'id': '13434334'}) adjust_mock = MagicMock(return_value=False) mocker.patch.multiple( - 'freqtrade.exchange.Binance', + 'freqtrade.exchange.binance.Binance', create_stoploss=stoploss_mock, stoploss_adjust=adjust_mock, price_to_precision=price_mock, @@ -1649,7 +1643,7 @@ def test_handle_stoploss_on_exchange_custom_stop( get_fee=fee, ) mocker.patch.multiple( - 'freqtrade.exchange.Binance', + 'freqtrade.exchange.binance.Binance', create_stoploss=stoploss, stoploss_adjust=MagicMock(return_value=True), ) @@ -1692,14 +1686,14 @@ def test_handle_stoploss_on_exchange_custom_stop( } }) - mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order', stoploss_order_hanging) + mocker.patch('freqtrade.exchange.binance.Binance.fetch_stoploss_order', stoploss_order_hanging) assert freqtrade.handle_trade(trade) is False assert freqtrade.handle_stoploss_on_exchange(trade) is False # price jumped 2x mocker.patch( - 'freqtrade.exchange.Exchange.fetch_ticker', + f'{EXMS}.fetch_ticker', MagicMock(return_value={ 'bid': 4.38 if not is_short else 1.9 / 2, 'ask': 4.4 if not is_short else 2.2 / 2, @@ -1709,8 +1703,8 @@ def test_handle_stoploss_on_exchange_custom_stop( cancel_order_mock = MagicMock() stoploss_order_mock = MagicMock(return_value={'id': 'so1'}) - mocker.patch('freqtrade.exchange.Binance.cancel_stoploss_order', cancel_order_mock) - mocker.patch('freqtrade.exchange.Binance.create_stoploss', stoploss_order_mock) + mocker.patch('freqtrade.exchange.binance.Binance.cancel_stoploss_order', cancel_order_mock) + mocker.patch('freqtrade.exchange.binance.Binance.create_stoploss', stoploss_order_mock) # stoploss should not be updated as the interval is 60 seconds assert freqtrade.handle_trade(trade) is False @@ -1740,7 +1734,7 @@ def test_handle_stoploss_on_exchange_custom_stop( # price fell below stoploss, so dry-run sells trade. mocker.patch( - 'freqtrade.exchange.Exchange.fetch_ticker', + f'{EXMS}.fetch_ticker', MagicMock(return_value={ 'bid': 4.17, 'ask': 4.19, @@ -1817,7 +1811,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, limit_orde 'stopPrice': '2.178' }) - mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', stoploss_order_hanging) + mocker.patch(f'{EXMS}.fetch_stoploss_order', stoploss_order_hanging) # stoploss initially at 20% as edge dictated it. assert freqtrade.handle_trade(trade) is False @@ -1826,11 +1820,11 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, limit_orde cancel_order_mock = MagicMock() stoploss_order_mock = MagicMock() - mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order', cancel_order_mock) - mocker.patch('freqtrade.exchange.Binance.create_stoploss', stoploss_order_mock) + mocker.patch(f'{EXMS}.cancel_stoploss_order', cancel_order_mock) + mocker.patch('freqtrade.exchange.binance.Binance.create_stoploss', stoploss_order_mock) # price goes down 5% - mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={ + mocker.patch(f'{EXMS}.fetch_ticker', MagicMock(return_value={ 'bid': 2.19 * 0.95, 'ask': 2.2 * 0.95, 'last': 2.19 * 0.95 @@ -1845,7 +1839,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, limit_orde cancel_order_mock.assert_not_called() # price jumped 2x - mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={ + mocker.patch(f'{EXMS}.fetch_ticker', MagicMock(return_value={ 'bid': 4.38, 'ask': 4.4, 'last': 4.38 @@ -1895,9 +1889,8 @@ def test_exit_positions(mocker, default_conf_usdt, limit_order, is_short, caplog freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_trade', MagicMock(return_value=True)) - mocker.patch('freqtrade.exchange.Exchange.fetch_order', - return_value=limit_order[entry_side(is_short)]) - mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[]) + mocker.patch(f'{EXMS}.fetch_order', return_value=limit_order[entry_side(is_short)]) + mocker.patch(f'{EXMS}.get_trades_for_order', return_value=[]) # TODO: should not be magicmock trade = MagicMock() @@ -1921,7 +1914,7 @@ def test_exit_positions(mocker, default_conf_usdt, limit_order, is_short, caplog def test_exit_positions_exception(mocker, default_conf_usdt, limit_order, caplog, is_short) -> None: freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) order = limit_order[entry_side(is_short)] - mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=order) + mocker.patch(f'{EXMS}.fetch_order', return_value=order) # TODO: should not be magicmock trade = MagicMock() @@ -1947,8 +1940,8 @@ def test_update_trade_state(mocker, default_conf_usdt, limit_order, is_short, ca order = limit_order[entry_side(is_short)] mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_trade', MagicMock(return_value=True)) - mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=order) - mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[]) + mocker.patch(f'{EXMS}.fetch_order', return_value=order) + mocker.patch(f'{EXMS}.get_trades_for_order', return_value=[]) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', return_value=0.0) order_id = order['id'] @@ -1999,7 +1992,7 @@ def test_update_trade_state(mocker, default_conf_usdt, limit_order, is_short, ca limit_buy_order_usdt_new['status'] = 'canceled' mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', side_effect=ValueError) - mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=limit_buy_order_usdt_new) + mocker.patch(f'{EXMS}.fetch_order', return_value=limit_buy_order_usdt_new) res = freqtrade.update_trade_state(trade, order_id) # Cancelled empty assert res is True @@ -2018,9 +2011,9 @@ def test_update_trade_state_withorderdict( trades_for_order[0]['amount'] = initial_amount order_id = "oid_123456" order['id'] = order_id - mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) + mocker.patch(f'{EXMS}.get_trades_for_order', return_value=trades_for_order) # fetch_order should not be called!! - mocker.patch('freqtrade.exchange.Exchange.fetch_order', MagicMock(side_effect=ValueError)) + mocker.patch(f'{EXMS}.fetch_order', MagicMock(side_effect=ValueError)) patch_exchange(mocker) amount = sum(x['amount'] for x in trades_for_order) freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) @@ -2062,7 +2055,7 @@ def test_update_trade_state_exception(mocker, default_conf_usdt, is_short, limit caplog) -> None: order = limit_order[entry_side(is_short)] freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) - mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=order) + mocker.patch(f'{EXMS}.fetch_order', return_value=order) # TODO: should not be magicmock trade = MagicMock() @@ -2080,8 +2073,7 @@ def test_update_trade_state_exception(mocker, default_conf_usdt, is_short, limit def test_update_trade_state_orderexception(mocker, default_conf_usdt, caplog) -> None: freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) - mocker.patch('freqtrade.exchange.Exchange.fetch_order', - MagicMock(side_effect=InvalidOrderException)) + mocker.patch(f'{EXMS}.fetch_order', MagicMock(side_effect=InvalidOrderException)) # TODO: should not be magicmock trade = MagicMock() @@ -2101,9 +2093,9 @@ def test_update_trade_state_sell( buy_order = limit_order[entry_side(is_short)] open_order = limit_order_open[exit_side(is_short)] l_order = limit_order[exit_side(is_short)] - mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) + mocker.patch(f'{EXMS}.get_trades_for_order', return_value=trades_for_order) # fetch_order should not be called!! - mocker.patch('freqtrade.exchange.Exchange.fetch_order', MagicMock(side_effect=ValueError)) + mocker.patch(f'{EXMS}.fetch_order', MagicMock(side_effect=ValueError)) wallet_mock = MagicMock() mocker.patch('freqtrade.wallets.Wallets.update', wallet_mock) @@ -2685,7 +2677,7 @@ def test_manage_open_orders_exit_usercustom( rpc_mock = patch_RPCManager(mocker) cancel_order_mock = MagicMock() patch_exchange(mocker) - mocker.patch('freqtrade.exchange.Exchange.get_min_pair_stake_amount', return_value=0.0) + mocker.patch(f'{EXMS}.get_min_pair_stake_amount', return_value=0.0) et_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.execute_trade_exit') mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -3013,7 +3005,7 @@ def test_handle_cancel_enter(mocker, caplog, default_conf_usdt, limit_order, is_ del cancel_buy_order['filled'] cancel_order_mock = MagicMock(return_value=cancel_buy_order) - mocker.patch('freqtrade.exchange.Exchange.cancel_order_with_result', cancel_order_mock) + mocker.patch(f'{EXMS}.cancel_order_with_result', cancel_order_mock) freqtrade = FreqtradeBot(default_conf_usdt) freqtrade._notify_enter_cancel = MagicMock() @@ -3044,11 +3036,11 @@ def test_handle_cancel_enter(mocker, caplog, default_conf_usdt, limit_order, is_ # Order remained open for some reason (cancel failed) cancel_buy_order['status'] = 'open' cancel_order_mock = MagicMock(return_value=cancel_buy_order) - mocker.patch('freqtrade.exchange.Exchange.cancel_order_with_result', cancel_order_mock) + mocker.patch(f'{EXMS}.cancel_order_with_result', cancel_order_mock) assert not freqtrade.handle_cancel_enter(trade, l_order, reason) assert log_has_re(r"Order .* for .* not cancelled.", caplog) # min_pair_stake empty should not crash - mocker.patch('freqtrade.exchange.Exchange.get_min_pair_stake_amount', return_value=None) + mocker.patch(f'{EXMS}.get_min_pair_stake_amount', return_value=None) assert not freqtrade.handle_cancel_enter(trade, limit_order[entry_side(is_short)], reason) @@ -3060,9 +3052,9 @@ def test_handle_cancel_enter_exchanges(mocker, caplog, default_conf_usdt, is_sho patch_RPCManager(mocker) patch_exchange(mocker) cancel_order_mock = mocker.patch( - 'freqtrade.exchange.Exchange.cancel_order_with_result', + f'{EXMS}.cancel_order_with_result', return_value=limit_buy_order_canceled_empty) - nofiy_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot._notify_enter_cancel') + notify_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot._notify_enter_cancel') freqtrade = FreqtradeBot(default_conf_usdt) reason = CANCEL_REASON['TIMEOUT'] @@ -3077,7 +3069,7 @@ def test_handle_cancel_enter_exchanges(mocker, caplog, default_conf_usdt, is_sho r'Removing .* from database\.', caplog ) - assert nofiy_mock.call_count == 1 + assert notify_mock.call_count == 1 @pytest.mark.parametrize("is_short", [False, True]) @@ -3114,7 +3106,7 @@ def test_handle_cancel_enter_corder_empty(mocker, default_conf_usdt, limit_order l_order['filled'] = 1.0 order = deepcopy(l_order) order['status'] = 'canceled' - mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=order) + mocker.patch(f'{EXMS}.fetch_order', return_value=order) assert not freqtrade.handle_cancel_enter(trade, l_order, reason) assert cancel_order_mock.call_count == 1 @@ -3127,8 +3119,8 @@ def test_handle_cancel_exit_limit(mocker, default_conf_usdt, fee) -> None: 'freqtrade.exchange.Exchange', cancel_order=cancel_order_mock, ) - mocker.patch('freqtrade.exchange.Exchange.get_rate', return_value=0.245441) - mocker.patch('freqtrade.exchange.Exchange.get_min_pair_stake_amount', return_value=0.2) + mocker.patch(f'{EXMS}.get_rate', return_value=0.245441) + mocker.patch(f'{EXMS}.get_min_pair_stake_amount', return_value=0.2) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_order_fee') @@ -3230,9 +3222,8 @@ def test_handle_cancel_exit_limit(mocker, default_conf_usdt, fee) -> None: def test_handle_cancel_exit_cancel_exception(mocker, default_conf_usdt) -> None: patch_RPCManager(mocker) patch_exchange(mocker) - mocker.patch('freqtrade.exchange.Exchange.get_min_pair_stake_amount', return_value=0.0) - mocker.patch('freqtrade.exchange.Exchange.cancel_order_with_result', - side_effect=InvalidOrderException()) + mocker.patch(f'{EXMS}.get_min_pair_stake_amount', return_value=0.0) + mocker.patch(f'{EXMS}.cancel_order_with_result', side_effect=InvalidOrderException()) freqtrade = FreqtradeBot(default_conf_usdt) @@ -3555,8 +3546,7 @@ def test_execute_trade_exit_down_stoploss_on_exchange_dry_run( def test_execute_trade_exit_sloe_cancel_exception( mocker, default_conf_usdt, ticker_usdt, fee, caplog) -> None: freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) - mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order', - side_effect=InvalidOrderException()) + mocker.patch(f'{EXMS}.cancel_stoploss_order', side_effect=InvalidOrderException()) mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=300)) create_order_mock = MagicMock(side_effect=[ {'id': '12345554'}, @@ -3668,7 +3658,7 @@ def test_may_execute_trade_exit_after_stoploss_on_exchange_hit( } }) - mocker.patch('freqtrade.exchange.Binance.create_stoploss', stoploss) + mocker.patch('freqtrade.exchange.binance.Binance.create_stoploss', stoploss) freqtrade = FreqtradeBot(default_conf_usdt) freqtrade.strategy.order_types['stoploss_on_exchange'] = True @@ -3707,7 +3697,7 @@ def test_may_execute_trade_exit_after_stoploss_on_exchange_hit( "fee": None, "trades": None }) - mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', stoploss_executed) + mocker.patch(f'{EXMS}.fetch_stoploss_order', stoploss_executed) freqtrade.exit_positions(trades) assert trade.stoploss_order_id is None @@ -4127,7 +4117,7 @@ def test_trailing_stop_loss(default_conf_usdt, limit_order_open, assert freqtrade.handle_trade(trade) is False # Raise praise into profits - mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', + mocker.patch(f'{EXMS}.fetch_ticker', MagicMock(return_value={ 'bid': 2.0 * val1, 'ask': 2.0 * val1, @@ -4138,7 +4128,7 @@ def test_trailing_stop_loss(default_conf_usdt, limit_order_open, assert freqtrade.handle_trade(trade) is False caplog.clear() # Price fell - mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', + mocker.patch(f'{EXMS}.fetch_ticker', MagicMock(return_value={ 'bid': 2.0 * val2, 'ask': 2.0 * val2, @@ -4208,7 +4198,7 @@ def test_trailing_stop_loss_positive( # Raise ticker_usdt above buy price mocker.patch( - 'freqtrade.exchange.Exchange.fetch_ticker', + f'{EXMS}.fetch_ticker', MagicMock(return_value={ 'bid': enter_price + (-0.06 if is_short else 0.06), 'ask': enter_price + (-0.06 if is_short else 0.06), @@ -4230,7 +4220,7 @@ def test_trailing_stop_loss_positive( caplog.clear() mocker.patch( - 'freqtrade.exchange.Exchange.fetch_ticker', + f'{EXMS}.fetch_ticker', MagicMock(return_value={ 'bid': enter_price + (-0.135 if is_short else 0.125), 'ask': enter_price + (-0.135 if is_short else 0.125), @@ -4246,7 +4236,7 @@ def test_trailing_stop_loss_positive( assert log_has("ETH/USDT - Adjusting stoploss...", caplog) mocker.patch( - 'freqtrade.exchange.Exchange.fetch_ticker', + f'{EXMS}.fetch_ticker', MagicMock(return_value={ 'bid': enter_price + (-0.02 if is_short else 0.02), 'ask': enter_price + (-0.02 if is_short else 0.02), @@ -4312,7 +4302,7 @@ def test_disable_ignore_roi_if_entry_signal(default_conf_usdt, limit_order, limi def test_get_real_amount_quote(default_conf_usdt, trades_for_order, buy_order_fee, fee, caplog, mocker): - mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) + mocker.patch(f'{EXMS}.get_trades_for_order', return_value=trades_for_order) amount = sum(x['amount'] for x in trades_for_order) trade = Trade( pair='LTC/ETH', @@ -4338,7 +4328,7 @@ def test_get_real_amount_quote(default_conf_usdt, trades_for_order, buy_order_fe def test_get_real_amount_quote_dust(default_conf_usdt, trades_for_order, buy_order_fee, fee, caplog, mocker): - mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) + mocker.patch(f'{EXMS}.get_trades_for_order', return_value=trades_for_order) walletmock = mocker.patch('freqtrade.wallets.Wallets.update') mocker.patch('freqtrade.wallets.Wallets.get_free', return_value=8.1122) amount = sum(x['amount'] for x in trades_for_order) @@ -4363,7 +4353,7 @@ def test_get_real_amount_quote_dust(default_conf_usdt, trades_for_order, buy_ord def test_get_real_amount_no_trade(default_conf_usdt, buy_order_fee, caplog, mocker, fee): - mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[]) + mocker.patch(f'{EXMS}.get_trades_for_order', return_value=[]) amount = buy_order_fee['amount'] trade = Trade( @@ -4417,7 +4407,7 @@ def test_get_real_amount( buy_order['fee'] = fee_par trades_for_order[0]['fee'] = fee_par - mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) + mocker.patch(f'{EXMS}.get_trades_for_order', return_value=trades_for_order) amount = sum(x['amount'] for x in trades_for_order) trade = Trade( pair='LTC/ETH', @@ -4431,7 +4421,7 @@ def test_get_real_amount( freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) if not use_ticker_usdt_rate: - mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', side_effect=ExchangeError) + mocker.patch(f'{EXMS}.fetch_ticker', side_effect=ExchangeError) caplog.clear() order_obj = Order.parse_from_ccxt_object(buy_order_fee, 'LTC/ETH', 'buy') @@ -4463,7 +4453,7 @@ def test_get_real_amount_multi( if fee_currency: trades_for_order[0]['fee']['currency'] = fee_currency - mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) + mocker.patch(f'{EXMS}.get_trades_for_order', return_value=trades_for_order) amount = float(sum(x['amount'] for x in trades_for_order)) default_conf_usdt['stake_currency'] = "ETH" @@ -4480,8 +4470,8 @@ def test_get_real_amount_multi( # Fake markets entry to enable fee parsing markets['BNB/ETH'] = markets['ETH/USDT'] freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) - mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets)) - mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', + mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets)) + mocker.patch(f'{EXMS}.fetch_ticker', return_value={'ask': 0.19, 'last': 0.2}) # Amount is reduced by "fee" @@ -4510,7 +4500,7 @@ def test_get_real_amount_invalid_order(default_conf_usdt, trades_for_order, buy_ limit_buy_order_usdt = deepcopy(buy_order_fee) limit_buy_order_usdt['fee'] = {'cost': 0.004} - mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[]) + mocker.patch(f'{EXMS}.get_trades_for_order', return_value=[]) amount = float(sum(x['amount'] for x in trades_for_order)) trade = Trade( pair='LTC/ETH', @@ -4531,9 +4521,9 @@ def test_get_real_amount_invalid_order(default_conf_usdt, trades_for_order, buy_ def test_get_real_amount_fees_order(default_conf_usdt, market_buy_order_usdt_doublefee, fee, mocker): - tfo_mock = mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[]) - mocker.patch('freqtrade.exchange.Exchange.get_valid_pair_combination', return_value='BNB/USDT') - mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', return_value={'last': 200}) + tfo_mock = mocker.patch(f'{EXMS}.get_trades_for_order', return_value=[]) + mocker.patch(f'{EXMS}.get_valid_pair_combination', return_value='BNB/USDT') + mocker.patch(f'{EXMS}.fetch_ticker', return_value={'last': 200}) trade = Trade( pair='LTC/USDT', amount=30.0, @@ -4559,7 +4549,7 @@ def test_get_real_amount_wrong_amount(default_conf_usdt, trades_for_order, buy_o limit_buy_order_usdt = deepcopy(buy_order_fee) limit_buy_order_usdt['amount'] = limit_buy_order_usdt['amount'] - 0.001 - mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) + mocker.patch(f'{EXMS}.get_trades_for_order', return_value=trades_for_order) amount = float(sum(x['amount'] for x in trades_for_order)) trade = Trade( pair='LTC/ETH', @@ -4584,7 +4574,7 @@ def test_get_real_amount_wrong_amount_rounding(default_conf_usdt, trades_for_ord limit_buy_order_usdt = deepcopy(buy_order_fee) trades_for_order[0]['amount'] = trades_for_order[0]['amount'] + 1e-15 - mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) + mocker.patch(f'{EXMS}.get_trades_for_order', return_value=trades_for_order) amount = float(sum(x['amount'] for x in trades_for_order)) trade = Trade( pair='LTC/ETH', @@ -4664,7 +4654,7 @@ def test_get_real_amount_in_point(default_conf_usdt, buy_order_fee, fee, mocker, ] }] - mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades) + mocker.patch(f'{EXMS}.get_trades_for_order', return_value=trades) amount = float(sum(x['amount'] for x in trades)) trade = Trade( pair='CEL/USDT', @@ -4746,7 +4736,7 @@ def test_order_book_depth_of_market( default_conf_usdt['entry_pricing']['check_depth_of_market']['bids_to_ask_delta'] = delta patch_RPCManager(mocker) patch_exchange(mocker) - mocker.patch('freqtrade.exchange.Exchange.fetch_l2_order_book', order_book_l2) + mocker.patch(f'{EXMS}.fetch_l2_order_book', order_book_l2) mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker_usdt, @@ -4843,7 +4833,7 @@ def test_order_book_exit_pricing( """ test order book ask strategy """ - mocker.patch('freqtrade.exchange.Exchange.fetch_l2_order_book', order_book_l2) + mocker.patch(f'{EXMS}.fetch_l2_order_book', order_book_l2) default_conf_usdt['exchange']['name'] = 'binance' default_conf_usdt['exit_pricing']['use_order_book'] = True default_conf_usdt['exit_pricing']['order_book_top'] = 1 @@ -4884,8 +4874,7 @@ def test_order_book_exit_pricing( assert freqtrade.handle_trade(trade) is True assert trade.close_rate_requested == order_book_l2.return_value['asks'][0][0] - mocker.patch('freqtrade.exchange.Exchange.fetch_l2_order_book', - return_value={'bids': [[]], 'asks': [[]]}) + mocker.patch(f'{EXMS}.fetch_l2_order_book', return_value={'bids': [[]], 'asks': [[]]}) with pytest.raises(PricingError): freqtrade.handle_trade(trade) assert log_has_re( @@ -4897,14 +4886,14 @@ def test_startup_state(default_conf_usdt, mocker): default_conf_usdt['pairlist'] = {'method': 'VolumePairList', 'config': {'number_assets': 20} } - mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) + mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=True)) worker = get_patched_worker(mocker, default_conf_usdt) assert worker.freqtrade.state is State.RUNNING def test_startup_trade_reinit(default_conf_usdt, edge_conf, mocker): - mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) + mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=True)) reinit_mock = MagicMock() mocker.patch('freqtrade.persistence.Trade.stoploss_reinitialization', reinit_mock) @@ -4961,7 +4950,7 @@ def test_cancel_all_open_orders(mocker, default_conf_usdt, fee, limit_order, lim is_short, buy_calls, sell_calls): default_conf_usdt['cancel_open_orders_on_exit'] = True mocker.patch( - 'freqtrade.exchange.Exchange.fetch_order', + f'{EXMS}.fetch_order', side_effect=[ ExchangeError(), limit_order[exit_side(is_short)], @@ -5017,17 +5006,17 @@ def test_startup_update_open_orders(mocker, default_conf_usdt, fee, caplog, is_s matching_buy_order.update({ 'status': 'closed', }) - mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=matching_buy_order) + mocker.patch(f'{EXMS}.fetch_order', return_value=matching_buy_order) freqtrade.startup_update_open_orders() # Only stoploss and sell orders are kept open assert len(Order.get_open_orders()) == 2 caplog.clear() - mocker.patch('freqtrade.exchange.Exchange.fetch_order', side_effect=ExchangeError) + mocker.patch(f'{EXMS}.fetch_order', side_effect=ExchangeError) freqtrade.startup_update_open_orders() assert log_has_re(r"Error updating Order .*", caplog) - mocker.patch('freqtrade.exchange.Exchange.fetch_order', side_effect=InvalidOrderException) + mocker.patch(f'{EXMS}.fetch_order', side_effect=InvalidOrderException) hto_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_order') # Orders which are no longer found after X days should be assumed as canceled. freqtrade.startup_update_open_orders() @@ -5072,7 +5061,7 @@ def test_update_trades_without_assigned_fees(mocker, default_conf_usdt, fee, is_ 'currency': order['symbol'].split('/')[0]}}) return order - mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order', + mocker.patch(f'{EXMS}.fetch_order_or_stoploss_order', side_effect=[ patch_with_fee(mock_order_2_sell(is_short=is_short)), patch_with_fee(mock_order_3_sell(is_short=is_short)), @@ -5132,8 +5121,7 @@ def test_reupdate_enter_order_fees(mocker, default_conf_usdt, fee, caplog, is_sh freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) mock_uts = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.update_trade_state') - mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order', - return_value={'status': 'open'}) + mocker.patch(f'{EXMS}.fetch_order_or_stoploss_order', return_value={'status': 'open'}) create_mock_trades(fee, is_short) trades = Trade.get_trades().all() @@ -5173,7 +5161,7 @@ def test_handle_insufficient_funds(mocker, default_conf_usdt, fee, is_short, cap freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) mock_uts = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.update_trade_state') - mock_fo = mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order', + mock_fo = mocker.patch(f'{EXMS}.fetch_order_or_stoploss_order', return_value={'status': 'open'}) def reset_open_orders(trade): @@ -5259,7 +5247,7 @@ def test_handle_insufficient_funds(mocker, default_conf_usdt, fee, is_short, cap caplog.clear() # Test error case - mock_fo = mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order', + mock_fo = mocker.patch(f'{EXMS}.fetch_order_or_stoploss_order', side_effect=ExchangeError()) order = mock_order_5_stoploss(is_short=is_short) @@ -5439,8 +5427,7 @@ def test_update_funding_fees( return ret - mocker.patch('freqtrade.exchange.Exchange.refresh_latest_ohlcv', - side_effect=refresh_latest_ohlcv_mock) + mocker.patch(f'{EXMS}.refresh_latest_ohlcv', side_effect=refresh_latest_ohlcv_mock) mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -5467,7 +5454,7 @@ def test_update_funding_fees( assert len(trades) == 3 for trade in trades: assert pytest.approx(trade.funding_fees) == 0 - mocker.patch('freqtrade.exchange.Exchange.create_order', return_value=open_exit_order) + mocker.patch(f'{EXMS}.create_order', return_value=open_exit_order) time_machine.move_to("2021-09-01 08:00:00 +00:00") if schedule_off: for trade in trades: @@ -5497,7 +5484,7 @@ def test_update_funding_fees( def test_update_funding_fees_error(mocker, default_conf, caplog): - mocker.patch('freqtrade.exchange.Exchange.get_funding_fees', side_effect=ExchangeError()) + mocker.patch(f'{EXMS}.get_funding_fees', side_effect=ExchangeError()) default_conf['trading_mode'] = 'futures' default_conf['margin_mode'] = 'isolated' freqtrade = get_patched_freqtradebot(mocker, default_conf) @@ -5551,9 +5538,8 @@ def test_position_adjust(mocker, default_conf_usdt, fee) -> None: 'id': '650', 'order_id': '650' } - mocker.patch('freqtrade.exchange.Exchange.create_order', - MagicMock(return_value=closed_successful_buy_order)) - mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order', + mocker.patch(f'{EXMS}.create_order', MagicMock(return_value=closed_successful_buy_order)) + mocker.patch(f'{EXMS}.fetch_order_or_stoploss_order', MagicMock(return_value=closed_successful_buy_order)) assert freqtrade.execute_entry(pair, stake_amount) # Should create an closed trade with an no open order id @@ -5603,10 +5589,8 @@ def test_position_adjust(mocker, default_conf_usdt, fee) -> None: 'id': '651', 'order_id': '651' } - mocker.patch('freqtrade.exchange.Exchange.create_order', - MagicMock(return_value=open_dca_order_1)) - mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order', - MagicMock(return_value=open_dca_order_1)) + mocker.patch(f'{EXMS}.create_order', MagicMock(return_value=open_dca_order_1)) + mocker.patch(f'{EXMS}.fetch_order_or_stoploss_order', MagicMock(return_value=open_dca_order_1)) assert freqtrade.execute_entry(pair, stake_amount, trade=trade) orders = Order.query.all() @@ -5637,9 +5621,9 @@ def test_position_adjust(mocker, default_conf_usdt, fee) -> None: # Assume it does nothing since order is still open fetch_order_mm = MagicMock(side_effect=make_sure_its_651) - mocker.patch('freqtrade.exchange.Exchange.create_order', fetch_order_mm) - mocker.patch('freqtrade.exchange.Exchange.fetch_order', fetch_order_mm) - mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order', fetch_order_mm) + mocker.patch(f'{EXMS}.create_order', fetch_order_mm) + mocker.patch(f'{EXMS}.fetch_order', fetch_order_mm) + mocker.patch(f'{EXMS}.fetch_order_or_stoploss_order', fetch_order_mm) freqtrade.update_trades_without_assigned_fees() orders = Order.query.all() @@ -5679,11 +5663,9 @@ def test_position_adjust(mocker, default_conf_usdt, fee) -> None: 'datetime': arrow.utcnow().isoformat(), } - mocker.patch('freqtrade.exchange.Exchange.create_order', - MagicMock(return_value=closed_dca_order_1)) - mocker.patch('freqtrade.exchange.Exchange.fetch_order', - MagicMock(return_value=closed_dca_order_1)) - mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order', + mocker.patch(f'{EXMS}.create_order', MagicMock(return_value=closed_dca_order_1)) + mocker.patch(f'{EXMS}.fetch_order', MagicMock(return_value=closed_dca_order_1)) + mocker.patch(f'{EXMS}.fetch_order_or_stoploss_order', MagicMock(return_value=closed_dca_order_1)) freqtrade.manage_open_orders() @@ -5723,11 +5705,9 @@ def test_position_adjust(mocker, default_conf_usdt, fee) -> None: 'id': '652', 'order_id': '652' } - mocker.patch('freqtrade.exchange.Exchange.create_order', - MagicMock(return_value=closed_dca_order_2)) - mocker.patch('freqtrade.exchange.Exchange.fetch_order', - MagicMock(return_value=closed_dca_order_2)) - mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order', + mocker.patch(f'{EXMS}.create_order', MagicMock(return_value=closed_dca_order_2)) + mocker.patch(f'{EXMS}.fetch_order', MagicMock(return_value=closed_dca_order_2)) + mocker.patch(f'{EXMS}.fetch_order_or_stoploss_order', MagicMock(return_value=closed_dca_order_2)) assert freqtrade.execute_entry(pair, stake_amount, trade=trade) @@ -5761,11 +5741,9 @@ def test_position_adjust(mocker, default_conf_usdt, fee) -> None: 'id': '653', 'order_id': '653' } - mocker.patch('freqtrade.exchange.Exchange.create_order', - MagicMock(return_value=closed_sell_dca_order_1)) - mocker.patch('freqtrade.exchange.Exchange.fetch_order', - MagicMock(return_value=closed_sell_dca_order_1)) - mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order', + mocker.patch(f'{EXMS}.create_order', MagicMock(return_value=closed_sell_dca_order_1)) + mocker.patch(f'{EXMS}.fetch_order', MagicMock(return_value=closed_sell_dca_order_1)) + mocker.patch(f'{EXMS}.fetch_order_or_stoploss_order', MagicMock(return_value=closed_sell_dca_order_1)) assert freqtrade.execute_trade_exit(trade=trade, limit=8, exit_check=ExitCheckTuple(exit_type=ExitType.PARTIAL_EXIT), @@ -5839,9 +5817,8 @@ def test_position_adjust2(mocker, default_conf_usdt, fee) -> None: 'id': '600', 'order_id': '600' } - mocker.patch('freqtrade.exchange.Exchange.create_order', - MagicMock(return_value=closed_successful_buy_order)) - mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order', + mocker.patch(f'{EXMS}.create_order', MagicMock(return_value=closed_successful_buy_order)) + mocker.patch(f'{EXMS}.fetch_order_or_stoploss_order', MagicMock(return_value=closed_successful_buy_order)) assert freqtrade.execute_entry(pair, amount) # Should create an closed trade with an no open order id @@ -5894,11 +5871,9 @@ def test_position_adjust2(mocker, default_conf_usdt, fee) -> None: 'id': '601', 'order_id': '601' } - mocker.patch('freqtrade.exchange.Exchange.create_order', - MagicMock(return_value=closed_sell_dca_order_1)) - mocker.patch('freqtrade.exchange.Exchange.fetch_order', - MagicMock(return_value=closed_sell_dca_order_1)) - mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order', + mocker.patch(f'{EXMS}.create_order', MagicMock(return_value=closed_sell_dca_order_1)) + mocker.patch(f'{EXMS}.fetch_order', MagicMock(return_value=closed_sell_dca_order_1)) + mocker.patch(f'{EXMS}.fetch_order_or_stoploss_order', MagicMock(return_value=closed_sell_dca_order_1)) assert freqtrade.execute_trade_exit(trade=trade, limit=ask, exit_check=ExitCheckTuple(exit_type=ExitType.PARTIAL_EXIT), @@ -5940,11 +5915,9 @@ def test_position_adjust2(mocker, default_conf_usdt, fee) -> None: 'id': '602', 'order_id': '602' } - mocker.patch('freqtrade.exchange.Exchange.create_order', - MagicMock(return_value=closed_sell_dca_order_2)) - mocker.patch('freqtrade.exchange.Exchange.fetch_order', - MagicMock(return_value=closed_sell_dca_order_2)) - mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order', + mocker.patch(f'{EXMS}.create_order', MagicMock(return_value=closed_sell_dca_order_2)) + mocker.patch(f'{EXMS}.fetch_order', MagicMock(return_value=closed_sell_dca_order_2)) + mocker.patch(f'{EXMS}.fetch_order_or_stoploss_order', MagicMock(return_value=closed_sell_dca_order_2)) assert freqtrade.execute_trade_exit(trade=trade, limit=ask, exit_check=ExitCheckTuple(exit_type=ExitType.PARTIAL_EXIT), @@ -6034,9 +6007,8 @@ def test_position_adjust3(mocker, default_conf_usdt, fee, data) -> None: 'id': f'60{idx}', 'order_id': f'60{idx}' } - mocker.patch('freqtrade.exchange.Exchange.create_order', - MagicMock(return_value=closed_successful_order)) - mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order', + mocker.patch(f'{EXMS}.create_order', MagicMock(return_value=closed_successful_order)) + mocker.patch(f'{EXMS}.fetch_order_or_stoploss_order', MagicMock(return_value=closed_successful_order)) if order[0] == 'buy': assert freqtrade.execute_entry(pair, amount, trade=trade) diff --git a/tests/test_integration.py b/tests/test_integration.py index 4d8b282c9..489027051 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -6,7 +6,7 @@ from freqtrade.enums import ExitCheckTuple, ExitType, TradingMode from freqtrade.persistence import Trade from freqtrade.persistence.models import Order from freqtrade.rpc.rpc import RPC -from tests.conftest import get_patched_freqtradebot, log_has_re, patch_get_signal +from tests.conftest import EXMS, get_patched_freqtradebot, log_has_re, patch_get_signal def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee, @@ -56,7 +56,7 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee, [ExitCheckTuple(exit_type=ExitType.EXIT_SIGNAL)]] ) cancel_order_mock = MagicMock() - mocker.patch('freqtrade.exchange.Binance.create_stoploss', stoploss) + mocker.patch('freqtrade.exchange.binance.Binance.create_stoploss', stoploss) mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, @@ -239,7 +239,7 @@ def test_dca_buying(default_conf_usdt, ticker_usdt, fee, mocker) -> None: # Reduce bid amount ticker_usdt_modif = ticker_usdt.return_value ticker_usdt_modif['bid'] = ticker_usdt_modif['bid'] * 0.995 - mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', return_value=ticker_usdt_modif) + mocker.patch(f'{EXMS}.fetch_ticker', return_value=ticker_usdt_modif) # additional buy order freqtrade.process() @@ -311,7 +311,7 @@ def test_dca_short(default_conf_usdt, ticker_usdt, fee, mocker) -> None: # Reduce bid amount ticker_usdt_modif = ticker_usdt.return_value ticker_usdt_modif['ask'] = ticker_usdt_modif['ask'] * 1.004 - mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', return_value=ticker_usdt_modif) + mocker.patch(f'{EXMS}.fetch_ticker', return_value=ticker_usdt_modif) # additional buy order freqtrade.process() @@ -367,10 +367,10 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker) amount_to_precision=lambda s, x, y: y, price_to_precision=lambda s, x, y: y, ) - mocker.patch('freqtrade.exchange.Exchange._dry_is_price_crossed', return_value=False) - mocker.patch("freqtrade.exchange.Exchange.get_max_leverage", return_value=10) - mocker.patch("freqtrade.exchange.Exchange.get_funding_fees", return_value=0) - mocker.patch("freqtrade.exchange.Exchange.get_maintenance_ratio_and_amt", return_value=(0, 0)) + mocker.patch(f'{EXMS}._dry_is_price_crossed', return_value=False) + mocker.patch(f"{EXMS}.get_max_leverage", return_value=10) + mocker.patch(f"{EXMS}.get_funding_fees", return_value=0) + mocker.patch(f"{EXMS}.get_maintenance_ratio_and_amt", return_value=(0, 0)) patch_get_signal(freqtrade) freqtrade.strategy.custom_entry_price = lambda **kwargs: ticker_usdt['ask'] * 0.96 @@ -413,7 +413,7 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker) assert trade.initial_stop_loss_pct is None # Fill order - mocker.patch('freqtrade.exchange.Exchange._dry_is_price_crossed', return_value=True) + mocker.patch(f'{EXMS}._dry_is_price_crossed', return_value=True) freqtrade.process() trade = Trade.get_trades().first() assert len(trade.orders) == 2 @@ -428,7 +428,7 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker) # 2nd order - not filling freqtrade.strategy.adjust_trade_position = MagicMock(return_value=120) - mocker.patch('freqtrade.exchange.Exchange._dry_is_price_crossed', return_value=False) + mocker.patch(f'{EXMS}._dry_is_price_crossed', return_value=False) freqtrade.process() trade = Trade.get_trades().first() @@ -452,7 +452,7 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker) # Fill DCA order freqtrade.strategy.adjust_trade_position = MagicMock(return_value=None) - mocker.patch('freqtrade.exchange.Exchange._dry_is_price_crossed', return_value=True) + mocker.patch(f'{EXMS}._dry_is_price_crossed', return_value=True) freqtrade.strategy.adjust_entry_price = MagicMock(side_effect=ValueError) freqtrade.process() @@ -484,7 +484,7 @@ def test_dca_exiting(default_conf_usdt, ticker_usdt, fee, mocker, caplog, levera price_to_precision=lambda s, x, y: y, get_min_pair_stake_amount=MagicMock(return_value=10), ) - mocker.patch("freqtrade.exchange.Exchange.get_max_leverage", return_value=10) + mocker.patch(f"{EXMS}.get_max_leverage", return_value=10) patch_get_signal(freqtrade) freqtrade.strategy.leverage = MagicMock(return_value=leverage) @@ -532,8 +532,7 @@ def test_dca_exiting(default_conf_usdt, ticker_usdt, fee, mocker, caplog, levera assert trade.is_open # use amount that would trunc to 0.0 once selling - mocker.patch("freqtrade.exchange.Exchange.amount_to_contract_precision", - lambda s, p, v: round(v, 1)) + mocker.patch(f"{EXMS}.amount_to_contract_precision", lambda s, p, v: round(v, 1)) freqtrade.strategy.adjust_trade_position = MagicMock(return_value=-0.01) freqtrade.process() trade = Trade.get_trades().first() diff --git a/tests/test_worker.py b/tests/test_worker.py index 88d495e13..79e2f35d4 100644 --- a/tests/test_worker.py +++ b/tests/test_worker.py @@ -8,11 +8,11 @@ import time_machine from freqtrade.data.dataprovider import DataProvider from freqtrade.enums import State from freqtrade.worker import Worker -from tests.conftest import get_patched_worker, log_has, log_has_re +from tests.conftest import EXMS, get_patched_worker, log_has, log_has_re def test_worker_state(mocker, default_conf, markets) -> None: - mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets)) + mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets)) worker = get_patched_worker(mocker, default_conf) assert worker.freqtrade.state is State.RUNNING From 2ca8b0b12e1b725c04f0ec6cdf4187dda5e17a6f Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 1 Mar 2023 20:14:15 +0100 Subject: [PATCH 095/360] Update more exchange mocks to use EXMS --- tests/commands/test_commands.py | 2 +- tests/data/test_dataprovider.py | 2 +- tests/exchange/test_exchange.py | 14 +- tests/optimize/test_backtest_detail.py | 2 +- tests/plugins/test_pairlist.py | 66 +++++----- tests/rpc/test_rpc.py | 42 +++--- tests/rpc/test_rpc_apiserver.py | 22 ++-- tests/rpc/test_rpc_telegram.py | 44 +++---- tests/test_freqtradebot.py | 174 ++++++++++++------------- tests/test_integration.py | 12 +- tests/test_wallets.py | 12 +- 11 files changed, 196 insertions(+), 196 deletions(-) diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index d2ce287e9..0ba1924a7 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -953,7 +953,7 @@ def test_start_list_freqAI_models(capsys): def test_start_test_pairlist(mocker, caplog, tickers, default_conf, capsys): patch_exchange(mocker, mock_markets=True) - mocker.patch.multiple('freqtrade.exchange.Exchange', + mocker.patch.multiple(EXMS, exchange_has=MagicMock(return_value=True), get_tickers=tickers, ) diff --git a/tests/data/test_dataprovider.py b/tests/data/test_dataprovider.py index ff748e976..0e10b5848 100644 --- a/tests/data/test_dataprovider.py +++ b/tests/data/test_dataprovider.py @@ -301,7 +301,7 @@ def test_current_whitelist(mocker, default_conf, tickers): # patch default conf to volumepairlist default_conf['pairlists'][0] = {'method': 'VolumePairList', "number_assets": 5} - mocker.patch.multiple('freqtrade.exchange.Exchange', + mocker.patch.multiple(EXMS, exchange_has=MagicMock(return_value=True), get_tickers=tickers) exchange = get_patched_exchange(mocker, default_conf) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index c9d1b6cab..843195796 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1234,7 +1234,7 @@ def test_create_dry_run_order_limit_fill(default_conf, mocker, side, price, fill exchange_name, order_book_l2_usd, converted): default_conf['dry_run'] = True exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) - mocker.patch.multiple('freqtrade.exchange.Exchange', + mocker.patch.multiple(EXMS, exchange_has=MagicMock(return_value=True), fetch_l2_order_book=order_book_l2_usd, ) @@ -1296,7 +1296,7 @@ def test_create_dry_run_order_market_fill(default_conf, mocker, side, rate, amou exchange_name, order_book_l2_usd): default_conf['dry_run'] = True exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) - mocker.patch.multiple('freqtrade.exchange.Exchange', + mocker.patch.multiple(EXMS, exchange_has=MagicMock(return_value=True), fetch_l2_order_book=order_book_l2_usd, ) @@ -3238,7 +3238,7 @@ def test_fetch_order_or_stoploss_order(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf, id='binance') fetch_order_mock = MagicMock() fetch_stoploss_order_mock = MagicMock() - mocker.patch.multiple('freqtrade.exchange.Exchange', + mocker.patch.multiple(EXMS, fetch_order=fetch_order_mock, fetch_stoploss_order=fetch_stoploss_order_mock, ) @@ -3367,7 +3367,7 @@ def test_stoploss_order_unsupported_exchange(default_conf, mocker): def test_merge_ft_has_dict(default_conf, mocker): - mocker.patch.multiple('freqtrade.exchange.Exchange', + mocker.patch.multiple(EXMS, _init_ccxt=MagicMock(return_value=MagicMock()), _load_async_markets=MagicMock(), validate_pairs=MagicMock(), @@ -3402,7 +3402,7 @@ def test_merge_ft_has_dict(default_conf, mocker): def test_get_valid_pair_combination(default_conf, mocker, markets): - mocker.patch.multiple('freqtrade.exchange.Exchange', + mocker.patch.multiple(EXMS, _init_ccxt=MagicMock(return_value=MagicMock()), _load_async_markets=MagicMock(), validate_pairs=MagicMock(), @@ -3494,7 +3494,7 @@ def test_get_markets(default_conf, mocker, markets_static, spot_only, futures_only, expected_keys, test_comment # Here for debugging purposes (Not used within method) ): - mocker.patch.multiple('freqtrade.exchange.Exchange', + mocker.patch.multiple(EXMS, _init_ccxt=MagicMock(return_value=MagicMock()), _load_async_markets=MagicMock(), validate_pairs=MagicMock(), @@ -5071,7 +5071,7 @@ def test_get_liquidation_price1(mocker, default_conf): ] api_mock.fetch_positions = MagicMock(return_value=positions) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, exchange_has=MagicMock(return_value=True), ) default_conf['dry_run'] = False diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index 90efc40b1..ae06fca1d 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -924,7 +924,7 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data: BTContainer) mocker.patch(f"{EXMS}.get_fee", return_value=0.0) mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001) mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf')) - mocker.patch("freqtrade.exchange.Binance.get_max_leverage", return_value=100) + mocker.patch('freqtrade.exchange.binance.Binance.get_max_leverage', return_value=100) patch_exchange(mocker) frame = _build_backtest_dataframe(data.data) backtesting = Backtesting(default_conf) diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index 2a7d4ccec..40a3871d7 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -116,7 +116,7 @@ def static_pl_conf(whitelist_conf): def test_log_cached(mocker, static_pl_conf, markets, tickers): - mocker.patch.multiple('freqtrade.exchange.Exchange', + mocker.patch.multiple(EXMS, markets=PropertyMock(return_value=markets), exchange_has=MagicMock(return_value=True), get_tickers=tickers @@ -180,7 +180,7 @@ def test_refresh_market_pair_not_in_whitelist(mocker, markets, static_pl_conf): def test_refresh_static_pairlist(mocker, markets, static_pl_conf): freqtrade = get_patched_freqtradebot(mocker, static_pl_conf) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, exchange_has=MagicMock(return_value=True), markets=PropertyMock(return_value=markets), ) @@ -204,7 +204,7 @@ def test_refresh_static_pairlist_noexist(mocker, markets, static_pl_conf, pairs, static_pl_conf['exchange']['pair_whitelist'] += pairs freqtrade = get_patched_freqtradebot(mocker, static_pl_conf) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, exchange_has=MagicMock(return_value=True), markets=PropertyMock(return_value=markets), ) @@ -221,7 +221,7 @@ def test_invalid_blacklist(mocker, markets, static_pl_conf, caplog): static_pl_conf['exchange']['pair_blacklist'] = ['*/BTC'] freqtrade = get_patched_freqtradebot(mocker, static_pl_conf) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, exchange_has=MagicMock(return_value=True), markets=PropertyMock(return_value=markets), ) @@ -237,7 +237,7 @@ def test_remove_logs_for_pairs_already_in_blacklist(mocker, markets, static_pl_c logger = logging.getLogger(__name__) freqtrade = get_patched_freqtradebot(mocker, static_pl_conf) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, exchange_has=MagicMock(return_value=True), markets=PropertyMock(return_value=markets), ) @@ -264,14 +264,14 @@ def test_remove_logs_for_pairs_already_in_blacklist(mocker, markets, static_pl_c def test_refresh_pairlist_dynamic(mocker, shitcoinmarkets, tickers, whitelist_conf): mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, get_tickers=tickers, exchange_has=MagicMock(return_value=True), ) freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) # Remock markets with shitcoinmarkets since get_patched_freqtradebot uses the markets fixture mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, markets=PropertyMock(return_value=shitcoinmarkets), ) # argument: use the whitelist dynamically by exchange-volume @@ -291,7 +291,7 @@ def test_refresh_pairlist_dynamic_2(mocker, shitcoinmarkets, tickers, whitelist_ tickers_dict = tickers() mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, exchange_has=MagicMock(return_value=True), ) # Remove caching of ticker data to emulate changing volume by the time of second call @@ -302,7 +302,7 @@ def test_refresh_pairlist_dynamic_2(mocker, shitcoinmarkets, tickers, whitelist_ freqtrade = get_patched_freqtradebot(mocker, whitelist_conf_2) # Remock markets with shitcoinmarkets since get_patched_freqtradebot uses the markets fixture mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, markets=PropertyMock(return_value=shitcoinmarkets), ) @@ -320,7 +320,7 @@ def test_refresh_pairlist_dynamic_2(mocker, shitcoinmarkets, tickers, whitelist_ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, exchange_has=MagicMock(return_value=True), ) freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) @@ -526,12 +526,12 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=True)) freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) - mocker.patch.multiple('freqtrade.exchange.Exchange', + mocker.patch.multiple(EXMS, get_tickers=tickers, markets=PropertyMock(return_value=shitcoinmarkets) ) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, refresh_latest_ohlcv=MagicMock(return_value=ohlcv_data), ) @@ -675,7 +675,7 @@ def test_VolumePairList_range(mocker, whitelist_conf, shitcoinmarkets, tickers, else: freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, get_tickers=tickers, markets=PropertyMock(return_value=shitcoinmarkets) ) @@ -687,7 +687,7 @@ def test_VolumePairList_range(mocker, whitelist_conf, shitcoinmarkets, tickers, ohlcv_data = [] mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, refresh_latest_ohlcv=MagicMock(return_value=ohlcv_data), ) @@ -806,7 +806,7 @@ def test_PerformanceFilter_keep_mid_order(mocker, default_conf_usdt, fee, caplog def test_gen_pair_whitelist_not_supported(mocker, default_conf, tickers) -> None: default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}] - mocker.patch.multiple('freqtrade.exchange.Exchange', + mocker.patch.multiple(EXMS, get_tickers=tickers, exchange_has=MagicMock(return_value=False), ) @@ -819,7 +819,7 @@ def test_gen_pair_whitelist_not_supported(mocker, default_conf, tickers) -> None def test_pair_whitelist_not_supported_Spread(mocker, default_conf, tickers) -> None: default_conf['pairlists'] = [{'method': 'StaticPairList'}, {'method': 'SpreadFilter'}] - mocker.patch.multiple('freqtrade.exchange.Exchange', + mocker.patch.multiple(EXMS, get_tickers=tickers, exchange_has=MagicMock(return_value=False), ) @@ -832,7 +832,7 @@ def test_pair_whitelist_not_supported_Spread(mocker, default_conf, tickers) -> N @pytest.mark.parametrize("pairlist", TESTABLE_PAIRLISTS) def test_pairlist_class(mocker, whitelist_conf, markets, pairlist): whitelist_conf['pairlists'][0]['method'] = pairlist - mocker.patch.multiple('freqtrade.exchange.Exchange', + mocker.patch.multiple(EXMS, markets=PropertyMock(return_value=markets), exchange_has=MagicMock(return_value=True) ) @@ -861,7 +861,7 @@ def test_pairlist_class(mocker, whitelist_conf, markets, pairlist): def test__whitelist_for_active_markets(mocker, whitelist_conf, markets, pairlist, whitelist, caplog, log_message, tickers): whitelist_conf['pairlists'][0]['method'] = pairlist - mocker.patch.multiple('freqtrade.exchange.Exchange', + mocker.patch.multiple(EXMS, markets=PropertyMock(return_value=markets), exchange_has=MagicMock(return_value=True), get_tickers=tickers @@ -884,7 +884,7 @@ def test__whitelist_for_active_markets_empty(mocker, whitelist_conf, pairlist, t mocker.patch(f'{EXMS}.exchange_has', return_value=True) freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) - mocker.patch.multiple('freqtrade.exchange.Exchange', + mocker.patch.multiple(EXMS, markets=PropertyMock(return_value=None), get_tickers=tickers ) @@ -905,7 +905,7 @@ def test_volumepairlist_invalid_sortvalue(mocker, whitelist_conf): def test_volumepairlist_caching(mocker, markets, whitelist_conf, tickers): - mocker.patch.multiple('freqtrade.exchange.Exchange', + mocker.patch.multiple(EXMS, markets=PropertyMock(return_value=markets), exchange_has=MagicMock(return_value=True), get_tickers=tickers @@ -925,7 +925,7 @@ def test_agefilter_min_days_listed_too_small(mocker, default_conf, markets, tick default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}, {'method': 'AgeFilter', 'min_days_listed': -1}] - mocker.patch.multiple('freqtrade.exchange.Exchange', + mocker.patch.multiple(EXMS, markets=PropertyMock(return_value=markets), exchange_has=MagicMock(return_value=True), get_tickers=tickers @@ -941,7 +941,7 @@ def test_agefilter_max_days_lower_than_min_days(mocker, default_conf, markets, t {'method': 'AgeFilter', 'min_days_listed': 3, "max_days_listed": 2}] - mocker.patch.multiple('freqtrade.exchange.Exchange', + mocker.patch.multiple(EXMS, markets=PropertyMock(return_value=markets), exchange_has=MagicMock(return_value=True), get_tickers=tickers @@ -956,7 +956,7 @@ def test_agefilter_min_days_listed_too_large(mocker, default_conf, markets, tick default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}, {'method': 'AgeFilter', 'min_days_listed': 99999}] - mocker.patch.multiple('freqtrade.exchange.Exchange', + mocker.patch.multiple(EXMS, markets=PropertyMock(return_value=markets), exchange_has=MagicMock(return_value=True), get_tickers=tickers @@ -976,7 +976,7 @@ def test_agefilter_caching(mocker, markets, whitelist_conf_agefilter, tickers, o ('LTC/BTC', '1d', CandleType.SPOT): ohlcv_history, } mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, markets=PropertyMock(return_value=markets), exchange_has=MagicMock(return_value=True), get_tickers=tickers, @@ -1044,7 +1044,7 @@ def test_rangestabilityfilter_checks(mocker, default_conf, markets, tickers): default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}, {'method': 'RangeStabilityFilter', 'lookback_days': 99999}] - mocker.patch.multiple('freqtrade.exchange.Exchange', + mocker.patch.multiple(EXMS, markets=PropertyMock(return_value=markets), exchange_has=MagicMock(return_value=True), get_tickers=tickers @@ -1074,7 +1074,7 @@ def test_rangestabilityfilter_caching(mocker, markets, default_conf, tickers, oh 'min_rate_of_change': min_rate_of_change, "max_rate_of_change": max_rate_of_change}] - mocker.patch.multiple('freqtrade.exchange.Exchange', + mocker.patch.multiple(EXMS, markets=PropertyMock(return_value=markets), exchange_has=MagicMock(return_value=True), get_tickers=tickers @@ -1088,7 +1088,7 @@ def test_rangestabilityfilter_caching(mocker, markets, default_conf, tickers, oh ('BLK/BTC', '1d', CandleType.SPOT): ohlcv_history, } mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, refresh_latest_ohlcv=MagicMock(return_value=ohlcv_data), ) @@ -1109,7 +1109,7 @@ def test_spreadfilter_invalid_data(mocker, default_conf, markets, tickers, caplo default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}, {'method': 'SpreadFilter', 'max_spread_ratio': 0.1}] - mocker.patch.multiple('freqtrade.exchange.Exchange', + mocker.patch.multiple(EXMS, markets=PropertyMock(return_value=markets), exchange_has=MagicMock(return_value=True), get_tickers=tickers @@ -1123,7 +1123,7 @@ def test_spreadfilter_invalid_data(mocker, default_conf, markets, tickers, caplo tickers.return_value['ETH/BTC']['ask'] = 0.0 del tickers.return_value['TKN/BTC'] del tickers.return_value['LTC/BTC'] - mocker.patch.multiple('freqtrade.exchange.Exchange', get_tickers=tickers) + mocker.patch.multiple(EXMS, get_tickers=tickers) ftbot.pairlists.refresh_pairlist() assert log_has_re(r'Removed .* invalid ticker data.*', caplog) @@ -1197,7 +1197,7 @@ def test_spreadfilter_invalid_data(mocker, default_conf, markets, tickers, caplo ]) def test_pricefilter_desc(mocker, whitelist_conf, markets, pairlistconfig, desc_expected, exception_expected): - mocker.patch.multiple('freqtrade.exchange.Exchange', + mocker.patch.multiple(EXMS, markets=PropertyMock(return_value=markets), exchange_has=MagicMock(return_value=True) ) @@ -1269,11 +1269,11 @@ def test_performance_filter(mocker, whitelist_conf, pairlists, pair_allowlist, o mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=True)) freqtrade = get_patched_freqtradebot(mocker, allowlist_conf) - mocker.patch.multiple('freqtrade.exchange.Exchange', + mocker.patch.multiple(EXMS, get_tickers=tickers, markets=PropertyMock(return_value=markets) ) - mocker.patch.multiple('freqtrade.exchange.Exchange', + mocker.patch.multiple(EXMS, get_historic_ohlcv=MagicMock(return_value=ohlcv_history_list), ) mocker.patch.multiple('freqtrade.persistence.Trade', @@ -1389,7 +1389,7 @@ def test_ProducerPairlist_no_emc(mocker, whitelist_conf): def test_ProducerPairlist(mocker, whitelist_conf, markets): mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=True)) - mocker.patch.multiple('freqtrade.exchange.Exchange', + mocker.patch.multiple(EXMS, markets=PropertyMock(return_value=markets), exchange_has=MagicMock(return_value=True), ) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 40d14212f..d368107df 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -96,7 +96,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: } mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker, get_fee=fee, _dry_is_price_crossed=MagicMock(side_effect=[False, True]), @@ -197,7 +197,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None: mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker, get_fee=fee, ) @@ -264,7 +264,7 @@ def test__rpc_timeunit_profit(default_conf_usdt, ticker, fee, limit_buy_order, limit_sell_order, markets, mocker) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker, get_fee=fee, markets=PropertyMock(return_value=markets) @@ -305,7 +305,7 @@ def test__rpc_timeunit_profit(default_conf_usdt, ticker, fee, def test_rpc_trade_history(mocker, default_conf, markets, fee, is_short): mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, markets=PropertyMock(return_value=markets) ) @@ -333,7 +333,7 @@ def test_rpc_delete_trade(mocker, default_conf, fee, markets, caplog, is_short): stoploss_mock = MagicMock() cancel_mock = MagicMock() mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, markets=PropertyMock(return_value=markets), cancel_order=cancel_mock, cancel_stoploss_order=stoploss_mock, @@ -384,7 +384,7 @@ def test_rpc_trade_statistics(default_conf_usdt, ticker, fee, mocker) -> None: mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=1.1) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker, get_fee=fee, ) @@ -455,7 +455,7 @@ def test_rpc_balance_handle_error(default_conf, mocker): mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, get_balances=MagicMock(return_value=mock_balance), get_tickers=MagicMock(side_effect=TemporaryError('Could not load ticker due to xxx')) ) @@ -518,7 +518,7 @@ def test_rpc_balance_handle(default_conf, mocker, tickers): mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, validate_trading_mode_and_margin_mode=MagicMock(), get_balances=MagicMock(return_value=mock_balance), fetch_positions=MagicMock(return_value=mock_pos), @@ -595,7 +595,7 @@ def test_rpc_balance_handle(default_conf, mocker, tickers): def test_rpc_start(mocker, default_conf) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=MagicMock() ) @@ -616,7 +616,7 @@ def test_rpc_start(mocker, default_conf) -> None: def test_rpc_stop(mocker, default_conf) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=MagicMock() ) @@ -638,7 +638,7 @@ def test_rpc_stop(mocker, default_conf) -> None: def test_rpc_stopentry(mocker, default_conf) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=MagicMock() ) @@ -658,7 +658,7 @@ def test_rpc_force_exit(default_conf, ticker, fee, mocker) -> None: cancel_order_mock = MagicMock() mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker, cancel_order=cancel_order_mock, fetch_order=MagicMock( @@ -787,7 +787,7 @@ def test_rpc_force_exit(default_conf, ticker, fee, mocker) -> None: def test_performance_handle(default_conf_usdt, ticker, fee, mocker) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, get_balances=MagicMock(return_value=ticker), fetch_ticker=ticker, get_fee=fee, @@ -810,7 +810,7 @@ def test_enter_tag_performance_handle(default_conf, ticker, fee, mocker) -> None mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, get_balances=MagicMock(return_value=ticker), fetch_ticker=ticker, get_fee=fee, @@ -842,7 +842,7 @@ def test_enter_tag_performance_handle(default_conf, ticker, fee, mocker) -> None def test_enter_tag_performance_handle_2(mocker, default_conf, markets, fee): mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, markets=PropertyMock(return_value=markets) ) @@ -871,7 +871,7 @@ def test_enter_tag_performance_handle_2(mocker, default_conf, markets, fee): def test_exit_reason_performance_handle(default_conf_usdt, ticker, fee, mocker) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, get_balances=MagicMock(return_value=ticker), fetch_ticker=ticker, get_fee=fee, @@ -898,7 +898,7 @@ def test_exit_reason_performance_handle(default_conf_usdt, ticker, fee, mocker) def test_exit_reason_performance_handle_2(mocker, default_conf, markets, fee): mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, markets=PropertyMock(return_value=markets) ) @@ -927,7 +927,7 @@ def test_exit_reason_performance_handle_2(mocker, default_conf, markets, fee): def test_mix_tag_performance_handle(default_conf, ticker, fee, mocker) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, get_balances=MagicMock(return_value=ticker), fetch_ticker=ticker, get_fee=fee, @@ -951,7 +951,7 @@ def test_mix_tag_performance_handle(default_conf, ticker, fee, mocker) -> None: 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', + EXMS, markets=PropertyMock(return_value=markets) ) @@ -981,7 +981,7 @@ def test_mix_tag_performance_handle_2(mocker, default_conf, markets, fee): def test_rpc_count(mocker, default_conf, ticker, fee) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, get_balances=MagicMock(return_value=ticker), fetch_ticker=ticker, get_fee=fee, @@ -1006,7 +1006,7 @@ def test_rpc_force_entry(mocker, default_conf, ticker, fee, limit_buy_order_open mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) buy_mm = MagicMock(return_value=limit_buy_order_open) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, get_balances=MagicMock(return_value=ticker), fetch_ticker=ticker, get_fee=fee, diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 46cfd07d7..b104ec854 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -507,7 +507,7 @@ def test_api_count(botclient, mocker, ticker, fee, markets, is_short): ftbot, client = botclient patch_get_signal(ftbot) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, get_balances=MagicMock(return_value=ticker), fetch_ticker=ticker, get_fee=fee, @@ -594,7 +594,7 @@ def test_api_daily(botclient, mocker, ticker, fee, markets): ftbot, client = botclient patch_get_signal(ftbot) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, get_balances=MagicMock(return_value=ticker), fetch_ticker=ticker, get_fee=fee, @@ -613,7 +613,7 @@ def test_api_trades(botclient, mocker, fee, markets, is_short): ftbot, client = botclient patch_get_signal(ftbot) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, markets=PropertyMock(return_value=markets) ) rc = client_get(client, f"{BASE_URI}/trades") @@ -644,7 +644,7 @@ def test_api_trade_single(botclient, mocker, fee, ticker, markets, is_short): ftbot, client = botclient patch_get_signal(ftbot, enter_long=not is_short, enter_short=is_short) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, markets=PropertyMock(return_value=markets), fetch_ticker=ticker, ) @@ -668,7 +668,7 @@ def test_api_delete_trade(botclient, mocker, fee, markets, is_short): stoploss_mock = MagicMock() cancel_mock = MagicMock() mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, markets=PropertyMock(return_value=markets), cancel_order=cancel_mock, cancel_stoploss_order=stoploss_mock, @@ -713,7 +713,7 @@ def test_api_delete_open_order(botclient, mocker, fee, markets, ticker, is_short stoploss_mock = MagicMock() cancel_mock = MagicMock() mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, markets=PropertyMock(return_value=markets), fetch_ticker=ticker, cancel_order=cancel_mock, @@ -780,7 +780,7 @@ def test_api_edge_disabled(botclient, mocker, ticker, fee, markets): ftbot, client = botclient patch_get_signal(ftbot) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, get_balances=MagicMock(return_value=ticker), fetch_ticker=ticker, get_fee=fee, @@ -842,7 +842,7 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, is_short, expected) ftbot, client = botclient patch_get_signal(ftbot) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, get_balances=MagicMock(return_value=ticker), fetch_ticker=ticker, get_fee=fee, @@ -900,7 +900,7 @@ def test_api_stats(botclient, mocker, ticker, fee, markets, is_short): ftbot, client = botclient patch_get_signal(ftbot, enter_long=not is_short, enter_short=is_short) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, get_balances=MagicMock(return_value=ticker), fetch_ticker=ticker, get_fee=fee, @@ -981,7 +981,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets, is_short, ftbot, client = botclient patch_get_signal(ftbot) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, get_balances=MagicMock(return_value=ticker), fetch_ticker=ticker, get_fee=fee, @@ -1274,7 +1274,7 @@ def test_api_force_entry(botclient, mocker, fee, endpoint): def test_api_forceexit(botclient, mocker, ticker, fee, markets): ftbot, client = botclient mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, get_balances=MagicMock(return_value=ticker), fetch_ticker=ticker, get_fee=fee, diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 84ca1914e..3e1421cb5 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -240,7 +240,7 @@ def test_telegram_status_multi_entry(default_conf, update, mocker, fee) -> None: default_conf['telegram']['chat_id'] = "123" default_conf['position_adjustment_enable'] = True mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_order=MagicMock(return_value=None), get_rate=MagicMock(return_value=0.22), ) @@ -292,7 +292,7 @@ def test_telegram_status_closed_trade(default_conf, update, mocker, fee) -> None default_conf['telegram']['chat_id'] = "123" default_conf['position_adjustment_enable'] = True mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_order=MagicMock(return_value=None), get_rate=MagicMock(return_value=0.22), ) @@ -314,7 +314,7 @@ def test_telegram_status_closed_trade(default_conf, update, mocker, fee) -> None def test_status_handle(default_conf, update, ticker, fee, mocker) -> None: default_conf['max_open_trades'] = 3 mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker, get_fee=fee, _dry_is_price_crossed=MagicMock(return_value=True), @@ -391,7 +391,7 @@ def test_status_handle(default_conf, update, ticker, fee, mocker) -> None: def test_status_table_handle(default_conf, update, ticker, fee, mocker) -> None: mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker, get_fee=fee, ) @@ -436,7 +436,7 @@ def test_daily_handle(default_conf_usdt, update, ticker, fee, mocker, time_machi return_value=1.1 ) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker, get_fee=fee, ) @@ -491,7 +491,7 @@ def test_daily_handle(default_conf_usdt, update, ticker, fee, mocker, time_machi def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None: mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker ) @@ -525,7 +525,7 @@ def test_weekly_handle(default_conf_usdt, update, ticker, fee, mocker, time_mach return_value=1.1 ) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker, get_fee=fee, ) @@ -595,7 +595,7 @@ def test_monthly_handle(default_conf_usdt, update, ticker, fee, mocker, time_mac return_value=1.1 ) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker, get_fee=fee, ) @@ -676,7 +676,7 @@ def test_profit_handle(default_conf_usdt, update, ticker_usdt, ticker_sell_up, f limit_sell_order_usdt, mocker) -> None: mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=1.1) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker_usdt, get_fee=fee, ) @@ -739,7 +739,7 @@ def test_profit_handle(default_conf_usdt, update, ticker_usdt, ticker_sell_up, f def test_telegram_stats(default_conf, update, ticker, fee, mocker, is_short) -> None: mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker, get_fee=fee, ) @@ -930,7 +930,7 @@ def test_telegram_forceexit_handle(default_conf, update, ticker, fee, patch_exchange(mocker) patch_whitelist(mocker, default_conf) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker, get_fee=fee, _dry_is_price_crossed=MagicMock(return_value=True), @@ -999,7 +999,7 @@ def test_telegram_force_exit_down_handle(default_conf, update, ticker, fee, patch_whitelist(mocker, default_conf) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker, get_fee=fee, _dry_is_price_crossed=MagicMock(return_value=True), @@ -1015,7 +1015,7 @@ def test_telegram_force_exit_down_handle(default_conf, update, ticker, fee, # Decrease the price and sell it mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker_sell_down ) @@ -1070,7 +1070,7 @@ def test_forceexit_all_handle(default_conf, update, ticker, fee, mocker) -> None mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) patch_whitelist(mocker, default_conf) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker, get_fee=fee, _dry_is_price_crossed=MagicMock(return_value=True), @@ -1155,7 +1155,7 @@ def test_forceexit_handle_invalid(default_conf, update, mocker) -> None: def test_force_exit_no_pair(default_conf, update, ticker, fee, mocker) -> None: default_conf['max_open_trades'] = 4 mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker, get_fee=fee, _dry_is_price_crossed=MagicMock(return_value=True), @@ -1282,7 +1282,7 @@ def test_force_enter_no_pair(default_conf, update, mocker) -> None: def test_telegram_performance_handle(default_conf_usdt, update, ticker, fee, mocker) -> None: mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker, get_fee=fee, ) @@ -1300,7 +1300,7 @@ def test_telegram_performance_handle(default_conf_usdt, update, ticker, fee, moc def test_telegram_entry_tag_performance_handle( default_conf_usdt, update, ticker, fee, mocker) -> None: mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker, get_fee=fee, ) @@ -1331,7 +1331,7 @@ def test_telegram_entry_tag_performance_handle( def test_telegram_exit_reason_performance_handle(default_conf_usdt, update, ticker, fee, mocker) -> None: mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker, get_fee=fee, ) @@ -1362,7 +1362,7 @@ def test_telegram_exit_reason_performance_handle(default_conf_usdt, update, tick def test_telegram_mix_tag_performance_handle(default_conf_usdt, update, ticker, fee, mocker) -> None: mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker, get_fee=fee, ) @@ -1394,7 +1394,7 @@ def test_telegram_mix_tag_performance_handle(default_conf_usdt, update, ticker, def test_count_handle(default_conf, update, ticker, fee, mocker) -> None: mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker, get_fee=fee, ) @@ -1423,7 +1423,7 @@ def test_count_handle(default_conf, update, ticker, fee, mocker) -> None: def test_telegram_lock_handle(default_conf, update, ticker, fee, mocker) -> None: mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker, get_fee=fee, ) @@ -1685,7 +1685,7 @@ def test_telegram_delete_trade(mocker, update, default_conf, fee, is_short): def test_telegram_delete_open_order(mocker, update, default_conf, fee, is_short, ticker): mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker, ) telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index a80937e1a..1ea3ebfc6 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -165,7 +165,7 @@ def test_check_available_stake_amount( patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker_usdt, create_order=MagicMock(return_value=limit_buy_order_usdt_open), get_fee=fee @@ -235,7 +235,7 @@ def test_edge_overrides_stoploss(limit_order, fee, caplog, mocker, 'last': enter_price, } mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=MagicMock(return_value=ticker_val), get_fee=fee, ) @@ -270,7 +270,7 @@ def test_total_open_trades_stakes(mocker, default_conf_usdt, ticker_usdt, fee) - patch_exchange(mocker) default_conf_usdt['max_open_trades'] = 2 mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker_usdt, get_fee=fee, _dry_is_price_crossed=MagicMock(return_value=False), @@ -305,7 +305,7 @@ def test_create_trade(default_conf_usdt, ticker_usdt, limit_order, patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker_usdt, get_fee=fee, _dry_is_price_crossed=MagicMock(return_value=False), @@ -341,7 +341,7 @@ def test_create_trade_no_stake_amount(default_conf_usdt, ticker_usdt, fee, mocke patch_exchange(mocker) patch_wallet(mocker, free=default_conf_usdt['stake_amount'] * 0.5) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker_usdt, get_fee=fee, ) @@ -367,7 +367,7 @@ def test_create_trade_minimal_amount( patch_exchange(mocker) enter_mock = MagicMock(return_value=limit_order_open[entry_side(is_short)]) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker_usdt, create_order=enter_mock, get_fee=fee, @@ -402,7 +402,7 @@ def test_enter_positions_no_pairs_left(default_conf_usdt, ticker_usdt, limit_buy patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker_usdt, create_order=MagicMock(return_value=limit_buy_order_usdt_open), get_fee=fee, @@ -429,7 +429,7 @@ def test_enter_positions_global_pairlock(default_conf_usdt, ticker_usdt, limit_b patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker_usdt, create_order=MagicMock(return_value={'id': limit_buy_order_usdt['id']}), get_fee=fee, @@ -480,7 +480,7 @@ def test_create_trade_no_signal(default_conf_usdt, fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, get_fee=fee, ) default_conf_usdt['stake_amount'] = 10 @@ -503,7 +503,7 @@ def test_create_trades_multiple_trades( default_conf_usdt['dry_run_wallet'] = 60.0 * max_open mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker_usdt, create_order=MagicMock(return_value=limit_buy_order_usdt_open), get_fee=fee, @@ -525,7 +525,7 @@ def test_create_trades_preopen(default_conf_usdt, ticker_usdt, fee, mocker, patch_exchange(mocker) default_conf_usdt['max_open_trades'] = 4 mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker_usdt, create_order=MagicMock(return_value=limit_buy_order_usdt_open), get_fee=fee, @@ -559,7 +559,7 @@ def test_process_trade_creation(default_conf_usdt, ticker_usdt, limit_order, lim patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker_usdt, create_order=MagicMock(return_value=limit_order_open[entry_side(is_short)]), fetch_order=MagicMock(return_value=limit_order[entry_side(is_short)]), @@ -595,7 +595,7 @@ def test_process_exchange_failures(default_conf_usdt, ticker_usdt, mocker) -> No patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker_usdt, create_order=MagicMock(side_effect=TemporaryError) ) @@ -612,7 +612,7 @@ def test_process_operational_exception(default_conf_usdt, ticker_usdt, mocker) - msg_mock = patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker_usdt, create_order=MagicMock(side_effect=OperationalException) ) @@ -631,7 +631,7 @@ def test_process_trade_handling(default_conf_usdt, ticker_usdt, limit_buy_order_ patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker_usdt, create_order=MagicMock(return_value=limit_buy_order_usdt_open), fetch_order=MagicMock(return_value=limit_buy_order_usdt_open), @@ -658,7 +658,7 @@ def test_process_trade_no_whitelist_pair(default_conf_usdt, ticker_usdt, limit_b patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker_usdt, create_order=MagicMock(return_value={'id': limit_buy_order_usdt['id']}), fetch_order=MagicMock(return_value=limit_buy_order_usdt), @@ -706,7 +706,7 @@ def test_process_informative_pairs_added(default_conf_usdt, ticker_usdt, mocker) refresh_mock = MagicMock() mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker_usdt, create_order=MagicMock(side_effect=TemporaryError), refresh_latest_ohlcv=refresh_mock, @@ -797,7 +797,7 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order, enter_rate_mock = MagicMock(return_value=bid) enter_mm = MagicMock(return_value=open_order) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, get_rate=enter_rate_mock, fetch_ticker=MagicMock(return_value={ 'bid': 1.9, @@ -946,7 +946,7 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order, freqtrade.strategy.custom_entry_price = lambda **kwargs: None mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, get_rate=MagicMock(return_value=10), ) @@ -976,7 +976,7 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order, order['id'] = '55672' mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, get_max_pair_stake_amount=MagicMock(return_value=500), ) freqtrade.exchange.get_max_pair_stake_amount = MagicMock(return_value=500) @@ -999,7 +999,7 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order, def test_execute_entry_confirm_error(mocker, default_conf_usdt, fee, limit_order, is_short) -> None: freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=MagicMock(return_value={ 'bid': 1.9, 'ask': 2.2, @@ -1034,7 +1034,7 @@ def test_execute_entry_min_leverage(mocker, default_conf_usdt, fee, limit_order, default_conf_usdt['margin_mode'] = 'isolated' freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=MagicMock(return_value={ 'bid': 1.9, 'ask': 2.2, @@ -1096,7 +1096,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_ patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=MagicMock(return_value={ 'bid': 1.9, 'ask': 2.2, @@ -1250,7 +1250,7 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf_usdt, fee, caplog, patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=MagicMock(return_value={ 'bid': 1.9, 'ask': 2.2, @@ -1297,7 +1297,7 @@ def test_create_stoploss_order_invalid_order( {'id': order['id']} ]) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=MagicMock(return_value={ 'bid': 1.9, 'ask': 2.2, @@ -1346,7 +1346,7 @@ def test_create_stoploss_order_insufficient_funds( mock_insuf = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_insufficient_funds') mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=MagicMock(return_value={ 'bid': 1.9, 'ask': 2.2, @@ -1397,7 +1397,7 @@ def test_handle_stoploss_on_exchange_trailing( stoploss = MagicMock(return_value={'id': 13434334}) patch_RPCManager(mocker) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=MagicMock(return_value={ 'bid': 2.19, 'ask': 2.2, @@ -1522,7 +1522,7 @@ def test_handle_stoploss_on_exchange_trailing_error( patch_exchange(mocker) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=MagicMock(return_value={ 'bid': 1.9, 'ask': 2.2, @@ -1586,8 +1586,8 @@ def test_handle_stoploss_on_exchange_trailing_error( # Fail creating stoploss order trade.stoploss_last_update = arrow.utcnow().shift(minutes=-601).datetime caplog.clear() - cancel_mock = mocker.patch("freqtrade.exchange.Binance.cancel_stoploss_order", MagicMock()) - mocker.patch("freqtrade.exchange.Binance.create_stoploss", side_effect=ExchangeError()) + cancel_mock = mocker.patch('freqtrade.exchange.binance.Binance.cancel_stoploss_order') + mocker.patch('freqtrade.exchange.binance.Binance.create_stoploss', side_effect=ExchangeError()) freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging) assert cancel_mock.call_count == 1 assert log_has_re(r"Could not create trailing stoploss order for pair ETH/USDT\..*", caplog) @@ -1597,7 +1597,7 @@ def test_stoploss_on_exchange_price_rounding( mocker, default_conf_usdt, fee, open_trade_usdt) -> None: patch_RPCManager(mocker) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, get_fee=fee, ) price_mock = MagicMock(side_effect=lambda p, s: int(s)) @@ -1630,7 +1630,7 @@ def test_handle_stoploss_on_exchange_custom_stop( stoploss = MagicMock(return_value={'id': 13434334}) patch_RPCManager(mocker) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=MagicMock(return_value={ 'bid': 1.9, 'ask': 2.2, @@ -1758,7 +1758,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, limit_orde edge_conf['dry_run_wallet'] = 999.9 edge_conf['exchange']['name'] = 'binance' mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=MagicMock(return_value={ 'bid': 2.19, 'ask': 2.2, @@ -2145,7 +2145,7 @@ def test_handle_trade( patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=MagicMock(return_value={ 'bid': 2.19, 'ask': 2.2, @@ -2198,7 +2198,7 @@ def test_handle_overlapping_signals( patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker_usdt, create_order=MagicMock(side_effect=[ open_order, @@ -2276,7 +2276,7 @@ def test_handle_trade_roi(default_conf_usdt, ticker_usdt, limit_order_open, fee, patch_RPCManager(mocker) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker_usdt, create_order=MagicMock(side_effect=[ open_order, @@ -2319,7 +2319,7 @@ def test_handle_trade_use_exit_signal( caplog.set_level(logging.DEBUG) patch_RPCManager(mocker) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker_usdt, create_order=MagicMock(side_effect=[ enter_open_order, @@ -2359,7 +2359,7 @@ def test_close_trade( patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker_usdt, create_order=MagicMock(return_value=open_order), get_fee=fee, @@ -2416,7 +2416,7 @@ def test_manage_open_orders_entry_usercustom( patch_exchange(mocker) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker_usdt, fetch_order=MagicMock(return_value=old_order), cancel_order=cancel_order_mock, @@ -2477,7 +2477,7 @@ def test_manage_open_orders_entry( cancel_order_mock = MagicMock(return_value=limit_buy_cancel) patch_exchange(mocker) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker_usdt, fetch_order=MagicMock(return_value=old_order), cancel_order_with_result=cancel_order_mock, @@ -2516,7 +2516,7 @@ def test_adjust_entry_cancel( limit_buy_cancel['status'] = 'canceled' cancel_order_mock = MagicMock(return_value=limit_buy_cancel) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker_usdt, fetch_order=MagicMock(return_value=old_order), cancel_order_with_result=cancel_order_mock, @@ -2557,7 +2557,7 @@ def test_adjust_entry_maintain_replace( limit_buy_cancel['status'] = 'canceled' cancel_order_mock = MagicMock(return_value=limit_buy_cancel) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker_usdt, fetch_order=MagicMock(return_value=old_order), cancel_order_with_result=cancel_order_mock, @@ -2609,7 +2609,7 @@ def test_check_handle_cancelled_buy( patch_exchange(mocker) old_order.update({"status": "canceled", 'filled': 0.0}) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker_usdt, fetch_order=MagicMock(return_value=old_order), cancel_order=cancel_order_mock, @@ -2639,7 +2639,7 @@ def test_manage_open_orders_buy_exception( cancel_order_mock = MagicMock() patch_exchange(mocker) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, validate_pairs=MagicMock(), fetch_ticker=ticker_usdt, fetch_order=MagicMock(side_effect=ExchangeError), @@ -2680,7 +2680,7 @@ def test_manage_open_orders_exit_usercustom( mocker.patch(f'{EXMS}.get_min_pair_stake_amount', return_value=0.0) et_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.execute_trade_exit') mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker_usdt, fetch_order=MagicMock(return_value=limit_sell_order_old), cancel_order=cancel_order_mock @@ -2758,7 +2758,7 @@ def test_manage_open_orders_exit( limit_sell_order_old['side'] = 'buy' if is_short else 'sell' patch_exchange(mocker) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker_usdt, fetch_order=MagicMock(return_value=limit_sell_order_old), cancel_order=cancel_order_mock, @@ -2800,7 +2800,7 @@ def test_check_handle_cancelled_exit( patch_exchange(mocker) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker_usdt, fetch_order=MagicMock(return_value=limit_sell_order_old), cancel_order_with_result=cancel_order_mock @@ -2841,7 +2841,7 @@ def test_manage_open_orders_partial( cancel_order_mock = MagicMock(return_value=limit_buy_canceled) patch_exchange(mocker) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker_usdt, fetch_order=MagicMock(return_value=limit_buy_order_old_partial), cancel_order_with_result=cancel_order_mock @@ -2881,7 +2881,7 @@ def test_manage_open_orders_partial_fee( mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=0)) patch_exchange(mocker) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker_usdt, fetch_order=MagicMock(return_value=limit_buy_order_old_partial), cancel_order_with_result=cancel_order_mock, @@ -2929,7 +2929,7 @@ def test_manage_open_orders_partial_except( cancel_order_mock = MagicMock(return_value=limit_buy_order_old_partial_canceled) patch_exchange(mocker) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker_usdt, fetch_order=MagicMock(return_value=limit_buy_order_old_partial), cancel_order_with_result=cancel_order_mock, @@ -2975,7 +2975,7 @@ def test_manage_open_orders_exception(default_conf_usdt, ticker_usdt, open_trade handle_cancel_exit=MagicMock(), ) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker_usdt, fetch_order=MagicMock(side_effect=ExchangeError('Oh snap')), cancel_order=cancel_order_mock @@ -3086,7 +3086,7 @@ def test_handle_cancel_enter_corder_empty(mocker, default_conf_usdt, limit_order l_order = limit_order[entry_side(is_short)] cancel_order_mock = MagicMock(return_value=cancelorder) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, cancel_order=cancel_order_mock, fetch_order=MagicMock(side_effect=InvalidOrderException) ) @@ -3116,7 +3116,7 @@ def test_handle_cancel_exit_limit(mocker, default_conf_usdt, fee) -> None: patch_exchange(mocker) cancel_order_mock = MagicMock() mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, cancel_order=cancel_order_mock, ) mocker.patch(f'{EXMS}.get_rate', return_value=0.245441) @@ -3245,7 +3245,7 @@ def test_execute_trade_exit_up(default_conf_usdt, ticker_usdt, fee, ticker_usdt_ rpc_mock = patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker_usdt, get_fee=fee, _dry_is_price_crossed=MagicMock(return_value=False), @@ -3266,7 +3266,7 @@ def test_execute_trade_exit_up(default_conf_usdt, ticker_usdt, fee, ticker_usdt_ # Increase the price and sell it mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker_usdt_sell_down if is_short else ticker_usdt_sell_up ) # Prevented sell ... @@ -3328,7 +3328,7 @@ def test_execute_trade_exit_down(default_conf_usdt, ticker_usdt, fee, ticker_usd rpc_mock = patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker_usdt, get_fee=fee, _dry_is_price_crossed=MagicMock(return_value=False), @@ -3346,7 +3346,7 @@ def test_execute_trade_exit_down(default_conf_usdt, ticker_usdt, fee, ticker_usd # Decrease the price and sell it mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker_usdt_sell_up if is_short else ticker_usdt_sell_down ) freqtrade.execute_trade_exit( @@ -3397,7 +3397,7 @@ def test_execute_trade_exit_custom_exit_price( rpc_mock = patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker_usdt, get_fee=fee, _dry_is_price_crossed=MagicMock(return_value=False), @@ -3420,7 +3420,7 @@ def test_execute_trade_exit_custom_exit_price( # Increase the price and sell it mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker_usdt_sell_up ) @@ -3478,7 +3478,7 @@ def test_execute_trade_exit_down_stoploss_on_exchange_dry_run( rpc_mock = patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker_usdt, get_fee=fee, _dry_is_price_crossed=MagicMock(return_value=False), @@ -3496,7 +3496,7 @@ def test_execute_trade_exit_down_stoploss_on_exchange_dry_run( # Decrease the price and sell it mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker_usdt_sell_up if is_short else ticker_usdt_sell_down ) @@ -3554,7 +3554,7 @@ def test_execute_trade_exit_sloe_cancel_exception( ]) patch_exchange(mocker) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker_usdt, get_fee=fee, create_order=create_order_mock, @@ -3592,7 +3592,7 @@ def test_execute_trade_exit_with_stoploss_on_exchange( cancel_order = MagicMock(return_value=True) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker_usdt, get_fee=fee, amount_to_precision=lambda s, x, y: y, @@ -3619,7 +3619,7 @@ def test_execute_trade_exit_with_stoploss_on_exchange( # Increase the price and sell it mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker_usdt_sell_up ) @@ -3643,7 +3643,7 @@ def test_may_execute_trade_exit_after_stoploss_on_exchange_hit( rpc_mock = patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker_usdt, get_fee=fee, amount_to_precision=lambda s, x, y: y, @@ -3740,7 +3740,7 @@ def test_execute_trade_exit_market_order( rpc_mock = patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker_usdt, get_fee=fee, _dry_is_price_crossed=MagicMock(return_value=True), @@ -3759,7 +3759,7 @@ def test_execute_trade_exit_market_order( # Increase the price and sell it mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker_usdt_sell_up, _dry_is_price_crossed=MagicMock(return_value=False), ) @@ -3815,7 +3815,7 @@ def test_execute_trade_exit_insufficient_funds_error(default_conf_usdt, ticker_u freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) mock_insuf = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_insufficient_funds') mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker_usdt, get_fee=fee, create_order=MagicMock(side_effect=[ @@ -3834,7 +3834,7 @@ def test_execute_trade_exit_insufficient_funds_error(default_conf_usdt, ticker_u # Increase the price and sell it mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker_usdt_sell_up ) @@ -3869,7 +3869,7 @@ def test_exit_profit_only( patch_exchange(mocker) eside = entry_side(is_short) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=MagicMock(return_value={ 'bid': bid, 'ask': ask, @@ -3920,7 +3920,7 @@ def test_sell_not_enough_balance(default_conf_usdt, limit_order, limit_order_ope patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=MagicMock(return_value={ 'bid': 0.00002172, 'ask': 0.00002173, @@ -3997,7 +3997,7 @@ def test_locked_pairs(default_conf_usdt, ticker_usdt, fee, patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker_usdt, get_fee=fee, ) @@ -4013,7 +4013,7 @@ def test_locked_pairs(default_conf_usdt, ticker_usdt, fee, # Decrease the price and sell it mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker_usdt_sell_down ) @@ -4042,7 +4042,7 @@ def test_ignore_roi_if_entry_signal(default_conf_usdt, limit_order, limit_order_ patch_exchange(mocker) eside = entry_side(is_short) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=MagicMock(return_value={ 'bid': 2.19, 'ask': 2.2, @@ -4093,7 +4093,7 @@ def test_trailing_stop_loss(default_conf_usdt, limit_order_open, patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=MagicMock(return_value={ 'bid': 2.0, 'ask': 2.0, @@ -4163,7 +4163,7 @@ def test_trailing_stop_loss_positive( patch_exchange(mocker) eside = entry_side(is_short) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=MagicMock(return_value={ 'bid': enter_price - (-0.01 if is_short else 0.01), 'ask': enter_price - (-0.01 if is_short else 0.01), @@ -4261,7 +4261,7 @@ def test_disable_ignore_roi_if_entry_signal(default_conf_usdt, limit_order, limi patch_exchange(mocker) eside = entry_side(is_short) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=MagicMock(return_value={ 'bid': 2.0, 'ask': 2.0, @@ -4738,7 +4738,7 @@ def test_order_book_depth_of_market( patch_exchange(mocker) mocker.patch(f'{EXMS}.fetch_l2_order_book', order_book_l2) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker_usdt, create_order=MagicMock(return_value=limit_order_open[entry_side(is_short)]), get_fee=fee, @@ -4784,7 +4784,7 @@ def test_order_book_entry_pricing1(mocker, default_conf_usdt, order_book_l2, exc patch_exchange(mocker) ticker_usdt_mock = MagicMock(return_value={'ask': ask, 'last': last}) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_l2_order_book=MagicMock(return_value=order_book) if order_book else order_book_l2, fetch_ticker=ticker_usdt_mock, ) @@ -4812,7 +4812,7 @@ def test_check_depth_of_market(default_conf_usdt, mocker, order_book_l2) -> None """ patch_exchange(mocker) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_l2_order_book=order_book_l2 ) default_conf_usdt['telegram']['enabled'] = False @@ -4841,7 +4841,7 @@ def test_order_book_exit_pricing( patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=MagicMock(return_value={ 'bid': 1.9, 'ask': 2.2, @@ -4918,7 +4918,7 @@ def test_sync_wallet_dry_run(mocker, default_conf_usdt, ticker_usdt, fee, limit_ default_conf_usdt['tradable_balance_ratio'] = 1.0 patch_exchange(mocker) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker_usdt, create_order=MagicMock(return_value=limit_buy_order_usdt_open), get_fee=fee, @@ -5430,7 +5430,7 @@ def test_update_funding_fees( mocker.patch(f'{EXMS}.refresh_latest_ohlcv', side_effect=refresh_latest_ohlcv_mock) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, get_rate=enter_rate_mock, fetch_ticker=MagicMock(return_value={ 'bid': 1.9, @@ -5509,7 +5509,7 @@ def test_position_adjust(mocker, default_conf_usdt, fee) -> None: stake_amount = 10 buy_rate_mock = MagicMock(return_value=bid) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, get_rate=buy_rate_mock, fetch_ticker=MagicMock(return_value={ 'bid': 10, @@ -5789,7 +5789,7 @@ def test_position_adjust2(mocker, default_conf_usdt, fee) -> None: amount = 100 buy_rate_mock = MagicMock(return_value=bid) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, get_rate=buy_rate_mock, fetch_ticker=MagicMock(return_value={ 'bid': 10, @@ -5980,7 +5980,7 @@ def test_position_adjust3(mocker, default_conf_usdt, fee, data) -> None: price = order[2] price_mock = MagicMock(return_value=price) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, get_rate=price_mock, fetch_ticker=MagicMock(return_value={ 'bid': 10, @@ -6066,7 +6066,7 @@ def test_check_and_call_adjust_trade_position(mocker, default_conf_usdt, fee, ca freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) buy_rate_mock = MagicMock(return_value=10) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, get_rate=buy_rate_mock, fetch_ticker=MagicMock(return_value={ 'bid': 10, diff --git a/tests/test_integration.py b/tests/test_integration.py index 489027051..a3dd8d935 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -58,7 +58,7 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee, cancel_order_mock = MagicMock() mocker.patch('freqtrade.exchange.binance.Binance.create_stoploss', stoploss) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker, get_fee=fee, amount_to_precision=lambda s, x, y: y, @@ -147,7 +147,7 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, mocker, balance_rati default_conf['telegram']['enabled'] = True mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker, get_fee=fee, amount_to_precision=lambda s, x, y: y, @@ -217,7 +217,7 @@ def test_dca_buying(default_conf_usdt, ticker_usdt, fee, mocker) -> None: freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker_usdt, get_fee=fee, ) @@ -286,7 +286,7 @@ def test_dca_short(default_conf_usdt, ticker_usdt, fee, mocker) -> None: freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker_usdt, get_fee=fee, amount_to_precision=lambda s, x, y: round(y, 4), @@ -361,7 +361,7 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker) freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker_usdt, get_fee=fee, amount_to_precision=lambda s, x, y: y, @@ -477,7 +477,7 @@ def test_dca_exiting(default_conf_usdt, ticker_usdt, fee, mocker, caplog, levera freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) freqtrade.trading_mode = TradingMode.FUTURES mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker_usdt, get_fee=fee, amount_to_precision=lambda s, x, y: y, diff --git a/tests/test_wallets.py b/tests/test_wallets.py index 61e8f279d..7ccc8d0f5 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -6,13 +6,13 @@ import pytest from freqtrade.constants import UNLIMITED_STAKE_AMOUNT from freqtrade.exceptions import DependencyException -from tests.conftest import create_mock_trades, get_patched_freqtradebot, patch_wallet +from tests.conftest import EXMS, create_mock_trades, get_patched_freqtradebot, patch_wallet def test_sync_wallet_at_boot(mocker, default_conf): default_conf['dry_run'] = False mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, get_balances=MagicMock(return_value={ "BNT": { "free": 1.0, @@ -45,7 +45,7 @@ def test_sync_wallet_at_boot(mocker, default_conf): assert 'USDT' in freqtrade.wallets._wallets assert freqtrade.wallets._last_wallet_refresh > 0 mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, get_balances=MagicMock(return_value={ "BNT": { "free": 1.2, @@ -87,7 +87,7 @@ def test_sync_wallet_at_boot(mocker, default_conf): def test_sync_wallet_missing_data(mocker, default_conf): default_conf['dry_run'] = False mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, get_balances=MagicMock(return_value={ "BNT": { "free": 1.0, @@ -136,7 +136,7 @@ def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, balance_r result1, result2, limit_buy_order_open, fee, mocker) -> None: mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker, create_order=MagicMock(return_value=limit_buy_order_open), get_fee=fee @@ -312,7 +312,7 @@ def test_sync_wallet_futures_live(mocker, default_conf): } ] mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, get_balances=MagicMock(return_value={ "USDT": { "free": 900, From feabed30a30a13c5a69b505fecedc143f2ca9641 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 1 Mar 2023 20:19:14 +0100 Subject: [PATCH 096/360] Update remaining exchange mock occurances --- tests/exchange/test_exchange.py | 10 +++++----- tests/exchange/test_kucoin.py | 4 ++-- tests/exchange/test_okx.py | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 843195796..9bc176f41 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -3120,24 +3120,24 @@ def test_cancel_stoploss_order(default_conf, mocker, exchange_name): def test_cancel_stoploss_order_with_result(default_conf, mocker, exchange_name): default_conf['dry_run'] = False mocker.patch(f'{EXMS}.fetch_stoploss_order', return_value={'for': 123}) - mocker.patch('freqtrade.exchange.Gate.fetch_stoploss_order', return_value={'for': 123}) + mocker.patch('freqtrade.exchange.gate.Gate.fetch_stoploss_order', return_value={'for': 123}) exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) res = {'fee': {}, 'status': 'canceled', 'amount': 1234} mocker.patch(f'{EXMS}.cancel_stoploss_order', return_value=res) - mocker.patch('freqtrade.exchange.Gate.cancel_stoploss_order', return_value=res) + mocker.patch('freqtrade.exchange.gate.Gate.cancel_stoploss_order', return_value=res) co = exchange.cancel_stoploss_order_with_result(order_id='_', pair='TKN/BTC', amount=555) assert co == res mocker.patch(f'{EXMS}.cancel_stoploss_order', return_value='canceled') - mocker.patch('freqtrade.exchange.Gate.cancel_stoploss_order', return_value='canceled') + mocker.patch('freqtrade.exchange.gate.Gate.cancel_stoploss_order', return_value='canceled') # Fall back to fetch_stoploss_order co = exchange.cancel_stoploss_order_with_result(order_id='_', pair='TKN/BTC', amount=555) assert co == {'for': 123} exc = InvalidOrderException("") mocker.patch(f'{EXMS}.fetch_stoploss_order', side_effect=exc) - mocker.patch('freqtrade.exchange.Gate.fetch_stoploss_order', side_effect=exc) + mocker.patch('freqtrade.exchange.gate.Gate.fetch_stoploss_order', side_effect=exc) co = exchange.cancel_stoploss_order_with_result(order_id='_', pair='TKN/BTC', amount=555) assert co['amount'] == 555 assert co == {'fee': {}, 'status': 'canceled', 'amount': 555, 'info': {}} @@ -3145,7 +3145,7 @@ def test_cancel_stoploss_order_with_result(default_conf, mocker, exchange_name): with pytest.raises(InvalidOrderException): exc = InvalidOrderException("Did not find order") mocker.patch(f'{EXMS}.cancel_stoploss_order', side_effect=exc) - mocker.patch('freqtrade.exchange.Gate.cancel_stoploss_order', side_effect=exc) + mocker.patch('freqtrade.exchange.gate.Gate.cancel_stoploss_order', side_effect=exc) exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) exchange.cancel_stoploss_order_with_result(order_id='_', pair='TKN/BTC', amount=123) diff --git a/tests/exchange/test_kucoin.py b/tests/exchange/test_kucoin.py index d2cb23091..e0bb32b7c 100644 --- a/tests/exchange/test_kucoin.py +++ b/tests/exchange/test_kucoin.py @@ -146,8 +146,8 @@ def test_kucoin_create_order(default_conf, mocker, side, ordertype, rate): 'amount': 1 }) default_conf['dry_run'] = False - mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y) - mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y) exchange = get_patched_exchange(mocker, default_conf, api_mock, id='kucoin') exchange._set_leverage = MagicMock() exchange.set_margin_mode = MagicMock() diff --git a/tests/exchange/test_okx.py b/tests/exchange/test_okx.py index 46b1852a0..fce77f4c7 100644 --- a/tests/exchange/test_okx.py +++ b/tests/exchange/test_okx.py @@ -46,7 +46,7 @@ def test_get_maintenance_ratio_and_amt_okx( default_conf['margin_mode'] = 'isolated' default_conf['dry_run'] = False mocker.patch.multiple( - 'freqtrade.exchange.Okx', + 'freqtrade.exchange.okx.Okx', exchange_has=MagicMock(return_value=True), load_leverage_tiers=MagicMock(return_value={ 'ETH/USDT:USDT': [ From a629d455fb534510d06551be661846daf75dd117 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Feb 2023 08:50:57 +0000 Subject: [PATCH 097/360] Bump sqlalchemy from 1.4.46 to 2.0.3 Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 1.4.46 to 2.0.3. - [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases) - [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/main/CHANGES.rst) - [Commits](https://github.com/sqlalchemy/sqlalchemy/commits) --- updated-dependencies: - dependency-name: sqlalchemy dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index ea0e8ecb4..3e6c66757 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ pandas-ta==0.3.14b ccxt==2.8.54 cryptography==39.0.1 aiohttp==3.8.4 -SQLAlchemy==1.4.46 +SQLAlchemy==2.0.3 python-telegram-bot==13.15 arrow==1.2.3 cachetools==4.2.2 From a553a9923ad57feda53b911fac918bae1e0516ef Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 14 Feb 2023 08:51:00 +0000 Subject: [PATCH 098/360] Update types for pairlock --- freqtrade/persistence/pairlock.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/persistence/pairlock.py b/freqtrade/persistence/pairlock.py index 938cd14bc..b8af1421f 100644 --- a/freqtrade/persistence/pairlock.py +++ b/freqtrade/persistence/pairlock.py @@ -21,9 +21,9 @@ class PairLock(_DECL_BASE): side = Column(String(25), nullable=False, default="*") reason = Column(String(255), nullable=True) # Time the pair was locked (start time) - lock_time = Column(DateTime(), nullable=False) + lock_time: datetime = Column(DateTime(), nullable=False) # Time until the pair is locked (end time) - lock_end_time = Column(DateTime(), nullable=False, index=True) + lock_end_time: datetime = Column(DateTime(), nullable=False, index=True) active = Column(Boolean, nullable=False, default=True, index=True) From b62830031f0f70689d985d2150343fe6a6249695 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 14 Feb 2023 09:56:42 +0000 Subject: [PATCH 099/360] Dummy-type query objects --- freqtrade/persistence/pairlock.py | 2 ++ freqtrade/persistence/trade_model.py | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/freqtrade/persistence/pairlock.py b/freqtrade/persistence/pairlock.py index b8af1421f..2c4cfa3b4 100644 --- a/freqtrade/persistence/pairlock.py +++ b/freqtrade/persistence/pairlock.py @@ -13,6 +13,8 @@ class PairLock(_DECL_BASE): Pair Locks database model. """ __tablename__ = 'pairlocks' + # TODO: Properly type query. + query: Any id = Column(Integer, primary_key=True) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index c84fcec9e..e6f4f5b08 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -36,6 +36,8 @@ class Order(_DECL_BASE): Mirrors CCXT Order structure """ __tablename__ = 'orders' + # TODO: Properly type query. + query: Any # Uniqueness should be ensured over pair, order_id # its likely that order_id is unique per Pair on some exchanges. __table_args__ = (UniqueConstraint('ft_pair', 'order_id', name="_order_pair_order_id"),) @@ -1167,6 +1169,9 @@ class Trade(_DECL_BASE, LocalTrade): Note: Fields must be aligned with LocalTrade class """ __tablename__ = 'trades' + # TODO: Type query type throughout. + query: Any + _session: Any = None use_db: bool = True From 829e10ff87907bbcb0495c75e88a1e4e462a798f Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 14 Feb 2023 10:04:09 +0000 Subject: [PATCH 100/360] Improve Type for models.py --- freqtrade/persistence/models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 7f851322e..38dbf212f 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -2,6 +2,7 @@ This module contains the class to persist trades into SQLite """ import logging +from typing import Any, Dict from sqlalchemy import create_engine, inspect from sqlalchemy.exc import NoSuchModuleError @@ -29,7 +30,7 @@ def init_db(db_url: str) -> None: :param db_url: Database to use :return: None """ - kwargs = {} + kwargs: Dict[str, Any] = {} if db_url == 'sqlite:///': raise OperationalException( From 9d455f58b1d643974add4b92177562a6312c77ad Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 14 Feb 2023 10:04:36 +0000 Subject: [PATCH 101/360] Improve some trade model Types --- freqtrade/persistence/trade_model.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index e6f4f5b08..c0019eaf2 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -1386,7 +1386,7 @@ class Trade(_DECL_BASE, LocalTrade): Returns List of dicts containing all Trades, including profit and trade count NOTE: Not supported in Backtesting. """ - filters = [Trade.is_open.is_(False)] + filters: List = [Trade.is_open.is_(False)] if minutes: start_date = datetime.now(timezone.utc) - timedelta(minutes=minutes) filters.append(Trade.close_date >= start_date) @@ -1419,7 +1419,7 @@ class Trade(_DECL_BASE, LocalTrade): NOTE: Not supported in Backtesting. """ - filters = [Trade.is_open.is_(False)] + filters: List = [Trade.is_open.is_(False)] if (pair is not None): filters.append(Trade.pair == pair) @@ -1452,7 +1452,7 @@ class Trade(_DECL_BASE, LocalTrade): NOTE: Not supported in Backtesting. """ - filters = [Trade.is_open.is_(False)] + filters: List = [Trade.is_open.is_(False)] if (pair is not None): filters.append(Trade.pair == pair) @@ -1485,7 +1485,7 @@ class Trade(_DECL_BASE, LocalTrade): NOTE: Not supported in Backtesting. """ - filters = [Trade.is_open.is_(False)] + filters: List = [Trade.is_open.is_(False)] if (pair is not None): filters.append(Trade.pair == pair) From 3a9d83f86c79ccdd90a2ad250dc2af47e7e9868f Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 14 Feb 2023 10:05:04 +0000 Subject: [PATCH 102/360] Mypy: define sqlalchemy plugin --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 6f9e5205c..71687961d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,6 +35,9 @@ warn_unused_ignores = true exclude = [ '^build_helpers\.py$' ] +plugins = [ + "sqlalchemy.ext.mypy.plugin" +] [[tool.mypy.overrides]] module = "tests.*" From 41e27ba62198c228a2e06dafeb6822381ec54030 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 14 Feb 2023 10:35:09 +0000 Subject: [PATCH 103/360] Enhance some type info --- freqtrade/freqtradebot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 633e9dc71..ceb078669 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -633,7 +633,7 @@ class FreqtradeBot(LoggingMixin): return remaining = (trade.amount - amount) * current_exit_rate - if remaining < min_exit_stake: + if min_exit_stake and remaining < min_exit_stake: logger.info(f"Remaining amount of {remaining} would be smaller " f"than the minimum of {min_exit_stake}.") return @@ -1694,7 +1694,7 @@ class FreqtradeBot(LoggingMixin): raise DependencyException( f"Order_obj not found for {order_id}. This should not have happened.") - profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested + profit_rate: float = 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, side='exit', is_short=trade.is_short, refresh=False) From 3c019e0e16c64a6d488baa056f3f117c51ee3e24 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 14 Feb 2023 10:35:18 +0000 Subject: [PATCH 104/360] tentative augmented typing of Trade object --- freqtrade/freqtradebot.py | 3 +- freqtrade/persistence/trade_model.py | 72 ++++++++++++++-------------- 2 files changed, 39 insertions(+), 36 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index ceb078669..adc630036 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1737,7 +1737,8 @@ class FreqtradeBot(LoggingMixin): # def update_trade_state( - self, trade: Trade, order_id: str, action_order: Optional[Dict[str, Any]] = None, + self, trade: Trade, order_id: Optional[str], + action_order: Optional[Dict[str, Any]] = None, stoploss_order: bool = False, send_msg: bool = True) -> bool: """ Checks trades with open orders and updates the amount if necessary diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index c0019eaf2..9355d2a84 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -45,34 +45,34 @@ class Order(_DECL_BASE): id = Column(Integer, primary_key=True) ft_trade_id = Column(Integer, ForeignKey('trades.id'), index=True) - trade = relationship("Trade", back_populates="orders") + trade: List["Trade"] = relationship("Trade", back_populates="orders") # order_side can only be 'buy', 'sell' or 'stoploss' - ft_order_side = Column(String(25), nullable=False) - ft_pair = Column(String(25), nullable=False) - ft_is_open = Column(Boolean, nullable=False, default=True, index=True) - ft_amount = Column(Float(), nullable=False) - ft_price = Column(Float(), nullable=False) + ft_order_side: str = Column(String(25), nullable=False) + ft_pair: str = Column(String(25), nullable=False) + ft_is_open: bool = Column(Boolean, nullable=False, default=True, index=True) + ft_amount: float = Column(Float(), nullable=False) + ft_price: float = Column(Float(), nullable=False) order_id = Column(String(255), nullable=False, index=True) status = Column(String(255), nullable=True) symbol = Column(String(25), nullable=True) - order_type = Column(String(50), nullable=True) + # TODO: type: order_type type is Optional[str] + order_type: str = Column(String(50), nullable=True) side = Column(String(25), nullable=True) - price = Column(Float(), nullable=True) - average = Column(Float(), nullable=True) - amount = Column(Float(), nullable=True) - filled = Column(Float(), nullable=True) - remaining = Column(Float(), nullable=True) - cost = Column(Float(), nullable=True) - stop_price = Column(Float(), nullable=True) - order_date = Column(DateTime(), nullable=True, default=datetime.utcnow) + price: Optional[float] = Column(Float(), nullable=True) + average: Optional[float] = Column(Float(), nullable=True) + amount: Optional[float] = Column(Float(), nullable=True) + filled: Optional[float] = Column(Float(), nullable=True) + remaining: Optional[float] = Column(Float(), nullable=True) + cost: Optional[float] = Column(Float(), nullable=True) + stop_price: Optional[float] = Column(Float(), nullable=True) + order_date: datetime = Column(DateTime(), nullable=True, default=datetime.utcnow) order_filled_date = Column(DateTime(), nullable=True) order_update_date = Column(DateTime(), nullable=True) + funding_fee: Optional[float] = Column(Float(), nullable=True) - funding_fee = Column(Float(), nullable=True) - - ft_fee_base = Column(Float(), nullable=True) + ft_fee_base: Optional[float] = Column(Float(), nullable=True) @property def order_date_utc(self) -> datetime: @@ -1175,13 +1175,13 @@ class Trade(_DECL_BASE, LocalTrade): use_db: bool = True - id = Column(Integer, primary_key=True) + id: int = Column(Integer, primary_key=True) - orders = relationship("Order", order_by="Order.id", cascade="all, delete-orphan", - lazy="selectin", innerjoin=True) + orders: List[Order] = relationship("Order", order_by="Order.id", cascade="all, delete-orphan", + lazy="selectin", innerjoin=True) - exchange = Column(String(25), nullable=False) - pair = Column(String(25), nullable=False, index=True) + exchange: str = Column(String(25), nullable=False) + pair: str = Column(String(25), nullable=False, index=True) base_currency = Column(String(25), nullable=True) stake_currency = Column(String(25), nullable=True) is_open = Column(Boolean, nullable=False, default=True, index=True) @@ -1192,21 +1192,23 @@ class Trade(_DECL_BASE, LocalTrade): fee_close_cost = Column(Float(), nullable=True) fee_close_currency = Column(String(25), nullable=True) open_rate: float = Column(Float()) - open_rate_requested = Column(Float()) + open_rate_requested: float = Column(Float()) # open_trade_value - calculated via _calc_open_trade_value open_trade_value = Column(Float()) close_rate: Optional[float] = Column(Float()) - close_rate_requested = Column(Float()) - realized_profit = Column(Float(), default=0.0) + close_rate_requested: Optional[float] = Column(Float()) + # TODO: is the below type really correct? + realized_profit: float = Column(Float(), default=0.0) close_profit = Column(Float()) - close_profit_abs = Column(Float()) - stake_amount = Column(Float(), nullable=False) - max_stake_amount = Column(Float()) - amount = Column(Float()) - amount_requested = Column(Float()) + close_profit_abs: Optional[float] = Column(Float()) + stake_amount: float = Column(Float(), nullable=False) + max_stake_amount: Optional[float] = Column(Float()) + amount: float = Column(Float()) + amount_requested: Optional[float] = Column(Float()) open_date = Column(DateTime(), nullable=False, default=datetime.utcnow) close_date = Column(DateTime()) - open_order_id = Column(String(255)) + # TODO: open_order_id type should be Optional[str] + open_order_id: str = Column(String(255)) # absolute value of the stop loss stop_loss = Column(Float(), nullable=True, default=0.0) # percentage value of the stop loss @@ -1236,15 +1238,15 @@ class Trade(_DECL_BASE, LocalTrade): contract_size = Column(Float(), nullable=True) # Leverage trading properties - leverage = Column(Float(), nullable=True, default=1.0) - is_short = Column(Boolean, nullable=False, default=False) + leverage: float = Column(Float(), nullable=True, default=1.0) + is_short: bool = Column(Boolean, nullable=False, default=False) liquidation_price = Column(Float(), nullable=True) # Margin Trading Properties interest_rate = Column(Float(), nullable=False, default=0.0) # Futures properties - funding_fees = Column(Float(), nullable=True, default=None) + funding_fees: Optional[float] = Column(Float(), nullable=True, default=None) def __init__(self, **kwargs): super().__init__(**kwargs) From 39a658eac22a70726d4b7bfa8139ee423325731e Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 16 Feb 2023 07:04:45 +0000 Subject: [PATCH 105/360] Update DeclarativeBase --- freqtrade/persistence/base.py | 7 +++---- freqtrade/persistence/models.py | 6 +++--- freqtrade/persistence/pairlock.py | 4 ++-- freqtrade/persistence/trade_model.py | 6 +++--- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/freqtrade/persistence/base.py b/freqtrade/persistence/base.py index fb2d561e1..2fed715d7 100644 --- a/freqtrade/persistence/base.py +++ b/freqtrade/persistence/base.py @@ -1,7 +1,6 @@ -from typing import Any - -from sqlalchemy.orm import declarative_base +from sqlalchemy.orm import DeclarativeBase -_DECL_BASE: Any = declarative_base() +class ModelBase(DeclarativeBase): + pass diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 38dbf212f..9f90f8a74 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -10,7 +10,7 @@ from sqlalchemy.orm import scoped_session, sessionmaker from sqlalchemy.pool import StaticPool from freqtrade.exceptions import OperationalException -from freqtrade.persistence.base import _DECL_BASE +from freqtrade.persistence.base import ModelBase from freqtrade.persistence.migrations import check_migrate from freqtrade.persistence.pairlock import PairLock from freqtrade.persistence.trade_model import Order, Trade @@ -60,5 +60,5 @@ def init_db(db_url: str) -> None: PairLock.query = Trade._session.query_property() previous_tables = inspect(engine).get_table_names() - _DECL_BASE.metadata.create_all(engine) - check_migrate(engine, decl_base=_DECL_BASE, previous_tables=previous_tables) + ModelBase.metadata.create_all(engine) + check_migrate(engine, decl_base=ModelBase, previous_tables=previous_tables) diff --git a/freqtrade/persistence/pairlock.py b/freqtrade/persistence/pairlock.py index 2c4cfa3b4..e1f659b0a 100644 --- a/freqtrade/persistence/pairlock.py +++ b/freqtrade/persistence/pairlock.py @@ -5,10 +5,10 @@ from sqlalchemy import Boolean, Column, DateTime, Integer, String, or_ from sqlalchemy.orm import Query from freqtrade.constants import DATETIME_PRINT_FORMAT -from freqtrade.persistence.base import _DECL_BASE +from freqtrade.persistence.base import ModelBase -class PairLock(_DECL_BASE): +class PairLock(ModelBase): """ Pair Locks database model. """ diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 9355d2a84..ebec36e81 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -17,14 +17,14 @@ from freqtrade.enums import ExitType, TradingMode from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.exchange import amount_to_contract_precision, price_to_precision from freqtrade.leverage import interest -from freqtrade.persistence.base import _DECL_BASE +from freqtrade.persistence.base import ModelBase from freqtrade.util import FtPrecise logger = logging.getLogger(__name__) -class Order(_DECL_BASE): +class Order(ModelBase): """ Order database model Keeps a record of all orders placed on the exchange @@ -1161,7 +1161,7 @@ class LocalTrade(): logger.info(f"New stoploss: {trade.stop_loss}.") -class Trade(_DECL_BASE, LocalTrade): +class Trade(ModelBase, LocalTrade): """ Trade database model. Also handles updating and querying trades From 0bd9b00132623db0499325545ef210ed0512e5c5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 16 Feb 2023 07:05:57 +0000 Subject: [PATCH 106/360] Pairlock to mappedColumn --- freqtrade/persistence/pairlock.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/freqtrade/persistence/pairlock.py b/freqtrade/persistence/pairlock.py index e1f659b0a..b721cceac 100644 --- a/freqtrade/persistence/pairlock.py +++ b/freqtrade/persistence/pairlock.py @@ -1,8 +1,8 @@ from datetime import datetime, timezone from typing import Any, Dict, Optional -from sqlalchemy import Boolean, Column, DateTime, Integer, String, or_ -from sqlalchemy.orm import Query +from sqlalchemy import Boolean, DateTime, Integer, String, or_ +from sqlalchemy.orm import Query, mapped_column from freqtrade.constants import DATETIME_PRINT_FORMAT from freqtrade.persistence.base import ModelBase @@ -16,18 +16,18 @@ class PairLock(ModelBase): # TODO: Properly type query. query: Any - id = Column(Integer, primary_key=True) + id = mapped_column(Integer, primary_key=True) - pair = Column(String(25), nullable=False, index=True) + pair = mapped_column(String(25), nullable=False, index=True) # lock direction - long, short or * (for both) - side = Column(String(25), nullable=False, default="*") - reason = Column(String(255), nullable=True) + side = mapped_column(String(25), nullable=False, default="*") + reason = mapped_column(String(255), nullable=True) # Time the pair was locked (start time) - lock_time: datetime = Column(DateTime(), nullable=False) + lock_time: datetime = mapped_column(DateTime(), nullable=False) # Time until the pair is locked (end time) - lock_end_time: datetime = Column(DateTime(), nullable=False, index=True) + lock_end_time: datetime = mapped_column(DateTime(), nullable=False, index=True) - active = Column(Boolean, nullable=False, default=True, index=True) + active = mapped_column(Boolean, nullable=False, default=True, index=True) def __repr__(self): lock_time = self.lock_time.strftime(DATETIME_PRINT_FORMAT) From 98791752a9c39827385bd0c20509f560bbff88d4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 16 Feb 2023 07:07:08 +0000 Subject: [PATCH 107/360] Update TradeModels to mapped_column --- freqtrade/persistence/trade_model.py | 152 +++++++++++++-------------- 1 file changed, 76 insertions(+), 76 deletions(-) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index ebec36e81..e8f78661e 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -7,9 +7,9 @@ from datetime import datetime, timedelta, timezone from math import isclose from typing import Any, Dict, List, Optional -from sqlalchemy import (Boolean, Column, DateTime, Enum, Float, ForeignKey, Integer, String, +from sqlalchemy import (Boolean, DateTime, Enum, Float, ForeignKey, Integer, String, UniqueConstraint, desc, func) -from sqlalchemy.orm import Query, lazyload, relationship +from sqlalchemy.orm import Query, lazyload, mapped_column, relationship from freqtrade.constants import (DATETIME_PRINT_FORMAT, MATH_CLOSE_PREC, NON_OPEN_EXCHANGE_STATES, BuySell, LongShort) @@ -42,37 +42,37 @@ class Order(ModelBase): # its likely that order_id is unique per Pair on some exchanges. __table_args__ = (UniqueConstraint('ft_pair', 'order_id', name="_order_pair_order_id"),) - id = Column(Integer, primary_key=True) - ft_trade_id = Column(Integer, ForeignKey('trades.id'), index=True) + id = mapped_column(Integer, primary_key=True) + ft_trade_id = mapped_column(Integer, ForeignKey('trades.id'), index=True) trade: List["Trade"] = relationship("Trade", back_populates="orders") # order_side can only be 'buy', 'sell' or 'stoploss' - ft_order_side: str = Column(String(25), nullable=False) - ft_pair: str = Column(String(25), nullable=False) - ft_is_open: bool = Column(Boolean, nullable=False, default=True, index=True) - ft_amount: float = Column(Float(), nullable=False) - ft_price: float = Column(Float(), nullable=False) + ft_order_side: str = mapped_column(String(25), nullable=False) + ft_pair: str = mapped_column(String(25), nullable=False) + ft_is_open: bool = mapped_column(Boolean, nullable=False, default=True, index=True) + ft_amount: float = mapped_column(Float(), nullable=False) + ft_price: float = mapped_column(Float(), nullable=False) - order_id = Column(String(255), nullable=False, index=True) - status = Column(String(255), nullable=True) - symbol = Column(String(25), nullable=True) + order_id = mapped_column(String(255), nullable=False, index=True) + status = mapped_column(String(255), nullable=True) + symbol = mapped_column(String(25), nullable=True) # TODO: type: order_type type is Optional[str] - order_type: str = Column(String(50), nullable=True) - side = Column(String(25), nullable=True) - price: Optional[float] = Column(Float(), nullable=True) - average: Optional[float] = Column(Float(), nullable=True) - amount: Optional[float] = Column(Float(), nullable=True) - filled: Optional[float] = Column(Float(), nullable=True) - remaining: Optional[float] = Column(Float(), nullable=True) - cost: Optional[float] = Column(Float(), nullable=True) - stop_price: Optional[float] = Column(Float(), nullable=True) - order_date: datetime = Column(DateTime(), nullable=True, default=datetime.utcnow) - order_filled_date = Column(DateTime(), nullable=True) - order_update_date = Column(DateTime(), nullable=True) - funding_fee: Optional[float] = Column(Float(), nullable=True) + order_type: str = mapped_column(String(50), nullable=True) + side = mapped_column(String(25), nullable=True) + price: Optional[float] = mapped_column(Float(), nullable=True) + average: Optional[float] = mapped_column(Float(), nullable=True) + amount: Optional[float] = mapped_column(Float(), nullable=True) + filled: Optional[float] = mapped_column(Float(), nullable=True) + remaining: Optional[float] = mapped_column(Float(), nullable=True) + cost: Optional[float] = mapped_column(Float(), nullable=True) + stop_price: Optional[float] = mapped_column(Float(), nullable=True) + order_date: datetime = mapped_column(DateTime(), nullable=True, default=datetime.utcnow) + order_filled_date = mapped_column(DateTime(), nullable=True) + order_update_date = mapped_column(DateTime(), nullable=True) + funding_fee: Optional[float] = mapped_column(Float(), nullable=True) - ft_fee_base: Optional[float] = Column(Float(), nullable=True) + ft_fee_base: Optional[float] = mapped_column(Float(), nullable=True) @property def order_date_utc(self) -> datetime: @@ -1175,78 +1175,78 @@ class Trade(ModelBase, LocalTrade): use_db: bool = True - id: int = Column(Integer, primary_key=True) + id: int = mapped_column(Integer, primary_key=True) orders: List[Order] = relationship("Order", order_by="Order.id", cascade="all, delete-orphan", lazy="selectin", innerjoin=True) - exchange: str = Column(String(25), nullable=False) - pair: str = Column(String(25), nullable=False, index=True) - base_currency = Column(String(25), nullable=True) - stake_currency = Column(String(25), nullable=True) - is_open = Column(Boolean, nullable=False, default=True, index=True) - fee_open = Column(Float(), nullable=False, default=0.0) - fee_open_cost = Column(Float(), nullable=True) - fee_open_currency = Column(String(25), nullable=True) - fee_close = Column(Float(), nullable=False, default=0.0) - fee_close_cost = Column(Float(), nullable=True) - fee_close_currency = Column(String(25), nullable=True) - open_rate: float = Column(Float()) - open_rate_requested: float = Column(Float()) + exchange: str = mapped_column(String(25), nullable=False) + pair: str = mapped_column(String(25), nullable=False, index=True) + base_currency = mapped_column(String(25), nullable=True) + stake_currency = mapped_column(String(25), nullable=True) + is_open = mapped_column(Boolean, nullable=False, default=True, index=True) + fee_open = mapped_column(Float(), nullable=False, default=0.0) + fee_open_cost = mapped_column(Float(), nullable=True) + fee_open_currency = mapped_column(String(25), nullable=True) + fee_close = mapped_column(Float(), nullable=False, default=0.0) + fee_close_cost = mapped_column(Float(), nullable=True) + fee_close_currency = mapped_column(String(25), nullable=True) + open_rate: float = mapped_column(Float()) + open_rate_requested: float = mapped_column(Float()) # open_trade_value - calculated via _calc_open_trade_value - open_trade_value = Column(Float()) - close_rate: Optional[float] = Column(Float()) - close_rate_requested: Optional[float] = Column(Float()) + open_trade_value = mapped_column(Float()) + close_rate: Optional[float] = mapped_column(Float()) + close_rate_requested: Optional[float] = mapped_column(Float()) # TODO: is the below type really correct? - realized_profit: float = Column(Float(), default=0.0) - close_profit = Column(Float()) - close_profit_abs: Optional[float] = Column(Float()) - stake_amount: float = Column(Float(), nullable=False) - max_stake_amount: Optional[float] = Column(Float()) - amount: float = Column(Float()) - amount_requested: Optional[float] = Column(Float()) - open_date = Column(DateTime(), nullable=False, default=datetime.utcnow) - close_date = Column(DateTime()) + realized_profit: float = mapped_column(Float(), default=0.0) + close_profit = mapped_column(Float()) + close_profit_abs: Optional[float] = mapped_column(Float()) + stake_amount: float = mapped_column(Float(), nullable=False) + max_stake_amount: Optional[float] = mapped_column(Float()) + amount: float = mapped_column(Float()) + amount_requested: Optional[float] = mapped_column(Float()) + open_date = mapped_column(DateTime(), nullable=False, default=datetime.utcnow) + close_date = mapped_column(DateTime()) # TODO: open_order_id type should be Optional[str] - open_order_id: str = Column(String(255)) + open_order_id: str = mapped_column(String(255)) # absolute value of the stop loss - stop_loss = Column(Float(), nullable=True, default=0.0) + stop_loss = mapped_column(Float(), nullable=True, default=0.0) # percentage value of the stop loss - stop_loss_pct = Column(Float(), nullable=True) + stop_loss_pct = mapped_column(Float(), nullable=True) # absolute value of the initial stop loss - initial_stop_loss = Column(Float(), nullable=True, default=0.0) + initial_stop_loss = mapped_column(Float(), nullable=True, default=0.0) # percentage value of the initial stop loss - initial_stop_loss_pct = Column(Float(), nullable=True) + initial_stop_loss_pct = mapped_column(Float(), nullable=True) # stoploss order id which is on exchange - stoploss_order_id = Column(String(255), nullable=True, index=True) + stoploss_order_id = mapped_column(String(255), nullable=True, index=True) # last update time of the stoploss order on exchange - stoploss_last_update = Column(DateTime(), nullable=True) + stoploss_last_update = mapped_column(DateTime(), nullable=True) # absolute value of the highest reached price - max_rate = Column(Float(), nullable=True, default=0.0) + max_rate = mapped_column(Float(), nullable=True, default=0.0) # Lowest price reached - min_rate = Column(Float(), nullable=True) - exit_reason = Column(String(100), nullable=True) - exit_order_status = Column(String(100), nullable=True) - strategy = Column(String(100), nullable=True) - enter_tag = Column(String(100), nullable=True) - timeframe = Column(Integer, nullable=True) + min_rate = mapped_column(Float(), nullable=True) + exit_reason = mapped_column(String(100), nullable=True) + exit_order_status = mapped_column(String(100), nullable=True) + strategy = mapped_column(String(100), nullable=True) + enter_tag = mapped_column(String(100), nullable=True) + timeframe = mapped_column(Integer, nullable=True) - trading_mode = Column(Enum(TradingMode), nullable=True) - amount_precision = Column(Float(), nullable=True) - price_precision = Column(Float(), nullable=True) - precision_mode = Column(Integer, nullable=True) - contract_size = Column(Float(), nullable=True) + trading_mode = mapped_column(Enum(TradingMode), nullable=True) + amount_precision = mapped_column(Float(), nullable=True) + price_precision = mapped_column(Float(), nullable=True) + precision_mode = mapped_column(Integer, nullable=True) + contract_size = mapped_column(Float(), nullable=True) # Leverage trading properties - leverage: float = Column(Float(), nullable=True, default=1.0) - is_short: bool = Column(Boolean, nullable=False, default=False) - liquidation_price = Column(Float(), nullable=True) + leverage: float = mapped_column(Float(), nullable=True, default=1.0) + is_short: bool = mapped_column(Boolean, nullable=False, default=False) + liquidation_price = mapped_column(Float(), nullable=True) # Margin Trading Properties - interest_rate = Column(Float(), nullable=False, default=0.0) + interest_rate = mapped_column(Float(), nullable=False, default=0.0) # Futures properties - funding_fees: Optional[float] = Column(Float(), nullable=True, default=None) + funding_fees: Optional[float] = mapped_column(Float(), nullable=True, default=None) def __init__(self, **kwargs): super().__init__(**kwargs) From 13b1a3e737689effd4dc14dcc1a60abda309375b Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 16 Feb 2023 07:14:07 +0000 Subject: [PATCH 108/360] Properly pairlock columns using mapped --- freqtrade/persistence/pairlock.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/freqtrade/persistence/pairlock.py b/freqtrade/persistence/pairlock.py index b721cceac..2c81beb85 100644 --- a/freqtrade/persistence/pairlock.py +++ b/freqtrade/persistence/pairlock.py @@ -1,8 +1,8 @@ from datetime import datetime, timezone from typing import Any, Dict, Optional -from sqlalchemy import Boolean, DateTime, Integer, String, or_ -from sqlalchemy.orm import Query, mapped_column +from sqlalchemy import String, or_ +from sqlalchemy.orm import Mapped, Query, mapped_column from freqtrade.constants import DATETIME_PRINT_FORMAT from freqtrade.persistence.base import ModelBase @@ -16,20 +16,20 @@ class PairLock(ModelBase): # TODO: Properly type query. query: Any - id = mapped_column(Integer, primary_key=True) + id: Mapped[int] = mapped_column(primary_key=True) - pair = mapped_column(String(25), nullable=False, index=True) + pair: Mapped[str] = mapped_column(String(25), nullable=False, index=True) # lock direction - long, short or * (for both) - side = mapped_column(String(25), nullable=False, default="*") - reason = mapped_column(String(255), nullable=True) + side: Mapped[str] = mapped_column(String(25), nullable=False, default="*") + reason: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) # Time the pair was locked (start time) - lock_time: datetime = mapped_column(DateTime(), nullable=False) + lock_time: Mapped[datetime] = mapped_column(nullable=False) # Time until the pair is locked (end time) - lock_end_time: datetime = mapped_column(DateTime(), nullable=False, index=True) + lock_end_time: Mapped[datetime] = mapped_column(nullable=False, index=True) - active = mapped_column(Boolean, nullable=False, default=True, index=True) + active: Mapped[bool] = mapped_column(nullable=False, default=True, index=True) - def __repr__(self): + def __repr__(self) -> str: lock_time = self.lock_time.strftime(DATETIME_PRINT_FORMAT) lock_end_time = self.lock_end_time.strftime(DATETIME_PRINT_FORMAT) return ( From bb116456a957defcb5fe422b91094cd7dd640a51 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 16 Feb 2023 07:17:56 +0000 Subject: [PATCH 109/360] Update Types for Order object --- freqtrade/persistence/trade_model.py | 52 ++++++++++++++-------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index e8f78661e..c4bfc7b1f 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -9,7 +9,7 @@ from typing import Any, Dict, List, Optional from sqlalchemy import (Boolean, DateTime, Enum, Float, ForeignKey, Integer, String, UniqueConstraint, desc, func) -from sqlalchemy.orm import Query, lazyload, mapped_column, relationship +from sqlalchemy.orm import Mapped, Query, lazyload, mapped_column, relationship from freqtrade.constants import (DATETIME_PRINT_FORMAT, MATH_CLOSE_PREC, NON_OPEN_EXCHANGE_STATES, BuySell, LongShort) @@ -42,37 +42,37 @@ class Order(ModelBase): # its likely that order_id is unique per Pair on some exchanges. __table_args__ = (UniqueConstraint('ft_pair', 'order_id', name="_order_pair_order_id"),) - id = mapped_column(Integer, primary_key=True) - ft_trade_id = mapped_column(Integer, ForeignKey('trades.id'), index=True) + id: Mapped[int] = mapped_column(Integer, primary_key=True) + ft_trade_id: Mapped[int] = mapped_column(Integer, ForeignKey('trades.id'), index=True) - trade: List["Trade"] = relationship("Trade", back_populates="orders") + trade: Mapped[List["Trade"]] = relationship("Trade", back_populates="orders") # order_side can only be 'buy', 'sell' or 'stoploss' - ft_order_side: str = mapped_column(String(25), nullable=False) - ft_pair: str = mapped_column(String(25), nullable=False) - ft_is_open: bool = mapped_column(Boolean, nullable=False, default=True, index=True) - ft_amount: float = mapped_column(Float(), nullable=False) - ft_price: float = mapped_column(Float(), nullable=False) + ft_order_side: Mapped[str] = mapped_column(String(25), nullable=False) + ft_pair: Mapped[str] = mapped_column(String(25), nullable=False) + ft_is_open: Mapped[bool] = mapped_column(nullable=False, default=True, index=True) + ft_amount: Mapped[float] = mapped_column(Float(), nullable=False) + ft_price: Mapped[float] = mapped_column(Float(), nullable=False) - order_id = mapped_column(String(255), nullable=False, index=True) - status = mapped_column(String(255), nullable=True) - symbol = mapped_column(String(25), nullable=True) + order_id: Mapped[str] = mapped_column(String(255), nullable=False, index=True) + status: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) + symbol: Mapped[Optional[str]] = mapped_column(String(25), nullable=True) # TODO: type: order_type type is Optional[str] - order_type: str = mapped_column(String(50), nullable=True) - side = mapped_column(String(25), nullable=True) - price: Optional[float] = mapped_column(Float(), nullable=True) - average: Optional[float] = mapped_column(Float(), nullable=True) - amount: Optional[float] = mapped_column(Float(), nullable=True) - filled: Optional[float] = mapped_column(Float(), nullable=True) - remaining: Optional[float] = mapped_column(Float(), nullable=True) - cost: Optional[float] = mapped_column(Float(), nullable=True) - stop_price: Optional[float] = mapped_column(Float(), nullable=True) - order_date: datetime = mapped_column(DateTime(), nullable=True, default=datetime.utcnow) - order_filled_date = mapped_column(DateTime(), nullable=True) - order_update_date = mapped_column(DateTime(), nullable=True) - funding_fee: Optional[float] = mapped_column(Float(), nullable=True) + order_type: Mapped[str] = mapped_column(String(50), nullable=True) + side: Mapped[str] = mapped_column(String(25), nullable=True) + price: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) + average: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) + amount: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) + filled: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) + remaining: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) + cost: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) + stop_price: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) + order_date: Mapped[datetime] = mapped_column(nullable=True, default=datetime.utcnow) + order_filled_date: Mapped[Optional[datetime]] = mapped_column(nullable=True) + order_update_date: Mapped[Optional[datetime]] = mapped_column(nullable=True) + funding_fee: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) - ft_fee_base: Optional[float] = mapped_column(Float(), nullable=True) + ft_fee_base: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) @property def order_date_utc(self) -> datetime: From 491f49388cc1387e1aa6716f1181c150a396670b Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 16 Feb 2023 07:27:57 +0000 Subject: [PATCH 110/360] "Mapped" for trade_model --- freqtrade/persistence/trade_model.py | 95 ++++++++++++++-------------- 1 file changed, 47 insertions(+), 48 deletions(-) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index c4bfc7b1f..9db79fd8b 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -215,7 +215,7 @@ class Order(ModelBase): # Assumes backtesting will use date_last_filled_utc to calculate future funding fees. self.funding_fee = trade.funding_fees - if (self.ft_order_side == trade.entry_side): + if (self.ft_order_side == trade.entry_side and self.price): trade.open_rate = self.price trade.recalc_trade_from_orders() trade.adjust_stop_loss(trade.open_rate, trade.stop_loss_pct, refresh=True) @@ -1175,78 +1175,77 @@ class Trade(ModelBase, LocalTrade): use_db: bool = True - id: int = mapped_column(Integer, primary_key=True) + id: Mapped[int] = mapped_column(Integer, primary_key=True) - orders: List[Order] = relationship("Order", order_by="Order.id", cascade="all, delete-orphan", - lazy="selectin", innerjoin=True) + orders: Mapped[List[Order]] = relationship( + "Order", order_by="Order.id", cascade="all, delete-orphan", lazy="selectin", + innerjoin=True) - exchange: str = mapped_column(String(25), nullable=False) - pair: str = mapped_column(String(25), nullable=False, index=True) + exchange: Mapped[str] = mapped_column(String(25), nullable=False) + pair: Mapped[str] = mapped_column(String(25), nullable=False, index=True) base_currency = mapped_column(String(25), nullable=True) stake_currency = mapped_column(String(25), nullable=True) - is_open = mapped_column(Boolean, nullable=False, default=True, index=True) - fee_open = mapped_column(Float(), nullable=False, default=0.0) - fee_open_cost = mapped_column(Float(), nullable=True) - fee_open_currency = mapped_column(String(25), nullable=True) - fee_close = mapped_column(Float(), nullable=False, default=0.0) - fee_close_cost = mapped_column(Float(), nullable=True) - fee_close_currency = mapped_column(String(25), nullable=True) - open_rate: float = mapped_column(Float()) + is_open: Mapped[bool] = mapped_column(nullable=False, default=True, index=True) + fee_open: Mapped[float] = mapped_column(Float(), nullable=False, default=0.0) + fee_open_cost: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) + fee_open_currency: Mapped[Optional[str]] = mapped_column(String(25), nullable=True) + fee_close: Mapped[Optional[float]] = mapped_column(Float(), nullable=False, default=0.0) + fee_close_cost: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) + fee_close_currency: Mapped[Optional[str]] = mapped_column(String(25), nullable=True) + open_rate: Mapped[float] = mapped_column(Float()) open_rate_requested: float = mapped_column(Float()) # open_trade_value - calculated via _calc_open_trade_value open_trade_value = mapped_column(Float()) close_rate: Optional[float] = mapped_column(Float()) close_rate_requested: Optional[float] = mapped_column(Float()) - # TODO: is the below type really correct? - realized_profit: float = mapped_column(Float(), default=0.0) - close_profit = mapped_column(Float()) - close_profit_abs: Optional[float] = mapped_column(Float()) - stake_amount: float = mapped_column(Float(), nullable=False) - max_stake_amount: Optional[float] = mapped_column(Float()) - amount: float = mapped_column(Float()) - amount_requested: Optional[float] = mapped_column(Float()) - open_date = mapped_column(DateTime(), nullable=False, default=datetime.utcnow) - close_date = mapped_column(DateTime()) - # TODO: open_order_id type should be Optional[str] - open_order_id: str = mapped_column(String(255)) + realized_profit: Mapped[float] = mapped_column(Float(), default=0.0) + close_profit: Mapped[Optional[float]] = mapped_column(Float()) + close_profit_abs: Mapped[Optional[float]] = mapped_column(Float()) + stake_amount: Mapped[float] = mapped_column(Float(), nullable=False) + max_stake_amount: Mapped[Optional[float]] = mapped_column(Float()) + amount: Mapped[float] = mapped_column(Float()) + amount_requested: Mapped[Optional[float]] = mapped_column(Float()) + open_date: Mapped[datetime] = mapped_column(nullable=False, default=datetime.utcnow) + close_date: Mapped[Optional[datetime]] = mapped_column() + open_order_id: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) # absolute value of the stop loss - stop_loss = mapped_column(Float(), nullable=True, default=0.0) + stop_loss: Mapped[Optional[float]] = mapped_column(Float(), nullable=True, default=0.0) # percentage value of the stop loss - stop_loss_pct = mapped_column(Float(), nullable=True) + stop_loss_pct: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) # absolute value of the initial stop loss - initial_stop_loss = mapped_column(Float(), nullable=True, default=0.0) + initial_stop_loss: Mapped[Optional[float]] = mapped_column(Float(), nullable=True, default=0.0) # percentage value of the initial stop loss - initial_stop_loss_pct = mapped_column(Float(), nullable=True) + initial_stop_loss_pct: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) # stoploss order id which is on exchange - stoploss_order_id = mapped_column(String(255), nullable=True, index=True) + stoploss_order_id: Mapped[Optional[str]] = mapped_column(String(255), nullable=True, index=True) # last update time of the stoploss order on exchange - stoploss_last_update = mapped_column(DateTime(), nullable=True) + stoploss_last_update: Mapped[Optional[datetime]] = mapped_column(nullable=True) # absolute value of the highest reached price - max_rate = mapped_column(Float(), nullable=True, default=0.0) + max_rate: Mapped[Optional[float]] = mapped_column(Float(), nullable=True, default=0.0) # Lowest price reached - min_rate = mapped_column(Float(), nullable=True) - exit_reason = mapped_column(String(100), nullable=True) - exit_order_status = mapped_column(String(100), nullable=True) - strategy = mapped_column(String(100), nullable=True) - enter_tag = mapped_column(String(100), nullable=True) - timeframe = mapped_column(Integer, nullable=True) + min_rate: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) + exit_reason: Mapped[Optional[str]] = mapped_column(String(100), nullable=True) + exit_order_status: Mapped[Optional[str]] = mapped_column(String(100), nullable=True) + strategy: Mapped[Optional[str]] = mapped_column(String(100), nullable=True) + enter_tag: Mapped[Optional[str]] = mapped_column(String(100), nullable=True) + timeframe: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) trading_mode = mapped_column(Enum(TradingMode), nullable=True) - amount_precision = mapped_column(Float(), nullable=True) - price_precision = mapped_column(Float(), nullable=True) - precision_mode = mapped_column(Integer, nullable=True) - contract_size = mapped_column(Float(), nullable=True) + amount_precision: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) + price_precision: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) + precision_mode: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) + contract_size: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) # Leverage trading properties - leverage: float = mapped_column(Float(), nullable=True, default=1.0) - is_short: bool = mapped_column(Boolean, nullable=False, default=False) - liquidation_price = mapped_column(Float(), nullable=True) + leverage: Mapped[Optional[float]] = mapped_column(Float(), nullable=True, default=1.0) + is_short: Mapped[bool] = mapped_column(nullable=False, default=False) + liquidation_price: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) # Margin Trading Properties - interest_rate = mapped_column(Float(), nullable=False, default=0.0) + interest_rate: Mapped[Optional[float]] = mapped_column(Float(), nullable=False, default=0.0) # Futures properties - funding_fees: Optional[float] = mapped_column(Float(), nullable=True, default=None) + funding_fees: Mapped[Optional[float]] = mapped_column(Float(), nullable=True, default=None) def __init__(self, **kwargs): super().__init__(**kwargs) From 47b66f322076ac22b986bac0930016adcf3d66bf Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 16 Feb 2023 08:40:34 +0000 Subject: [PATCH 111/360] More fun with types --- freqtrade/persistence/trade_model.py | 10 +++++----- freqtrade/rpc/rpc.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 9db79fd8b..be296c52c 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -1183,8 +1183,8 @@ class Trade(ModelBase, LocalTrade): exchange: Mapped[str] = mapped_column(String(25), nullable=False) pair: Mapped[str] = mapped_column(String(25), nullable=False, index=True) - base_currency = mapped_column(String(25), nullable=True) - stake_currency = mapped_column(String(25), nullable=True) + base_currency: Mapped[Optional[str]] = mapped_column(String(25), nullable=True) + stake_currency: Mapped[Optional[str]] = mapped_column(String(25), nullable=True) is_open: Mapped[bool] = mapped_column(nullable=False, default=True, index=True) fee_open: Mapped[float] = mapped_column(Float(), nullable=False, default=0.0) fee_open_cost: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) @@ -1209,7 +1209,7 @@ class Trade(ModelBase, LocalTrade): close_date: Mapped[Optional[datetime]] = mapped_column() open_order_id: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) # absolute value of the stop loss - stop_loss: Mapped[Optional[float]] = mapped_column(Float(), nullable=True, default=0.0) + stop_loss: Mapped[float] = mapped_column(Float(), nullable=True, default=0.0) # percentage value of the stop loss stop_loss_pct: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) # absolute value of the initial stop loss @@ -1230,14 +1230,14 @@ class Trade(ModelBase, LocalTrade): enter_tag: Mapped[Optional[str]] = mapped_column(String(100), nullable=True) timeframe: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) - trading_mode = mapped_column(Enum(TradingMode), nullable=True) + trading_mode: Mapped[TradingMode] = mapped_column(Enum(TradingMode), nullable=True) amount_precision: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) price_precision: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) precision_mode: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) contract_size: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) # Leverage trading properties - leverage: Mapped[Optional[float]] = mapped_column(Float(), nullable=True, default=1.0) + leverage: Mapped[float] = mapped_column(Float(), nullable=True, default=1.0) is_short: Mapped[bool] = mapped_column(nullable=False, default=False) liquidation_price: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 82f892101..0c1a8fbbd 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -506,7 +506,7 @@ class RPC: trades_df = DataFrame([{'close_date': trade.close_date.strftime(DATETIME_PRINT_FORMAT), 'profit_abs': trade.close_profit_abs} - for trade in trades if not trade.is_open]) + for trade in trades if not trade.is_open and trade.close_date]) max_drawdown_abs = 0.0 max_drawdown = 0.0 if len(trades_df) > 0: From e59eaf33e09f2b613c44f3ac67a326140c8b6176 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 16 Feb 2023 08:51:32 +0000 Subject: [PATCH 112/360] Update _session to session --- freqtrade/commands/db_commands.py | 2 +- freqtrade/persistence/models.py | 10 ++++++---- tests/persistence/test_migrations.py | 4 ++-- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/freqtrade/commands/db_commands.py b/freqtrade/commands/db_commands.py index c424016b1..b4997582d 100644 --- a/freqtrade/commands/db_commands.py +++ b/freqtrade/commands/db_commands.py @@ -20,7 +20,7 @@ def start_convert_db(args: Dict[str, Any]) -> None: config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) init_db(config['db_url']) - session_target = Trade._session + session_target = Trade.session init_db(config['db_url_from']) logger.info("Starting db migration.") diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 9f90f8a74..f4058b4eb 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -54,10 +54,12 @@ def init_db(db_url: str) -> None: # https://docs.sqlalchemy.org/en/13/orm/contextual.html#thread-local-scope # Scoped sessions proxy requests to the appropriate thread-local session. # We should use the scoped_session object - not a seperately initialized version - Trade._session = scoped_session(sessionmaker(bind=engine, autoflush=False)) - Trade.query = Trade._session.query_property() - Order.query = Trade._session.query_property() - PairLock.query = Trade._session.query_property() + Trade.session = scoped_session(sessionmaker(bind=engine, autoflush=False)) + Order.session = Trade.session + PairLock.session = Trade.session + Trade.query = Trade.session.query_property() + Order.query = Trade.session.query_property() + PairLock.query = Trade.session.query_property() previous_tables = inspect(engine).get_table_names() ModelBase.metadata.create_all(engine) diff --git a/tests/persistence/test_migrations.py b/tests/persistence/test_migrations.py index 2a6959d58..d49b7d207 100644 --- a/tests/persistence/test_migrations.py +++ b/tests/persistence/test_migrations.py @@ -22,7 +22,7 @@ def test_init_create_session(default_conf): # Check if init create a session init_db(default_conf['db_url']) assert hasattr(Trade, '_session') - assert 'scoped_session' in type(Trade._session).__name__ + assert 'scoped_session' in type(Trade.session).__name__ def test_init_custom_db_url(default_conf, tmpdir): @@ -34,7 +34,7 @@ def test_init_custom_db_url(default_conf, tmpdir): init_db(default_conf['db_url']) assert Path(filename).is_file() - r = Trade._session.execute(text("PRAGMA journal_mode")) + r = Trade.session.execute(text("PRAGMA journal_mode")) assert r.first() == ('wal',) From 608a7c2d384e588233d92e37c795e24c7689c0f8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 16 Feb 2023 09:02:57 +0000 Subject: [PATCH 113/360] Add safe_close_rate --- freqtrade/freqtradebot.py | 4 ++-- freqtrade/persistence/trade_model.py | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index adc630036..0bed27cb9 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1639,7 +1639,7 @@ class FreqtradeBot(LoggingMixin): profit = trade.calc_profit(rate=order_rate, amount=amount, open_rate=trade.open_rate) profit_ratio = trade.calc_profit_ratio(order_rate, amount, trade.open_rate) else: - order_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested + order_rate = trade.safe_close_rate profit = trade.calc_profit(rate=order_rate) + (0.0 if fill else trade.realized_profit) profit_ratio = trade.calc_profit_ratio(order_rate) amount = trade.amount @@ -1694,7 +1694,7 @@ class FreqtradeBot(LoggingMixin): raise DependencyException( f"Order_obj not found for {order_id}. This should not have happened.") - profit_rate: float = trade.close_rate if trade.close_rate else trade.close_rate_requested + profit_rate: float = trade.safe_close_rate profit_trade = trade.calc_profit(rate=profit_rate) current_rate = self.exchange.get_rate( trade.pair, side='exit', is_short=trade.is_short, refresh=False) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index be296c52c..091152196 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -1065,6 +1065,10 @@ class LocalTrade(): """ DEPRECATED! Please use exit_reason instead.""" return self.exit_reason + @property + def safe_close_rate(self) -> float: + return self.close_rate or self.close_rate_requested or 0.0 + @staticmethod def get_trades_proxy(*, pair: Optional[str] = None, is_open: Optional[bool] = None, open_date: Optional[datetime] = None, From 65a5cf64df91f51e5c82bf0fecc8821fa59cd717 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 16 Feb 2023 09:03:32 +0000 Subject: [PATCH 114/360] Re-type session --- freqtrade/persistence/trade_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 091152196..b3ccf7949 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -1175,7 +1175,7 @@ class Trade(ModelBase, LocalTrade): __tablename__ = 'trades' # TODO: Type query type throughout. query: Any - _session: Any = None + session: Any = None use_db: bool = True From 101d9ab87f6208375fc1e990dff38ed275e4cbce Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 19 Feb 2023 19:51:25 +0100 Subject: [PATCH 115/360] Improvements - tests runnable again --- freqtrade/persistence/pairlock.py | 4 ++-- freqtrade/persistence/trade_model.py | 17 ++++++++--------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/freqtrade/persistence/pairlock.py b/freqtrade/persistence/pairlock.py index 2c81beb85..f8c0586a4 100644 --- a/freqtrade/persistence/pairlock.py +++ b/freqtrade/persistence/pairlock.py @@ -1,5 +1,5 @@ from datetime import datetime, timezone -from typing import Any, Dict, Optional +from typing import Any, ClassVar, Dict, Optional from sqlalchemy import String, or_ from sqlalchemy.orm import Mapped, Query, mapped_column @@ -14,7 +14,7 @@ class PairLock(ModelBase): """ __tablename__ = 'pairlocks' # TODO: Properly type query. - query: Any + query: ClassVar[Any] id: Mapped[int] = mapped_column(primary_key=True) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index b3ccf7949..4b7a09cbe 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -5,10 +5,9 @@ import logging from collections import defaultdict from datetime import datetime, timedelta, timezone from math import isclose -from typing import Any, Dict, List, Optional +from typing import Any, ClassVar, Dict, List, Optional -from sqlalchemy import (Boolean, DateTime, Enum, Float, ForeignKey, Integer, String, - UniqueConstraint, desc, func) +from sqlalchemy import Enum, Float, ForeignKey, Integer, String, UniqueConstraint, desc, func from sqlalchemy.orm import Mapped, Query, lazyload, mapped_column, relationship from freqtrade.constants import (DATETIME_PRINT_FORMAT, MATH_CLOSE_PREC, NON_OPEN_EXCHANGE_STATES, @@ -37,7 +36,7 @@ class Order(ModelBase): """ __tablename__ = 'orders' # TODO: Properly type query. - query: Any + query: ClassVar[Any] # Uniqueness should be ensured over pair, order_id # its likely that order_id is unique per Pair on some exchanges. __table_args__ = (UniqueConstraint('ft_pair', 'order_id', name="_order_pair_order_id"),) @@ -1174,8 +1173,8 @@ class Trade(ModelBase, LocalTrade): """ __tablename__ = 'trades' # TODO: Type query type throughout. - query: Any - session: Any = None + query: ClassVar[Any] + session: ClassVar[Any] = None use_db: bool = True @@ -1197,11 +1196,11 @@ class Trade(ModelBase, LocalTrade): fee_close_cost: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) fee_close_currency: Mapped[Optional[str]] = mapped_column(String(25), nullable=True) open_rate: Mapped[float] = mapped_column(Float()) - open_rate_requested: float = mapped_column(Float()) + open_rate_requested: Mapped[float] = mapped_column(Float()) # open_trade_value - calculated via _calc_open_trade_value open_trade_value = mapped_column(Float()) - close_rate: Optional[float] = mapped_column(Float()) - close_rate_requested: Optional[float] = mapped_column(Float()) + close_rate: Mapped[Optional[float]] = mapped_column(Float()) + close_rate_requested: Mapped[Optional[float]] = mapped_column(Float()) realized_profit: Mapped[float] = mapped_column(Float(), default=0.0) close_profit: Mapped[Optional[float]] = mapped_column(Float()) close_profit_abs: Mapped[Optional[float]] = mapped_column(Float()) From 0691bbaad9aeda6fdcc1ec9a9c75edf358974931 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 19 Feb 2023 20:18:46 +0100 Subject: [PATCH 116/360] Update some db types --- freqtrade/persistence/trade_model.py | 2 +- tests/persistence/test_migrations.py | 2 +- tests/persistence/test_persistence.py | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 4b7a09cbe..abb989940 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -1196,7 +1196,7 @@ class Trade(ModelBase, LocalTrade): fee_close_cost: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) fee_close_currency: Mapped[Optional[str]] = mapped_column(String(25), nullable=True) open_rate: Mapped[float] = mapped_column(Float()) - open_rate_requested: Mapped[float] = mapped_column(Float()) + open_rate_requested: Mapped[float] = mapped_column(Float(), nullable=True) # open_trade_value - calculated via _calc_open_trade_value open_trade_value = mapped_column(Float()) close_rate: Mapped[Optional[float]] = mapped_column(Float()) diff --git a/tests/persistence/test_migrations.py b/tests/persistence/test_migrations.py index d49b7d207..5254164c1 100644 --- a/tests/persistence/test_migrations.py +++ b/tests/persistence/test_migrations.py @@ -21,7 +21,7 @@ spot, margin, futures = TradingMode.SPOT, TradingMode.MARGIN, TradingMode.FUTURE def test_init_create_session(default_conf): # Check if init create a session init_db(default_conf['db_url']) - assert hasattr(Trade, '_session') + assert hasattr(Trade, 'session') assert 'scoped_session' in type(Trade.session).__name__ diff --git a/tests/persistence/test_persistence.py b/tests/persistence/test_persistence.py index d06f05179..6d907ccf0 100644 --- a/tests/persistence/test_persistence.py +++ b/tests/persistence/test_persistence.py @@ -2440,6 +2440,7 @@ def test_select_filled_orders(fee): def test_order_to_ccxt(limit_buy_order_open): order = Order.parse_from_ccxt_object(limit_buy_order_open, 'mocked', 'buy') + order.ft_trade_id = 1 order.query.session.add(order) Order.query.session.commit() From f6b3998bbda8c8ac22e7c13f40b2dfe40217212e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 19 Feb 2023 20:23:04 +0100 Subject: [PATCH 117/360] Fix backtesting type incompatibilities --- freqtrade/freqtradebot.py | 3 ++- freqtrade/optimize/backtesting.py | 21 ++++++++++++--------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 0bed27cb9..ad0628c59 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1330,7 +1330,8 @@ class FreqtradeBot(LoggingMixin): # place new order only if new price is supplied self.execute_entry( pair=trade.pair, - stake_amount=(order_obj.remaining * order_obj.price / trade.leverage), + stake_amount=( + order_obj.safe_remaining * order_obj.safe_price / trade.leverage), price=adjusted_entry_price, trade=trade, is_short=trade.is_short, diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 065a88f40..7f3036037 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -563,7 +563,7 @@ class Backtesting: pos_trade = self._get_exit_for_signal(trade, row, exit_, amount) if pos_trade is not None: order = pos_trade.orders[-1] - if self._get_order_filled(order.price, row): + if self._get_order_filled(order.ft_price, row): order.close_bt_order(current_date, trade) trade.recalc_trade_from_orders() self.wallets.update() @@ -664,6 +664,7 @@ class Backtesting: side=trade.exit_side, order_type=order_type, status="open", + ft_price=close_rate, price=close_rate, average=close_rate, amount=amount, @@ -887,6 +888,7 @@ class Backtesting: order_date=current_time, order_filled_date=current_time, order_update_date=current_time, + ft_price=propose_rate, price=propose_rate, average=propose_rate, amount=amount, @@ -895,7 +897,7 @@ class Backtesting: cost=stake_amount + trade.fee_open, ) trade.orders.append(order) - if pos_adjust and self._get_order_filled(order.price, row): + if pos_adjust and self._get_order_filled(order.ft_price, row): order.close_bt_order(current_time, trade) else: trade.open_order_id = str(self.order_id_counter) @@ -1008,15 +1010,15 @@ class Backtesting: # only check on new candles for open entry orders if order.side == trade.entry_side and current_time > order.order_date_utc: requested_rate = strategy_safe_wrapper(self.strategy.adjust_entry_price, - default_retval=order.price)( + default_retval=order.ft_price)( trade=trade, # type: ignore[arg-type] order=order, pair=trade.pair, current_time=current_time, - proposed_rate=row[OPEN_IDX], current_order_rate=order.price, + proposed_rate=row[OPEN_IDX], current_order_rate=order.ft_price, entry_tag=trade.enter_tag, side=trade.trade_direction ) # default value is current order price # cancel existing order whenever a new rate is requested (or None) - if requested_rate == order.price: + if requested_rate == order.ft_price: # assumption: there can't be multiple open entry orders at any given time return False else: @@ -1028,7 +1030,8 @@ class Backtesting: if requested_rate: self._enter_trade(pair=trade.pair, row=row, trade=trade, requested_rate=requested_rate, - requested_stake=(order.remaining * order.price / trade.leverage), + requested_stake=( + order.safe_remaining * order.ft_price / trade.leverage), direction='short' if trade.is_short else 'long') self.replaced_entry_orders += 1 else: @@ -1095,7 +1098,7 @@ class Backtesting: for trade in list(LocalTrade.bt_trades_open_pp[pair]): # 3. Process entry orders. order = trade.select_order(trade.entry_side, is_open=True) - if order and self._get_order_filled(order.price, row): + if order and self._get_order_filled(order.ft_price, row): order.close_bt_order(current_time, trade) trade.open_order_id = None self.wallets.update() @@ -1106,7 +1109,7 @@ class Backtesting: # 5. Process exit orders. order = trade.select_order(trade.exit_side, is_open=True) - if order and self._get_order_filled(order.price, row): + if order and self._get_order_filled(order.ft_price, row): order.close_bt_order(current_time, trade) trade.open_order_id = None sub_trade = order.safe_amount_after_fee != trade.amount @@ -1115,7 +1118,7 @@ class Backtesting: trade.recalc_trade_from_orders() else: trade.close_date = current_time - trade.close(order.price, show_msg=False) + trade.close(order.ft_price, show_msg=False) # logger.debug(f"{pair} - Backtesting exit {trade}") LocalTrade.close_bt_trade(trade) From 8765e3a4d63a30e1711d6701553966d74d867694 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 20 Feb 2023 07:12:09 +0100 Subject: [PATCH 118/360] Fix some Type issues --- freqtrade/rpc/rpc.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 0c1a8fbbd..f8537b0f8 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -189,8 +189,8 @@ class RPC: else: # Closed trade ... current_rate = trade.close_rate - current_profit = trade.close_profit - current_profit_abs = trade.close_profit_abs + current_profit = trade.close_profit or 0.0 + current_profit_abs = trade.close_profit_abs or 0.0 total_profit_abs = trade.realized_profit + current_profit_abs # Calculate fiat profit @@ -449,11 +449,11 @@ class RPC: durations.append((trade.close_date - trade.open_date).total_seconds()) if not trade.is_open: - profit_ratio = trade.close_profit - profit_abs = trade.close_profit_abs + profit_ratio = trade.close_profit or 0.0 + profit_abs = trade.close_profit_abs or 0.0 profit_closed_coin.append(profit_abs) profit_closed_ratio.append(profit_ratio) - if trade.close_profit >= 0: + if profit_ratio >= 0: winning_trades += 1 winning_profit += profit_abs else: From c2c039151cbcdc176ecc506bf6ee6b8fc2d311f4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 20 Feb 2023 18:26:32 +0100 Subject: [PATCH 119/360] Improve typesafety around trade object --- freqtrade/freqtradebot.py | 9 +++++++-- freqtrade/rpc/rpc.py | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index ad0628c59..cec7176f6 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1314,7 +1314,7 @@ class FreqtradeBot(LoggingMixin): default_retval=order_obj.price)( trade=trade, order=order_obj, pair=trade.pair, current_time=datetime.now(timezone.utc), proposed_rate=proposed_rate, - current_order_rate=order_obj.price, entry_tag=trade.enter_tag, + current_order_rate=order_obj.safe_price, entry_tag=trade.enter_tag, side=trade.entry_side) replacing = True @@ -1345,6 +1345,8 @@ class FreqtradeBot(LoggingMixin): """ for trade in Trade.get_open_order_trades(): + if not trade.open_order_id: + continue try: order = self.exchange.fetch_order(trade.open_order_id, trade.pair) except (ExchangeError): @@ -1369,6 +1371,9 @@ class FreqtradeBot(LoggingMixin): """ was_trade_fully_canceled = False side = trade.entry_side.capitalize() + if not trade.open_order_id: + logger.warning(f"No open order for {trade}.") + return False # Cancelled orders may have the status of 'canceled' or 'closed' if order['status'] not in constants.NON_OPEN_EXCHANGE_STATES: @@ -1455,7 +1460,7 @@ class FreqtradeBot(LoggingMixin): return False try: - co = self.exchange.cancel_order_with_result(trade.open_order_id, trade.pair, + co = self.exchange.cancel_order_with_result(order['id'], trade.pair, trade.amount) except InvalidOrderException: logger.exception( diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index f8537b0f8..38f478f4c 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -410,7 +410,7 @@ class RPC: exit_reasons[trade.exit_reason][trade_win_loss(trade)] += 1 # Duration - dur: Dict[str, List[int]] = {'wins': [], 'draws': [], 'losses': []} + dur: Dict[str, List[float]] = {'wins': [], 'draws': [], 'losses': []} for trade in trades: if trade.close_date is not None and trade.open_date is not None: trade_dur = (trade.close_date - trade.open_date).total_seconds() From db4f4498dc6583e6a1eb9e3183f6b11c6232de96 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 20 Feb 2023 20:00:01 +0100 Subject: [PATCH 120/360] Experimentally type query property ... --- freqtrade/persistence/models.py | 3 ++- freqtrade/persistence/pairlock.py | 6 ++++-- freqtrade/persistence/trade_model.py | 12 +++++++----- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index f4058b4eb..b94be950a 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -6,7 +6,7 @@ from typing import Any, Dict from sqlalchemy import create_engine, inspect from sqlalchemy.exc import NoSuchModuleError -from sqlalchemy.orm import scoped_session, sessionmaker +from sqlalchemy.orm import Session, scoped_session, sessionmaker from sqlalchemy.pool import StaticPool from freqtrade.exceptions import OperationalException @@ -20,6 +20,7 @@ logger = logging.getLogger(__name__) _SQL_DOCS_URL = 'http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls' +SessionType = scoped_session[Session] def init_db(db_url: str) -> None: diff --git a/freqtrade/persistence/pairlock.py b/freqtrade/persistence/pairlock.py index f8c0586a4..b10d74693 100644 --- a/freqtrade/persistence/pairlock.py +++ b/freqtrade/persistence/pairlock.py @@ -3,9 +3,11 @@ from typing import Any, ClassVar, Dict, Optional from sqlalchemy import String, or_ from sqlalchemy.orm import Mapped, Query, mapped_column +from sqlalchemy.orm.scoping import _QueryDescriptorType from freqtrade.constants import DATETIME_PRINT_FORMAT from freqtrade.persistence.base import ModelBase +from freqtrade.persistence.models import SessionType class PairLock(ModelBase): @@ -13,8 +15,8 @@ class PairLock(ModelBase): Pair Locks database model. """ __tablename__ = 'pairlocks' - # TODO: Properly type query. - query: ClassVar[Any] + query: ClassVar[_QueryDescriptorType] + session: ClassVar[SessionType] id: Mapped[int] = mapped_column(primary_key=True) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index abb989940..10643db17 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -9,6 +9,7 @@ from typing import Any, ClassVar, Dict, List, Optional from sqlalchemy import Enum, Float, ForeignKey, Integer, String, UniqueConstraint, desc, func from sqlalchemy.orm import Mapped, Query, lazyload, mapped_column, relationship +from sqlalchemy.orm.scoping import _QueryDescriptorType from freqtrade.constants import (DATETIME_PRINT_FORMAT, MATH_CLOSE_PREC, NON_OPEN_EXCHANGE_STATES, BuySell, LongShort) @@ -17,6 +18,7 @@ from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.exchange import amount_to_contract_precision, price_to_precision from freqtrade.leverage import interest from freqtrade.persistence.base import ModelBase +from freqtrade.persistence.models import SessionType from freqtrade.util import FtPrecise @@ -35,8 +37,9 @@ class Order(ModelBase): Mirrors CCXT Order structure """ __tablename__ = 'orders' - # TODO: Properly type query. - query: ClassVar[Any] + query: ClassVar[_QueryDescriptorType] + session: ClassVar[SessionType] + # Uniqueness should be ensured over pair, order_id # its likely that order_id is unique per Pair on some exchanges. __table_args__ = (UniqueConstraint('ft_pair', 'order_id', name="_order_pair_order_id"),) @@ -1172,9 +1175,8 @@ class Trade(ModelBase, LocalTrade): Note: Fields must be aligned with LocalTrade class """ __tablename__ = 'trades' - # TODO: Type query type throughout. - query: ClassVar[Any] - session: ClassVar[Any] = None + query: ClassVar[_QueryDescriptorType] + session: ClassVar[SessionType] = None use_db: bool = True From b65cff0adcd6498a0dfe288a97c93488c3bb7fab Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 20 Feb 2023 20:22:41 +0100 Subject: [PATCH 121/360] Update "Query" type --- freqtrade/persistence/trade_model.py | 2 +- freqtrade/rpc/rpc.py | 9 +++++---- freqtrade/rpc/telegram.py | 10 +++++++--- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 10643db17..0f85528e0 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -1305,7 +1305,7 @@ class Trade(ModelBase, LocalTrade): ) @staticmethod - def get_trades(trade_filter=None, include_orders: bool = True) -> Query: + def get_trades(trade_filter=None, include_orders: bool = True) -> Query['Trade']: """ Helper function to query Trades using filters. NOTE: Not supported in Backtesting. diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 38f478f4c..8692c477f 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -373,13 +373,13 @@ class RPC: def _rpc_trade_history(self, limit: int, offset: int = 0, order_by_id: bool = False) -> Dict: """ Returns the X last trades """ - order_by = Trade.id if order_by_id else Trade.close_date.desc() + order_by: Any = Trade.id if order_by_id else Trade.close_date.desc() if limit: trades = Trade.get_trades([Trade.is_open.is_(False)]).order_by( order_by).limit(limit).offset(offset) else: trades = Trade.get_trades([Trade.is_open.is_(False)]).order_by( - Trade.close_date.desc()).all() + Trade.close_date.desc()) output = [trade.to_json() for trade in trades] @@ -401,7 +401,7 @@ class RPC: return 'losses' else: return 'draws' - trades: List[Trade] = Trade.get_trades([Trade.is_open.is_(False)], include_orders=False) + trades = Trade.get_trades([Trade.is_open.is_(False)], include_orders=False) # Sell reason exit_reasons = {} for trade in trades: @@ -785,7 +785,8 @@ class RPC: # check if valid pair # check if pair already has an open pair - trade: Trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first() + trade: Optional[Trade] = Trade.get_trades( + [Trade.is_open.is_(True), Trade.pair == pair]).first() is_short = (order_side == SignalDirection.SHORT) if trade: is_short = trade.is_short diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 7bbeea2a2..dc92478ab 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -1055,10 +1055,14 @@ class Telegram(RPCHandler): query.answer() query.edit_message_text(text="Force exit canceled.") return - trade: Trade = Trade.get_trades(trade_filter=Trade.id == trade_id).first() + trade: Optional[Trade] = Trade.get_trades(trade_filter=Trade.id == trade_id).first() query.answer() - query.edit_message_text(text=f"Manually exiting Trade #{trade_id}, {trade.pair}") - self._force_exit_action(trade_id) + if trade: + query.edit_message_text( + text=f"Manually exiting Trade #{trade_id}, {trade.pair}") + self._force_exit_action(trade_id) + else: + query.edit_message_text(text=f"Trade {trade_id} not found.") def _force_enter_action(self, pair, price: Optional[float], order_side: SignalDirection): if pair != 'cancel': From 764001a4c290ba9bca30a224171f4f88ef20001f Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 21 Feb 2023 06:49:15 +0100 Subject: [PATCH 122/360] Don't reuse variable --- freqtrade/data/btanalysis.py | 2 +- freqtrade/persistence/pairlock_middleware.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index c682436c7..9772506a7 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -346,7 +346,7 @@ def evaluate_result_multi(results: pd.DataFrame, timeframe: str, return df_final[df_final['open_trades'] > max_open_trades] -def trade_list_to_dataframe(trades: List[LocalTrade]) -> pd.DataFrame: +def trade_list_to_dataframe(trades: Union[List[Trade], List[LocalTrade]]) -> pd.DataFrame: """ Convert list of Trade objects to pandas Dataframe :param trades: List of trade objects diff --git a/freqtrade/persistence/pairlock_middleware.py b/freqtrade/persistence/pairlock_middleware.py index 4485bb88e..5ed131a9b 100644 --- a/freqtrade/persistence/pairlock_middleware.py +++ b/freqtrade/persistence/pairlock_middleware.py @@ -133,8 +133,8 @@ class PairLocks(): PairLock.query.session.commit() else: # used in backtesting mode; don't show log messages for speed - locks = PairLocks.get_pair_locks(None) - for lock in locks: + locksb = PairLocks.get_pair_locks(None) + for lock in locksb: if lock.reason == reason: lock.active = False From f2f4158974b294339ba3c22352b44c4dd9f31c5d Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 21 Feb 2023 06:58:21 +0100 Subject: [PATCH 123/360] Bump sqlalchemy to 2.0.4 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 3e6c66757..6b1c888b8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ pandas-ta==0.3.14b ccxt==2.8.54 cryptography==39.0.1 aiohttp==3.8.4 -SQLAlchemy==2.0.3 +SQLAlchemy==2.0.4 python-telegram-bot==13.15 arrow==1.2.3 cachetools==4.2.2 From d175ab495b39a9a249274b97ea52292840592283 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 28 Feb 2023 07:03:22 +0100 Subject: [PATCH 124/360] Move SessionType to base module --- freqtrade/persistence/base.py | 4 +++- freqtrade/persistence/models.py | 5 ++--- freqtrade/persistence/pairlock.py | 3 +-- freqtrade/persistence/trade_model.py | 3 +-- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/freqtrade/persistence/base.py b/freqtrade/persistence/base.py index 2fed715d7..98e483c90 100644 --- a/freqtrade/persistence/base.py +++ b/freqtrade/persistence/base.py @@ -1,6 +1,8 @@ -from sqlalchemy.orm import DeclarativeBase +from sqlalchemy.orm import DeclarativeBase, Session, scoped_session +SessionType = scoped_session[Session] + class ModelBase(DeclarativeBase): pass diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index b94be950a..98d1d7a8a 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -6,11 +6,11 @@ from typing import Any, Dict from sqlalchemy import create_engine, inspect from sqlalchemy.exc import NoSuchModuleError -from sqlalchemy.orm import Session, scoped_session, sessionmaker +from sqlalchemy.orm import scoped_session, sessionmaker from sqlalchemy.pool import StaticPool from freqtrade.exceptions import OperationalException -from freqtrade.persistence.base import ModelBase +from freqtrade.persistence.base import ModelBase, SessionType from freqtrade.persistence.migrations import check_migrate from freqtrade.persistence.pairlock import PairLock from freqtrade.persistence.trade_model import Order, Trade @@ -20,7 +20,6 @@ logger = logging.getLogger(__name__) _SQL_DOCS_URL = 'http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls' -SessionType = scoped_session[Session] def init_db(db_url: str) -> None: diff --git a/freqtrade/persistence/pairlock.py b/freqtrade/persistence/pairlock.py index b10d74693..d5a8d7ae1 100644 --- a/freqtrade/persistence/pairlock.py +++ b/freqtrade/persistence/pairlock.py @@ -6,8 +6,7 @@ from sqlalchemy.orm import Mapped, Query, mapped_column from sqlalchemy.orm.scoping import _QueryDescriptorType from freqtrade.constants import DATETIME_PRINT_FORMAT -from freqtrade.persistence.base import ModelBase -from freqtrade.persistence.models import SessionType +from freqtrade.persistence.base import ModelBase, SessionType class PairLock(ModelBase): diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 0f85528e0..ce7d56745 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -17,8 +17,7 @@ from freqtrade.enums import ExitType, TradingMode from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.exchange import amount_to_contract_precision, price_to_precision from freqtrade.leverage import interest -from freqtrade.persistence.base import ModelBase -from freqtrade.persistence.models import SessionType +from freqtrade.persistence.base import ModelBase, SessionType from freqtrade.util import FtPrecise From 0f914cf2bd54902ceb96c29499eca888b4422b03 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 28 Feb 2023 07:24:20 +0100 Subject: [PATCH 125/360] Use Mapped for LocalTrade this won't initialize sqlalchemy, as the base class is not inheriting from sqlalchemy. --- freqtrade/persistence/trade_model.py | 108 +++++++++++++-------------- 1 file changed, 54 insertions(+), 54 deletions(-) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index ce7d56745..93701b1ae 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -288,76 +288,76 @@ class LocalTrade(): bt_trades_open_pp: Dict[str, List['LocalTrade']] = defaultdict(list) bt_open_open_trade_count: int = 0 total_profit: float = 0 - realized_profit: float = 0 + realized_profit: Mapped[float] = 0 # type: ignore - id: int = 0 + id: Mapped[int] = 0 # type: ignore - orders: List[Order] = [] + orders: Mapped[List[Order]] = [] # type: ignore - exchange: str = '' - pair: str = '' - base_currency: str = '' - stake_currency: str = '' - is_open: bool = True - fee_open: float = 0.0 - fee_open_cost: Optional[float] = None - fee_open_currency: str = '' - fee_close: float = 0.0 - fee_close_cost: Optional[float] = None - fee_close_currency: str = '' - open_rate: float = 0.0 - open_rate_requested: Optional[float] = None + exchange: Mapped[str] = '' # type: ignore + pair: Mapped[str] = '' # type: ignore + base_currency: Mapped[Optional[str]] = '' # type: ignore + stake_currency: Mapped[Optional[str]] = '' # type: ignore + is_open: Mapped[bool] = True # type: ignore + fee_open: Mapped[float] = 0.0 # type: ignore + fee_open_cost: Mapped[Optional[float]] = None # type: ignore + fee_open_currency: Mapped[Optional[str]] = '' # type: ignore + fee_close: Mapped[Optional[float]] = 0.0 # type: ignore + fee_close_cost: Mapped[Optional[float]] = None # type: ignore + fee_close_currency: Mapped[Optional[str]] = '' # type: ignore + open_rate: Mapped[float] = 0.0 # type: ignore + open_rate_requested: Mapped[Optional[float]] = None # type: ignore # open_trade_value - calculated via _calc_open_trade_value - open_trade_value: float = 0.0 - close_rate: Optional[float] = None - close_rate_requested: Optional[float] = None - close_profit: Optional[float] = None - close_profit_abs: Optional[float] = None - stake_amount: float = 0.0 - max_stake_amount: float = 0.0 - amount: float = 0.0 - amount_requested: Optional[float] = None - open_date: datetime - close_date: Optional[datetime] = None - open_order_id: Optional[str] = None + open_trade_value: Mapped[float] = 0.0 # type: ignore + close_rate: Mapped[Optional[float]] = None # type: ignore + close_rate_requested: Mapped[Optional[float]] = None # type: ignore + close_profit: Mapped[Optional[float]] = None # type: ignore + close_profit_abs: Mapped[Optional[float]] = None # type: ignore + stake_amount: Mapped[float] = 0.0 # type: ignore + max_stake_amount: Mapped[Optional[float]] = 0.0 # type: ignore + amount: Mapped[float] = 0.0 # type: ignore + amount_requested: Mapped[Optional[float]] = None # type: ignore + open_date: Mapped[datetime] + close_date: Mapped[Optional[datetime]] = None # type: ignore + open_order_id: Mapped[Optional[str]] = None # type: ignore # absolute value of the stop loss - stop_loss: float = 0.0 + stop_loss: Mapped[float] = 0.0 # type: ignore # percentage value of the stop loss - stop_loss_pct: float = 0.0 + stop_loss_pct: Mapped[Optional[float]] = 0.0 # type: ignore # absolute value of the initial stop loss - initial_stop_loss: float = 0.0 + initial_stop_loss: Mapped[Optional[float]] = 0.0 # type: ignore # percentage value of the initial stop loss - initial_stop_loss_pct: Optional[float] = None + initial_stop_loss_pct: Mapped[Optional[float]] = None # type: ignore # stoploss order id which is on exchange - stoploss_order_id: Optional[str] = None + stoploss_order_id: Mapped[Optional[str]] = None # type: ignore # last update time of the stoploss order on exchange - stoploss_last_update: Optional[datetime] = None + stoploss_last_update: Mapped[Optional[datetime]] = None # type: ignore # absolute value of the highest reached price - max_rate: float = 0.0 + max_rate: Mapped[Optional[float]] = None # type: ignore # Lowest price reached - min_rate: float = 0.0 - exit_reason: str = '' - exit_order_status: str = '' - strategy: str = '' - enter_tag: Optional[str] = None - timeframe: Optional[int] = None + min_rate: Mapped[Optional[float]] = None # type: ignore + exit_reason: Mapped[Optional[str]] = '' # type: ignore + exit_order_status: Mapped[Optional[str]] = '' # type: ignore + strategy: Mapped[Optional[str]] = '' # type: ignore + enter_tag: Mapped[Optional[str]] = None # type: ignore + timeframe: Mapped[Optional[int]] = None # type: ignore - trading_mode: TradingMode = TradingMode.SPOT - amount_precision: Optional[float] = None - price_precision: Optional[float] = None - precision_mode: Optional[int] = None - contract_size: Optional[float] = None + trading_mode: Mapped[TradingMode] = TradingMode.SPOT # type: ignore + amount_precision: Mapped[Optional[float]] = None # type: ignore + price_precision: Mapped[Optional[float]] = None # type: ignore + precision_mode: Mapped[Optional[int]] = None # type: ignore + contract_size: Mapped[Optional[float]] = None # type: ignore # Leverage trading properties - liquidation_price: Optional[float] = None - is_short: bool = False - leverage: float = 1.0 + liquidation_price: Mapped[Optional[float]] = None # type: ignore + is_short: Mapped[bool] = False # type: ignore + leverage: Mapped[float] = 1.0 # type: ignore # Margin trading properties - interest_rate: float = 0.0 + interest_rate: Mapped[float] = 0.0 # type: ignore # Futures properties - funding_fees: Optional[float] = None + funding_fees: Mapped[Optional[float]] = None # type: ignore @property def stoploss_or_liquidation(self) -> float: @@ -1175,7 +1175,7 @@ class Trade(ModelBase, LocalTrade): """ __tablename__ = 'trades' query: ClassVar[_QueryDescriptorType] - session: ClassVar[SessionType] = None + session: ClassVar[SessionType] use_db: bool = True @@ -1197,7 +1197,7 @@ class Trade(ModelBase, LocalTrade): fee_close_cost: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) fee_close_currency: Mapped[Optional[str]] = mapped_column(String(25), nullable=True) open_rate: Mapped[float] = mapped_column(Float()) - open_rate_requested: Mapped[float] = mapped_column(Float(), nullable=True) + open_rate_requested: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) # open_trade_value - calculated via _calc_open_trade_value open_trade_value = mapped_column(Float()) close_rate: Mapped[Optional[float]] = mapped_column(Float()) @@ -1246,7 +1246,7 @@ class Trade(ModelBase, LocalTrade): liquidation_price: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) # Margin Trading Properties - interest_rate: Mapped[Optional[float]] = mapped_column(Float(), nullable=False, default=0.0) + interest_rate: Mapped[float] = mapped_column(Float(), nullable=False, default=0.0) # Futures properties funding_fees: Mapped[Optional[float]] = mapped_column(Float(), nullable=True, default=None) From 7c09c0178815a725a3ac0df5509e7a0d5100a475 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 28 Feb 2023 07:27:01 +0100 Subject: [PATCH 126/360] Add some more typehints --- freqtrade/persistence/trade_model.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 93701b1ae..887784be3 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -154,7 +154,7 @@ class Order(ModelBase): self.order_update_date = datetime.now(timezone.utc) def to_ccxt_object(self) -> Dict[str, Any]: - order = { + order: Dict[str, Any] = { 'id': self.order_id, 'symbol': self.ft_pair, 'price': self.price, @@ -1062,7 +1062,7 @@ class LocalTrade(): return len(self.select_filled_orders('sell')) @property - def sell_reason(self) -> str: + def sell_reason(self) -> Optional[str]: """ DEPRECATED! Please use exit_reason instead.""" return self.exit_reason @@ -1276,7 +1276,7 @@ class Trade(ModelBase, LocalTrade): def get_trades_proxy(*, pair: Optional[str] = None, is_open: Optional[bool] = None, open_date: Optional[datetime] = None, close_date: Optional[datetime] = None, - ) -> List['LocalTrade']: + ) -> List['LocalTrade', 'Trade']: """ Helper function to query Trades.j Returns a List of trades, filtered on the parameters given. From b5f55c9b145129567d5d642efb7229fbf36c4ff9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 1 Mar 2023 06:40:07 +0100 Subject: [PATCH 127/360] Improve type safety in backtesting --- freqtrade/optimize/backtesting.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 7f3036037..1f868f7bf 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -440,7 +440,8 @@ class Backtesting: side_1 * abs(self.strategy.trailing_stop_positive / leverage))) else: # Worst case: price ticks tiny bit above open and dives down. - stop_rate = row[OPEN_IDX] * (1 - side_1 * abs(trade.stop_loss_pct / leverage)) + stop_rate = row[OPEN_IDX] * (1 - side_1 * abs( + (trade.stop_loss_pct or 0.0) / leverage)) if is_short: assert stop_rate > row[LOW_IDX] else: @@ -472,7 +473,7 @@ class Backtesting: # - (Expected abs profit - open_rate - open_fee) / (fee_close -1) roi_rate = trade.open_rate * roi / leverage open_fee_rate = side_1 * trade.open_rate * (1 + side_1 * trade.fee_open) - close_rate = -(roi_rate + open_fee_rate) / (trade.fee_close - side_1 * 1) + close_rate = -(roi_rate + open_fee_rate) / ((trade.fee_close or 0.0) - side_1 * 1) if is_short: is_new_roi = row[OPEN_IDX] < close_rate else: From e5c9cde36f713c50a7085c7ad4c9377a97bd34b8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 1 Mar 2023 07:23:26 +0100 Subject: [PATCH 128/360] Update trades_proxy typing --- freqtrade/persistence/trade_model.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 887784be3..531e86282 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -5,7 +5,7 @@ import logging from collections import defaultdict from datetime import datetime, timedelta, timezone from math import isclose -from typing import Any, ClassVar, Dict, List, Optional +from typing import Any, ClassVar, Dict, List, Optional, cast from sqlalchemy import Enum, Float, ForeignKey, Integer, String, UniqueConstraint, desc, func from sqlalchemy.orm import Mapped, Query, lazyload, mapped_column, relationship @@ -1131,7 +1131,7 @@ class LocalTrade(): @staticmethod def get_open_trades() -> List[Any]: """ - Query trades from persistence layer + Retrieve open trades """ return Trade.get_trades_proxy(is_open=True) @@ -1276,7 +1276,7 @@ class Trade(ModelBase, LocalTrade): def get_trades_proxy(*, pair: Optional[str] = None, is_open: Optional[bool] = None, open_date: Optional[datetime] = None, close_date: Optional[datetime] = None, - ) -> List['LocalTrade', 'Trade']: + ) -> List['LocalTrade']: """ Helper function to query Trades.j Returns a List of trades, filtered on the parameters given. @@ -1295,7 +1295,7 @@ class Trade(ModelBase, LocalTrade): trade_filter.append(Trade.close_date > close_date) if is_open is not None: trade_filter.append(Trade.is_open.is_(is_open)) - return Trade.get_trades(trade_filter).all() + return cast(List[LocalTrade], Trade.get_trades(trade_filter).all()) else: return LocalTrade.get_trades_proxy( pair=pair, is_open=is_open, From a1166b1077ca96db704d97e5d3775d960ee67ba9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 1 Mar 2023 07:25:33 +0100 Subject: [PATCH 129/360] allow null fee on calc_base_close --- freqtrade/persistence/trade_model.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 531e86282..7eabfab0f 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -795,10 +795,10 @@ class LocalTrade(): return interest(exchange_name=self.exchange, borrowed=borrowed, rate=rate, hours=hours) - def _calc_base_close(self, amount: FtPrecise, rate: float, fee: float) -> FtPrecise: + def _calc_base_close(self, amount: FtPrecise, rate: float, fee: Optional[float]) -> FtPrecise: close_trade = amount * FtPrecise(rate) - fees = close_trade * FtPrecise(fee) + fees = close_trade * FtPrecise(fee or 0.0) if self.is_short: return close_trade + fees From 4a35d32b6ae84b2c0e372a7e00df283b7df76078 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 1 Mar 2023 07:26:35 +0100 Subject: [PATCH 130/360] Improve trade stop types --- freqtrade/persistence/trade_model.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 7eabfab0f..d747971fb 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -595,7 +595,7 @@ class LocalTrade(): self.stop_loss_pct = -1 * abs(percent) - def adjust_stop_loss(self, current_price: float, stoploss: float, + def adjust_stop_loss(self, current_price: float, stoploss: Optional[float], initial: bool = False, refresh: bool = False) -> None: """ This adjusts the stop loss to it's most recently observed setting @@ -604,7 +604,7 @@ class LocalTrade(): :param initial: Called to initiate stop_loss. Skips everything if self.stop_loss is already set. """ - if initial and not (self.stop_loss is None or self.stop_loss == 0): + if stoploss is None or (initial and not (self.stop_loss is None or self.stop_loss == 0)): # Don't modify if called with initial and nothing to do return refresh = True if refresh and self.nr_of_successful_entries == 1 else False From 874413ccc59ebe0ab7346de74ea3ff06c82b513c Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 1 Mar 2023 11:16:56 +0000 Subject: [PATCH 131/360] Fix some style violations --- freqtrade/persistence/base.py | 1 + freqtrade/persistence/models.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/persistence/base.py b/freqtrade/persistence/base.py index 98e483c90..fc2dac75e 100644 --- a/freqtrade/persistence/base.py +++ b/freqtrade/persistence/base.py @@ -4,5 +4,6 @@ from sqlalchemy.orm import DeclarativeBase, Session, scoped_session SessionType = scoped_session[Session] + class ModelBase(DeclarativeBase): pass diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 98d1d7a8a..f4058b4eb 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -10,7 +10,7 @@ from sqlalchemy.orm import scoped_session, sessionmaker from sqlalchemy.pool import StaticPool from freqtrade.exceptions import OperationalException -from freqtrade.persistence.base import ModelBase, SessionType +from freqtrade.persistence.base import ModelBase from freqtrade.persistence.migrations import check_migrate from freqtrade.persistence.pairlock import PairLock from freqtrade.persistence.trade_model import Order, Trade From 388dfec50bbf3ff4884dae25b4edf213a203edf8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 1 Mar 2023 18:17:37 +0100 Subject: [PATCH 132/360] Remove last type error --- freqtrade/persistence/trade_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index d747971fb..ab7d56766 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -643,7 +643,7 @@ class LocalTrade(): f"initial_stop_loss={self.initial_stop_loss:.8f}, " f"stop_loss={self.stop_loss:.8f}. " f"Trailing stoploss saved us: " - f"{float(self.stop_loss) - float(self.initial_stop_loss):.8f}.") + f"{float(self.stop_loss) - float(self.initial_stop_loss or 0.0):.8f}.") def update_trade(self, order: Order) -> None: """ From f0f72fdd33db951f6bbc24a8e93a6f0acdadafb3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 1 Mar 2023 19:48:15 +0100 Subject: [PATCH 133/360] Don't define "mapped" on LocalTrade class --- freqtrade/persistence/trade_model.py | 220 ++++++++++++++------------- 1 file changed, 118 insertions(+), 102 deletions(-) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index ab7d56766..62e2e2a7f 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -288,76 +288,76 @@ class LocalTrade(): bt_trades_open_pp: Dict[str, List['LocalTrade']] = defaultdict(list) bt_open_open_trade_count: int = 0 total_profit: float = 0 - realized_profit: Mapped[float] = 0 # type: ignore + realized_profit: float = 0 - id: Mapped[int] = 0 # type: ignore + id: int = 0 - orders: Mapped[List[Order]] = [] # type: ignore + orders: List[Order] = [] - exchange: Mapped[str] = '' # type: ignore - pair: Mapped[str] = '' # type: ignore - base_currency: Mapped[Optional[str]] = '' # type: ignore - stake_currency: Mapped[Optional[str]] = '' # type: ignore - is_open: Mapped[bool] = True # type: ignore - fee_open: Mapped[float] = 0.0 # type: ignore - fee_open_cost: Mapped[Optional[float]] = None # type: ignore - fee_open_currency: Mapped[Optional[str]] = '' # type: ignore - fee_close: Mapped[Optional[float]] = 0.0 # type: ignore - fee_close_cost: Mapped[Optional[float]] = None # type: ignore - fee_close_currency: Mapped[Optional[str]] = '' # type: ignore - open_rate: Mapped[float] = 0.0 # type: ignore - open_rate_requested: Mapped[Optional[float]] = None # type: ignore + exchange: str = '' + pair: str = '' + base_currency: Optional[str] = '' + stake_currency: Optional[str] = '' + is_open: bool = True + fee_open: float = 0.0 + fee_open_cost: Optional[float] = None + fee_open_currency: Optional[str] = '' + fee_close: Optional[float] = 0.0 + fee_close_cost: Optional[float] = None + fee_close_currency: Optional[str] = '' + open_rate: float = 0.0 + open_rate_requested: Optional[float] = None # open_trade_value - calculated via _calc_open_trade_value - open_trade_value: Mapped[float] = 0.0 # type: ignore - close_rate: Mapped[Optional[float]] = None # type: ignore - close_rate_requested: Mapped[Optional[float]] = None # type: ignore - close_profit: Mapped[Optional[float]] = None # type: ignore - close_profit_abs: Mapped[Optional[float]] = None # type: ignore - stake_amount: Mapped[float] = 0.0 # type: ignore - max_stake_amount: Mapped[Optional[float]] = 0.0 # type: ignore - amount: Mapped[float] = 0.0 # type: ignore - amount_requested: Mapped[Optional[float]] = None # type: ignore - open_date: Mapped[datetime] - close_date: Mapped[Optional[datetime]] = None # type: ignore - open_order_id: Mapped[Optional[str]] = None # type: ignore + open_trade_value: float = 0.0 + close_rate: Optional[float] = None + close_rate_requested: Optional[float] = None + close_profit: Optional[float] = None + close_profit_abs: Optional[float] = None + stake_amount: float = 0.0 + max_stake_amount: Optional[float] = 0.0 + amount: float = 0.0 + amount_requested: Optional[float] = None + open_date: datetime + close_date: Optional[datetime] = None + open_order_id: Optional[str] = None # absolute value of the stop loss - stop_loss: Mapped[float] = 0.0 # type: ignore + stop_loss: float = 0.0 # percentage value of the stop loss - stop_loss_pct: Mapped[Optional[float]] = 0.0 # type: ignore + stop_loss_pct: Optional[float] = 0.0 # absolute value of the initial stop loss - initial_stop_loss: Mapped[Optional[float]] = 0.0 # type: ignore + initial_stop_loss: Optional[float] = 0.0 # percentage value of the initial stop loss - initial_stop_loss_pct: Mapped[Optional[float]] = None # type: ignore + initial_stop_loss_pct: Optional[float] = None # stoploss order id which is on exchange - stoploss_order_id: Mapped[Optional[str]] = None # type: ignore + stoploss_order_id: Optional[str] = None # last update time of the stoploss order on exchange - stoploss_last_update: Mapped[Optional[datetime]] = None # type: ignore + stoploss_last_update: Optional[datetime] = None # absolute value of the highest reached price - max_rate: Mapped[Optional[float]] = None # type: ignore + max_rate: Optional[float] = None # Lowest price reached - min_rate: Mapped[Optional[float]] = None # type: ignore - exit_reason: Mapped[Optional[str]] = '' # type: ignore - exit_order_status: Mapped[Optional[str]] = '' # type: ignore - strategy: Mapped[Optional[str]] = '' # type: ignore - enter_tag: Mapped[Optional[str]] = None # type: ignore - timeframe: Mapped[Optional[int]] = None # type: ignore + min_rate: Optional[float] = None + exit_reason: Optional[str] = '' + exit_order_status: Optional[str] = '' + strategy: Optional[str] = '' + enter_tag: Optional[str] = None + timeframe: Optional[int] = None - trading_mode: Mapped[TradingMode] = TradingMode.SPOT # type: ignore - amount_precision: Mapped[Optional[float]] = None # type: ignore - price_precision: Mapped[Optional[float]] = None # type: ignore - precision_mode: Mapped[Optional[int]] = None # type: ignore - contract_size: Mapped[Optional[float]] = None # type: ignore + trading_mode: TradingMode = TradingMode.SPOT + amount_precision: Optional[float] = None + price_precision: Optional[float] = None + precision_mode: Optional[int] = None + contract_size: Optional[float] = None # Leverage trading properties - liquidation_price: Mapped[Optional[float]] = None # type: ignore - is_short: Mapped[bool] = False # type: ignore - leverage: Mapped[float] = 1.0 # type: ignore + liquidation_price: Optional[float] = None + is_short: bool = False + leverage: float = 1.0 # Margin trading properties - interest_rate: Mapped[float] = 0.0 # type: ignore + interest_rate: float = 0.0 # Futures properties - funding_fees: Mapped[Optional[float]] = None # type: ignore + funding_fees: Optional[float] = None @property def stoploss_or_liquidation(self) -> float: @@ -1179,77 +1179,93 @@ class Trade(ModelBase, LocalTrade): use_db: bool = True - id: Mapped[int] = mapped_column(Integer, primary_key=True) + id: Mapped[int] = mapped_column(Integer, primary_key=True) # type: ignore orders: Mapped[List[Order]] = relationship( "Order", order_by="Order.id", cascade="all, delete-orphan", lazy="selectin", - innerjoin=True) + innerjoin=True) # type: ignore - exchange: Mapped[str] = mapped_column(String(25), nullable=False) - pair: Mapped[str] = mapped_column(String(25), nullable=False, index=True) - base_currency: Mapped[Optional[str]] = mapped_column(String(25), nullable=True) - stake_currency: Mapped[Optional[str]] = mapped_column(String(25), nullable=True) - is_open: Mapped[bool] = mapped_column(nullable=False, default=True, index=True) - fee_open: Mapped[float] = mapped_column(Float(), nullable=False, default=0.0) - fee_open_cost: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) - fee_open_currency: Mapped[Optional[str]] = mapped_column(String(25), nullable=True) - fee_close: Mapped[Optional[float]] = mapped_column(Float(), nullable=False, default=0.0) - fee_close_cost: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) - fee_close_currency: Mapped[Optional[str]] = mapped_column(String(25), nullable=True) - open_rate: Mapped[float] = mapped_column(Float()) - open_rate_requested: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) + exchange: Mapped[str] = mapped_column(String(25), nullable=False) # type: ignore + pair: Mapped[str] = mapped_column(String(25), nullable=False, index=True) # type: ignore + base_currency: Mapped[Optional[str]] = mapped_column(String(25), nullable=True) # type: ignore + stake_currency: Mapped[Optional[str]] = mapped_column(String(25), nullable=True) # type: ignore + is_open: Mapped[bool] = mapped_column(nullable=False, default=True, index=True) # type: ignore + fee_open: Mapped[float] = mapped_column(Float(), nullable=False, default=0.0) # type: ignore + fee_open_cost: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) # type: ignore + fee_open_currency: Mapped[Optional[str]] = mapped_column( + String(25), nullable=True) # type: ignore + fee_close: Mapped[Optional[float]] = mapped_column( + Float(), nullable=False, default=0.0) # type: ignore + fee_close_cost: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) # type: ignore + fee_close_currency: Mapped[Optional[str]] = mapped_column( + String(25), nullable=True) # type: ignore + open_rate: Mapped[float] = mapped_column(Float()) # type: ignore + open_rate_requested: Mapped[Optional[float]] = mapped_column( + Float(), nullable=True) # type: ignore # open_trade_value - calculated via _calc_open_trade_value - open_trade_value = mapped_column(Float()) - close_rate: Mapped[Optional[float]] = mapped_column(Float()) - close_rate_requested: Mapped[Optional[float]] = mapped_column(Float()) - realized_profit: Mapped[float] = mapped_column(Float(), default=0.0) - close_profit: Mapped[Optional[float]] = mapped_column(Float()) - close_profit_abs: Mapped[Optional[float]] = mapped_column(Float()) - stake_amount: Mapped[float] = mapped_column(Float(), nullable=False) - max_stake_amount: Mapped[Optional[float]] = mapped_column(Float()) - amount: Mapped[float] = mapped_column(Float()) - amount_requested: Mapped[Optional[float]] = mapped_column(Float()) - open_date: Mapped[datetime] = mapped_column(nullable=False, default=datetime.utcnow) - close_date: Mapped[Optional[datetime]] = mapped_column() - open_order_id: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) + open_trade_value: Mapped[float] = mapped_column(Float(), nullable=True) # type: ignore + close_rate: Mapped[Optional[float]] = mapped_column(Float()) # type: ignore + close_rate_requested: Mapped[Optional[float]] = mapped_column(Float()) # type: ignore + realized_profit: Mapped[float] = mapped_column( + Float(), default=0.0, nullable=True) # type: ignore + close_profit: Mapped[Optional[float]] = mapped_column(Float()) # type: ignore + close_profit_abs: Mapped[Optional[float]] = mapped_column(Float()) # type: ignore + stake_amount: Mapped[float] = mapped_column(Float(), nullable=False) # type: ignore + max_stake_amount: Mapped[Optional[float]] = mapped_column(Float()) # type: ignore + amount: Mapped[float] = mapped_column(Float()) # type: ignore + amount_requested: Mapped[Optional[float]] = mapped_column(Float()) # type: ignore + open_date: Mapped[datetime] = mapped_column( + nullable=False, default=datetime.utcnow) # type: ignore + close_date: Mapped[Optional[datetime]] = mapped_column() # type: ignore + open_order_id: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) # type: ignore # absolute value of the stop loss - stop_loss: Mapped[float] = mapped_column(Float(), nullable=True, default=0.0) + stop_loss: Mapped[float] = mapped_column(Float(), nullable=True, default=0.0) # type: ignore # percentage value of the stop loss - stop_loss_pct: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) + stop_loss_pct: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) # type: ignore # absolute value of the initial stop loss - initial_stop_loss: Mapped[Optional[float]] = mapped_column(Float(), nullable=True, default=0.0) + initial_stop_loss: Mapped[Optional[float]] = mapped_column( + Float(), nullable=True, default=0.0) # type: ignore # percentage value of the initial stop loss - initial_stop_loss_pct: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) + initial_stop_loss_pct: Mapped[Optional[float]] = mapped_column( + Float(), nullable=True) # type: ignore # stoploss order id which is on exchange - stoploss_order_id: Mapped[Optional[str]] = mapped_column(String(255), nullable=True, index=True) + stoploss_order_id: Mapped[Optional[str]] = mapped_column( + String(255), nullable=True, index=True) # type: ignore # last update time of the stoploss order on exchange - stoploss_last_update: Mapped[Optional[datetime]] = mapped_column(nullable=True) + stoploss_last_update: Mapped[Optional[datetime]] = mapped_column(nullable=True) # type: ignore # absolute value of the highest reached price - max_rate: Mapped[Optional[float]] = mapped_column(Float(), nullable=True, default=0.0) + max_rate: Mapped[Optional[float]] = mapped_column( + Float(), nullable=True, default=0.0) # type: ignore # Lowest price reached - min_rate: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) - exit_reason: Mapped[Optional[str]] = mapped_column(String(100), nullable=True) - exit_order_status: Mapped[Optional[str]] = mapped_column(String(100), nullable=True) - strategy: Mapped[Optional[str]] = mapped_column(String(100), nullable=True) - enter_tag: Mapped[Optional[str]] = mapped_column(String(100), nullable=True) - timeframe: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) + min_rate: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) # type: ignore + exit_reason: Mapped[Optional[str]] = mapped_column(String(100), nullable=True) # type: ignore + exit_order_status: Mapped[Optional[str]] = mapped_column( + String(100), nullable=True) # type: ignore + strategy: Mapped[Optional[str]] = mapped_column(String(100), nullable=True) # type: ignore + enter_tag: Mapped[Optional[str]] = mapped_column(String(100), nullable=True) # type: ignore + timeframe: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) # type: ignore - trading_mode: Mapped[TradingMode] = mapped_column(Enum(TradingMode), nullable=True) - amount_precision: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) - price_precision: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) - precision_mode: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) - contract_size: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) + trading_mode: Mapped[TradingMode] = mapped_column( + Enum(TradingMode), nullable=True) # type: ignore + amount_precision: Mapped[Optional[float]] = mapped_column( + Float(), nullable=True) # type: ignore + price_precision: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) # type: ignore + precision_mode: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) # type: ignore + contract_size: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) # type: ignore # Leverage trading properties - leverage: Mapped[float] = mapped_column(Float(), nullable=True, default=1.0) - is_short: Mapped[bool] = mapped_column(nullable=False, default=False) - liquidation_price: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) + leverage: Mapped[float] = mapped_column(Float(), nullable=True, default=1.0) # type: ignore + is_short: Mapped[bool] = mapped_column(nullable=False, default=False) # type: ignore + liquidation_price: Mapped[Optional[float]] = mapped_column( + Float(), nullable=True) # type: ignore # Margin Trading Properties - interest_rate: Mapped[float] = mapped_column(Float(), nullable=False, default=0.0) + interest_rate: Mapped[float] = mapped_column( + Float(), nullable=False, default=0.0) # type: ignore # Futures properties - funding_fees: Mapped[Optional[float]] = mapped_column(Float(), nullable=True, default=None) + funding_fees: Mapped[Optional[float]] = mapped_column( + Float(), nullable=True, default=None) # type: ignore def __init__(self, **kwargs): super().__init__(**kwargs) From 59d57d34667524e3eeed404fbfb0e3c62b4faa3b Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 1 Mar 2023 20:01:43 +0100 Subject: [PATCH 134/360] Improve test resiliance --- tests/test_freqtradebot.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 1ea3ebfc6..800e76342 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -3036,6 +3036,7 @@ def test_handle_cancel_enter(mocker, caplog, default_conf_usdt, limit_order, is_ # Order remained open for some reason (cancel failed) cancel_buy_order['status'] = 'open' cancel_order_mock = MagicMock(return_value=cancel_buy_order) + trade.open_order_id = 'some_open_order' mocker.patch(f'{EXMS}.cancel_order_with_result', cancel_order_mock) assert not freqtrade.handle_cancel_enter(trade, l_order, reason) assert log_has_re(r"Order .* for .* not cancelled.", caplog) @@ -3231,6 +3232,7 @@ def test_handle_cancel_exit_cancel_exception(mocker, default_conf_usdt) -> None: trade = MagicMock() reason = CANCEL_REASON['TIMEOUT'] order = {'remaining': 1, + 'id': '125', 'amount': 1, 'status': "open"} assert not freqtrade.handle_cancel_exit(trade, order, reason) From b4b8dde4fb9a0646297c17af8175f5f1cc3d70af Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 1 Mar 2023 20:41:49 +0100 Subject: [PATCH 135/360] Add sqlalchemy to pre-commit dependencies --- .pre-commit-config.yaml | 1 + build_helpers/pre_commit_update.py | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 05f4df92b..acc0c8a17 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,6 +18,7 @@ repos: - types-requests==2.28.11.15 - types-tabulate==0.9.0.1 - types-python-dateutil==2.8.19.9 + - SQLAlchemy==2.0.4 # stages: [push] - repo: https://github.com/pycqa/isort diff --git a/build_helpers/pre_commit_update.py b/build_helpers/pre_commit_update.py index 8724d8ade..e6b47d100 100644 --- a/build_helpers/pre_commit_update.py +++ b/build_helpers/pre_commit_update.py @@ -8,12 +8,17 @@ import yaml pre_commit_file = Path('.pre-commit-config.yaml') require_dev = Path('requirements-dev.txt') +require = Path('requirements.txt') with require_dev.open('r') as rfile: requirements = rfile.readlines() +with require.open('r') as rfile: + requirements.extend(rfile.readlines()) + # Extract types only -type_reqs = [r.strip('\n') for r in requirements if r.startswith('types-')] +type_reqs = [r.strip('\n') for r in requirements if r.startswith( + 'types-') or r.startswith('SQLAlchemy')] with pre_commit_file.open('r') as file: f = yaml.load(file, Loader=yaml.FullLoader) From b980f45b2b3bbb5ef1b381af9708694f9d1685d3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 2 Mar 2023 06:23:01 +0100 Subject: [PATCH 136/360] Fix test mypy errors --- freqtrade/persistence/trade_model.py | 4 ++++ .../strats/broken_strats/broken_futures_strategies.py | 5 +++-- tests/strategy/strats/strategy_test_v3.py | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 62e2e2a7f..a05eb7409 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -99,6 +99,10 @@ class Order(ModelBase): def safe_filled(self) -> float: return self.filled if self.filled is not None else self.amount or 0.0 + @property + def safe_cost(self) -> float: + return self.cost or 0.0 + @property def safe_remaining(self) -> float: return ( diff --git a/tests/strategy/strats/broken_strats/broken_futures_strategies.py b/tests/strategy/strats/broken_strats/broken_futures_strategies.py index 7e6955d37..bb7ce2b32 100644 --- a/tests/strategy/strats/broken_strats/broken_futures_strategies.py +++ b/tests/strategy/strats/broken_strats/broken_futures_strategies.py @@ -7,6 +7,7 @@ from datetime import datetime from pandas import DataFrame +from freqtrade.persistence.trade_model import Order from freqtrade.strategy.interface import IStrategy @@ -35,7 +36,7 @@ class TestStrategyImplementBuyTimeout(TestStrategyNoImplementSell): def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: return super().populate_exit_trend(dataframe, metadata) - def check_buy_timeout(self, pair: str, trade, order: dict, + def check_buy_timeout(self, pair: str, trade, order: Order, current_time: datetime, **kwargs) -> bool: return False @@ -44,6 +45,6 @@ class TestStrategyImplementSellTimeout(TestStrategyNoImplementSell): def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: return super().populate_exit_trend(dataframe, metadata) - def check_sell_timeout(self, pair: str, trade, order: dict, + def check_sell_timeout(self, pair: str, trade, order: Order, current_time: datetime, **kwargs) -> bool: return False diff --git a/tests/strategy/strats/strategy_test_v3.py b/tests/strategy/strats/strategy_test_v3.py index 6f5ff573b..2d5121403 100644 --- a/tests/strategy/strats/strategy_test_v3.py +++ b/tests/strategy/strats/strategy_test_v3.py @@ -197,7 +197,7 @@ class StrategyTestV3(IStrategy): if current_profit < -0.0075: orders = trade.select_filled_orders(trade.entry_side) - return round(orders[0].cost, 0) + return round(orders[0].safe_cost, 0) return None From 8103656ae17e4e851584f2f9146491faf7529bf9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 2 Mar 2023 06:36:03 +0100 Subject: [PATCH 137/360] Bump mypy in pre-commit --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index acc0c8a17..565eb96f7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,7 +8,7 @@ repos: # stages: [push] - repo: https://github.com/pre-commit/mirrors-mypy - rev: "v0.991" + rev: "v1.0.1" hooks: - id: mypy exclude: build_helpers From ba38a826e95bee9d33e3c9c692faf561adc04437 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 2 Mar 2023 06:46:17 +0100 Subject: [PATCH 138/360] Update missing mocks --- tests/exchange/test_exchange.py | 2 +- tests/test_freqtradebot.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 9bc176f41..940319a45 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -5241,7 +5241,7 @@ def test_get_liquidation_price( default_conf_usdt['trading_mode'] = trading_mode default_conf_usdt['exchange']['name'] = exchange_name default_conf_usdt['margin_mode'] = margin_mode - mocker.patch('freqtrade.exchange.Gate.validate_ordertypes') + mocker.patch('freqtrade.exchange.gate.Gate.validate_ordertypes') exchange = get_patched_exchange(mocker, default_conf_usdt, id=exchange_name) exchange.get_maintenance_ratio_and_amt = MagicMock(return_value=(0.01, 0.01)) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 1ea3ebfc6..e2fb1618b 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -786,7 +786,7 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order, default_conf_usdt['exchange']['name'] = exchange_name if margin_mode: default_conf_usdt['margin_mode'] = margin_mode - mocker.patch('freqtrade.exchange.Gate.validate_ordertypes') + mocker.patch('freqtrade.exchange.gate.Gate.validate_ordertypes') patch_RPCManager(mocker) patch_exchange(mocker, id=exchange_name) freqtrade = FreqtradeBot(default_conf_usdt) @@ -814,7 +814,7 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order, get_max_leverage=MagicMock(return_value=10), ) mocker.patch.multiple( - 'freqtrade.exchange.Okx', + 'freqtrade.exchange.okx.Okx', get_max_pair_stake_amount=MagicMock(return_value=500000), ) pair = 'ETH/USDT' From 103bd9e2f217bbf277153f9f0fb63f25b30246d0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 2 Mar 2023 06:55:33 +0100 Subject: [PATCH 139/360] keep Trade.session private --- freqtrade/commands/db_commands.py | 2 +- freqtrade/persistence/models.py | 12 ++++++------ freqtrade/persistence/pairlock.py | 2 +- freqtrade/persistence/trade_model.py | 4 ++-- tests/persistence/test_migrations.py | 6 +++--- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/freqtrade/commands/db_commands.py b/freqtrade/commands/db_commands.py index b4997582d..c424016b1 100644 --- a/freqtrade/commands/db_commands.py +++ b/freqtrade/commands/db_commands.py @@ -20,7 +20,7 @@ def start_convert_db(args: Dict[str, Any]) -> None: config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) init_db(config['db_url']) - session_target = Trade.session + session_target = Trade._session init_db(config['db_url_from']) logger.info("Starting db migration.") diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index f4058b4eb..d718af2f4 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -54,12 +54,12 @@ def init_db(db_url: str) -> None: # https://docs.sqlalchemy.org/en/13/orm/contextual.html#thread-local-scope # Scoped sessions proxy requests to the appropriate thread-local session. # We should use the scoped_session object - not a seperately initialized version - Trade.session = scoped_session(sessionmaker(bind=engine, autoflush=False)) - Order.session = Trade.session - PairLock.session = Trade.session - Trade.query = Trade.session.query_property() - Order.query = Trade.session.query_property() - PairLock.query = Trade.session.query_property() + Trade._session = scoped_session(sessionmaker(bind=engine, autoflush=False)) + Order._session = Trade._session + PairLock._session = Trade._session + Trade.query = Trade._session.query_property() + Order.query = Trade._session.query_property() + PairLock.query = Trade._session.query_property() previous_tables = inspect(engine).get_table_names() ModelBase.metadata.create_all(engine) diff --git a/freqtrade/persistence/pairlock.py b/freqtrade/persistence/pairlock.py index d5a8d7ae1..a6d1eeaf0 100644 --- a/freqtrade/persistence/pairlock.py +++ b/freqtrade/persistence/pairlock.py @@ -15,7 +15,7 @@ class PairLock(ModelBase): """ __tablename__ = 'pairlocks' query: ClassVar[_QueryDescriptorType] - session: ClassVar[SessionType] + _session: ClassVar[SessionType] id: Mapped[int] = mapped_column(primary_key=True) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index a05eb7409..0ae5fba25 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -37,7 +37,7 @@ class Order(ModelBase): """ __tablename__ = 'orders' query: ClassVar[_QueryDescriptorType] - session: ClassVar[SessionType] + _session: ClassVar[SessionType] # Uniqueness should be ensured over pair, order_id # its likely that order_id is unique per Pair on some exchanges. @@ -1179,7 +1179,7 @@ class Trade(ModelBase, LocalTrade): """ __tablename__ = 'trades' query: ClassVar[_QueryDescriptorType] - session: ClassVar[SessionType] + _session: ClassVar[SessionType] use_db: bool = True diff --git a/tests/persistence/test_migrations.py b/tests/persistence/test_migrations.py index 5254164c1..2a6959d58 100644 --- a/tests/persistence/test_migrations.py +++ b/tests/persistence/test_migrations.py @@ -21,8 +21,8 @@ spot, margin, futures = TradingMode.SPOT, TradingMode.MARGIN, TradingMode.FUTURE def test_init_create_session(default_conf): # Check if init create a session init_db(default_conf['db_url']) - assert hasattr(Trade, 'session') - assert 'scoped_session' in type(Trade.session).__name__ + assert hasattr(Trade, '_session') + assert 'scoped_session' in type(Trade._session).__name__ def test_init_custom_db_url(default_conf, tmpdir): @@ -34,7 +34,7 @@ def test_init_custom_db_url(default_conf, tmpdir): init_db(default_conf['db_url']) assert Path(filename).is_file() - r = Trade.session.execute(text("PRAGMA journal_mode")) + r = Trade._session.execute(text("PRAGMA journal_mode")) assert r.first() == ('wal',) From 49bfa556bfa413833fbb59b6bde2e3de6de9f65f Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 25 Oct 2022 21:04:40 +0200 Subject: [PATCH 140/360] Update CI to test against python 3.11 --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 17c0efd6d..191a10d1c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,7 +24,7 @@ jobs: strategy: matrix: os: [ ubuntu-20.04, ubuntu-22.04 ] - python-version: ["3.8", "3.9", "3.10"] + python-version: ["3.8", "3.9", "3.10", "3.11"] steps: - uses: actions/checkout@v3 @@ -115,7 +115,7 @@ jobs: strategy: matrix: os: [ macos-latest ] - python-version: ["3.8", "3.9", "3.10"] + python-version: ["3.8", "3.9", "3.10", "3.11"] steps: - uses: actions/checkout@v3 @@ -212,7 +212,7 @@ jobs: strategy: matrix: os: [ windows-latest ] - python-version: ["3.8", "3.9", "3.10"] + python-version: ["3.8", "3.9", "3.10", "3.11"] steps: - uses: actions/checkout@v3 From 684d310ea0d7c05996fe08b6e509cc1d6771c61e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 Feb 2023 13:20:26 +0100 Subject: [PATCH 141/360] Limit catboost to python <3.11 --- requirements-freqai.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-freqai.txt b/requirements-freqai.txt index 5b27ecf95..bc0be85e5 100644 --- a/requirements-freqai.txt +++ b/requirements-freqai.txt @@ -5,7 +5,7 @@ # Required for freqai scikit-learn==1.1.3 joblib==1.2.0 -catboost==1.1.1; platform_machine != 'aarch64' +catboost==1.1.1; platform_machine != 'aarch64' and python_version < '3.11' lightgbm==3.3.5 xgboost==1.7.4 tensorboard==2.12.0 From 7a7f16b6589fc2b36ceb0bd17651405bb8f5a4ce Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 Feb 2023 13:27:28 +0100 Subject: [PATCH 142/360] Skip catboost tests on py3.11 --- tests/freqai/test_freqai_interface.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index cdfc943af..7d229434f 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -1,5 +1,6 @@ import platform import shutil +import sys from pathlib import Path from unittest.mock import MagicMock @@ -17,6 +18,10 @@ from tests.conftest import EXMS, create_mock_trades, get_patched_exchange, log_h from tests.freqai.conftest import get_patched_freqai_strategy, make_rl_config +def is_py11() -> bool: + return sys.version_info >= (3, 11) + + def is_arm() -> bool: machine = platform.machine() return "arm" in machine or "aarch64" in machine @@ -41,7 +46,7 @@ def is_mac() -> bool: def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca, dbscan, float32, can_short, shuffle, buffer): - if is_arm() and model == 'CatboostRegressor': + if (is_arm() or is_py11()) and model == 'CatboostRegressor': pytest.skip("CatBoost is not supported on ARM") if is_mac() and not is_arm() and 'Reinforcement' in model: @@ -117,7 +122,7 @@ def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca, ('CatboostClassifierMultiTarget', "freqai_test_multimodel_classifier_strat") ]) def test_extract_data_and_train_model_MultiTargets(mocker, freqai_conf, model, strat): - if is_arm() and 'Catboost' in model: + if (is_arm() or is_py11()) and 'Catboost' in model: pytest.skip("CatBoost is not supported on ARM") freqai_conf.update({"timerange": "20180110-20180130"}) @@ -159,7 +164,7 @@ def test_extract_data_and_train_model_MultiTargets(mocker, freqai_conf, model, s 'XGBoostRFClassifier', ]) def test_extract_data_and_train_model_Classifiers(mocker, freqai_conf, model): - if is_arm() and model == 'CatboostClassifier': + if (is_arm() or is_py11()) and model == 'CatboostClassifier': pytest.skip("CatBoost is not supported on ARM") freqai_conf.update({"freqaimodel": model}) @@ -208,7 +213,7 @@ def test_extract_data_and_train_model_Classifiers(mocker, freqai_conf, model): def test_start_backtesting(mocker, freqai_conf, model, num_files, strat, caplog): freqai_conf.get("freqai", {}).update({"save_backtest_models": True}) freqai_conf['runmode'] = RunMode.BACKTEST - if is_arm() and "Catboost" in model: + if (is_arm() or is_py11()) and "Catboost" in model: pytest.skip("CatBoost is not supported on ARM") if is_mac() and 'Reinforcement' in model: From b1a5776f1480461f87403e136df4eb591a3b0c97 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 Feb 2023 15:21:12 +0100 Subject: [PATCH 143/360] Skip reinforcement learning for python 3.11 --- requirements-freqai-rl.txt | 8 ++++---- tests/freqai/test_freqai_interface.py | 7 ++++++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/requirements-freqai-rl.txt b/requirements-freqai-rl.txt index c242af43e..4de7d8fab 100644 --- a/requirements-freqai-rl.txt +++ b/requirements-freqai-rl.txt @@ -2,9 +2,9 @@ -r requirements-freqai.txt # Required for freqai-rl -torch==1.13.1 -stable-baselines3==1.7.0 -sb3-contrib==1.7.0 +torch==1.13.1; python_version < '3.11' +stable-baselines3==1.7.0; python_version < '3.11' +sb3-contrib==1.7.0; python_version < '3.11' # Gym is forced to this version by stable-baselines3. setuptools==65.5.1 # Should be removed when gym is fixed. -gym==0.21 +gym==0.21; python_version < '3.11' diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index 7d229434f..0584ed9f5 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -51,7 +51,8 @@ def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca, if is_mac() and not is_arm() and 'Reinforcement' in model: pytest.skip("Reinforcement learning module not available on intel based Mac OS") - + if is_py11() and 'Reinforcement' in model: + pytest.skip("Reinforcement learning currently not available on python 3.11.") model_save_ext = 'joblib' freqai_conf.update({"freqaimodel": model}) freqai_conf.update({"timerange": "20180110-20180130"}) @@ -218,6 +219,8 @@ def test_start_backtesting(mocker, freqai_conf, model, num_files, strat, caplog) if is_mac() and 'Reinforcement' in model: pytest.skip("Reinforcement learning module not available on intel based Mac OS") + if is_py11() and 'Reinforcement' in model: + pytest.skip("Reinforcement learning currently not available on python 3.11.") Trade.use_db = False freqai_conf.update({"freqaimodel": model}) @@ -514,6 +517,8 @@ def test_get_state_info(mocker, freqai_conf, dp_exists, caplog, tickers): if is_mac(): pytest.skip("Reinforcement learning module not available on intel based Mac OS") + if is_py11(): + pytest.skip("Reinforcement learning currently not available on python 3.11.") freqai_conf.update({"freqaimodel": "ReinforcementLearner"}) freqai_conf.update({"timerange": "20180110-20180130"}) From 38050b5346fa563e9592bdb3258602adb1b753c1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 Feb 2023 15:25:47 +0100 Subject: [PATCH 144/360] Simplify "model-run" conditions --- tests/freqai/test_freqai_interface.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index 0584ed9f5..f8bee3659 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -32,6 +32,17 @@ def is_mac() -> bool: return "Darwin" in machine +def can_run_model(model: str) -> None: + if (is_arm() or is_py11()) and "Catboost" in model: + pytest.skip("CatBoost is not supported on ARM") + + if is_mac() and not is_arm() and 'Reinforcement' in model: + pytest.skip("Reinforcement learning module not available on intel based Mac OS") + + if is_py11() and 'Reinforcement' in model: + pytest.skip("Reinforcement learning currently not available on python 3.11.") + + @pytest.mark.parametrize('model, pca, dbscan, float32, can_short, shuffle, buffer', [ ('LightGBMRegressor', True, False, True, True, False, 0), ('XGBoostRegressor', False, True, False, True, False, 10), @@ -46,13 +57,7 @@ def is_mac() -> bool: def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca, dbscan, float32, can_short, shuffle, buffer): - if (is_arm() or is_py11()) and model == 'CatboostRegressor': - pytest.skip("CatBoost is not supported on ARM") - - if is_mac() and not is_arm() and 'Reinforcement' in model: - pytest.skip("Reinforcement learning module not available on intel based Mac OS") - if is_py11() and 'Reinforcement' in model: - pytest.skip("Reinforcement learning currently not available on python 3.11.") + can_run_model(model) model_save_ext = 'joblib' freqai_conf.update({"freqaimodel": model}) freqai_conf.update({"timerange": "20180110-20180130"}) @@ -212,15 +217,11 @@ def test_extract_data_and_train_model_Classifiers(mocker, freqai_conf, model): ], ) def test_start_backtesting(mocker, freqai_conf, model, num_files, strat, caplog): + can_run_model(model) + freqai_conf.get("freqai", {}).update({"save_backtest_models": True}) freqai_conf['runmode'] = RunMode.BACKTEST - if (is_arm() or is_py11()) and "Catboost" in model: - pytest.skip("CatBoost is not supported on ARM") - if is_mac() and 'Reinforcement' in model: - pytest.skip("Reinforcement learning module not available on intel based Mac OS") - if is_py11() and 'Reinforcement' in model: - pytest.skip("Reinforcement learning currently not available on python 3.11.") Trade.use_db = False freqai_conf.update({"freqaimodel": model}) From 5d0e14b5644d100ed2a5fe7ddcc6b987cbc69741 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 2 Mar 2023 07:07:09 +0100 Subject: [PATCH 145/360] Don't mock full modules --- tests/rpc/test_rpc_apiserver.py | 4 ++-- tests/rpc/test_rpc_telegram.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index b104ec854..f898dd476 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -1179,7 +1179,7 @@ def test_api_force_entry(botclient, mocker, fee, endpoint): ftbot.config['force_entry_enable'] = True fbuy_mock = MagicMock(return_value=None) - mocker.patch("freqtrade.rpc.RPC._rpc_force_entry", fbuy_mock) + mocker.patch("freqtrade.rpc.rpc.RPC._rpc_force_entry", fbuy_mock) rc = client_post(client, f"{BASE_URI}/{endpoint}", data={"pair": "ETH/BTC"}) assert_response(rc) @@ -1205,7 +1205,7 @@ def test_api_force_entry(botclient, mocker, fee, endpoint): strategy=CURRENT_TEST_STRATEGY, trading_mode=TradingMode.SPOT )) - mocker.patch("freqtrade.rpc.RPC._rpc_force_entry", fbuy_mock) + mocker.patch("freqtrade.rpc.rpc.RPC._rpc_force_entry", fbuy_mock) rc = client_post(client, f"{BASE_URI}/{endpoint}", data={"pair": "ETH/BTC"}) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 3e1421cb5..7b3411113 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1209,7 +1209,7 @@ def test_force_enter_handle(default_conf, update, mocker) -> None: mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) fbuy_mock = MagicMock(return_value=None) - mocker.patch('freqtrade.rpc.RPC._rpc_force_entry', fbuy_mock) + mocker.patch('freqtrade.rpc.rpc.RPC._rpc_force_entry', fbuy_mock) telegram, freqtradebot, _ = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot) @@ -1226,7 +1226,7 @@ def test_force_enter_handle(default_conf, update, mocker) -> None: # Reset and retry with specified price fbuy_mock = MagicMock(return_value=None) - mocker.patch('freqtrade.rpc.RPC._rpc_force_entry', fbuy_mock) + mocker.patch('freqtrade.rpc.rpc.RPC._rpc_force_entry', fbuy_mock) # /forcelong ETH/BTC 0.055 context = MagicMock() context.args = ["ETH/BTC", "0.055"] @@ -1255,7 +1255,7 @@ def test_force_enter_no_pair(default_conf, update, mocker) -> None: mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) fbuy_mock = MagicMock(return_value=None) - mocker.patch('freqtrade.rpc.RPC._rpc_force_entry', fbuy_mock) + mocker.patch('freqtrade.rpc.rpc.RPC._rpc_force_entry', fbuy_mock) telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) From 5b0c1437130a502f322e915a4095b43e68ba88b2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 2 Mar 2023 19:29:41 +0100 Subject: [PATCH 146/360] Update some comments about 3.11 --- docs/freqai.md | 4 ++++ docs/windows_installation.md | 2 +- setup.cfg | 1 + 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/freqai.md b/docs/freqai.md index 5c6b5b2ce..ef8efb840 100644 --- a/docs/freqai.md +++ b/docs/freqai.md @@ -71,6 +71,10 @@ pip install -r requirements-freqai.txt !!! Note Catboost will not be installed on arm devices (raspberry, Mac M1, ARM based VPS, ...), since it does not provide wheels for this platform. +!!! Note "python 3.11" + Some dependencies (Catboost, Torch) currently don't support python 3.11. Freqtrade therefore only supports python 3.10 for these models/dependencies. + Tests involving these dependencies are skipped on 3.11. + ### Usage with docker If you are using docker, a dedicated tag with FreqAI dependencies is available as `:freqai`. As such - you can replace the image line in your docker compose file with `image: freqtradeorg/freqtrade:develop_freqai`. This image contains the regular FreqAI dependencies. Similar to native installs, Catboost will not be available on ARM based devices. diff --git a/docs/windows_installation.md b/docs/windows_installation.md index 1b0d9d724..43d6728ee 100644 --- a/docs/windows_installation.md +++ b/docs/windows_installation.md @@ -26,7 +26,7 @@ Install ta-lib according to the [ta-lib documentation](https://github.com/mrjbq7 As compiling from source on windows has heavy dependencies (requires a partial visual studio installation), there is also a repository of unofficial pre-compiled windows Wheels [here](https://www.lfd.uci.edu/~gohlke/pythonlibs/#ta-lib), which need to be downloaded and installed using `pip install TA_Lib-0.4.25-cp38-cp38-win_amd64.whl` (make sure to use the version matching your python version). -Freqtrade provides these dependencies for the latest 3 Python versions (3.8, 3.9 and 3.10) and for 64bit Windows. +Freqtrade provides these dependencies for the latest 3 Python versions (3.8, 3.9, 3.10 and 3.11) and for 64bit Windows. Other versions must be downloaded from the above link. ``` powershell diff --git a/setup.cfg b/setup.cfg index 60ec8a75f..b54b62619 100644 --- a/setup.cfg +++ b/setup.cfg @@ -17,6 +17,7 @@ classifiers = Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 + Programming Language :: Python :: 3.11 Operating System :: MacOS Operating System :: Unix Topic :: Office/Business :: Financial :: Investment From 022f85095e2100901d1457d1014fc879ae033843 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 2 Mar 2023 19:50:01 +0100 Subject: [PATCH 147/360] Show Number of exits part of #8234 --- freqtrade/rpc/telegram.py | 3 +++ tests/rpc/test_rpc_telegram.py | 1 + 2 files changed, 4 insertions(+) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index dc92478ab..6f82a7316 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -561,6 +561,8 @@ class Telegram(RPCHandler): for r in results: r['open_date_hum'] = arrow.get(r['open_date']).humanize() r['num_entries'] = len([o for o in r['orders'] if o['ft_is_entry']]) + r['num_exits'] = len([o for o in r['orders'] if not o['ft_is_entry'] + and not o['ft_order_side'] == 'stoploss']) r['exit_reason'] = r.get('exit_reason', "") r['stake_amount_r'] = round_coin_value(r['stake_amount'], r['quote_currency']) r['profit_abs_r'] = round_coin_value(r['profit_abs'], r['quote_currency']) @@ -581,6 +583,7 @@ class Telegram(RPCHandler): if position_adjust: max_buy_str = (f"/{max_entries + 1}" if (max_entries > 0) else "") lines.append("*Number of Entries:* `{num_entries}`" + max_buy_str) + lines.append("*Number of Exits:* `{num_exits}`") lines.extend([ "*Open Rate:* `{open_rate:.8f}`", diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 7b3411113..26cb93821 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -279,6 +279,7 @@ def test_telegram_status_multi_entry(default_conf, update, mocker, fee) -> None: assert msg_mock.call_count == 4 msg = msg_mock.call_args_list[0][0][0] assert re.search(r'Number of Entries.*2', msg) + assert re.search(r'Number of Exits.*0', msg) assert re.search(r'Average Entry Price', msg) assert re.search(r'Order filled', msg) assert re.search(r'Close Date:', msg) is None From 9573974c4798982a87c9e70a7a341ac4235ba956 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 3 Mar 2023 06:36:35 +0100 Subject: [PATCH 148/360] Update deprecations document --- docs/deprecated.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/deprecated.md b/docs/deprecated.md index 3b5b28b81..6719ce56d 100644 --- a/docs/deprecated.md +++ b/docs/deprecated.md @@ -74,3 +74,8 @@ Webhook terminology changed from "sell" to "exit", and from "buy" to "entry", re * `webhooksell`, `webhookexit` -> `exit` * `webhooksellfill`, `webhookexitfill` -> `exit_fill` * `webhooksellcancel`, `webhookexitcancel` -> `exit_cancel` + + +## Removal of `populate_any_indicators` + +version 2023.3 saw the removal of `populate_any_indicators` in favor of split methods for feature engineering and targets. Please read the [migration document](strategy_migration.md#freqai-strategy) for full details. From c03c3a57066ef20833174bdd95a3367b405ed696 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 3 Mar 2023 18:12:41 +0100 Subject: [PATCH 149/360] improve order REPR display --- freqtrade/persistence/trade_model.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 0ae5fba25..1c37635f0 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -120,8 +120,9 @@ class Order(ModelBase): def __repr__(self): - return (f'Order(id={self.id}, order_id={self.order_id}, trade_id={self.ft_trade_id}, ' - f'side={self.side}, order_type={self.order_type}, status={self.status})') + return (f"Order(id={self.id}, order_id={self.order_id}, trade_id={self.ft_trade_id}, " + f"side={self.side}, filled={self.safe_filled}, price={self.safe_price}, " + f"order_type={self.order_type}, status={self.status})") def update_from_ccxt_object(self, order): """ From 87b75134016ed748b5d6732a21fc7ca053ca2631 Mon Sep 17 00:00:00 2001 From: hippocritical Date: Fri, 3 Mar 2023 18:53:09 +0100 Subject: [PATCH 150/360] fixed --strategy-list moved ast comments to requirements.txt >=1.0.0 (since that is the first version that adds the comments unparsing) --- freqtrade/commands/strategy_utils_commands.py | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/commands/strategy_utils_commands.py b/freqtrade/commands/strategy_utils_commands.py index dc94f2b67..0124b73b6 100644 --- a/freqtrade/commands/strategy_utils_commands.py +++ b/freqtrade/commands/strategy_utils_commands.py @@ -25,7 +25,7 @@ def start_strategy_update(args: Dict[str, Any]) -> None: config, enum_failed=False, recursive=config.get('recursive_strategy_search', False)) filtered_strategy_objs = [] - if hasattr(args, "strategy_list"): + if 'strategy_list' in args: for args_strategy in args['strategy_list']: for strategy_obj in strategy_objs: if (strategy_obj['name'] == args_strategy diff --git a/requirements.txt b/requirements.txt index 40bae63b6..14c468da0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -58,4 +58,4 @@ schedule==1.1.0 websockets==10.4 janus==1.0.0 -astor~=0.8.1 +ast-comments>=1.0.0 From d0045673faafeb4d6aa4c83f18cb65e192007153 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 3 Mar 2023 19:56:16 +0100 Subject: [PATCH 151/360] Add explicit test for stoploss_from_open --- tests/strategy/test_strategy_helpers.py | 30 +++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/strategy/test_strategy_helpers.py b/tests/strategy/test_strategy_helpers.py index 36e997f7b..cb79ac171 100644 --- a/tests/strategy/test_strategy_helpers.py +++ b/tests/strategy/test_strategy_helpers.py @@ -169,6 +169,36 @@ def test_stoploss_from_open(side, profitrange): assert pytest.approx(stop_price) == expected_stop_price +@pytest.mark.parametrize("side,rel_stop,curr_profit,leverage,expected", [ + # profit range for long is [-1, inf] while for shorts is [-inf, 1] + ("long", 0, -1, 1, 1), + ("long", 0, 0.1, 1, 0.09090909), + ("long", -0.1, 0.1, 1, 0.18181818), + ("long", 0.1, 0.2, 1, 0.08333333), + ("long", 0.1, 0.5, 1, 0.266666666), + ("long", 0.1, 5, 1, 0.816666666), # 500% profit, set stoploss to 10% above open price + + ("short", 0, 0.1, 1, 0.1111111), + ("short", -0.1, 0.1, 1, 0.2222222), + ("short", 0.1, 0.2, 1, 0.125), + ("short", 0.1, 1, 1, 1), +]) +def test_stoploss_from_open_leverage(side, rel_stop, curr_profit, leverage, expected): + + stoploss = stoploss_from_open(rel_stop, curr_profit, side == 'short') + assert pytest.approx(stoploss) == expected + open_rate = 100 + if stoploss != 1: + if side == 'long': + current_rate = open_rate * (1 + curr_profit) + stop = current_rate * (1 - stoploss) + assert pytest.approx(stop) == open_rate * (1 + rel_stop) + else: + current_rate = open_rate * (1 - curr_profit) + stop = current_rate * (1 + stoploss) + assert pytest.approx(stop) == open_rate * (1 - rel_stop) + + def test_stoploss_from_absolute(): assert pytest.approx(stoploss_from_absolute(90, 100)) == 1 - (90 / 100) assert pytest.approx(stoploss_from_absolute(90, 100)) == 0.1 From a3dee9350f2a4c9f17491b402a4613c66cc1e3d0 Mon Sep 17 00:00:00 2001 From: Andy Lawless Date: Fri, 3 Mar 2023 20:37:05 +0000 Subject: [PATCH 152/360] Move bot_loop_start call to run on every candle --- freqtrade/optimize/backtesting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 83b65d24b..ccb027317 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -205,7 +205,6 @@ class Backtesting: self.strategy.order_types['stoploss_on_exchange'] = False self.strategy.ft_bot_start() - strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)() def _load_protections(self, strategy: IStrategy): if self.config.get('enable_protections', False): @@ -1159,6 +1158,7 @@ class Backtesting: while current_time <= end_date: open_trade_count_start = LocalTrade.bt_open_open_trade_count self.check_abort() + strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)() for i, pair in enumerate(data): row_index = indexes[pair] row = self.validate_row(data, pair, row_index, current_time) From b262f0b3742033df7688e5ffb1d3affede620090 Mon Sep 17 00:00:00 2001 From: Andy Lawless Date: Fri, 3 Mar 2023 20:46:43 +0000 Subject: [PATCH 153/360] Update docs re: bot_loop_start in backtest --- docs/bot-basics.md | 2 +- docs/strategy-callbacks.md | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/bot-basics.md b/docs/bot-basics.md index 925fc7862..1d5668eb3 100644 --- a/docs/bot-basics.md +++ b/docs/bot-basics.md @@ -57,10 +57,10 @@ This loop will be repeated again and again until the bot is stopped. * Load historic data for configured pairlist. * Calls `bot_start()` once. -* Calls `bot_loop_start()` once. * Calculate indicators (calls `populate_indicators()` once per pair). * Calculate entry / exit signals (calls `populate_entry_trend()` and `populate_exit_trend()` once per pair). * Loops per candle simulating entry and exit points. + * Calls `bot_loop_start()` strategy callback. * Check for Order timeouts, either via the `unfilledtimeout` configuration, or via `check_entry_timeout()` / `check_exit_timeout()` strategy callbacks. * Calls `adjust_entry_price()` strategy callback for open entry orders. * Check for trade entry signals (`enter_long` / `enter_short` columns). diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index 81366c66e..64b6bd551 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -51,7 +51,8 @@ During hyperopt, this runs only once at startup. ## Bot loop start -A simple callback which is called once at the start of every bot throttling iteration (roughly every 5 seconds, unless configured differently). +A simple callback which is called once at the start of every bot throttling iteration in dry/live mode (roughly every 5 +seconds, unless configured differently) or once per candle in backtest/hyperopt mode. This can be used to perform calculations which are pair independent (apply to all pairs), loading of external data, etc. ``` python From 51c15d894b4ce98583875c37c149a0e3ad137909 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 Mar 2023 15:27:01 +0100 Subject: [PATCH 154/360] Bump ccxt to 2.8.88 closes #8270 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 6b1c888b8..efd49a7f3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.24.2 pandas==1.5.3 pandas-ta==0.3.14b -ccxt==2.8.54 +ccxt==2.8.88 cryptography==39.0.1 aiohttp==3.8.4 SQLAlchemy==2.0.4 From 027e0234430a6e9fb794c13b2a73aa73f529decd Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 Mar 2023 18:00:06 +0100 Subject: [PATCH 155/360] Stop from open with leverage --- freqtrade/strategy/strategy_helper.py | 13 ++++++++----- tests/strategy/test_strategy_helpers.py | 18 +++++++++++------- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/freqtrade/strategy/strategy_helper.py b/freqtrade/strategy/strategy_helper.py index aa753a829..3ba1850b3 100644 --- a/freqtrade/strategy/strategy_helper.py +++ b/freqtrade/strategy/strategy_helper.py @@ -86,7 +86,8 @@ def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame, def stoploss_from_open( open_relative_stop: float, current_profit: float, - is_short: bool = False + is_short: bool = False, + leverage: float = 1.0 ) -> float: """ @@ -102,21 +103,23 @@ def stoploss_from_open( :param open_relative_stop: Desired stop loss percentage relative to open price :param current_profit: The current profit percentage :param is_short: When true, perform the calculation for short instead of long + :param leverage: Leverage to use for the calculation :return: Stop loss value relative to current price """ # formula is undefined for current_profit -1 (longs) or 1 (shorts), return maximum value - if (current_profit == -1 and not is_short) or (is_short and current_profit == 1): + _current_profit = current_profit / leverage + if (_current_profit == -1 and not is_short) or (is_short and _current_profit == 1): return 1 if is_short is True: - stoploss = -1 + ((1 - open_relative_stop) / (1 - current_profit)) + stoploss = -1 + ((1 - open_relative_stop / leverage) / (1 - _current_profit)) else: - stoploss = 1 - ((1 + open_relative_stop) / (1 + current_profit)) + stoploss = 1 - ((1 + open_relative_stop / leverage) / (1 + _current_profit)) # negative stoploss values indicate the requested stop price is higher/lower # (long/short) than the current price - return max(stoploss, 0.0) + return max(stoploss * leverage, 0.0) def stoploss_from_absolute(stop_rate: float, current_rate: float, is_short: bool = False) -> float: diff --git a/tests/strategy/test_strategy_helpers.py b/tests/strategy/test_strategy_helpers.py index cb79ac171..a55580780 100644 --- a/tests/strategy/test_strategy_helpers.py +++ b/tests/strategy/test_strategy_helpers.py @@ -177,26 +177,30 @@ def test_stoploss_from_open(side, profitrange): ("long", 0.1, 0.2, 1, 0.08333333), ("long", 0.1, 0.5, 1, 0.266666666), ("long", 0.1, 5, 1, 0.816666666), # 500% profit, set stoploss to 10% above open price + ("long", 0, 5, 10, 3.3333333), # 500% profit, set stoploss break even + ("long", 0.1, 5, 10, 3.26666666), # 500% profit, set stoploss to 10% above open price + ("long", -0.1, 5, 10, 3.3999999), # 500% profit, set stoploss to 10% belowopen price ("short", 0, 0.1, 1, 0.1111111), ("short", -0.1, 0.1, 1, 0.2222222), ("short", 0.1, 0.2, 1, 0.125), ("short", 0.1, 1, 1, 1), + ("short", -0.01, 5, 10, 10.01999999), # 500% profit at 10x ]) def test_stoploss_from_open_leverage(side, rel_stop, curr_profit, leverage, expected): - stoploss = stoploss_from_open(rel_stop, curr_profit, side == 'short') + stoploss = stoploss_from_open(rel_stop, curr_profit, side == 'short', leverage) assert pytest.approx(stoploss) == expected open_rate = 100 if stoploss != 1: if side == 'long': - current_rate = open_rate * (1 + curr_profit) - stop = current_rate * (1 - stoploss) - assert pytest.approx(stop) == open_rate * (1 + rel_stop) + current_rate = open_rate * (1 + curr_profit / leverage) + stop = current_rate * (1 - stoploss / leverage) + assert pytest.approx(stop) == open_rate * (1 + rel_stop / leverage) else: - current_rate = open_rate * (1 - curr_profit) - stop = current_rate * (1 + stoploss) - assert pytest.approx(stop) == open_rate * (1 - rel_stop) + current_rate = open_rate * (1 - curr_profit / leverage) + stop = current_rate * (1 + stoploss / leverage) + assert pytest.approx(stop) == open_rate * (1 - rel_stop / leverage) def test_stoploss_from_absolute(): From f0cbb4f94973435a99037a4d7f3ba45a005ddd0c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 Mar 2023 18:20:31 +0100 Subject: [PATCH 156/360] Expose relative realized profit --- freqtrade/persistence/trade_model.py | 2 ++ freqtrade/rpc/api_server/api_schemas.py | 1 + tests/persistence/test_persistence.py | 2 ++ tests/rpc/test_rpc.py | 1 + 4 files changed, 6 insertions(+) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 1c37635f0..21fe80819 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -519,6 +519,8 @@ class LocalTrade(): 'close_timestamp': int(self.close_date.replace( tzinfo=timezone.utc).timestamp() * 1000) if self.close_date else None, 'realized_profit': self.realized_profit or 0.0, + # Close-profit corresponds to relative realized_profit ratio + 'realized_profit_ratio': self.close_profit or None, 'close_rate': self.close_rate, 'close_rate_requested': self.close_rate_requested, 'close_profit': self.close_profit, # Deprecated diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 562c9aa7d..a751179b2 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -250,6 +250,7 @@ class TradeSchema(BaseModel): profit_fiat: Optional[float] realized_profit: float + realized_profit_ratio: Optional[float] exit_reason: Optional[str] exit_order_status: Optional[str] diff --git a/tests/persistence/test_persistence.py b/tests/persistence/test_persistence.py index 6d907ccf0..0598d4134 100644 --- a/tests/persistence/test_persistence.py +++ b/tests/persistence/test_persistence.py @@ -1362,6 +1362,7 @@ def test_to_json(fee): 'trade_duration': None, 'trade_duration_s': None, 'realized_profit': 0.0, + 'realized_profit_ratio': None, 'close_profit': None, 'close_profit_pct': None, 'close_profit_abs': None, @@ -1438,6 +1439,7 @@ def test_to_json(fee): 'initial_stop_loss_pct': None, 'initial_stop_loss_ratio': None, 'realized_profit': 0.0, + 'realized_profit_ratio': None, 'close_profit': None, 'close_profit_pct': None, 'close_profit_abs': None, diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index d368107df..cd72da763 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -76,6 +76,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'stoploss_entry_dist_ratio': -0.10376381, 'open_order': None, 'realized_profit': 0.0, + 'realized_profit_ratio': None, 'total_profit_abs': -4.09e-06, 'total_profit_fiat': ANY, 'exchange': 'binance', From aec11618cefb3af94d19105610442d111edd4ddf Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 Mar 2023 18:26:16 +0100 Subject: [PATCH 157/360] Telegram improved formatting --- freqtrade/rpc/telegram.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 6f82a7316..175d0ce93 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -321,28 +321,30 @@ class Telegram(RPCHandler): and self._rpc._fiat_converter): msg['profit_fiat'] = self._rpc._fiat_converter.convert_amount( msg['profit_amount'], msg['stake_currency'], msg['fiat_currency']) - msg['profit_extra'] = ( - f" / {msg['profit_fiat']:.3f} {msg['fiat_currency']}") + msg['profit_extra'] = f" / {msg['profit_fiat']:.3f} {msg['fiat_currency']}" else: msg['profit_extra'] = '' msg['profit_extra'] = ( f" ({msg['gain']}: {msg['profit_amount']:.8f} {msg['stake_currency']}" f"{msg['profit_extra']})") + is_fill = msg['type'] == RPCMessageType.EXIT_FILL is_sub_trade = msg.get('sub_trade') is_sub_profit = msg['profit_amount'] != msg.get('cumulative_profit') - profit_prefix = ('Sub ' if is_sub_profit - else 'Cumulative ') if is_sub_trade else '' + profit_prefix = ('Sub ' if is_sub_profit else 'Cumulative ') if is_sub_trade else '' cp_extra = '' + if is_sub_profit and is_sub_trade: if self._rpc._fiat_converter: cp_fiat = self._rpc._fiat_converter.convert_amount( msg['cumulative_profit'], msg['stake_currency'], msg['fiat_currency']) cp_extra = f" / {cp_fiat:.3f} {msg['fiat_currency']}" - else: - cp_extra = '' - cp_extra = f"*Cumulative Profit:* (`{msg['cumulative_profit']:.8f} " \ - f"{msg['stake_currency']}{cp_extra}`)\n" + + cp_extra = ( + f"*Cumulative Profit:* (`{msg['cumulative_profit']:.8f} " + f"{msg['stake_currency']}{cp_extra}`)\n" + ) + message = ( f"{msg['emoji']} *{self._exchange_from_msg(msg)}:* " f"{'Exited' if is_fill else 'Exiting'} {msg['pair']} (#{msg['trade_id']})\n" @@ -364,7 +366,7 @@ class Telegram(RPCHandler): elif msg['type'] == RPCMessageType.EXIT_FILL: message += f"*Exit Rate:* `{msg['close_rate']:.8f}`" - if msg.get('sub_trade'): + if is_sub_trade: if self._rpc._fiat_converter: msg['stake_amount_fiat'] = self._rpc._fiat_converter.convert_amount( msg['stake_amount'], msg['stake_currency'], msg['fiat_currency']) @@ -597,7 +599,8 @@ class Telegram(RPCHandler): if r['is_open']: if r.get('realized_profit'): - lines.append("*Realized Profit:* `{realized_profit_r}`") + lines.append( + "*Realized Profit:* `{realized_profit_r} {realized_profit_ratio:.2%}`") lines.append("*Total Profit:* `{total_profit_abs_r}` ") if (r['stop_loss_abs'] != r['initial_stop_loss_abs'] From 548db188571f999c3315302fb5f3dff67765ac9b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 Mar 2023 19:27:55 +0100 Subject: [PATCH 158/360] Improve wording on partial exit notifications --- freqtrade/rpc/telegram.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 175d0ce93..874ae884a 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -333,13 +333,13 @@ class Telegram(RPCHandler): is_sub_profit = msg['profit_amount'] != msg.get('cumulative_profit') profit_prefix = ('Sub ' if is_sub_profit else 'Cumulative ') if is_sub_trade else '' cp_extra = '' - + exit_wording = 'Exited' if is_fill else 'Exiting' if is_sub_profit and is_sub_trade: if self._rpc._fiat_converter: cp_fiat = self._rpc._fiat_converter.convert_amount( msg['cumulative_profit'], msg['stake_currency'], msg['fiat_currency']) cp_extra = f" / {cp_fiat:.3f} {msg['fiat_currency']}" - + exit_wording = f"Partially {exit_wording.lower()}" cp_extra = ( f"*Cumulative Profit:* (`{msg['cumulative_profit']:.8f} " f"{msg['stake_currency']}{cp_extra}`)\n" @@ -347,7 +347,7 @@ class Telegram(RPCHandler): message = ( f"{msg['emoji']} *{self._exchange_from_msg(msg)}:* " - f"{'Exited' if is_fill else 'Exiting'} {msg['pair']} (#{msg['trade_id']})\n" + f"{exit_wording} {msg['pair']} (#{msg['trade_id']})\n" f"{self._add_analyzed_candle(msg['pair'])}" f"*{f'{profit_prefix}Profit' if is_fill else f'Unrealized {profit_prefix}Profit'}:* " f"`{msg['profit_ratio']:.2%}{msg['profit_extra']}`\n" From 60e651b48103571074c9181112279cd7880735b7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 Mar 2023 19:49:37 +0100 Subject: [PATCH 159/360] Updat bybit ohlcv data to v5 --- freqtrade/exchange/bybit.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/freqtrade/exchange/bybit.py b/freqtrade/exchange/bybit.py index c565b891f..6f841b608 100644 --- a/freqtrade/exchange/bybit.py +++ b/freqtrade/exchange/bybit.py @@ -27,11 +27,10 @@ class Bybit(Exchange): """ _ft_has: Dict = { - "ohlcv_candle_limit": 1000, + "ohlcv_candle_limit": 200, "ohlcv_has_history": False, } _ft_has_futures: Dict = { - "ohlcv_candle_limit": 200, "ohlcv_has_history": True, "mark_ohlcv_timeframe": "4h", "funding_fee_timeframe": "8h", From 3f6795962f421710e3a5e5b359c4e6b2ad6a8551 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 Mar 2023 19:49:59 +0100 Subject: [PATCH 160/360] Update bybit orderbook test --- tests/exchange/test_ccxt_compat.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index f06a53308..872cf5059 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -463,7 +463,9 @@ class TestCCXTExchange(): if exchangename == 'gate': # TODO: Gate is unstable here at the moment, ignoring the limit partially. return - for val in [1, 2, 5, 25, 100]: + for val in [1, 2, 5, 25, 50, 100]: + if val > 50 and exchangename == 'bybit': + continue l2 = exch.fetch_l2_order_book(pair, val) if not l2_limit_range or val in l2_limit_range: if val > 50: From c1d395a7d88f6f33daca91a6885c0de17f3333a2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 Mar 2023 20:02:20 +0100 Subject: [PATCH 161/360] Revert "Bump ccxt to 2.8.88" This reverts commit 51c15d894b4ce98583875c37c149a0e3ad137909. --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index efd49a7f3..6b1c888b8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.24.2 pandas==1.5.3 pandas-ta==0.3.14b -ccxt==2.8.88 +ccxt==2.8.54 cryptography==39.0.1 aiohttp==3.8.4 SQLAlchemy==2.0.4 From 7c0c98a36881b339243c5f49a86231a63c4788d8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 Mar 2023 20:08:20 +0100 Subject: [PATCH 162/360] Properly format first entry value, too. --- freqtrade/rpc/telegram.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 874ae884a..fdfad902c 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -488,7 +488,9 @@ class Telegram(RPCHandler): if order_nr == 1: lines.append(f"*{wording} #{order_nr}:*") lines.append( - f"*Amount:* {cur_entry_amount} ({order['cost']:.8f} {quote_currency})") + f"*Amount:* {cur_entry_amount} " + f"({round_coin_value(order['cost'], quote_currency)})" + ) lines.append(f"*Average Price:* {cur_entry_average}") else: sum_stake = 0 From 9444bbb6f3e98611c01968e3f6f01fa3c8b4285f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 Mar 2023 20:09:39 +0100 Subject: [PATCH 163/360] `/maxentries` should be in single tics. --- freqtrade/rpc/telegram.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index fdfad902c..1a96b1671 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -586,7 +586,7 @@ class Telegram(RPCHandler): if position_adjust: max_buy_str = (f"/{max_entries + 1}" if (max_entries > 0) else "") - lines.append("*Number of Entries:* `{num_entries}`" + max_buy_str) + lines.append("*Number of Entries:* `{num_entries}" + max_buy_str + "`") lines.append("*Number of Exits:* `{num_exits}`") lines.extend([ From 108a578772564b440a856a2268560de32fe2d5ec Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 Mar 2023 20:17:19 +0100 Subject: [PATCH 164/360] Update tests to latest rpc changes --- tests/rpc/test_rpc_apiserver.py | 2 ++ tests/rpc/test_rpc_telegram.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index f898dd476..e140a43f1 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -1013,6 +1013,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets, is_short, 'total_profit_abs': ANY, 'total_profit_fiat': ANY, 'realized_profit': 0.0, + 'realized_profit_ratio': None, 'current_rate': current_rate, 'open_date': ANY, 'open_timestamp': ANY, @@ -1243,6 +1244,7 @@ def test_api_force_entry(botclient, mocker, fee, endpoint): 'profit_abs': None, 'profit_fiat': None, 'realized_profit': 0.0, + 'realized_profit_ratio': None, 'fee_close': 0.0025, 'fee_close_cost': None, 'fee_close_currency': None, diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 26cb93821..69d0f805d 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -2012,7 +2012,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: 'sub_trade': True, }) assert msg_mock.call_args[0][0] == ( - '\N{WARNING SIGN} *Binance (dry):* Exiting KEY/ETH (#1)\n' + '\N{WARNING SIGN} *Binance (dry):* Partially exiting KEY/ETH (#1)\n' '*Unrealized Sub Profit:* `-57.41% (loss: -0.05746268 ETH / -24.812 USD)`\n' '*Cumulative Profit:* (`-0.15746268 ETH / -24.812 USD`)\n' '*Enter Tag:* `buy_signal1`\n' From d80760d20c9b663f64bcd530ed2e4c84d7a7ab2e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 5 Mar 2023 14:16:53 +0100 Subject: [PATCH 165/360] bump ccxt to 2.8.98 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 6b1c888b8..a6b3ddd51 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.24.2 pandas==1.5.3 pandas-ta==0.3.14b -ccxt==2.8.54 +ccxt==2.8.98 cryptography==39.0.1 aiohttp==3.8.4 SQLAlchemy==2.0.4 From d0d6f53dec489d022fd301375bf4478646b0a41f Mon Sep 17 00:00:00 2001 From: hippocritical Date: Sun, 5 Mar 2023 16:19:26 +0100 Subject: [PATCH 166/360] fixed github formatting errors --- freqtrade/commands/strategy_utils_commands.py | 6 +++--- freqtrade/strategy/strategyupdater.py | 7 +++---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/freqtrade/commands/strategy_utils_commands.py b/freqtrade/commands/strategy_utils_commands.py index 0124b73b6..aca368742 100644 --- a/freqtrade/commands/strategy_utils_commands.py +++ b/freqtrade/commands/strategy_utils_commands.py @@ -1,6 +1,6 @@ import logging -import os import time +from pathlib import Path from typing import Any, Dict from freqtrade.configuration import setup_utils_configuration @@ -45,11 +45,11 @@ def start_strategy_update(args: Dict[str, Any]) -> None: def start_conversion(strategy_obj, config): # try: - print(f"Conversion of {os.path.basename(strategy_obj['location'])} started.") + print(f"Conversion of {Path(strategy_obj['location']).name} started.") instance_strategy_updater = StrategyUpdater() start = time.perf_counter() instance_strategy_updater.start(config, strategy_obj) elapsed = time.perf_counter() - start - print(f"Conversion of {os.path.basename(strategy_obj['location'])} took {elapsed:.1f} seconds.") + print(f"Conversion of {Path(strategy_obj['location']).name} took {elapsed:.1f} seconds.") # except: # pass diff --git a/freqtrade/strategy/strategyupdater.py b/freqtrade/strategy/strategyupdater.py index db19d4fba..6fe1f326c 100644 --- a/freqtrade/strategy/strategyupdater.py +++ b/freqtrade/strategy/strategyupdater.py @@ -1,4 +1,3 @@ -import os import shutil from pathlib import Path @@ -55,10 +54,10 @@ class StrategyUpdater: target_file = Path.joinpath(strategies_backup_folder, strategy_obj['location_rel']) # read the file - with open(source_file, 'r') as f: + with Path(source_file).open('r') as f: old_code = f.read() if not strategies_backup_folder.is_dir(): - os.makedirs(strategies_backup_folder) + Path(strategies_backup_folder).mkdir(parents=True, exist_ok=True) # backup original # => currently no date after the filename, @@ -69,7 +68,7 @@ class StrategyUpdater: # update the code new_code = self.update_code(old_code) # write the modified code to the destination folder - with open(source_file, 'w') as f: + with Path(source_file).open('w') as f: f.write(new_code) # define the function to update the code From b072fae507200321d2a4cb3ce81f27e2eb879d34 Mon Sep 17 00:00:00 2001 From: hippocritical Date: Sun, 5 Mar 2023 18:48:32 +0100 Subject: [PATCH 167/360] added strategy-updater compartment inside utils.md --- docs/utils.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/utils.md b/docs/utils.md index 87c7f6aa6..3d6eda3ce 100644 --- a/docs/utils.md +++ b/docs/utils.md @@ -947,7 +947,6 @@ Common arguments: --userdir PATH, --user-data-dir PATH Path to userdata directory. ``` - ### Examples Print trades with id 2 and 3 as json @@ -955,3 +954,13 @@ Print trades with id 2 and 3 as json ``` bash freqtrade show-trades --db-url sqlite:///tradesv3.sqlite --trade-ids 2 3 --print-json ``` + +### Strategy-Updater +Updates a list strategies or all strategies within the strategies folder to be v3 compliant including futures. +If the command runs without --strategy-list then all files inside the strategies folder will be converted. +``` +usage: freqtrade strategy_updater + +optional arguments: + --strategy-list defines a list of strategies that should be converted +``` From 0bdd238d7ff2ebf748b705f0cb239767925eeade Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Mar 2023 03:56:37 +0000 Subject: [PATCH 168/360] Bump orjson from 3.8.6 to 3.8.7 Bumps [orjson](https://github.com/ijl/orjson) from 3.8.6 to 3.8.7. - [Release notes](https://github.com/ijl/orjson/releases) - [Changelog](https://github.com/ijl/orjson/blob/master/CHANGELOG.md) - [Commits](https://github.com/ijl/orjson/compare/3.8.6...3.8.7) --- updated-dependencies: - dependency-name: orjson 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 a6b3ddd51..8f5c2f46d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -28,7 +28,7 @@ py_find_1st==1.1.5 # Load ticker files 30% faster python-rapidjson==1.9 # Properly format api responses -orjson==3.8.6 +orjson==3.8.7 # Notify systemd sdnotify==0.3.2 From f4c17be8dec5dd176a73fa5fb07d752e79443087 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Mar 2023 03:56:44 +0000 Subject: [PATCH 169/360] Bump ruff from 0.0.253 to 0.0.254 Bumps [ruff](https://github.com/charliermarsh/ruff) from 0.0.253 to 0.0.254. - [Release notes](https://github.com/charliermarsh/ruff/releases) - [Changelog](https://github.com/charliermarsh/ruff/blob/main/BREAKING_CHANGES.md) - [Commits](https://github.com/charliermarsh/ruff/compare/v0.0.253...v0.0.254) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 2ba004f8d..4c009fc42 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -7,7 +7,7 @@ -r docs/requirements-docs.txt coveralls==3.3.1 -ruff==0.0.253 +ruff==0.0.254 mypy==1.0.1 pre-commit==3.1.1 pytest==7.2.1 From 8484427cf879a4dae7aee02a504811d6c7e99b75 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Mar 2023 03:56:54 +0000 Subject: [PATCH 170/360] Bump cryptography from 39.0.1 to 39.0.2 Bumps [cryptography](https://github.com/pyca/cryptography) from 39.0.1 to 39.0.2. - [Release notes](https://github.com/pyca/cryptography/releases) - [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pyca/cryptography/compare/39.0.1...39.0.2) --- updated-dependencies: - dependency-name: cryptography 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 a6b3ddd51..1f22bc033 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ pandas==1.5.3 pandas-ta==0.3.14b ccxt==2.8.98 -cryptography==39.0.1 +cryptography==39.0.2 aiohttp==3.8.4 SQLAlchemy==2.0.4 python-telegram-bot==13.15 From 57969f8b0164579df9f5bd1b80504598a2b84e70 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Mar 2023 03:56:58 +0000 Subject: [PATCH 171/360] Bump prompt-toolkit from 3.0.37 to 3.0.38 Bumps [prompt-toolkit](https://github.com/prompt-toolkit/python-prompt-toolkit) from 3.0.37 to 3.0.38. - [Release notes](https://github.com/prompt-toolkit/python-prompt-toolkit/releases) - [Changelog](https://github.com/prompt-toolkit/python-prompt-toolkit/blob/master/CHANGELOG) - [Commits](https://github.com/prompt-toolkit/python-prompt-toolkit/compare/3.0.37...3.0.38) --- updated-dependencies: - dependency-name: prompt-toolkit dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a6b3ddd51..efd858f45 100644 --- a/requirements.txt +++ b/requirements.txt @@ -45,7 +45,7 @@ psutil==5.9.4 colorama==0.4.6 # Building config files interactively questionary==1.10.0 -prompt-toolkit==3.0.37 +prompt-toolkit==3.0.38 # Extensions to datetime library python-dateutil==2.8.2 From d1d9e25c2e8707edbbc905f53e7722bfe5b5af4b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Mar 2023 03:57:03 +0000 Subject: [PATCH 172/360] Bump mkdocs-material from 9.0.15 to 9.1.1 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 9.0.15 to 9.1.1. - [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/9.0.15...9.1.1) --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:production update-type: version-update:semver-minor ... 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 065411018..2ca11dabf 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,6 +1,6 @@ markdown==3.3.7 mkdocs==1.4.2 -mkdocs-material==9.0.15 +mkdocs-material==9.1.1 mdx_truly_sane_lists==1.3 pymdown-extensions==9.9.2 jinja2==3.1.2 From 48e16f6aba7a4d2ed33958a473144e62e3cac32d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Mar 2023 03:57:18 +0000 Subject: [PATCH 173/360] Bump sqlalchemy from 2.0.4 to 2.0.5.post1 Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 2.0.4 to 2.0.5.post1. - [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases) - [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/main/CHANGES.rst) - [Commits](https://github.com/sqlalchemy/sqlalchemy/commits) --- updated-dependencies: - dependency-name: sqlalchemy dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a6b3ddd51..6eeecfde6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ pandas-ta==0.3.14b ccxt==2.8.98 cryptography==39.0.1 aiohttp==3.8.4 -SQLAlchemy==2.0.4 +SQLAlchemy==2.0.5.post1 python-telegram-bot==13.15 arrow==1.2.3 cachetools==4.2.2 From a57b033745558bc6e61de9704dfab3e2e5f20e1a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Mar 2023 03:57:27 +0000 Subject: [PATCH 174/360] Bump types-python-dateutil from 2.8.19.9 to 2.8.19.10 Bumps [types-python-dateutil](https://github.com/python/typeshed) from 2.8.19.9 to 2.8.19.10. - [Release notes](https://github.com/python/typeshed/releases) - [Commits](https://github.com/python/typeshed/commits) --- updated-dependencies: - dependency-name: types-python-dateutil dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 2ba004f8d..a945ffc63 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -29,4 +29,4 @@ types-cachetools==5.3.0.4 types-filelock==3.2.7 types-requests==2.28.11.15 types-tabulate==0.9.0.1 -types-python-dateutil==2.8.19.9 +types-python-dateutil==2.8.19.10 From 9750e9ca4ef54d11f4525b79ea3ff9664b4a43e5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 6 Mar 2023 06:32:33 +0100 Subject: [PATCH 175/360] pre-commit python-dateutil --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 565eb96f7..402e5641d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: - types-filelock==3.2.7 - types-requests==2.28.11.15 - types-tabulate==0.9.0.1 - - types-python-dateutil==2.8.19.9 + - types-python-dateutil==2.8.19.10 - SQLAlchemy==2.0.4 # stages: [push] From 25fd4a04d6460ed2a00af144d7fcb291669a1943 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 6 Mar 2023 06:34:37 +0100 Subject: [PATCH 176/360] Update sqlalchemy QueryPropertyDescriptor to match latest version --- .pre-commit-config.yaml | 2 +- freqtrade/persistence/pairlock.py | 5 ++--- freqtrade/persistence/trade_model.py | 8 ++++---- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 565eb96f7..895916c15 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,7 +18,7 @@ repos: - types-requests==2.28.11.15 - types-tabulate==0.9.0.1 - types-python-dateutil==2.8.19.9 - - SQLAlchemy==2.0.4 + - SQLAlchemy==2.0.5.post1 # stages: [push] - repo: https://github.com/pycqa/isort diff --git a/freqtrade/persistence/pairlock.py b/freqtrade/persistence/pairlock.py index a6d1eeaf0..1e5699145 100644 --- a/freqtrade/persistence/pairlock.py +++ b/freqtrade/persistence/pairlock.py @@ -2,8 +2,7 @@ from datetime import datetime, timezone from typing import Any, ClassVar, Dict, Optional from sqlalchemy import String, or_ -from sqlalchemy.orm import Mapped, Query, mapped_column -from sqlalchemy.orm.scoping import _QueryDescriptorType +from sqlalchemy.orm import Mapped, Query, QueryPropertyDescriptor, mapped_column from freqtrade.constants import DATETIME_PRINT_FORMAT from freqtrade.persistence.base import ModelBase, SessionType @@ -14,7 +13,7 @@ class PairLock(ModelBase): Pair Locks database model. """ __tablename__ = 'pairlocks' - query: ClassVar[_QueryDescriptorType] + query: ClassVar[QueryPropertyDescriptor] _session: ClassVar[SessionType] id: Mapped[int] = mapped_column(primary_key=True) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 21fe80819..8e8a414c8 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -8,8 +8,8 @@ from math import isclose from typing import Any, ClassVar, Dict, List, Optional, cast from sqlalchemy import Enum, Float, ForeignKey, Integer, String, UniqueConstraint, desc, func -from sqlalchemy.orm import Mapped, Query, lazyload, mapped_column, relationship -from sqlalchemy.orm.scoping import _QueryDescriptorType +from sqlalchemy.orm import (Mapped, Query, QueryPropertyDescriptor, lazyload, mapped_column, + relationship) from freqtrade.constants import (DATETIME_PRINT_FORMAT, MATH_CLOSE_PREC, NON_OPEN_EXCHANGE_STATES, BuySell, LongShort) @@ -36,7 +36,7 @@ class Order(ModelBase): Mirrors CCXT Order structure """ __tablename__ = 'orders' - query: ClassVar[_QueryDescriptorType] + query: ClassVar[QueryPropertyDescriptor] _session: ClassVar[SessionType] # Uniqueness should be ensured over pair, order_id @@ -1181,7 +1181,7 @@ class Trade(ModelBase, LocalTrade): Note: Fields must be aligned with LocalTrade class """ __tablename__ = 'trades' - query: ClassVar[_QueryDescriptorType] + query: ClassVar[QueryPropertyDescriptor] _session: ClassVar[SessionType] use_db: bool = True From 0fe72510d5cbaa8389070cf75e1e4cedf880008d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Mar 2023 05:36:16 +0000 Subject: [PATCH 177/360] Bump pymdown-extensions from 9.9.2 to 9.10 Bumps [pymdown-extensions](https://github.com/facelessuser/pymdown-extensions) from 9.9.2 to 9.10. - [Release notes](https://github.com/facelessuser/pymdown-extensions/releases) - [Commits](https://github.com/facelessuser/pymdown-extensions/compare/9.9.2...9.10) --- updated-dependencies: - dependency-name: pymdown-extensions dependency-type: direct:production update-type: version-update:semver-minor ... 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 2ca11dabf..1b9a1f9b7 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -2,5 +2,5 @@ markdown==3.3.7 mkdocs==1.4.2 mkdocs-material==9.1.1 mdx_truly_sane_lists==1.3 -pymdown-extensions==9.9.2 +pymdown-extensions==9.10 jinja2==3.1.2 From de015a2d7e23e4d006e1cd84c7e12e1cd2378dad Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 5 Mar 2023 18:04:30 +0100 Subject: [PATCH 178/360] Improve telegram message formatting --- freqtrade/rpc/telegram.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 1a96b1671..e8a8a941f 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -510,14 +510,14 @@ class Telegram(RPCHandler): if prev_avg_price: minus_on_entry = (cur_entry_average - prev_avg_price) / prev_avg_price - lines.append(f"*{wording} #{order_nr}:* at {minus_on_entry:.2%} avg profit") + lines.append(f"*{wording} #{order_nr}:* at {minus_on_entry:.2%} avg Profit") if is_open: lines.append("({})".format(cur_entry_datetime .humanize(granularity=["day", "hour", "minute"]))) lines.append(f"*Amount:* {cur_entry_amount} " f"({round_coin_value(order['cost'], quote_currency)})") lines.append(f"*Average {wording} Price:* {cur_entry_average} " - f"({price_to_1st_entry:.2%} from 1st entry rate)") + f"({price_to_1st_entry:.2%} from 1st entry Rate)") lines.append(f"*Order filled:* {order['order_filled_date']}") # TODO: is this really useful? From 11eea9b4e1cef0a9bfceb27b37cfe8b7a10cc89f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 5 Mar 2023 18:05:42 +0100 Subject: [PATCH 179/360] Fix formatting for /status Realized profit --- freqtrade/rpc/telegram.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index e8a8a941f..7452005db 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -602,7 +602,7 @@ class Telegram(RPCHandler): if r['is_open']: if r.get('realized_profit'): lines.append( - "*Realized Profit:* `{realized_profit_r} {realized_profit_ratio:.2%}`") + "*Realized Profit:* `{realized_profit_ratio:.2%} ({realized_profit_r})`") lines.append("*Total Profit:* `{total_profit_abs_r}` ") if (r['stop_loss_abs'] != r['initial_stop_loss_abs'] From ca789b3282926c3e40f2be1e905034be001c2abb Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 5 Mar 2023 18:11:10 +0100 Subject: [PATCH 180/360] /status - whitespace --- freqtrade/rpc/telegram.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 7452005db..2e87eabc9 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -594,7 +594,7 @@ class Telegram(RPCHandler): "*Close Rate:* `{close_rate:.8f}`" if r['close_rate'] else "", "*Open Date:* `{open_date}`", "*Close Date:* `{close_date}`" if r['close_date'] else "", - "*Current Rate:* `{current_rate:.8f}`" if r['is_open'] else "", + "\n*Current Rate:* `{current_rate:.8f}`" if r['is_open'] else "", ("*Unrealized Profit:* " if r['is_open'] else "*Close Profit: *") + "`{profit_ratio:.2%}` `({profit_abs_r})`", ]) @@ -605,6 +605,8 @@ class Telegram(RPCHandler): "*Realized Profit:* `{realized_profit_ratio:.2%} ({realized_profit_r})`") lines.append("*Total Profit:* `{total_profit_abs_r}` ") + # Append empty line to improve readability + lines.append(" ") if (r['stop_loss_abs'] != r['initial_stop_loss_abs'] and r['initial_stop_loss_ratio'] is not None): # Adding initial stoploss only if it is different from stoploss From fff08f737f29471ada06a336c4b0aefe0ec6c621 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 5 Mar 2023 19:17:39 +0100 Subject: [PATCH 181/360] /status msg - improve formatting further --- freqtrade/rpc/telegram.py | 11 ++++++++--- tests/rpc/test_rpc_telegram.py | 1 + 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 2e87eabc9..19027f4d5 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -569,6 +569,8 @@ class Telegram(RPCHandler): and not o['ft_order_side'] == 'stoploss']) r['exit_reason'] = r.get('exit_reason', "") r['stake_amount_r'] = round_coin_value(r['stake_amount'], r['quote_currency']) + r['max_stake_amount_r'] = round_coin_value( + r['max_stake_amount'] or r['stake_amount'], r['quote_currency']) r['profit_abs_r'] = round_coin_value(r['profit_abs'], r['quote_currency']) r['realized_profit_r'] = round_coin_value(r['realized_profit'], r['quote_currency']) r['total_profit_abs_r'] = round_coin_value( @@ -580,21 +582,24 @@ class Telegram(RPCHandler): f"*Direction:* {'`Short`' if r.get('is_short') else '`Long`'}" + " ` ({leverage}x)`" if r.get('leverage') else "", "*Amount:* `{amount} ({stake_amount_r})`", + "*Total invested:* `{max_stake_amount_r}`" if position_adjust else "", "*Enter Tag:* `{enter_tag}`" if r['enter_tag'] else "", "*Exit Reason:* `{exit_reason}`" if r['exit_reason'] else "", ] if position_adjust: max_buy_str = (f"/{max_entries + 1}" if (max_entries > 0) else "") - lines.append("*Number of Entries:* `{num_entries}" + max_buy_str + "`") - lines.append("*Number of Exits:* `{num_exits}`") + lines.extend([ + "*Number of Entries:* `{num_entries}" + max_buy_str + "`", + "*Number of Exits:* `{num_exits}`" + ]) lines.extend([ "*Open Rate:* `{open_rate:.8f}`", "*Close Rate:* `{close_rate:.8f}`" if r['close_rate'] else "", "*Open Date:* `{open_date}`", "*Close Date:* `{close_date}`" if r['close_date'] else "", - "\n*Current Rate:* `{current_rate:.8f}`" if r['is_open'] else "", + " \n*Current Rate:* `{current_rate:.8f}`" if r['is_open'] else "", ("*Unrealized Profit:* " if r['is_open'] else "*Close Profit: *") + "`{profit_ratio:.2%}` `({profit_abs_r})`", ]) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 69d0f805d..1dc255b3e 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -198,6 +198,7 @@ def test_telegram_status(default_conf, update, mocker) -> None: 'current_rate': 1.098e-05, 'amount': 90.99181074, 'stake_amount': 90.99181074, + 'max_stake_amount': 90.99181074, 'buy_tag': None, 'enter_tag': None, 'close_profit_ratio': None, From 9d285e3dc04e1b4c2c0ae1d01a17198f4f0cac86 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 5 Mar 2023 19:35:06 +0100 Subject: [PATCH 182/360] Add total_profit_ratio to telegram output part of #8234 --- freqtrade/rpc/rpc.py | 6 ++++++ freqtrade/rpc/telegram.py | 7 ++++--- tests/rpc/test_rpc.py | 2 ++ 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 8692c477f..4b3f6209e 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -192,6 +192,11 @@ class RPC: current_profit = trade.close_profit or 0.0 current_profit_abs = trade.close_profit_abs or 0.0 total_profit_abs = trade.realized_profit + current_profit_abs + total_profit_ratio = 0.0 + if trade.max_stake_amount: + total_profit_ratio = ( + (total_profit_abs / trade.max_stake_amount) * trade.leverage + ) # Calculate fiat profit if not isnan(current_profit_abs) and self._fiat_converter: @@ -224,6 +229,7 @@ class RPC: total_profit_abs=total_profit_abs, total_profit_fiat=total_profit_fiat, + total_profit_ratio=total_profit_ratio, stoploss_current_dist=stoploss_current_dist, stoploss_current_dist_ratio=round(stoploss_current_dist_ratio, 8), stoploss_current_dist_pct=round(stoploss_current_dist_ratio * 100, 2), diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 19027f4d5..ced1bb5ca 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -606,9 +606,10 @@ class Telegram(RPCHandler): if r['is_open']: if r.get('realized_profit'): - lines.append( - "*Realized Profit:* `{realized_profit_ratio:.2%} ({realized_profit_r})`") - lines.append("*Total Profit:* `{total_profit_abs_r}` ") + lines.extend([ + "*Realized Profit:* `{realized_profit_ratio:.2%} ({realized_profit_r})`" + "*Total Profit:* `{total_profit_ratio:.2%} ({total_profit_abs_r})`" + ]) # Append empty line to improve readability lines.append(" ") diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index cd72da763..cf7d2bdeb 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -79,6 +79,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'realized_profit_ratio': None, 'total_profit_abs': -4.09e-06, 'total_profit_fiat': ANY, + 'total_profit_ratio': ANY, 'exchange': 'binance', 'leverage': 1.0, 'interest_rate': 0.0, @@ -185,6 +186,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'profit_pct': ANY, 'profit_abs': ANY, 'total_profit_abs': ANY, + 'total_profit_ratio': ANY, 'current_rate': ANY, }) assert results[0] == response_norate From cab1b750b35f4d356d64206c2b20aa2cb338f167 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 5 Mar 2023 19:45:04 +0100 Subject: [PATCH 183/360] Improve test accuracy --- freqtrade/rpc/rpc.py | 2 +- tests/rpc/test_rpc.py | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 4b3f6209e..c68ed2d48 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -192,7 +192,7 @@ class RPC: current_profit = trade.close_profit or 0.0 current_profit_abs = trade.close_profit_abs or 0.0 total_profit_abs = trade.realized_profit + current_profit_abs - total_profit_ratio = 0.0 + total_profit_ratio: Optional[float] = None if trade.max_stake_amount: total_profit_ratio = ( (total_profit_abs / trade.max_stake_amount) * trade.leverage diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index cf7d2bdeb..1a1802c68 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -50,7 +50,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'amount': 91.07468123, 'amount_requested': 91.07468124, 'stake_amount': 0.001, - 'max_stake_amount': ANY, + 'max_stake_amount': None, 'trade_duration': None, 'trade_duration_s': None, 'close_profit': None, @@ -79,7 +79,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'realized_profit_ratio': None, 'total_profit_abs': -4.09e-06, 'total_profit_fiat': ANY, - 'total_profit_ratio': ANY, + 'total_profit_ratio': None, 'exchange': 'binance', 'leverage': 1.0, 'interest_rate': 0.0, @@ -169,6 +169,10 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: results = rpc._rpc_trade_status() response = deepcopy(gen_response) + response.update({ + 'max_stake_amount': 0.001, + 'total_profit_ratio': pytest.approx(-0.00409), + }) assert results[0] == response mocker.patch(f'{EXMS}.get_rate', @@ -182,6 +186,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'stoploss_current_dist': ANY, 'stoploss_current_dist_ratio': ANY, 'stoploss_current_dist_pct': ANY, + 'max_stake_amount': 0.001, 'profit_ratio': ANY, 'profit_pct': ANY, 'profit_abs': ANY, From c4a80e33ea3e647fab91efd4ace1c8de3f4afc6b Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 6 Mar 2023 07:01:17 +0100 Subject: [PATCH 184/360] Fix missing newline in telegram /status --- freqtrade/rpc/telegram.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index ced1bb5ca..30aa55359 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -607,7 +607,7 @@ class Telegram(RPCHandler): if r['is_open']: if r.get('realized_profit'): lines.extend([ - "*Realized Profit:* `{realized_profit_ratio:.2%} ({realized_profit_r})`" + "*Realized Profit:* `{realized_profit_ratio:.2%} ({realized_profit_r})`", "*Total Profit:* `{total_profit_ratio:.2%} ({total_profit_abs_r})`" ]) From d779d60812f29553d029182e7b8b7907c2d5fea0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 6 Mar 2023 07:10:02 +0100 Subject: [PATCH 185/360] Expose total_profit_ratio through API --- freqtrade/rpc/api_server/api_schemas.py | 1 + freqtrade/rpc/api_server/api_v1.py | 3 ++- tests/rpc/test_rpc_apiserver.py | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index a751179b2..064a509fd 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -286,6 +286,7 @@ class OpenTradeSchema(TradeSchema): current_rate: float total_profit_abs: float total_profit_fiat: Optional[float] + total_profit_ratio: Optional[float] open_order: Optional[str] diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index f6bab3624..8ea70bb69 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -42,7 +42,8 @@ logger = logging.getLogger(__name__) # 2.22: Add FreqAI to backtesting # 2.23: Allow plot config request in webserver mode # 2.24: Add cancel_open_order endpoint -API_VERSION = 2.24 +# 2.25: Add several profit values to /status endpoint +API_VERSION = 2.25 # Public API, requires no auth. router_public = APIRouter() diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index e140a43f1..9c2c3ee3a 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -1012,6 +1012,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets, is_short, 'profit_fiat': ANY, 'total_profit_abs': ANY, 'total_profit_fiat': ANY, + 'total_profit_ratio': ANY, 'realized_profit': 0.0, 'realized_profit_ratio': None, 'current_rate': current_rate, From 85e64cd1215b4ef2e0d323fe4be08db90d1d884c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Mar 2023 07:21:23 +0000 Subject: [PATCH 186/360] Bump ccxt from 2.8.98 to 2.9.4 Bumps [ccxt](https://github.com/ccxt/ccxt) from 2.8.98 to 2.9.4. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/CHANGELOG.md) - [Commits](https://github.com/ccxt/ccxt/compare/2.8.98...2.9.4) --- updated-dependencies: - dependency-name: ccxt dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a702507f9..c972ae1d4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.24.2 pandas==1.5.3 pandas-ta==0.3.14b -ccxt==2.8.98 +ccxt==2.9.4 cryptography==39.0.2 aiohttp==3.8.4 SQLAlchemy==2.0.5.post1 From d9dc831772e67b3b04f26c9d160152c19940f5b9 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Tue, 7 Mar 2023 11:33:54 +0100 Subject: [PATCH 187/360] allow user to drop ohlc from features in RL --- docs/freqai-parameter-table.md | 1 + docs/freqai-reinforcement-learning.md | 4 +++- freqtrade/constants.py | 1 + .../freqai/RL/BaseReinforcementLearningModel.py | 16 +++++++++------- tests/freqai/conftest.py | 4 +++- tests/freqai/test_freqai_interface.py | 8 +------- 6 files changed, 18 insertions(+), 16 deletions(-) diff --git a/docs/freqai-parameter-table.md b/docs/freqai-parameter-table.md index 275062a33..f67ea8541 100644 --- a/docs/freqai-parameter-table.md +++ b/docs/freqai-parameter-table.md @@ -84,6 +84,7 @@ Mandatory parameters are marked as **Required** and have to be set in one of the | `add_state_info` | Tell FreqAI to include state information in the feature set for training and inferencing. The current state variables include trade duration, current profit, trade position. This is only available in dry/live runs, and is automatically switched to false for backtesting.
**Datatype:** bool.
Default: `False`. | `net_arch` | Network architecture which is well described in [`stable_baselines3` doc](https://stable-baselines3.readthedocs.io/en/master/guide/custom_policy.html#examples). In summary: `[, dict(vf=[], pi=[])]`. By default this is set to `[128, 128]`, which defines 2 shared hidden layers with 128 units each. | `randomize_starting_position` | Randomize the starting point of each episode to avoid overfitting.
**Datatype:** bool.
Default: `False`. +| `drop_ohlc_from_features` | Do not include the normalized ohlc data in the feature set passed to the agent during training (ohlc will still be used for driving the environment in all cases)
**Datatype:** Boolean.
**Default:** `False` ### Additional parameters diff --git a/docs/freqai-reinforcement-learning.md b/docs/freqai-reinforcement-learning.md index 3810aec4e..04ca42a5d 100644 --- a/docs/freqai-reinforcement-learning.md +++ b/docs/freqai-reinforcement-learning.md @@ -176,9 +176,11 @@ As you begin to modify the strategy and the prediction model, you will quickly r factor = 100 + pair = self.pair.replace(':', '') + # you can use feature values from dataframe # Assumes the shifted RSI indicator has been generated in the strategy. - rsi_now = self.raw_features[f"%-rsi-period-10_shift-1_{self.pair}_" + rsi_now = self.raw_features[f"%-rsi-period-10_shift-1_{pair}_" f"{self.config['timeframe']}"].iloc[self._current_tick] # reward agent for entering trades diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 1727da92e..46e9b5cd4 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -588,6 +588,7 @@ CONF_SCHEMA = { "rl_config": { "type": "object", "properties": { + "drop_ohlc_from_features": {"type": "boolean", "default": False}, "train_cycles": {"type": "integer"}, "max_trade_duration_candles": {"type": "integer"}, "add_state_info": {"type": "boolean", "default": False}, diff --git a/freqtrade/freqai/RL/BaseReinforcementLearningModel.py b/freqtrade/freqai/RL/BaseReinforcementLearningModel.py index a8ef69394..bac717f9f 100644 --- a/freqtrade/freqai/RL/BaseReinforcementLearningModel.py +++ b/freqtrade/freqai/RL/BaseReinforcementLearningModel.py @@ -114,6 +114,7 @@ class BaseReinforcementLearningModel(IFreqaiModel): # normalize all data based on train_dataset only prices_train, prices_test = self.build_ohlc_price_dataframes(dk.data_dictionary, pair, dk) + data_dictionary = dk.normalize_data(data_dictionary) # data cleaning/analysis @@ -148,12 +149,8 @@ class BaseReinforcementLearningModel(IFreqaiModel): env_info = self.pack_env_dict(dk.pair) - self.train_env = self.MyRLEnv(df=train_df, - prices=prices_train, - **env_info) - self.eval_env = Monitor(self.MyRLEnv(df=test_df, - prices=prices_test, - **env_info)) + self.train_env = self.MyRLEnv(df=train_df, prices=prices_train, **env_info) + self.eval_env = Monitor(self.MyRLEnv(df=test_df, prices=prices_test, **env_info)) self.eval_callback = EvalCallback(self.eval_env, deterministic=True, render=False, eval_freq=len(train_df), best_model_save_path=str(dk.data_path)) @@ -285,7 +282,6 @@ class BaseReinforcementLearningModel(IFreqaiModel): train_df = data_dictionary["train_features"] test_df = data_dictionary["test_features"] - # %-raw_volume_gen_shift-2_ETH/USDT_1h # price data for model training and evaluation tf = self.config['timeframe'] rename_dict = {'%-raw_open': 'open', '%-raw_low': 'low', @@ -318,6 +314,12 @@ class BaseReinforcementLearningModel(IFreqaiModel): prices_test.rename(columns=rename_dict, inplace=True) prices_test.reset_index(drop=True) + if self.rl_config["drop_ohlc_from_features"]: + train_df.drop(rename_dict.keys(), axis=1, inplace=True) + test_df.drop(rename_dict.keys(), axis=1, inplace=True) + feature_list = dk.training_features_list + feature_list = [e for e in feature_list if e not in rename_dict.keys()] + return prices_train, prices_test def load_model_from_disk(self, dk: FreqaiDataKitchen) -> Any: diff --git a/tests/freqai/conftest.py b/tests/freqai/conftest.py index 68e7ea49a..e140ee80b 100644 --- a/tests/freqai/conftest.py +++ b/tests/freqai/conftest.py @@ -78,7 +78,9 @@ def make_rl_config(conf): "rr": 1, "profit_aim": 0.02, "win_reward_factor": 2 - }} + }, + "drop_ohlc_from_features": False + } return conf diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index f8bee3659..3b370aea4 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -68,13 +68,6 @@ def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca, freqai_conf['freqai']['feature_parameters'].update({"shuffle_after_split": shuffle}) freqai_conf['freqai']['feature_parameters'].update({"buffer_train_data_candles": buffer}) - if 'ReinforcementLearner' in model: - model_save_ext = 'zip' - freqai_conf = make_rl_config(freqai_conf) - # test the RL guardrails - freqai_conf['freqai']['feature_parameters'].update({"use_SVM_to_remove_outliers": True}) - freqai_conf['freqai']['data_split_parameters'].update({'shuffle': True}) - if 'ReinforcementLearner' in model: model_save_ext = 'zip' freqai_conf = make_rl_config(freqai_conf) @@ -84,6 +77,7 @@ def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca, if 'test_3ac' in model or 'test_4ac' in model: freqai_conf["freqaimodel_path"] = str(Path(__file__).parents[1] / "freqai" / "test_models") + freqai_conf["freqai"]["rl_config"]["drop_ohlc_from_features"] = True strategy = get_patched_freqai_strategy(mocker, freqai_conf) exchange = get_patched_exchange(mocker, freqai_conf) From 2c7ae756f5b4576c7d393d09a6d57563d17be440 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 8 Mar 2023 07:05:59 +0100 Subject: [PATCH 188/360] Improve mock behavior --- tests/optimize/test_backtest_detail.py | 2 +- tests/test_freqtradebot.py | 40 +++++++++++++------------- tests/test_integration.py | 2 +- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index ae06fca1d..2cb42c003 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -924,7 +924,7 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data: BTContainer) mocker.patch(f"{EXMS}.get_fee", return_value=0.0) mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001) mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf')) - mocker.patch('freqtrade.exchange.binance.Binance.get_max_leverage', return_value=100) + mocker.patch(f"{EXMS}.get_max_leverage", return_value=100) patch_exchange(mocker) frame = _build_backtest_dataframe(data.data) backtesting = Backtesting(default_conf) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 5e9cca0f8..06832589c 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1068,7 +1068,7 @@ def test_add_stoploss_on_exchange(mocker, default_conf_usdt, limit_order, is_sho mocker.patch(f'{EXMS}.get_trades_for_order', return_value=[]) stoploss = MagicMock(return_value={'id': 13434334}) - mocker.patch('freqtrade.exchange.binance.Binance.create_stoploss', stoploss) + mocker.patch(f'{EXMS}.create_stoploss', stoploss) freqtrade = FreqtradeBot(default_conf_usdt) freqtrade.strategy.order_types['stoploss_on_exchange'] = True @@ -1263,7 +1263,7 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf_usdt, fee, caplog, get_fee=fee, ) mocker.patch.multiple( - 'freqtrade.exchange.binance.Binance', + EXMS, fetch_stoploss_order=MagicMock(return_value={'status': 'canceled', 'id': 100}), create_stoploss=MagicMock(side_effect=ExchangeError()), ) @@ -1307,7 +1307,7 @@ def test_create_stoploss_order_invalid_order( get_fee=fee, ) mocker.patch.multiple( - 'freqtrade.exchange.binance.Binance', + EXMS, fetch_order=MagicMock(return_value={'status': 'canceled'}), create_stoploss=MagicMock(side_effect=InvalidOrderException()), ) @@ -1360,7 +1360,7 @@ def test_create_stoploss_order_insufficient_funds( fetch_order=MagicMock(return_value={'status': 'canceled'}), ) mocker.patch.multiple( - 'freqtrade.exchange.binance.Binance', + EXMS, create_stoploss=MagicMock(side_effect=InsufficientFundsError()), ) patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) @@ -1410,7 +1410,7 @@ def test_handle_stoploss_on_exchange_trailing( get_fee=fee, ) mocker.patch.multiple( - 'freqtrade.exchange.binance.Binance', + EXMS, create_stoploss=stoploss, stoploss_adjust=MagicMock(return_value=True), ) @@ -1453,7 +1453,7 @@ def test_handle_stoploss_on_exchange_trailing( } }) - mocker.patch('freqtrade.exchange.binance.Binance.fetch_stoploss_order', stoploss_order_hanging) + mocker.patch(f'{EXMS}.fetch_stoploss_order', stoploss_order_hanging) # stoploss initially at 5% assert freqtrade.handle_trade(trade) is False @@ -1471,8 +1471,8 @@ def test_handle_stoploss_on_exchange_trailing( cancel_order_mock = MagicMock() stoploss_order_mock = MagicMock(return_value={'id': 'so1'}) - mocker.patch('freqtrade.exchange.binance.Binance.cancel_stoploss_order', cancel_order_mock) - mocker.patch('freqtrade.exchange.binance.Binance.create_stoploss', stoploss_order_mock) + mocker.patch(f'{EXMS}.cancel_stoploss_order', cancel_order_mock) + mocker.patch(f'{EXMS}.create_stoploss', stoploss_order_mock) # stoploss should not be updated as the interval is 60 seconds assert freqtrade.handle_trade(trade) is False @@ -1535,7 +1535,7 @@ def test_handle_stoploss_on_exchange_trailing_error( get_fee=fee, ) mocker.patch.multiple( - 'freqtrade.exchange.binance.Binance', + EXMS, create_stoploss=stoploss, stoploss_adjust=MagicMock(return_value=True), ) @@ -1573,9 +1573,9 @@ def test_handle_stoploss_on_exchange_trailing_error( 'stopPrice': '0.1' } } - mocker.patch('freqtrade.exchange.binance.Binance.cancel_stoploss_order', + mocker.patch(f'{EXMS}.cancel_stoploss_order', side_effect=InvalidOrderException()) - mocker.patch('freqtrade.exchange.binance.Binance.fetch_stoploss_order', + mocker.patch(f'{EXMS}.fetch_stoploss_order', return_value=stoploss_order_hanging) freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging) assert log_has_re(r"Could not cancel stoploss order abcd for pair ETH/USDT.*", caplog) @@ -1586,8 +1586,8 @@ def test_handle_stoploss_on_exchange_trailing_error( # Fail creating stoploss order trade.stoploss_last_update = arrow.utcnow().shift(minutes=-601).datetime caplog.clear() - cancel_mock = mocker.patch('freqtrade.exchange.binance.Binance.cancel_stoploss_order') - mocker.patch('freqtrade.exchange.binance.Binance.create_stoploss', side_effect=ExchangeError()) + cancel_mock = mocker.patch(f'{EXMS}.cancel_stoploss_order') + mocker.patch(f'{EXMS}.create_stoploss', side_effect=ExchangeError()) freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging) assert cancel_mock.call_count == 1 assert log_has_re(r"Could not create trailing stoploss order for pair ETH/USDT\..*", caplog) @@ -1604,7 +1604,7 @@ def test_stoploss_on_exchange_price_rounding( stoploss_mock = MagicMock(return_value={'id': '13434334'}) adjust_mock = MagicMock(return_value=False) mocker.patch.multiple( - 'freqtrade.exchange.binance.Binance', + EXMS, create_stoploss=stoploss_mock, stoploss_adjust=adjust_mock, price_to_precision=price_mock, @@ -1643,7 +1643,7 @@ def test_handle_stoploss_on_exchange_custom_stop( get_fee=fee, ) mocker.patch.multiple( - 'freqtrade.exchange.binance.Binance', + EXMS, create_stoploss=stoploss, stoploss_adjust=MagicMock(return_value=True), ) @@ -1686,7 +1686,7 @@ def test_handle_stoploss_on_exchange_custom_stop( } }) - mocker.patch('freqtrade.exchange.binance.Binance.fetch_stoploss_order', stoploss_order_hanging) + mocker.patch(f'{EXMS}.fetch_stoploss_order', stoploss_order_hanging) assert freqtrade.handle_trade(trade) is False assert freqtrade.handle_stoploss_on_exchange(trade) is False @@ -1703,8 +1703,8 @@ def test_handle_stoploss_on_exchange_custom_stop( cancel_order_mock = MagicMock() stoploss_order_mock = MagicMock(return_value={'id': 'so1'}) - mocker.patch('freqtrade.exchange.binance.Binance.cancel_stoploss_order', cancel_order_mock) - mocker.patch('freqtrade.exchange.binance.Binance.create_stoploss', stoploss_order_mock) + mocker.patch(f'{EXMS}.cancel_stoploss_order', cancel_order_mock) + mocker.patch(f'{EXMS}.create_stoploss', stoploss_order_mock) # stoploss should not be updated as the interval is 60 seconds assert freqtrade.handle_trade(trade) is False @@ -1821,7 +1821,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, limit_orde cancel_order_mock = MagicMock() stoploss_order_mock = MagicMock() mocker.patch(f'{EXMS}.cancel_stoploss_order', cancel_order_mock) - mocker.patch('freqtrade.exchange.binance.Binance.create_stoploss', stoploss_order_mock) + mocker.patch(f'{EXMS}.create_stoploss', stoploss_order_mock) # price goes down 5% mocker.patch(f'{EXMS}.fetch_ticker', MagicMock(return_value={ @@ -3660,7 +3660,7 @@ def test_may_execute_trade_exit_after_stoploss_on_exchange_hit( } }) - mocker.patch('freqtrade.exchange.binance.Binance.create_stoploss', stoploss) + mocker.patch(f'{EXMS}.create_stoploss', stoploss) freqtrade = FreqtradeBot(default_conf_usdt) freqtrade.strategy.order_types['stoploss_on_exchange'] = True diff --git a/tests/test_integration.py b/tests/test_integration.py index a3dd8d935..4c57c5669 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -56,9 +56,9 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee, [ExitCheckTuple(exit_type=ExitType.EXIT_SIGNAL)]] ) cancel_order_mock = MagicMock() - mocker.patch('freqtrade.exchange.binance.Binance.create_stoploss', stoploss) mocker.patch.multiple( EXMS, + create_stoploss=stoploss, fetch_ticker=ticker, get_fee=fee, amount_to_precision=lambda s, x, y: y, From 29d337fa02be2aa6d79e8be49ea2d5b16c9f9cb9 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Wed, 8 Mar 2023 11:26:28 +0100 Subject: [PATCH 189/360] ensure ohlc is dropped from both train and predict --- .../RL/BaseReinforcementLearningModel.py | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/freqtrade/freqai/RL/BaseReinforcementLearningModel.py b/freqtrade/freqai/RL/BaseReinforcementLearningModel.py index bac717f9f..c528d8910 100644 --- a/freqtrade/freqai/RL/BaseReinforcementLearningModel.py +++ b/freqtrade/freqai/RL/BaseReinforcementLearningModel.py @@ -235,6 +235,9 @@ class BaseReinforcementLearningModel(IFreqaiModel): filtered_dataframe, _ = dk.filter_features( unfiltered_df, dk.training_features_list, training_filter=False ) + + filtered_dataframe = self.drop_ohlc_from_df(filtered_dataframe, dk) + filtered_dataframe = dk.normalize_data_from_metadata(filtered_dataframe) dk.data_dictionary["prediction_features"] = filtered_dataframe @@ -314,14 +317,24 @@ class BaseReinforcementLearningModel(IFreqaiModel): prices_test.rename(columns=rename_dict, inplace=True) prices_test.reset_index(drop=True) - if self.rl_config["drop_ohlc_from_features"]: - train_df.drop(rename_dict.keys(), axis=1, inplace=True) - test_df.drop(rename_dict.keys(), axis=1, inplace=True) - feature_list = dk.training_features_list - feature_list = [e for e in feature_list if e not in rename_dict.keys()] + train_df = self.drop_ohlc_from_df(train_df, dk) + test_df = self.drop_ohlc_from_df(test_df, dk) return prices_train, prices_test + def drop_ohlc_from_df(self, df: DataFrame, dk: FreqaiDataKitchen): + """ + Given a dataframe, drop the ohlc data + """ + drop_list = ['%-raw_open', '%-raw_low', '%-raw_high', '%-raw_close'] + + if self.rl_config["drop_ohlc_from_features"]: + df.drop(drop_list, axis=1, inplace=True) + feature_list = dk.training_features_list + feature_list = [e for e in feature_list if e not in drop_list] + + return df + def load_model_from_disk(self, dk: FreqaiDataKitchen) -> Any: """ Can be used by user if they are trying to limit_ram_usage *and* From 85e345fc4839292a88fee0e101e34551aa30e6ec Mon Sep 17 00:00:00 2001 From: Robert Caulk Date: Wed, 8 Mar 2023 19:29:39 +0100 Subject: [PATCH 190/360] Update BaseReinforcementLearningModel.py --- freqtrade/freqai/RL/BaseReinforcementLearningModel.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/freqtrade/freqai/RL/BaseReinforcementLearningModel.py b/freqtrade/freqai/RL/BaseReinforcementLearningModel.py index c528d8910..acdcf3135 100644 --- a/freqtrade/freqai/RL/BaseReinforcementLearningModel.py +++ b/freqtrade/freqai/RL/BaseReinforcementLearningModel.py @@ -330,8 +330,6 @@ class BaseReinforcementLearningModel(IFreqaiModel): if self.rl_config["drop_ohlc_from_features"]: df.drop(drop_list, axis=1, inplace=True) - feature_list = dk.training_features_list - feature_list = [e for e in feature_list if e not in drop_list] return df From 0318486bee485dc2951871d0b9c23909b1865a9f Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 8 Mar 2023 19:35:26 +0100 Subject: [PATCH 191/360] Update stoploss_from_open documentation for leverage adjustment --- docs/strategy-callbacks.md | 6 +++--- docs/strategy-customization.md | 6 ++++-- freqtrade/strategy/strategy_helper.py | 7 ++++--- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index 81366c66e..f1cdc9f3b 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -316,11 +316,11 @@ class AwesomeStrategy(IStrategy): # evaluate highest to lowest, so that highest possible stop is used if current_profit > 0.40: - return stoploss_from_open(0.25, current_profit, is_short=trade.is_short) + return stoploss_from_open(0.25, current_profit, is_short=trade.is_short, leverage=trade.leverage) elif current_profit > 0.25: - return stoploss_from_open(0.15, current_profit, is_short=trade.is_short) + return stoploss_from_open(0.15, current_profit, is_short=trade.is_short, leverage=trade.leverage) elif current_profit > 0.20: - return stoploss_from_open(0.07, current_profit, is_short=trade.is_short) + return stoploss_from_open(0.07, current_profit, is_short=trade.is_short, leverage=trade.leverage) # return maximum stoploss value, keeping current stoploss price unchanged return 1 diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 3519a80cd..8ab0b1464 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -881,7 +881,7 @@ All columns of the informative dataframe will be available on the returning data ### *stoploss_from_open()* -Stoploss values returned from `custom_stoploss` must specify a percentage relative to `current_rate`, but sometimes you may want to specify a stoploss relative to the open price instead. `stoploss_from_open()` is a helper function to calculate a stoploss value that can be returned from `custom_stoploss` which will be equivalent to the desired percentage above the open price. +Stoploss values returned from `custom_stoploss` must specify a percentage relative to `current_rate`, but sometimes you may want to specify a stoploss relative to the entry point instead. `stoploss_from_open()` is a helper function to calculate a stoploss value that can be returned from `custom_stoploss` which will be equivalent to the desired trade profit above the entry point. ??? Example "Returning a stoploss relative to the open price from the custom stoploss function" @@ -889,6 +889,8 @@ Stoploss values returned from `custom_stoploss` must specify a percentage relati If we want a stop price at 7% above the open price we can call `stoploss_from_open(0.07, current_profit, False)` which will return `0.1157024793`. 11.57% below $121 is $107, which is the same as 7% above $100. + This function will consider leverage - so at 10x leverage, the actual stoploss would be 0.7% above $100 (0.7% * 10x = 7%). + ``` python @@ -907,7 +909,7 @@ Stoploss values returned from `custom_stoploss` must specify a percentage relati # once the profit has risen above 10%, keep the stoploss at 7% above the open price if current_profit > 0.10: - return stoploss_from_open(0.07, current_profit, is_short=trade.is_short) + return stoploss_from_open(0.07, current_profit, is_short=trade.is_short, leverage=trade.leverage) return 1 diff --git a/freqtrade/strategy/strategy_helper.py b/freqtrade/strategy/strategy_helper.py index 3ba1850b3..27ebe7e69 100644 --- a/freqtrade/strategy/strategy_helper.py +++ b/freqtrade/strategy/strategy_helper.py @@ -90,17 +90,18 @@ def stoploss_from_open( leverage: float = 1.0 ) -> float: """ - - Given the current profit, and a desired stop loss value relative to the open price, + Given the current profit, and a desired stop loss value relative to the trade entry price, return a stop loss value that is relative to the current price, and which can be returned from `custom_stoploss`. The requested stop can be positive for a stop above the open price, or negative for a stop below the open price. The return value is always >= 0. + `open_relative_stop` will be considered as adjusted for leverage if leverage is provided.. Returns 0 if the resulting stop price would be above/below (longs/shorts) the current price - :param open_relative_stop: Desired stop loss percentage relative to open price + :param open_relative_stop: Desired stop loss percentage, relative to the open price, + adjusted for leverage :param current_profit: The current profit percentage :param is_short: When true, perform the calculation for short instead of long :param leverage: Leverage to use for the calculation From d10ee0979a0fd9349b5ad1e2d6ba03e3c3d3104f Mon Sep 17 00:00:00 2001 From: robcaulk Date: Wed, 8 Mar 2023 19:37:11 +0100 Subject: [PATCH 192/360] ensure training_features_list is updated properly --- freqtrade/freqai/RL/BaseReinforcementLearningModel.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/freqtrade/freqai/RL/BaseReinforcementLearningModel.py b/freqtrade/freqai/RL/BaseReinforcementLearningModel.py index acdcf3135..e10880f46 100644 --- a/freqtrade/freqai/RL/BaseReinforcementLearningModel.py +++ b/freqtrade/freqai/RL/BaseReinforcementLearningModel.py @@ -330,6 +330,8 @@ class BaseReinforcementLearningModel(IFreqaiModel): if self.rl_config["drop_ohlc_from_features"]: df.drop(drop_list, axis=1, inplace=True) + feature_list = dk.training_features_list + dk.training_features_list = [e for e in feature_list if e not in drop_list] return df From 30fd1e742efead96b3ae12fa75b44d1be7231560 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 9 Mar 2023 07:14:54 +0000 Subject: [PATCH 193/360] Add 3.8 block for strategyUpdater --- freqtrade/commands/strategy_utils_commands.py | 4 ++++ tests/test_strategy_updater.py | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/freqtrade/commands/strategy_utils_commands.py b/freqtrade/commands/strategy_utils_commands.py index aca368742..ed4d0bf1a 100644 --- a/freqtrade/commands/strategy_utils_commands.py +++ b/freqtrade/commands/strategy_utils_commands.py @@ -1,4 +1,5 @@ import logging +import sys import time from pathlib import Path from typing import Any, Dict @@ -19,6 +20,9 @@ def start_strategy_update(args: Dict[str, Any]) -> None: :return: None """ + if sys.version_info == (3, 8): # pragma: no cover + sys.exit("Freqtrade strategy updater requires Python version >= 3.9") + config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) strategy_objs = StrategyResolver.search_all_objects( diff --git a/tests/test_strategy_updater.py b/tests/test_strategy_updater.py index 927c5e99f..ea971af72 100644 --- a/tests/test_strategy_updater.py +++ b/tests/test_strategy_updater.py @@ -1,8 +1,16 @@ # pragma pylint: disable=missing-docstring, protected-access, invalid-name +import sys + +import pytest + from freqtrade.strategy.strategyupdater import StrategyUpdater +if sys.version_info < (3, 9): + pytest.skip("StrategyUpdater is not compatible with Python 3.8", allow_module_level=True) + + def test_strategy_updater(default_conf, caplog) -> None: instance_strategy_updater = StrategyUpdater() modified_code1 = instance_strategy_updater.update_code(""" From d3a3ddbc614609a63a0fbea827d635b3673f59aa Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 9 Mar 2023 19:42:43 +0100 Subject: [PATCH 194/360] Check if exchang provides bid/ask via fetch_tickers - and fail with spread filter if it doesn't. closes #8286 --- freqtrade/exchange/exchange.py | 1 + freqtrade/exchange/gate.py | 1 + freqtrade/plugins/pairlist/SpreadFilter.py | 7 +++++++ tests/plugins/test_pairlist.py | 6 ++++++ 4 files changed, 15 insertions(+) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index cdbda1506..c0e07c6d7 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -69,6 +69,7 @@ class Exchange: # Check https://github.com/ccxt/ccxt/issues/10767 for removal of ohlcv_volume_currency "ohlcv_volume_currency": "base", # "base" or "quote" "tickers_have_quoteVolume": True, + "tickers_have_bid_ask": True, # bid / ask empty for fetch_tickers "tickers_have_price": True, "trades_pagination": "time", # Possible are "time" or "id" "trades_pagination_arg": "since", diff --git a/freqtrade/exchange/gate.py b/freqtrade/exchange/gate.py index 80ed4088a..03b568460 100644 --- a/freqtrade/exchange/gate.py +++ b/freqtrade/exchange/gate.py @@ -32,6 +32,7 @@ class Gate(Exchange): _ft_has_futures: Dict = { "needs_trading_fees": True, + "tickers_have_bid_ask": False, "fee_cost_in_contracts": False, # Set explicitly to false for clarity "order_props_in_contracts": ['amount', 'filled', 'remaining'], "stop_price_type_field": "price_type", diff --git a/freqtrade/plugins/pairlist/SpreadFilter.py b/freqtrade/plugins/pairlist/SpreadFilter.py index 207328d08..d47b68568 100644 --- a/freqtrade/plugins/pairlist/SpreadFilter.py +++ b/freqtrade/plugins/pairlist/SpreadFilter.py @@ -5,6 +5,7 @@ import logging from typing import Any, Dict, Optional from freqtrade.constants import Config +from freqtrade.exceptions import OperationalException from freqtrade.exchange.types import Ticker from freqtrade.plugins.pairlist.IPairList import IPairList @@ -22,6 +23,12 @@ class SpreadFilter(IPairList): self._max_spread_ratio = pairlistconfig.get('max_spread_ratio', 0.005) self._enabled = self._max_spread_ratio != 0 + if not self._exchange.get_option('tickers_have_bid_ask'): + raise OperationalException( + f"{self.name} requires exchange to have bid/ask data for tickers, " + "which is not available for the selected exchange / trading mode." + ) + @property def needstickers(self) -> bool: """ diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index 40a3871d7..18ee365e2 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -828,6 +828,12 @@ def test_pair_whitelist_not_supported_Spread(mocker, default_conf, tickers) -> N match=r'Exchange does not support fetchTickers, .*'): get_patched_freqtradebot(mocker, default_conf) + mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=True)) + mocker.patch(f'{EXMS}.get_option', MagicMock(return_value=False)) + with pytest.raises(OperationalException, + match=r'.*requires exchange to have bid/ask data'): + get_patched_freqtradebot(mocker, default_conf) + @pytest.mark.parametrize("pairlist", TESTABLE_PAIRLISTS) def test_pairlist_class(mocker, whitelist_conf, markets, pairlist): From bfc7f48f17c82b759e8a012e372b3ee92017263a Mon Sep 17 00:00:00 2001 From: hippocritical Date: Fri, 10 Mar 2023 08:59:07 +0100 Subject: [PATCH 195/360] added checks for python3.8 or lower since ast_comments.unparse() needs python 3.9 or higher. testing with python 3.8 would make the build fail tests, skipping it there. --- freqtrade/commands/strategy_utils_commands.py | 47 ++-- tests/test_strategy_updater.py | 227 +++++++++--------- 2 files changed, 143 insertions(+), 131 deletions(-) diff --git a/freqtrade/commands/strategy_utils_commands.py b/freqtrade/commands/strategy_utils_commands.py index aca368742..56a28cf65 100644 --- a/freqtrade/commands/strategy_utils_commands.py +++ b/freqtrade/commands/strategy_utils_commands.py @@ -1,4 +1,5 @@ import logging +import sys import time from pathlib import Path from typing import Any, Dict @@ -19,28 +20,34 @@ def start_strategy_update(args: Dict[str, Any]) -> None: :return: None """ - config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) + if sys.version_info <= (3, 8): + print("This code requires Python 3.9 or higher. " + "We cannot continue. " + "Please upgrade your python version to use this command.") - strategy_objs = StrategyResolver.search_all_objects( - config, enum_failed=False, recursive=config.get('recursive_strategy_search', False)) - - filtered_strategy_objs = [] - if 'strategy_list' in args: - for args_strategy in args['strategy_list']: - for strategy_obj in strategy_objs: - if (strategy_obj['name'] == args_strategy - and strategy_obj not in filtered_strategy_objs): - filtered_strategy_objs.append(strategy_obj) - break - - for filtered_strategy_obj in filtered_strategy_objs: - start_conversion(filtered_strategy_obj, config) else: - processed_locations = set() - for strategy_obj in strategy_objs: - if strategy_obj['location'] not in processed_locations: - processed_locations.add(strategy_obj['location']) - start_conversion(strategy_obj, config) + config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) + + strategy_objs = StrategyResolver.search_all_objects( + config, enum_failed=False, recursive=config.get('recursive_strategy_search', False)) + + filtered_strategy_objs = [] + if 'strategy_list' in args: + for args_strategy in args['strategy_list']: + for strategy_obj in strategy_objs: + if (strategy_obj['name'] == args_strategy + and strategy_obj not in filtered_strategy_objs): + filtered_strategy_objs.append(strategy_obj) + break + + for filtered_strategy_obj in filtered_strategy_objs: + start_conversion(filtered_strategy_obj, config) + else: + processed_locations = set() + for strategy_obj in strategy_objs: + if strategy_obj['location'] not in processed_locations: + processed_locations.add(strategy_obj['location']) + start_conversion(strategy_obj, config) def start_conversion(strategy_obj, config): diff --git a/tests/test_strategy_updater.py b/tests/test_strategy_updater.py index 927c5e99f..3831c2ee6 100644 --- a/tests/test_strategy_updater.py +++ b/tests/test_strategy_updater.py @@ -1,130 +1,135 @@ # pragma pylint: disable=missing-docstring, protected-access, invalid-name +import sys + from freqtrade.strategy.strategyupdater import StrategyUpdater def test_strategy_updater(default_conf, caplog) -> None: - instance_strategy_updater = StrategyUpdater() - modified_code1 = instance_strategy_updater.update_code(""" -class testClass(IStrategy): - def populate_buy_trend(): - pass - def populate_sell_trend(): - pass - def check_buy_timeout(): - pass - def check_sell_timeout(): - pass - def custom_sell(): - pass -""") - modified_code2 = instance_strategy_updater.update_code(""" -ticker_interval = '15m' -buy_some_parameter = IntParameter(space='buy') -sell_some_parameter = IntParameter(space='sell') -""") - modified_code3 = instance_strategy_updater.update_code(""" -use_sell_signal = True -sell_profit_only = True -sell_profit_offset = True -ignore_roi_if_buy_signal = True -forcebuy_enable = True -""") - modified_code4 = instance_strategy_updater.update_code(""" -dataframe.loc[reduce(lambda x, y: x & y, conditions), ["buy", "buy_tag"]] = (1, "buy_signal_1") -dataframe.loc[reduce(lambda x, y: x & y, conditions), 'sell'] = 1 -""") - modified_code5 = instance_strategy_updater.update_code(""" -def confirm_trade_exit(sell_reason: str): - pass + if sys.version_info <= (3, 8): + print("skipped tests since python version is 3.8 or lower.") + else: + instance_strategy_updater = StrategyUpdater() + modified_code1 = instance_strategy_updater.update_code(""" + class testClass(IStrategy): + def populate_buy_trend(): + pass + def populate_sell_trend(): + pass + def check_buy_timeout(): + pass + def check_sell_timeout(): + pass + def custom_sell(): + pass """) - modified_code6 = instance_strategy_updater.update_code(""" -order_time_in_force = { - 'buy': 'gtc', - 'sell': 'ioc' -} -order_types = { - 'buy': 'limit', - 'sell': 'market', - 'stoploss': 'market', - 'stoploss_on_exchange': False -} -unfilledtimeout = { - 'buy': 1, - 'sell': 2 -} -""") - - modified_code7 = instance_strategy_updater.update_code(""" -def confirm_trade_exit(sell_reason): - if (sell_reason == 'stop_loss'): + modified_code2 = instance_strategy_updater.update_code(""" + ticker_interval = '15m' + buy_some_parameter = IntParameter(space='buy') + sell_some_parameter = IntParameter(space='sell') + """) + modified_code3 = instance_strategy_updater.update_code(""" + use_sell_signal = True + sell_profit_only = True + sell_profit_offset = True + ignore_roi_if_buy_signal = True + forcebuy_enable = True + """) + modified_code4 = instance_strategy_updater.update_code(""" + dataframe.loc[reduce(lambda x, y: x & y, conditions), ["buy", "buy_tag"]] = (1, "buy_signal_1") + dataframe.loc[reduce(lambda x, y: x & y, conditions), 'sell'] = 1 + """) + modified_code5 = instance_strategy_updater.update_code(""" + def confirm_trade_exit(sell_reason: str): pass -""") - modified_code8 = instance_strategy_updater.update_code(""" -sell_reason == 'sell_signal' -sell_reason == 'force_sell' -sell_reason == 'emergency_sell' -""") - modified_code9 = instance_strategy_updater.update_code(""" -# This is the 1st comment -import talib.abstract as ta -# This is the 2nd comment -import freqtrade.vendor.qtpylib.indicators as qtpylib - - -class someStrategy(IStrategy): - # This is the 3rd comment - # This attribute will be overridden if the config file contains "minimal_roi" - minimal_roi = { - "0": 0.50 + """) + modified_code6 = instance_strategy_updater.update_code(""" + order_time_in_force = { + 'buy': 'gtc', + 'sell': 'ioc' } + order_types = { + 'buy': 'limit', + 'sell': 'market', + 'stoploss': 'market', + 'stoploss_on_exchange': False + } + unfilledtimeout = { + 'buy': 1, + 'sell': 2 + } + """) - # This is the 4th comment - stoploss = -0.1 -""") - # currently still missing: - # Webhook terminology, Telegram notification settings, Strategy/Config settings + modified_code7 = instance_strategy_updater.update_code(""" + def confirm_trade_exit(sell_reason): + if (sell_reason == 'stop_loss'): + pass + """) + modified_code8 = instance_strategy_updater.update_code(""" + sell_reason == 'sell_signal' + sell_reason == 'force_sell' + sell_reason == 'emergency_sell' + """) + modified_code9 = instance_strategy_updater.update_code(""" + # This is the 1st comment + import talib.abstract as ta + # This is the 2nd comment + import freqtrade.vendor.qtpylib.indicators as qtpylib - assert "populate_entry_trend" in modified_code1 - assert "populate_exit_trend" in modified_code1 - assert "check_entry_timeout" in modified_code1 - assert "check_exit_timeout" in modified_code1 - assert "custom_exit" in modified_code1 - assert "INTERFACE_VERSION = 3" in modified_code1 - assert "timeframe" in modified_code2 - # check for not editing hyperopt spaces - assert "space='buy'" in modified_code2 - assert "space='sell'" in modified_code2 + class someStrategy(IStrategy): + # This is the 3rd comment + # This attribute will be overridden if the config file contains "minimal_roi" + minimal_roi = { + "0": 0.50 + } - assert "use_exit_signal" in modified_code3 - assert "exit_profit_only" in modified_code3 - assert "exit_profit_offset" in modified_code3 - assert "ignore_roi_if_entry_signal" in modified_code3 - assert "force_entry_enable" in modified_code3 + # This is the 4th comment + stoploss = -0.1 + """) + # currently still missing: + # Webhook terminology, Telegram notification settings, Strategy/Config settings - assert "enter_long" in modified_code4 - assert "exit_long" in modified_code4 - assert "enter_tag" in modified_code4 + assert "populate_entry_trend" in modified_code1 + assert "populate_exit_trend" in modified_code1 + assert "check_entry_timeout" in modified_code1 + assert "check_exit_timeout" in modified_code1 + assert "custom_exit" in modified_code1 + assert "INTERFACE_VERSION = 3" in modified_code1 - assert "exit_reason" in modified_code5 + assert "timeframe" in modified_code2 + # check for not editing hyperopt spaces + assert "space='buy'" in modified_code2 + assert "space='sell'" in modified_code2 - assert "'entry': 'gtc'" in modified_code6 - assert "'exit': 'ioc'" in modified_code6 - assert "'entry': 'limit'" in modified_code6 - assert "'exit': 'market'" in modified_code6 - assert "'entry': 1" in modified_code6 - assert "'exit': 2" in modified_code6 + assert "use_exit_signal" in modified_code3 + assert "exit_profit_only" in modified_code3 + assert "exit_profit_offset" in modified_code3 + assert "ignore_roi_if_entry_signal" in modified_code3 + assert "force_entry_enable" in modified_code3 - assert "exit_reason" in modified_code7 - assert "exit_reason == 'stop_loss'" in modified_code7 + assert "enter_long" in modified_code4 + assert "exit_long" in modified_code4 + assert "enter_tag" in modified_code4 - # those tests currently don't work, next in line. - assert "exit_signal" in modified_code8 - assert "exit_reason" in modified_code8 - assert "force_exit" in modified_code8 - assert "emergency_exit" in modified_code8 + assert "exit_reason" in modified_code5 - assert "This is the 1st comment" in modified_code9 - assert "This is the 2nd comment" in modified_code9 - assert "This is the 3rd comment" in modified_code9 + assert "'entry': 'gtc'" in modified_code6 + assert "'exit': 'ioc'" in modified_code6 + assert "'entry': 'limit'" in modified_code6 + assert "'exit': 'market'" in modified_code6 + assert "'entry': 1" in modified_code6 + assert "'exit': 2" in modified_code6 + + assert "exit_reason" in modified_code7 + assert "exit_reason == 'stop_loss'" in modified_code7 + + # those tests currently don't work, next in line. + assert "exit_signal" in modified_code8 + assert "exit_reason" in modified_code8 + assert "force_exit" in modified_code8 + assert "emergency_exit" in modified_code8 + + assert "This is the 1st comment" in modified_code9 + assert "This is the 2nd comment" in modified_code9 + assert "This is the 3rd comment" in modified_code9 From a3988f56b28ddef95fd425e9804d3154491ad994 Mon Sep 17 00:00:00 2001 From: hippocritical Date: Fri, 10 Mar 2023 09:23:56 +0100 Subject: [PATCH 196/360] Sorry matthias, did not see that you already committed something and did overwrite you. Added your version to it instead of mine and pushed again (since it was already overwritten by me). --- freqtrade/commands/strategy_utils_commands.py | 45 ++-- tests/test_strategy_updater.py | 232 +++++++++--------- 2 files changed, 138 insertions(+), 139 deletions(-) diff --git a/freqtrade/commands/strategy_utils_commands.py b/freqtrade/commands/strategy_utils_commands.py index 56a28cf65..ed4d0bf1a 100644 --- a/freqtrade/commands/strategy_utils_commands.py +++ b/freqtrade/commands/strategy_utils_commands.py @@ -20,34 +20,31 @@ def start_strategy_update(args: Dict[str, Any]) -> None: :return: None """ - if sys.version_info <= (3, 8): - print("This code requires Python 3.9 or higher. " - "We cannot continue. " - "Please upgrade your python version to use this command.") + if sys.version_info == (3, 8): # pragma: no cover + sys.exit("Freqtrade strategy updater requires Python version >= 3.9") - else: - config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) + config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) - strategy_objs = StrategyResolver.search_all_objects( - config, enum_failed=False, recursive=config.get('recursive_strategy_search', False)) + strategy_objs = StrategyResolver.search_all_objects( + config, enum_failed=False, recursive=config.get('recursive_strategy_search', False)) - filtered_strategy_objs = [] - if 'strategy_list' in args: - for args_strategy in args['strategy_list']: - for strategy_obj in strategy_objs: - if (strategy_obj['name'] == args_strategy - and strategy_obj not in filtered_strategy_objs): - filtered_strategy_objs.append(strategy_obj) - break - - for filtered_strategy_obj in filtered_strategy_objs: - start_conversion(filtered_strategy_obj, config) - else: - processed_locations = set() + filtered_strategy_objs = [] + if 'strategy_list' in args: + for args_strategy in args['strategy_list']: for strategy_obj in strategy_objs: - if strategy_obj['location'] not in processed_locations: - processed_locations.add(strategy_obj['location']) - start_conversion(strategy_obj, config) + if (strategy_obj['name'] == args_strategy + and strategy_obj not in filtered_strategy_objs): + filtered_strategy_objs.append(strategy_obj) + break + + for filtered_strategy_obj in filtered_strategy_objs: + start_conversion(filtered_strategy_obj, config) + else: + processed_locations = set() + for strategy_obj in strategy_objs: + if strategy_obj['location'] not in processed_locations: + processed_locations.add(strategy_obj['location']) + start_conversion(strategy_obj, config) def start_conversion(strategy_obj, config): diff --git a/tests/test_strategy_updater.py b/tests/test_strategy_updater.py index 3831c2ee6..26d173871 100644 --- a/tests/test_strategy_updater.py +++ b/tests/test_strategy_updater.py @@ -2,134 +2,136 @@ import sys +import pytest + from freqtrade.strategy.strategyupdater import StrategyUpdater def test_strategy_updater(default_conf, caplog) -> None: - if sys.version_info <= (3, 8): - print("skipped tests since python version is 3.8 or lower.") - else: - instance_strategy_updater = StrategyUpdater() - modified_code1 = instance_strategy_updater.update_code(""" - class testClass(IStrategy): - def populate_buy_trend(): - pass - def populate_sell_trend(): - pass - def check_buy_timeout(): - pass - def check_sell_timeout(): - pass - def custom_sell(): - pass - """) - modified_code2 = instance_strategy_updater.update_code(""" - ticker_interval = '15m' - buy_some_parameter = IntParameter(space='buy') - sell_some_parameter = IntParameter(space='sell') - """) - modified_code3 = instance_strategy_updater.update_code(""" - use_sell_signal = True - sell_profit_only = True - sell_profit_offset = True - ignore_roi_if_buy_signal = True - forcebuy_enable = True - """) - modified_code4 = instance_strategy_updater.update_code(""" - dataframe.loc[reduce(lambda x, y: x & y, conditions), ["buy", "buy_tag"]] = (1, "buy_signal_1") - dataframe.loc[reduce(lambda x, y: x & y, conditions), 'sell'] = 1 - """) - modified_code5 = instance_strategy_updater.update_code(""" - def confirm_trade_exit(sell_reason: str): + if sys.version_info < (3, 9): + pytest.skip("StrategyUpdater is not compatible with Python 3.8", allow_module_level=True) + + instance_strategy_updater = StrategyUpdater() + modified_code1 = instance_strategy_updater.update_code(""" +class testClass(IStrategy): + def populate_buy_trend(): pass - """) - modified_code6 = instance_strategy_updater.update_code(""" - order_time_in_force = { - 'buy': 'gtc', - 'sell': 'ioc' + def populate_sell_trend(): + pass + def check_buy_timeout(): + pass + def check_sell_timeout(): + pass + def custom_sell(): + pass +""") + modified_code2 = instance_strategy_updater.update_code(""" +ticker_interval = '15m' +buy_some_parameter = IntParameter(space='buy') +sell_some_parameter = IntParameter(space='sell') +""") + modified_code3 = instance_strategy_updater.update_code(""" +use_sell_signal = True +sell_profit_only = True +sell_profit_offset = True +ignore_roi_if_buy_signal = True +forcebuy_enable = True +""") + modified_code4 = instance_strategy_updater.update_code(""" +dataframe.loc[reduce(lambda x, y: x & y, conditions), ["buy", "buy_tag"]] = (1, "buy_signal_1") +dataframe.loc[reduce(lambda x, y: x & y, conditions), 'sell'] = 1 +""") + modified_code5 = instance_strategy_updater.update_code(""" +def confirm_trade_exit(sell_reason: str): + pass + """) + modified_code6 = instance_strategy_updater.update_code(""" +order_time_in_force = { + 'buy': 'gtc', + 'sell': 'ioc' +} +order_types = { + 'buy': 'limit', + 'sell': 'market', + 'stoploss': 'market', + 'stoploss_on_exchange': False +} +unfilledtimeout = { + 'buy': 1, + 'sell': 2 +} +""") + + modified_code7 = instance_strategy_updater.update_code(""" +def confirm_trade_exit(sell_reason): + if (sell_reason == 'stop_loss'): + pass +""") + modified_code8 = instance_strategy_updater.update_code(""" +sell_reason == 'sell_signal' +sell_reason == 'force_sell' +sell_reason == 'emergency_sell' +""") + modified_code9 = instance_strategy_updater.update_code(""" +# This is the 1st comment +import talib.abstract as ta +# This is the 2nd comment +import freqtrade.vendor.qtpylib.indicators as qtpylib + + +class someStrategy(IStrategy): + # This is the 3rd comment + # This attribute will be overridden if the config file contains "minimal_roi" + minimal_roi = { + "0": 0.50 } - order_types = { - 'buy': 'limit', - 'sell': 'market', - 'stoploss': 'market', - 'stoploss_on_exchange': False - } - unfilledtimeout = { - 'buy': 1, - 'sell': 2 - } - """) - modified_code7 = instance_strategy_updater.update_code(""" - def confirm_trade_exit(sell_reason): - if (sell_reason == 'stop_loss'): - pass - """) - modified_code8 = instance_strategy_updater.update_code(""" - sell_reason == 'sell_signal' - sell_reason == 'force_sell' - sell_reason == 'emergency_sell' - """) - modified_code9 = instance_strategy_updater.update_code(""" - # This is the 1st comment - import talib.abstract as ta - # This is the 2nd comment - import freqtrade.vendor.qtpylib.indicators as qtpylib + # This is the 4th comment + stoploss = -0.1 +""") + # currently still missing: + # Webhook terminology, Telegram notification settings, Strategy/Config settings + assert "populate_entry_trend" in modified_code1 + assert "populate_exit_trend" in modified_code1 + assert "check_entry_timeout" in modified_code1 + assert "check_exit_timeout" in modified_code1 + assert "custom_exit" in modified_code1 + assert "INTERFACE_VERSION = 3" in modified_code1 - class someStrategy(IStrategy): - # This is the 3rd comment - # This attribute will be overridden if the config file contains "minimal_roi" - minimal_roi = { - "0": 0.50 - } + assert "timeframe" in modified_code2 + # check for not editing hyperopt spaces + assert "space='buy'" in modified_code2 + assert "space='sell'" in modified_code2 - # This is the 4th comment - stoploss = -0.1 - """) - # currently still missing: - # Webhook terminology, Telegram notification settings, Strategy/Config settings + assert "use_exit_signal" in modified_code3 + assert "exit_profit_only" in modified_code3 + assert "exit_profit_offset" in modified_code3 + assert "ignore_roi_if_entry_signal" in modified_code3 + assert "force_entry_enable" in modified_code3 - assert "populate_entry_trend" in modified_code1 - assert "populate_exit_trend" in modified_code1 - assert "check_entry_timeout" in modified_code1 - assert "check_exit_timeout" in modified_code1 - assert "custom_exit" in modified_code1 - assert "INTERFACE_VERSION = 3" in modified_code1 + assert "enter_long" in modified_code4 + assert "exit_long" in modified_code4 + assert "enter_tag" in modified_code4 - assert "timeframe" in modified_code2 - # check for not editing hyperopt spaces - assert "space='buy'" in modified_code2 - assert "space='sell'" in modified_code2 + assert "exit_reason" in modified_code5 - assert "use_exit_signal" in modified_code3 - assert "exit_profit_only" in modified_code3 - assert "exit_profit_offset" in modified_code3 - assert "ignore_roi_if_entry_signal" in modified_code3 - assert "force_entry_enable" in modified_code3 + assert "'entry': 'gtc'" in modified_code6 + assert "'exit': 'ioc'" in modified_code6 + assert "'entry': 'limit'" in modified_code6 + assert "'exit': 'market'" in modified_code6 + assert "'entry': 1" in modified_code6 + assert "'exit': 2" in modified_code6 - assert "enter_long" in modified_code4 - assert "exit_long" in modified_code4 - assert "enter_tag" in modified_code4 + assert "exit_reason" in modified_code7 + assert "exit_reason == 'stop_loss'" in modified_code7 - assert "exit_reason" in modified_code5 + # those tests currently don't work, next in line. + assert "exit_signal" in modified_code8 + assert "exit_reason" in modified_code8 + assert "force_exit" in modified_code8 + assert "emergency_exit" in modified_code8 - assert "'entry': 'gtc'" in modified_code6 - assert "'exit': 'ioc'" in modified_code6 - assert "'entry': 'limit'" in modified_code6 - assert "'exit': 'market'" in modified_code6 - assert "'entry': 1" in modified_code6 - assert "'exit': 2" in modified_code6 - - assert "exit_reason" in modified_code7 - assert "exit_reason == 'stop_loss'" in modified_code7 - - # those tests currently don't work, next in line. - assert "exit_signal" in modified_code8 - assert "exit_reason" in modified_code8 - assert "force_exit" in modified_code8 - assert "emergency_exit" in modified_code8 - - assert "This is the 1st comment" in modified_code9 - assert "This is the 2nd comment" in modified_code9 - assert "This is the 3rd comment" in modified_code9 + assert "This is the 1st comment" in modified_code9 + assert "This is the 2nd comment" in modified_code9 + assert "This is the 3rd comment" in modified_code9 From a76ca771f8c557a6a596f7b7c0cbb748f6418ade Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 10 Mar 2023 18:00:20 +0100 Subject: [PATCH 197/360] telegram: Fix sending telegram message with exception --- freqtrade/enums/rpcmessagetype.py | 1 + freqtrade/freqtradebot.py | 4 ++-- freqtrade/rpc/telegram.py | 3 +++ freqtrade/rpc/webhook.py | 1 + freqtrade/worker.py | 7 +++++-- 5 files changed, 12 insertions(+), 4 deletions(-) diff --git a/freqtrade/enums/rpcmessagetype.py b/freqtrade/enums/rpcmessagetype.py index 404c75401..16d81b1d8 100644 --- a/freqtrade/enums/rpcmessagetype.py +++ b/freqtrade/enums/rpcmessagetype.py @@ -4,6 +4,7 @@ from enum import Enum class RPCMessageType(str, Enum): STATUS = 'status' WARNING = 'warning' + EXCEPTION = 'exception' STARTUP = 'startup' ENTRY = 'entry' diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index cec7176f6..feb0baf5a 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -133,13 +133,13 @@ class FreqtradeBot(LoggingMixin): # Initialize protections AFTER bot start - otherwise parameters are not loaded. self.protections = ProtectionManager(self.config, self.strategy.protections) - def notify_status(self, msg: str) -> None: + def notify_status(self, msg: str, msg_type=RPCMessageType.STATUS) -> 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, + 'type': msg_type, 'status': msg }) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 30aa55359..0c0b24f00 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -414,6 +414,9 @@ class Telegram(RPCHandler): elif msg_type == RPCMessageType.WARNING: message = f"\N{WARNING SIGN} *Warning:* `{msg['status']}`" + elif msg_type == RPCMessageType.EXCEPTION: + # Errors will contain exceptions, which are wrapped in tripple ticks. + message = f"\N{WARNING SIGN} *ERROR:* \n {msg['status']}" elif msg_type == RPCMessageType.STARTUP: message = f"{msg['status']}" diff --git a/freqtrade/rpc/webhook.py b/freqtrade/rpc/webhook.py index d81d8d24f..0967de70d 100644 --- a/freqtrade/rpc/webhook.py +++ b/freqtrade/rpc/webhook.py @@ -58,6 +58,7 @@ class Webhook(RPCHandler): valuedict = whconfig.get('webhookexitcancel') elif msg['type'] in (RPCMessageType.STATUS, RPCMessageType.STARTUP, + RPCMessageType.EXCEPTION, RPCMessageType.WARNING): valuedict = whconfig.get('webhookstatus') elif msg['type'].value in whconfig: diff --git a/freqtrade/worker.py b/freqtrade/worker.py index 388163678..fb89e7a2d 100644 --- a/freqtrade/worker.py +++ b/freqtrade/worker.py @@ -12,7 +12,7 @@ import sdnotify from freqtrade import __version__ from freqtrade.configuration import Configuration from freqtrade.constants import PROCESS_THROTTLE_SECS, RETRY_TIMEOUT, Config -from freqtrade.enums import State +from freqtrade.enums import RPCMessageType, State from freqtrade.exceptions import OperationalException, TemporaryError from freqtrade.exchange import timeframe_to_next_date from freqtrade.freqtradebot import FreqtradeBot @@ -185,7 +185,10 @@ class Worker: tb = traceback.format_exc() hint = 'Issue `/start` if you think it is safe to restart.' - self.freqtrade.notify_status(f'OperationalException:\n```\n{tb}```{hint}') + self.freqtrade.notify_status( + f'*OperationalException:*\n```\n{tb}```\n {hint}', + msg_type=RPCMessageType.EXCEPTION + ) logger.exception('OperationalException. Stopping trader ...') self.freqtrade.state = State.STOPPED From a2336f256bda321f54172693335a8dcdafad9e3f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 11 Mar 2023 08:25:45 +0100 Subject: [PATCH 198/360] Add profit descriptions closes #8234 --- docs/bot-basics.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/bot-basics.md b/docs/bot-basics.md index 925fc7862..1aa8f3085 100644 --- a/docs/bot-basics.md +++ b/docs/bot-basics.md @@ -12,6 +12,9 @@ This page provides you some basic concepts on how Freqtrade works and operates. * **Indicators**: Technical indicators (SMA, EMA, RSI, ...). * **Limit order**: Limit orders which execute at the defined limit price or better. * **Market order**: Guaranteed to fill, may move price depending on the order size. +* **Current Profit**: Currently pending (unrealized) profit for this trade. This is mainly used throughout the bot and UI. +* **Realized Profit**: Already realized profit. Only relevant in combination with [partial exits](strategy-callbacks.md#adjust-trade-position) - which also explains the calculation logic for this. +* **Total Profit**: Combined realized and unrealized profit. The relative number (%) is calculated against the total investment in this trade. ## Fee handling From 39c651e40c9d9f83b4dd2c58e82a17488b475a0c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 11 Mar 2023 15:03:06 +0100 Subject: [PATCH 199/360] Remove pointless reset of close_profit --- freqtrade/freqtradebot.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index feb0baf5a..281d90432 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1468,8 +1468,6 @@ class FreqtradeBot(LoggingMixin): return False trade.close_rate = None trade.close_rate_requested = None - trade.close_profit = None - trade.close_profit_abs = None # Set exit_reason for fill message exit_reason_prev = trade.exit_reason trade.exit_reason = trade.exit_reason + f", {reason}" if trade.exit_reason else reason From 59d2ff3ffabe55dfeb35b820c9376bf4e5ec32b0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 11 Mar 2023 15:12:32 +0100 Subject: [PATCH 200/360] Simplify `handle_cancel_exit ` --- freqtrade/freqtradebot.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 281d90432..3924b111f 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1460,32 +1460,32 @@ class FreqtradeBot(LoggingMixin): return False try: - co = self.exchange.cancel_order_with_result(order['id'], trade.pair, - trade.amount) + order = self.exchange.cancel_order_with_result(order['id'], trade.pair, + trade.amount) except InvalidOrderException: logger.exception( f"Could not cancel {trade.exit_side} order {trade.open_order_id}") return False - trade.close_rate = None - trade.close_rate_requested = None + # Set exit_reason for fill message exit_reason_prev = trade.exit_reason trade.exit_reason = trade.exit_reason + f", {reason}" if trade.exit_reason else reason - self.update_trade_state(trade, trade.open_order_id, co) # Order might be filled above in odd timing issues. - if co.get('status') in ('canceled', 'cancelled'): + if order.get('status') in ('canceled', 'cancelled'): trade.exit_reason = None - trade.open_order_id = None else: trade.exit_reason = exit_reason_prev - - logger.info(f'{trade.exit_side.capitalize()} order {reason} for {trade}.') cancelled = True else: reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE'] - logger.info(f'{trade.exit_side.capitalize()} order {reason} for {trade}.') - self.update_trade_state(trade, trade.open_order_id, order) - trade.open_order_id = None + trade.exit_reason = None + + self.update_trade_state(trade, trade.open_order_id, order) + + logger.info(f'{trade.exit_side.capitalize()} order {reason} for {trade}.') + trade.open_order_id = None + trade.close_rate = None + trade.close_rate_requested = None self._notify_exit_cancel( trade, From 8726a4645d7fc69bb2c9120bb49b039db60fb142 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 11 Mar 2023 15:15:32 +0100 Subject: [PATCH 201/360] Don't use deprecated Type construct --- freqtrade/misc.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/freqtrade/misc.py b/freqtrade/misc.py index 87cea54c0..0cd5c6ffd 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -6,8 +6,7 @@ import logging import re from datetime import datetime from pathlib import Path -from typing import Any, Dict, Iterator, List, Mapping, Optional, Union -from typing.io import IO +from typing import Any, Dict, Iterator, List, Mapping, Optional, TextIO, Union from urllib.parse import urlparse import orjson @@ -103,7 +102,7 @@ def file_dump_joblib(filename: Path, data: Any, log: bool = True) -> None: logger.debug(f'done joblib dump to "{filename}"') -def json_load(datafile: IO) -> Any: +def json_load(datafile: Union[gzip.GzipFile, TextIO]) -> Any: """ load data with rapidjson Use this to have a consistent experience, From b23841fbfedc970c1dc706f5ea057f1143babba4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 11 Mar 2023 17:35:30 +0100 Subject: [PATCH 202/360] Bump ccxt to 2.9.12 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c972ae1d4..5977a6440 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.24.2 pandas==1.5.3 pandas-ta==0.3.14b -ccxt==2.9.4 +ccxt==2.9.12 cryptography==39.0.2 aiohttp==3.8.4 SQLAlchemy==2.0.5.post1 From 82cb107520d2709afde40f40c342590f3d860a1b Mon Sep 17 00:00:00 2001 From: initrv Date: Sun, 12 Mar 2023 01:32:55 +0300 Subject: [PATCH 203/360] add tensorboard category --- docs/freqai-reinforcement-learning.md | 4 ++-- freqtrade/freqai/RL/Base3ActionRLEnv.py | 2 +- freqtrade/freqai/RL/Base4ActionRLEnv.py | 2 +- freqtrade/freqai/RL/Base5ActionRLEnv.py | 2 +- freqtrade/freqai/RL/BaseEnvironment.py | 21 ++++++++++++------- freqtrade/freqai/RL/TensorboardCallback.py | 14 ++++++------- .../prediction_models/ReinforcementLearner.py | 2 +- 7 files changed, 26 insertions(+), 21 deletions(-) diff --git a/docs/freqai-reinforcement-learning.md b/docs/freqai-reinforcement-learning.md index 04ca42a5d..ed6a41825 100644 --- a/docs/freqai-reinforcement-learning.md +++ b/docs/freqai-reinforcement-learning.md @@ -248,13 +248,13 @@ FreqAI also provides a built in episodic summary logger called `self.tensorboard """ def calculate_reward(self, action: int) -> float: if not self._is_valid(action): - self.tensorboard_log("is_valid") + self.tensorboard_log("invalid") return -2 ``` !!! Note - The `self.tensorboard_log()` function is designed for tracking incremented objects only i.e. events, actions inside the training environment. If the event of interest is a float, the float can be passed as the second argument e.g. `self.tensorboard_log("float_metric1", 0.23)` would add 0.23 to `float_metric`. In this case you can also disable incrementing using `inc=False` parameter. + The `self.tensorboard_log()` function is designed for tracking incremented objects only i.e. events, actions inside the training environment. If the event of interest is a float, the float can be passed as the second argument e.g. `self.tensorboard_log("float_metric1", 0.23)`. In this case the metric values are not incremented. ### Choosing a base environment diff --git a/freqtrade/freqai/RL/Base3ActionRLEnv.py b/freqtrade/freqai/RL/Base3ActionRLEnv.py index 3b5fffc58..a108d776e 100644 --- a/freqtrade/freqai/RL/Base3ActionRLEnv.py +++ b/freqtrade/freqai/RL/Base3ActionRLEnv.py @@ -47,7 +47,7 @@ class Base3ActionRLEnv(BaseEnvironment): self._update_unrealized_total_profit() step_reward = self.calculate_reward(action) self.total_reward += step_reward - self.tensorboard_log(self.actions._member_names_[action]) + self.tensorboard_log(self.actions._member_names_[action], category="actions") trade_type = None if self.is_tradesignal(action): diff --git a/freqtrade/freqai/RL/Base4ActionRLEnv.py b/freqtrade/freqai/RL/Base4ActionRLEnv.py index 8f45028b1..4f093f06c 100644 --- a/freqtrade/freqai/RL/Base4ActionRLEnv.py +++ b/freqtrade/freqai/RL/Base4ActionRLEnv.py @@ -48,7 +48,7 @@ class Base4ActionRLEnv(BaseEnvironment): self._update_unrealized_total_profit() step_reward = self.calculate_reward(action) self.total_reward += step_reward - self.tensorboard_log(self.actions._member_names_[action]) + self.tensorboard_log(self.actions._member_names_[action], category="actions") trade_type = None if self.is_tradesignal(action): diff --git a/freqtrade/freqai/RL/Base5ActionRLEnv.py b/freqtrade/freqai/RL/Base5ActionRLEnv.py index 22d3cae30..490ef3601 100644 --- a/freqtrade/freqai/RL/Base5ActionRLEnv.py +++ b/freqtrade/freqai/RL/Base5ActionRLEnv.py @@ -49,7 +49,7 @@ class Base5ActionRLEnv(BaseEnvironment): self._update_unrealized_total_profit() step_reward = self.calculate_reward(action) self.total_reward += step_reward - self.tensorboard_log(self.actions._member_names_[action]) + self.tensorboard_log(self.actions._member_names_[action], category="actions") trade_type = None if self.is_tradesignal(action): diff --git a/freqtrade/freqai/RL/BaseEnvironment.py b/freqtrade/freqai/RL/BaseEnvironment.py index 7a4467bf7..df2a89d81 100644 --- a/freqtrade/freqai/RL/BaseEnvironment.py +++ b/freqtrade/freqai/RL/BaseEnvironment.py @@ -137,7 +137,8 @@ class BaseEnvironment(gym.Env): self.np_random, seed = seeding.np_random(seed) return [seed] - def tensorboard_log(self, metric: str, value: Union[int, float] = 1, inc: bool = True): + def tensorboard_log(self, metric: str, value: Optional[Union[int, float]] = None, + category: str = "custom"): """ Function builds the tensorboard_metrics dictionary to be parsed by the TensorboardCallback. This @@ -149,17 +150,23 @@ class BaseEnvironment(gym.Env): def calculate_reward(self, action: int) -> float: if not self._is_valid(action): - self.tensorboard_log("is_valid") + self.tensorboard_log("invalid") return -2 :param metric: metric to be tracked and incremented - :param value: value to increment `metric` by - :param inc: sets whether the `value` is incremented or not + :param value: `metric` value + :param category: `metric` category """ - if not inc or metric not in self.tensorboard_metrics: - self.tensorboard_metrics[metric] = value + increment = True if not value else False + value = 1 if increment else value + + if category not in self.tensorboard_metrics: + self.tensorboard_metrics[category] = {} + + if not increment or metric not in self.tensorboard_metrics[category]: + self.tensorboard_metrics[category][metric] = value else: - self.tensorboard_metrics[metric] += value + self.tensorboard_metrics[category][metric] += value def reset_tensorboard_log(self): self.tensorboard_metrics = {} diff --git a/freqtrade/freqai/RL/TensorboardCallback.py b/freqtrade/freqai/RL/TensorboardCallback.py index b596742e9..1828319cd 100644 --- a/freqtrade/freqai/RL/TensorboardCallback.py +++ b/freqtrade/freqai/RL/TensorboardCallback.py @@ -46,14 +46,12 @@ class TensorboardCallback(BaseCallback): local_info = self.locals["infos"][0] tensorboard_metrics = self.training_env.get_attr("tensorboard_metrics")[0] - for info in local_info: - if info not in ["episode", "terminal_observation"]: - self.logger.record(f"_info/{info}", local_info[info]) + for metric in local_info: + if metric not in ["episode", "terminal_observation"]: + self.logger.record(f"info/{metric}", local_info[metric]) - for info in tensorboard_metrics: - if info in [action.name for action in self.actions]: - self.logger.record(f"_actions/{info}", tensorboard_metrics[info]) - else: - self.logger.record(f"_custom/{info}", tensorboard_metrics[info]) + for category in tensorboard_metrics: + for metric in tensorboard_metrics[category]: + self.logger.record(f"{category}/{metric}", tensorboard_metrics[category][metric]) return True diff --git a/freqtrade/freqai/prediction_models/ReinforcementLearner.py b/freqtrade/freqai/prediction_models/ReinforcementLearner.py index 2a87151f9..e795703d4 100644 --- a/freqtrade/freqai/prediction_models/ReinforcementLearner.py +++ b/freqtrade/freqai/prediction_models/ReinforcementLearner.py @@ -100,7 +100,7 @@ class ReinforcementLearner(BaseReinforcementLearningModel): """ # first, penalize if the action is not valid if not self._is_valid(action): - self.tensorboard_log("is_valid") + self.tensorboard_log("invalid", category="actions") return -2 pnl = self.get_unrealized_profit() From aa283a04478075a331d64f9fff1f2f310575c463 Mon Sep 17 00:00:00 2001 From: froggleston Date: Sun, 12 Mar 2023 12:44:12 +0000 Subject: [PATCH 204/360] Fix None limit on pair_candles RPC call --- scripts/rest_client.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/scripts/rest_client.py b/scripts/rest_client.py index 144d428e5..6c8e13f2c 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -340,12 +340,14 @@ class FtRestClient(): :param limit: Limit result to the last n candles. :return: json object """ - return self._get("pair_candles", params={ + params = { "pair": pair, "timeframe": timeframe, - "limit": limit, - }) - + } + if limit: + params['limit'] = limit + return self._get("pair_candles", params=params) + def pair_history(self, pair, timeframe, strategy, timerange=None): """Return historic, analyzed dataframe From 5bfee44bba51b0005ec126583ff343d073ba897c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 12 Mar 2023 15:24:27 +0100 Subject: [PATCH 205/360] Whitespace fix --- scripts/rest_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/rest_client.py b/scripts/rest_client.py index 6c8e13f2c..196542780 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -347,7 +347,7 @@ class FtRestClient(): if limit: params['limit'] = limit return self._get("pair_candles", params=params) - + def pair_history(self, pair, timeframe, strategy, timerange=None): """Return historic, analyzed dataframe From cb086f79ff263e075dc8cb7fcd841059b7153082 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 12 Mar 2023 15:46:34 +0100 Subject: [PATCH 206/360] Improve doc wording and command parameters --- docs/utils.md | 47 ++++++++++++++++++++++++++++----- freqtrade/commands/arguments.py | 2 +- requirements.txt | 2 +- 3 files changed, 43 insertions(+), 8 deletions(-) diff --git a/docs/utils.md b/docs/utils.md index 3d6eda3ce..eb675442f 100644 --- a/docs/utils.md +++ b/docs/utils.md @@ -947,6 +947,7 @@ Common arguments: --userdir PATH, --user-data-dir PATH Path to userdata directory. ``` + ### Examples Print trades with id 2 and 3 as json @@ -956,11 +957,45 @@ freqtrade show-trades --db-url sqlite:///tradesv3.sqlite --trade-ids 2 3 --print ``` ### Strategy-Updater -Updates a list strategies or all strategies within the strategies folder to be v3 compliant including futures. -If the command runs without --strategy-list then all files inside the strategies folder will be converted. -``` -usage: freqtrade strategy_updater -optional arguments: - --strategy-list defines a list of strategies that should be converted +Updates listed strategies or all strategies within the strategies folder to be v3 compliant. +If the command runs without --strategy-list then all strategies inside the strategies folder will be converted. +Your original strategy will remain available in the `user_data/strategies_orig_updater/` directory. + +!!! Warning "Conversion results" + Strategy updater will work on a "best effort" approach. Please do your due diligence and verify the results of the conversion. + We also recommend to run a python formatter (e.g. `black`) to format results in a sane manner. + +``` +usage: freqtrade strategy-updater [-h] [-v] [--logfile FILE] [-V] [-c PATH] + [-d PATH] [--userdir PATH] + [--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]] + +options: + -h, --help show this help message and exit + --strategy-list STRATEGY_LIST [STRATEGY_LIST ...] + Provide a space-separated list of strategies to + backtest. Please note that timeframe needs to be set + either in config or via command line. When using this + together with `--export trades`, the strategy-name is + injected into the filename (so `backtest-data.json` + becomes `backtest-data-SampleStrategy.json` + +Common arguments: + -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). + --logfile FILE, --log-file 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, --data-dir PATH + Path to directory with historical backtesting data. + --userdir PATH, --user-data-dir PATH + Path to userdata directory. + ``` diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index a3cdc378a..9b714a864 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -116,7 +116,7 @@ NO_CONF_REQURIED = ["convert-data", "convert-trade-data", "download-data", "list NO_CONF_ALLOWED = ["create-userdir", "list-exchanges", "new-strategy"] -ARGS_STRATEGY_UTILS = ARGS_COMMON_OPTIMIZE + ["strategy_list"] +ARGS_STRATEGY_UTILS = ["strategy_list"] class Arguments: diff --git a/requirements.txt b/requirements.txt index 7607a3664..a0ff8e03a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -56,4 +56,4 @@ schedule==1.1.0 websockets==10.4 janus==1.0.0 -ast-comments>=1.0.0 +ast-comments==1.0.0 From d2a412d2c65ee34576ac06638748700bb3ee4e03 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 12 Mar 2023 15:47:03 +0100 Subject: [PATCH 207/360] Simplify start_strategy_update --- freqtrade/commands/strategy_utils_commands.py | 30 ++++++++----------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/freqtrade/commands/strategy_utils_commands.py b/freqtrade/commands/strategy_utils_commands.py index ed4d0bf1a..e579ec475 100644 --- a/freqtrade/commands/strategy_utils_commands.py +++ b/freqtrade/commands/strategy_utils_commands.py @@ -29,31 +29,27 @@ def start_strategy_update(args: Dict[str, Any]) -> None: config, enum_failed=False, recursive=config.get('recursive_strategy_search', False)) filtered_strategy_objs = [] - if 'strategy_list' in args: - for args_strategy in args['strategy_list']: - for strategy_obj in strategy_objs: - if (strategy_obj['name'] == args_strategy - and strategy_obj not in filtered_strategy_objs): - filtered_strategy_objs.append(strategy_obj) - break + if args['strategy_list']: + filtered_strategy_objs = [ + strategy_obj for strategy_obj in strategy_objs + if strategy_obj['name'] in args['strategy_list'] + ] - for filtered_strategy_obj in filtered_strategy_objs: - start_conversion(filtered_strategy_obj, config) else: - processed_locations = set() - for strategy_obj in strategy_objs: - if strategy_obj['location'] not in processed_locations: - processed_locations.add(strategy_obj['location']) - start_conversion(strategy_obj, config) + # Use all available entries. + filtered_strategy_objs = strategy_objs + + processed_locations = set() + for strategy_obj in filtered_strategy_objs: + if strategy_obj['location'] not in processed_locations: + processed_locations.add(strategy_obj['location']) + start_conversion(strategy_obj, config) def start_conversion(strategy_obj, config): - # try: print(f"Conversion of {Path(strategy_obj['location']).name} started.") instance_strategy_updater = StrategyUpdater() start = time.perf_counter() instance_strategy_updater.start(config, strategy_obj) elapsed = time.perf_counter() - start print(f"Conversion of {Path(strategy_obj['location']).name} took {elapsed:.1f} seconds.") - # except: - # pass From 0911cd72a2fb2ed5fd87c6d3f7ad3eb2706edb6d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 12 Mar 2023 15:59:02 +0100 Subject: [PATCH 208/360] Add test for strategy-updater start method --- freqtrade/commands/arguments.py | 2 +- tests/commands/test_commands.py | 37 ++++++++++++++++++++++++++++++++- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index 9b714a864..47aa37fdf 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -116,7 +116,7 @@ NO_CONF_REQURIED = ["convert-data", "convert-trade-data", "download-data", "list NO_CONF_ALLOWED = ["create-userdir", "list-exchanges", "new-strategy"] -ARGS_STRATEGY_UTILS = ["strategy_list"] +ARGS_STRATEGY_UTILS = ["strategy_list", "strategy_path", "recursive_strategy_search"] class Arguments: diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 0ba1924a7..179712c6d 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -14,7 +14,8 @@ from freqtrade.commands import (start_backtesting_show, start_convert_data, star 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) + start_strategy_update, start_test_pairlist, start_trading, + start_webserver) from freqtrade.commands.db_commands import start_convert_db from freqtrade.commands.deploy_commands import (clean_ui_subdir, download_and_install_ui, get_ui_download_url, read_ui_version) @@ -1546,3 +1547,37 @@ def test_start_convert_db(mocker, fee, tmpdir, caplog): start_convert_db(pargs) assert db_target_file.is_file() + + +def test_start_strategy_updater(mocker, tmpdir): + sc_mock = mocker.patch('freqtrade.commands.strategy_utils_commands.start_conversion') + teststrats = Path(__file__).parent.parent / 'strategy/strats' + args = [ + "strategy-updater", + "--userdir", + str(tmpdir), + "--strategy-path", + str(teststrats), + ] + pargs = get_args(args) + pargs['config'] = None + start_strategy_update(pargs) + # Number of strategies in the test directory + assert sc_mock.call_count == 11 + + sc_mock.reset_mock() + args = [ + "strategy-updater", + "--userdir", + str(tmpdir), + "--strategy-path", + str(teststrats), + "--strategy-list", + "StrategyTestV3", + "StrategyTestV2" + ] + pargs = get_args(args) + pargs['config'] = None + start_strategy_update(pargs) + # Number of strategies in the test directory + assert sc_mock.call_count == 2 From b5c4f9ebe293097c08554e06c785a3e40ed993b1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 12 Mar 2023 16:26:12 +0100 Subject: [PATCH 209/360] Split updater_tests to be clearer --- tests/test_strategy_updater.py | 139 ++++++++++++++++++++------------- 1 file changed, 86 insertions(+), 53 deletions(-) diff --git a/tests/test_strategy_updater.py b/tests/test_strategy_updater.py index 26d173871..7205a609d 100644 --- a/tests/test_strategy_updater.py +++ b/tests/test_strategy_updater.py @@ -7,9 +7,11 @@ import pytest from freqtrade.strategy.strategyupdater import StrategyUpdater -def test_strategy_updater(default_conf, caplog) -> None: - if sys.version_info < (3, 9): - pytest.skip("StrategyUpdater is not compatible with Python 3.8", allow_module_level=True) +if sys.version_info < (3, 9): + pytest.skip("StrategyUpdater is not compatible with Python 3.8", allow_module_level=True) + + +def test_strategy_updater_methods(default_conf, caplog) -> None: instance_strategy_updater = StrategyUpdater() modified_code1 = instance_strategy_updater.update_code(""" @@ -25,11 +27,32 @@ class testClass(IStrategy): def custom_sell(): pass """) + + assert "populate_entry_trend" in modified_code1 + assert "populate_exit_trend" in modified_code1 + assert "check_entry_timeout" in modified_code1 + assert "check_exit_timeout" in modified_code1 + assert "custom_exit" in modified_code1 + assert "INTERFACE_VERSION = 3" in modified_code1 + + +def test_strategy_updater_params(default_conf, caplog) -> None: + instance_strategy_updater = StrategyUpdater() + modified_code2 = instance_strategy_updater.update_code(""" ticker_interval = '15m' buy_some_parameter = IntParameter(space='buy') sell_some_parameter = IntParameter(space='sell') """) + + assert "timeframe" in modified_code2 + # check for not editing hyperopt spaces + assert "space='buy'" in modified_code2 + assert "space='sell'" in modified_code2 + + +def test_strategy_updater_constants(default_conf, caplog) -> None: + instance_strategy_updater = StrategyUpdater() modified_code3 = instance_strategy_updater.update_code(""" use_sell_signal = True sell_profit_only = True @@ -37,15 +60,38 @@ sell_profit_offset = True ignore_roi_if_buy_signal = True forcebuy_enable = True """) - modified_code4 = instance_strategy_updater.update_code(""" + + assert "use_exit_signal" in modified_code3 + assert "exit_profit_only" in modified_code3 + assert "exit_profit_offset" in modified_code3 + assert "ignore_roi_if_entry_signal" in modified_code3 + assert "force_entry_enable" in modified_code3 + + +def test_strategy_updater_df_columns(default_conf, caplog) -> None: + instance_strategy_updater = StrategyUpdater() + modified_code = instance_strategy_updater.update_code(""" dataframe.loc[reduce(lambda x, y: x & y, conditions), ["buy", "buy_tag"]] = (1, "buy_signal_1") dataframe.loc[reduce(lambda x, y: x & y, conditions), 'sell'] = 1 """) - modified_code5 = instance_strategy_updater.update_code(""" + + assert "enter_long" in modified_code + assert "exit_long" in modified_code + assert "enter_tag" in modified_code + + +def test_strategy_updater_method_params(default_conf, caplog) -> None: + instance_strategy_updater = StrategyUpdater() + modified_code = instance_strategy_updater.update_code(""" def confirm_trade_exit(sell_reason: str): pass """) - modified_code6 = instance_strategy_updater.update_code(""" + assert "exit_reason" in modified_code + + +def test_strategy_updater_dicts(default_conf, caplog) -> None: + instance_strategy_updater = StrategyUpdater() + modified_code = instance_strategy_updater.update_code(""" order_time_in_force = { 'buy': 'gtc', 'sell': 'ioc' @@ -62,17 +108,44 @@ unfilledtimeout = { } """) - modified_code7 = instance_strategy_updater.update_code(""" + assert "'entry': 'gtc'" in modified_code + assert "'exit': 'ioc'" in modified_code + assert "'entry': 'limit'" in modified_code + assert "'exit': 'market'" in modified_code + assert "'entry': 1" in modified_code + assert "'exit': 2" in modified_code + + +def test_strategy_updater_comparisons(default_conf, caplog) -> None: + instance_strategy_updater = StrategyUpdater() + modified_code = instance_strategy_updater.update_code(""" def confirm_trade_exit(sell_reason): if (sell_reason == 'stop_loss'): pass """) - modified_code8 = instance_strategy_updater.update_code(""" + assert "exit_reason" in modified_code + assert "exit_reason == 'stop_loss'" in modified_code + + +def test_strategy_updater_strings(default_conf, caplog) -> None: + instance_strategy_updater = StrategyUpdater() + + modified_code = instance_strategy_updater.update_code(""" sell_reason == 'sell_signal' sell_reason == 'force_sell' sell_reason == 'emergency_sell' """) - modified_code9 = instance_strategy_updater.update_code(""" + + # those tests currently don't work, next in line. + assert "exit_signal" in modified_code + assert "exit_reason" in modified_code + assert "force_exit" in modified_code + assert "emergency_exit" in modified_code + + +def test_strategy_updater_comments(default_conf, caplog) -> None: + instance_strategy_updater = StrategyUpdater() + modified_code = instance_strategy_updater.update_code(""" # This is the 1st comment import talib.abstract as ta # This is the 2nd comment @@ -89,49 +162,9 @@ class someStrategy(IStrategy): # This is the 4th comment stoploss = -0.1 """) + + assert "This is the 1st comment" in modified_code + assert "This is the 2nd comment" in modified_code + assert "This is the 3rd comment" in modified_code # currently still missing: # Webhook terminology, Telegram notification settings, Strategy/Config settings - - assert "populate_entry_trend" in modified_code1 - assert "populate_exit_trend" in modified_code1 - assert "check_entry_timeout" in modified_code1 - assert "check_exit_timeout" in modified_code1 - assert "custom_exit" in modified_code1 - assert "INTERFACE_VERSION = 3" in modified_code1 - - assert "timeframe" in modified_code2 - # check for not editing hyperopt spaces - assert "space='buy'" in modified_code2 - assert "space='sell'" in modified_code2 - - assert "use_exit_signal" in modified_code3 - assert "exit_profit_only" in modified_code3 - assert "exit_profit_offset" in modified_code3 - assert "ignore_roi_if_entry_signal" in modified_code3 - assert "force_entry_enable" in modified_code3 - - assert "enter_long" in modified_code4 - assert "exit_long" in modified_code4 - assert "enter_tag" in modified_code4 - - assert "exit_reason" in modified_code5 - - assert "'entry': 'gtc'" in modified_code6 - assert "'exit': 'ioc'" in modified_code6 - assert "'entry': 'limit'" in modified_code6 - assert "'exit': 'market'" in modified_code6 - assert "'entry': 1" in modified_code6 - assert "'exit': 2" in modified_code6 - - assert "exit_reason" in modified_code7 - assert "exit_reason == 'stop_loss'" in modified_code7 - - # those tests currently don't work, next in line. - assert "exit_signal" in modified_code8 - assert "exit_reason" in modified_code8 - assert "force_exit" in modified_code8 - assert "emergency_exit" in modified_code8 - - assert "This is the 1st comment" in modified_code9 - assert "This is the 2nd comment" in modified_code9 - assert "This is the 3rd comment" in modified_code9 From f5848ea891723fcdf33e8794f2018c0547f8252d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 12 Mar 2023 16:29:18 +0100 Subject: [PATCH 210/360] Add test for successful_buys --- tests/test_strategy_updater.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_strategy_updater.py b/tests/test_strategy_updater.py index 7205a609d..4aad152bf 100644 --- a/tests/test_strategy_updater.py +++ b/tests/test_strategy_updater.py @@ -84,9 +84,11 @@ def test_strategy_updater_method_params(default_conf, caplog) -> None: instance_strategy_updater = StrategyUpdater() modified_code = instance_strategy_updater.update_code(""" def confirm_trade_exit(sell_reason: str): + nr_orders = trades.nr_of_successful_buys pass """) assert "exit_reason" in modified_code + assert "nr_orders = trades.nr_of_successful_entries" in modified_code def test_strategy_updater_dicts(default_conf, caplog) -> None: From f584edf809c5413f7136ef7c7e1cda5888f5363d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 12 Mar 2023 16:45:56 +0100 Subject: [PATCH 211/360] Improve tests by simply running a full strategy through everything --- freqtrade/strategy/strategyupdater.py | 4 ++- tests/commands/test_commands.py | 2 +- tests/test_strategy_updater.py | 42 +++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/freqtrade/strategy/strategyupdater.py b/freqtrade/strategy/strategyupdater.py index 6fe1f326c..bc692b71c 100644 --- a/freqtrade/strategy/strategyupdater.py +++ b/freqtrade/strategy/strategyupdater.py @@ -3,6 +3,8 @@ from pathlib import Path import ast_comments +from freqtrade.constants import Config + class StrategyUpdater: name_mapping = { @@ -42,7 +44,7 @@ class StrategyUpdater: # create a dictionary that maps the old column names to the new ones rename_dict = {'buy': 'enter_long', 'sell': 'exit_long', 'buy_tag': 'enter_tag'} - def start(self, config, strategy_obj: dict) -> None: + def start(self, config: Config, strategy_obj: dict) -> None: """ Run strategy updater It updates a strategy to v3 with the help of the ast-module diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 179712c6d..318590b32 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -1549,7 +1549,7 @@ def test_start_convert_db(mocker, fee, tmpdir, caplog): assert db_target_file.is_file() -def test_start_strategy_updater(mocker, tmpdir): +def test_start_strategy_updater(mocker, tmpdir): sc_mock = mocker.patch('freqtrade.commands.strategy_utils_commands.start_conversion') teststrats = Path(__file__).parent.parent / 'strategy/strats' args = [ diff --git a/tests/test_strategy_updater.py b/tests/test_strategy_updater.py index 4aad152bf..d3bdd27b5 100644 --- a/tests/test_strategy_updater.py +++ b/tests/test_strategy_updater.py @@ -1,16 +1,56 @@ # pragma pylint: disable=missing-docstring, protected-access, invalid-name +import re +import shutil import sys +from pathlib import Path import pytest +from freqtrade.commands.strategy_utils_commands import start_strategy_update from freqtrade.strategy.strategyupdater import StrategyUpdater +from tests.conftest import get_args if sys.version_info < (3, 9): pytest.skip("StrategyUpdater is not compatible with Python 3.8", allow_module_level=True) +def test_strategy_updater_start(tmpdir, capsys) -> None: + # Effective test without mocks. + teststrats = Path(__file__).parent / 'strategy/strats' + tmpdirp = Path(tmpdir) / "strategies" + tmpdirp.mkdir() + shutil.copy(teststrats / 'strategy_test_v2.py', tmpdirp) + old_code = (teststrats / 'strategy_test_v2.py').read_text() + + args = [ + "strategy-updater", + "--userdir", + str(tmpdir), + "--strategy-list", + "StrategyTestV2" + ] + pargs = get_args(args) + pargs['config'] = None + + start_strategy_update(pargs) + + assert Path(tmpdir / "strategies_orig_updater").exists() + # Backup file exists + assert Path(tmpdir / "strategies_orig_updater" / 'strategy_test_v2.py').exists() + # updated file exists + new_file = Path(tmpdirp / 'strategy_test_v2.py') + assert new_file.exists() + new_code = new_file.read_text() + assert 'INTERFACE_VERSION = 3' in new_code + assert 'INTERFACE_VERSION = 2' in old_code + captured = capsys.readouterr() + + assert 'Conversion of strategy_test_v2.py started.' in captured.out + assert re.search(r'Conversion of strategy_test_v2\.py took .* seconds', captured.out) + + def test_strategy_updater_methods(default_conf, caplog) -> None: instance_strategy_updater = StrategyUpdater() @@ -155,6 +195,7 @@ import freqtrade.vendor.qtpylib.indicators as qtpylib class someStrategy(IStrategy): + INTERFACE_VERSION = 2 # This is the 3rd comment # This attribute will be overridden if the config file contains "minimal_roi" minimal_roi = { @@ -168,5 +209,6 @@ class someStrategy(IStrategy): assert "This is the 1st comment" in modified_code assert "This is the 2nd comment" in modified_code assert "This is the 3rd comment" in modified_code + assert "INTERFACE_VERSION = 3" in modified_code # currently still missing: # Webhook terminology, Telegram notification settings, Strategy/Config settings From a10f78e3ef3d2ee3c1605ec8c22dee46d64525cf Mon Sep 17 00:00:00 2001 From: initrv Date: Sun, 12 Mar 2023 23:29:27 +0300 Subject: [PATCH 212/360] fix increment in case of 0 --- freqtrade/freqai/RL/BaseEnvironment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqai/RL/BaseEnvironment.py b/freqtrade/freqai/RL/BaseEnvironment.py index df2a89d81..a9a9a613c 100644 --- a/freqtrade/freqai/RL/BaseEnvironment.py +++ b/freqtrade/freqai/RL/BaseEnvironment.py @@ -157,7 +157,7 @@ class BaseEnvironment(gym.Env): :param value: `metric` value :param category: `metric` category """ - increment = True if not value else False + increment = True if value is None else False value = 1 if increment else value if category not in self.tensorboard_metrics: From fbca8e65874d78e004b9724bab3ca8dd91b5bf0e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 12 Mar 2023 21:31:08 +0100 Subject: [PATCH 213/360] Allow empty pairlock reasons through api closes #8312 --- freqtrade/rpc/api_server/api_schemas.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 064a509fd..18621ccbd 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -311,7 +311,7 @@ class LockModel(BaseModel): lock_timestamp: int pair: str side: str - reason: str + reason: Optional[str] class Locks(BaseModel): From 52a091e063e5a4d30557aede4ca11a2a9060275e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Mar 2023 03:57:23 +0000 Subject: [PATCH 214/360] Bump pydantic from 1.10.5 to 1.10.6 Bumps [pydantic](https://github.com/pydantic/pydantic) from 1.10.5 to 1.10.6. - [Release notes](https://github.com/pydantic/pydantic/releases) - [Changelog](https://github.com/pydantic/pydantic/blob/v1.10.6/HISTORY.md) - [Commits](https://github.com/pydantic/pydantic/compare/v1.10.5...v1.10.6) --- updated-dependencies: - dependency-name: pydantic 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 5977a6440..15a1df4a0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -35,7 +35,7 @@ sdnotify==0.3.2 # API Server fastapi==0.92.0 -pydantic==1.10.5 +pydantic==1.10.6 uvicorn==0.20.0 pyjwt==2.6.0 aiofiles==23.1.0 From 22ebf04daa202044596da38461704c8ede669402 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Mar 2023 03:57:29 +0000 Subject: [PATCH 215/360] Bump pytest from 7.2.1 to 7.2.2 Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.2.1 to 7.2.2. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/7.2.1...7.2.2) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index aa6012b1d..814a85894 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -10,7 +10,7 @@ coveralls==3.3.1 ruff==0.0.254 mypy==1.0.1 pre-commit==3.1.1 -pytest==7.2.1 +pytest==7.2.2 pytest-asyncio==0.20.3 pytest-cov==4.0.0 pytest-mock==3.10.0 From 31daf72cc6e5a945d3abf8b36653c59aae56ecef Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Mar 2023 03:57:40 +0000 Subject: [PATCH 216/360] Bump mypy from 1.0.1 to 1.1.1 Bumps [mypy](https://github.com/python/mypy) from 1.0.1 to 1.1.1. - [Release notes](https://github.com/python/mypy/releases) - [Commits](https://github.com/python/mypy/compare/v1.0.1...v1.1.1) --- updated-dependencies: - dependency-name: mypy dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index aa6012b1d..9ca83271a 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -8,7 +8,7 @@ coveralls==3.3.1 ruff==0.0.254 -mypy==1.0.1 +mypy==1.1.1 pre-commit==3.1.1 pytest==7.2.1 pytest-asyncio==0.20.3 From b800f270920fa3f41d9c60870ceff52a59424928 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Mar 2023 03:57:46 +0000 Subject: [PATCH 217/360] Bump mkdocs-material from 9.1.1 to 9.1.2 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 9.1.1 to 9.1.2. - [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/9.1.1...9.1.2) --- 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 1b9a1f9b7..d384a7ec5 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,6 +1,6 @@ markdown==3.3.7 mkdocs==1.4.2 -mkdocs-material==9.1.1 +mkdocs-material==9.1.2 mdx_truly_sane_lists==1.3 pymdown-extensions==9.10 jinja2==3.1.2 From 82707be7d05077b61a6da6842a351fdf0045ebb2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Mar 2023 03:57:48 +0000 Subject: [PATCH 218/360] Bump pypa/gh-action-pypi-publish from 1.6.4 to 1.7.1 Bumps [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) from 1.6.4 to 1.7.1. - [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases) - [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/v1.6.4...v1.7.1) --- updated-dependencies: - dependency-name: pypa/gh-action-pypi-publish dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 191a10d1c..7e0483c3d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -425,7 +425,7 @@ jobs: python setup.py sdist bdist_wheel - name: Publish to PyPI (Test) - uses: pypa/gh-action-pypi-publish@v1.6.4 + uses: pypa/gh-action-pypi-publish@v1.7.1 if: (github.event_name == 'release') with: user: __token__ @@ -433,7 +433,7 @@ jobs: repository_url: https://test.pypi.org/legacy/ - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@v1.6.4 + uses: pypa/gh-action-pypi-publish@v1.7.1 if: (github.event_name == 'release') with: user: __token__ From dc6af9a1a7f4c849628861e4b14ff21947c6c355 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Mar 2023 03:57:54 +0000 Subject: [PATCH 219/360] Bump urllib3 from 1.26.14 to 1.26.15 Bumps [urllib3](https://github.com/urllib3/urllib3) from 1.26.14 to 1.26.15. - [Release notes](https://github.com/urllib3/urllib3/releases) - [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst) - [Commits](https://github.com/urllib3/urllib3/compare/1.26.14...1.26.15) --- updated-dependencies: - dependency-name: urllib3 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 5977a6440..f2f7c4cf7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ python-telegram-bot==13.15 arrow==1.2.3 cachetools==4.2.2 requests==2.28.2 -urllib3==1.26.14 +urllib3==1.26.15 jsonschema==4.17.3 TA-Lib==0.4.25 technical==1.4.0 From 10c5adfa5076cbc9b763342a899ae5e67e661bf0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Mar 2023 05:37:55 +0000 Subject: [PATCH 220/360] Bump fastapi from 0.92.0 to 0.94.0 Bumps [fastapi](https://github.com/tiangolo/fastapi) from 0.92.0 to 0.94.0. - [Release notes](https://github.com/tiangolo/fastapi/releases) - [Commits](https://github.com/tiangolo/fastapi/compare/0.92.0...0.94.0) --- updated-dependencies: - dependency-name: fastapi dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 15a1df4a0..53991d9f1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -34,7 +34,7 @@ orjson==3.8.7 sdnotify==0.3.2 # API Server -fastapi==0.92.0 +fastapi==0.94.0 pydantic==1.10.6 uvicorn==0.20.0 pyjwt==2.6.0 From 0e663a5bf80ed3149e0fb7e60984d2869369583d Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 13 Mar 2023 07:06:16 +0100 Subject: [PATCH 221/360] Refresh binance cached leverage tiers --- .../exchange/binance_leverage_tiers.json | 3984 ++++++++++++----- 1 file changed, 2859 insertions(+), 1125 deletions(-) diff --git a/freqtrade/exchange/binance_leverage_tiers.json b/freqtrade/exchange/binance_leverage_tiers.json index 22db74f06..07fdcb5a4 100644 --- a/freqtrade/exchange/binance_leverage_tiers.json +++ b/freqtrade/exchange/binance_leverage_tiers.json @@ -104,10 +104,10 @@ "minNotional": 0.0, "maxNotional": 5000.0, "maintenanceMarginRate": 0.01, - "maxLeverage": 20.0, + "maxLeverage": 25.0, "info": { "bracket": "1", - "initialLeverage": "20", + "initialLeverage": "25", "notionalCap": "5000", "notionalFloor": "0", "maintMarginRatio": "0.01", @@ -120,10 +120,10 @@ "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, + "maxLeverage": 20.0, "info": { "bracket": "2", - "initialLeverage": "10", + "initialLeverage": "20", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", @@ -134,13 +134,13 @@ "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, + "maxNotional": 400000.0, "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, + "maxLeverage": 10.0, "info": { "bracket": "3", - "initialLeverage": "8", - "notionalCap": "100000", + "initialLeverage": "10", + "notionalCap": "400000", "notionalFloor": "25000", "maintMarginRatio": "0.05", "cum": "700.0" @@ -149,49 +149,65 @@ { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, + "minNotional": 400000.0, + "maxNotional": 1000000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", + "notionalCap": "1000000", + "notionalFloor": "400000", "maintMarginRatio": "0.1", - "cum": "5700.0" + "cum": "20700.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, + "minNotional": 1000000.0, + "maxNotional": 2000000.0, "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maxLeverage": 4.0, "info": { "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", + "initialLeverage": "4", + "notionalCap": "2000000", + "notionalFloor": "1000000", "maintMarginRatio": "0.125", - "cum": "11950.0" + "cum": "45700.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, + "minNotional": 2000000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "6000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.25", + "cum": "295700.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 6000000.0, + "maxNotional": 10000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", + "notionalCap": "10000000", + "notionalFloor": "6000000", "maintMarginRatio": "0.5", - "cum": "386950.0" + "cum": "1795700.0" } } ], @@ -658,10 +674,10 @@ "minNotional": 0.0, "maxNotional": 5000.0, "maintenanceMarginRate": 0.01, - "maxLeverage": 20.0, + "maxLeverage": 25.0, "info": { "bracket": "1", - "initialLeverage": "20", + "initialLeverage": "25", "notionalCap": "5000", "notionalFloor": "0", "maintMarginRatio": "0.01", @@ -674,10 +690,10 @@ "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, + "maxLeverage": 20.0, "info": { "bracket": "2", - "initialLeverage": "10", + "initialLeverage": "20", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", @@ -688,18 +704,132 @@ "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, + "maxNotional": 400000.0, "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, + "maxLeverage": 10.0, "info": { "bracket": "3", - "initialLeverage": "8", - "notionalCap": "100000", + "initialLeverage": "10", + "notionalCap": "400000", "notionalFloor": "25000", "maintMarginRatio": "0.05", "cum": "700.0" } }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 400000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "1000000", + "notionalFloor": "400000", + "maintMarginRatio": "0.1", + "cum": "20700.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "2000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.125", + "cum": "45700.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 2000000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "6000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.25", + "cum": "295700.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 6000000.0, + "maxNotional": 10000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "10000000", + "notionalFloor": "6000000", + "maintMarginRatio": "0.5", + "cum": "1795700.0" + } + } + ], + "ACH/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 15.0, + "info": { + "bracket": "2", + "initialLeverage": "15", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, { "tier": 4.0, "currency": "USDT", @@ -713,7 +843,7 @@ "notionalCap": "250000", "notionalFloor": "100000", "maintMarginRatio": "0.1", - "cum": "5700.0" + "cum": "5650.0" } }, { @@ -729,7 +859,7 @@ "notionalCap": "1000000", "notionalFloor": "250000", "maintMarginRatio": "0.125", - "cum": "11950.0" + "cum": "11900.0" } }, { @@ -745,7 +875,7 @@ "notionalCap": "5000000", "notionalFloor": "1000000", "maintMarginRatio": "0.5", - "cum": "386950.0" + "cum": "386900.0" } } ], @@ -1114,10 +1244,10 @@ "minNotional": 0.0, "maxNotional": 5000.0, "maintenanceMarginRate": 0.02, - "maxLeverage": 20.0, + "maxLeverage": 25.0, "info": { "bracket": "1", - "initialLeverage": "20", + "initialLeverage": "25", "notionalCap": "5000", "notionalFloor": "0", "maintMarginRatio": "0.02", @@ -1130,10 +1260,10 @@ "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 15.0, + "maxLeverage": 20.0, "info": { "bracket": "2", - "initialLeverage": "15", + "initialLeverage": "20", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", @@ -1144,13 +1274,13 @@ "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, + "maxNotional": 200000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "3", "initialLeverage": "10", - "notionalCap": "100000", + "notionalCap": "200000", "notionalFloor": "25000", "maintMarginRatio": "0.05", "cum": "650.0" @@ -1159,49 +1289,65 @@ { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, + "minNotional": 200000.0, + "maxNotional": 500000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", + "notionalCap": "500000", + "notionalFloor": "200000", "maintMarginRatio": "0.1", - "cum": "5650.0" + "cum": "10650.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, + "minNotional": 500000.0, "maxNotional": 1000000.0, "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maxLeverage": 4.0, "info": { "bracket": "5", - "initialLeverage": "2", + "initialLeverage": "4", "notionalCap": "1000000", - "notionalFloor": "250000", + "notionalFloor": "500000", "maintMarginRatio": "0.125", - "cum": "11900.0" + "cum": "23150.0" } }, { "tier": 6.0, "currency": "USDT", "minNotional": 1000000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "148150.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, "maxNotional": 5000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "1", "notionalCap": "5000000", - "notionalFloor": "1000000", + "notionalFloor": "3000000", "maintMarginRatio": "0.5", - "cum": "386900.0" + "cum": "898150.0" } } ], @@ -1701,14 +1847,14 @@ "currency": "USDT", "minNotional": 0.0, "maxNotional": 5000.0, - "maintenanceMarginRate": 0.012, - "maxLeverage": 20.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 25.0, "info": { "bracket": "1", - "initialLeverage": "20", + "initialLeverage": "25", "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.012", + "maintMarginRatio": "0.01", "cum": "0.0" } }, @@ -1718,78 +1864,94 @@ "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, + "maxLeverage": 20.0, "info": { "bracket": "2", - "initialLeverage": "10", + "initialLeverage": "20", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", - "cum": "65.0" + "cum": "75.0" } }, { "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, + "maxNotional": 400000.0, "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, + "maxLeverage": 10.0, "info": { "bracket": "3", - "initialLeverage": "8", - "notionalCap": "100000", + "initialLeverage": "10", + "notionalCap": "400000", "notionalFloor": "25000", "maintMarginRatio": "0.05", - "cum": "690.0" + "cum": "700.0" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, + "minNotional": 400000.0, + "maxNotional": 1000000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", + "notionalCap": "1000000", + "notionalFloor": "400000", "maintMarginRatio": "0.1", - "cum": "5690.0" + "cum": "20700.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, + "minNotional": 1000000.0, + "maxNotional": 2000000.0, "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maxLeverage": 4.0, "info": { "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", + "initialLeverage": "4", + "notionalCap": "2000000", + "notionalFloor": "1000000", "maintMarginRatio": "0.125", - "cum": "11940.0" + "cum": "45700.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, + "minNotional": 2000000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "6000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.25", + "cum": "295700.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 6000000.0, + "maxNotional": 10000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", + "notionalCap": "10000000", + "notionalFloor": "6000000", "maintMarginRatio": "0.5", - "cum": "386940.0" + "cum": "1795700.0" } } ], @@ -2353,14 +2515,14 @@ "currency": "USDT", "minNotional": 0.0, "maxNotional": 5000.0, - "maintenanceMarginRate": 0.015, - "maxLeverage": 25.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 50.0, "info": { "bracket": "1", - "initialLeverage": "25", + "initialLeverage": "50", "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.015", + "maintMarginRatio": "0.01", "cum": "0.0" } }, @@ -2369,14 +2531,14 @@ "currency": "USDT", "minNotional": 5000.0, "maxNotional": 25000.0, - "maintenanceMarginRate": 0.02, - "maxLeverage": 20.0, + "maintenanceMarginRate": 0.015, + "maxLeverage": 25.0, "info": { "bracket": "2", - "initialLeverage": "20", + "initialLeverage": "25", "notionalCap": "25000", "notionalFloor": "5000", - "maintMarginRatio": "0.02", + "maintMarginRatio": "0.015", "cum": "25.0" } }, @@ -2384,112 +2546,96 @@ "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 150000.0, - "maintenanceMarginRate": 0.0225, - "maxLeverage": 15.0, + "maxNotional": 300000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, "info": { "bracket": "3", - "initialLeverage": "15", - "notionalCap": "150000", + "initialLeverage": "20", + "notionalCap": "300000", "notionalFloor": "25000", - "maintMarginRatio": "0.0225", - "cum": "87.5" + "maintMarginRatio": "0.02", + "cum": "150.0" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 150000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.025, + "minNotional": 300000.0, + "maxNotional": 1200000.0, + "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "4", "initialLeverage": "10", - "notionalCap": "250000", - "notionalFloor": "150000", - "maintMarginRatio": "0.025", - "cum": "462.5" + "notionalCap": "1200000", + "notionalFloor": "300000", + "maintMarginRatio": "0.05", + "cum": "9150.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, + "minNotional": 1200000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, "info": { "bracket": "5", - "initialLeverage": "8", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.05", - "cum": "6712.5" + "initialLeverage": "5", + "notionalCap": "3000000", + "notionalFloor": "1200000", + "maintMarginRatio": "0.1", + "cum": "69150.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 2000000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, + "minNotional": 3000000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, "info": { "bracket": "6", - "initialLeverage": "5", - "notionalCap": "2000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.1", - "cum": "56712.5" + "initialLeverage": "4", + "notionalCap": "6000000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.125", + "cum": "144150.0" } }, { "tier": 7.0, "currency": "USDT", - "minNotional": 2000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 4.0, + "minNotional": 6000000.0, + "maxNotional": 18000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, "info": { "bracket": "7", - "initialLeverage": "4", - "notionalCap": "5000000", - "notionalFloor": "2000000", - "maintMarginRatio": "0.125", - "cum": "106712.5" + "initialLeverage": "2", + "notionalCap": "18000000", + "notionalFloor": "6000000", + "maintMarginRatio": "0.25", + "cum": "894150.0" } }, { "tier": 8.0, "currency": "USDT", - "minNotional": 5000000.0, - "maxNotional": 10000000.0, - "maintenanceMarginRate": 0.25, - "maxLeverage": 2.0, - "info": { - "bracket": "8", - "initialLeverage": "2", - "notionalCap": "10000000", - "notionalFloor": "5000000", - "maintMarginRatio": "0.25", - "cum": "731712.5" - } - }, - { - "tier": 9.0, - "currency": "USDT", - "minNotional": 10000000.0, - "maxNotional": 20000000.0, + "minNotional": 18000000.0, + "maxNotional": 30000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "9", + "bracket": "8", "initialLeverage": "1", - "notionalCap": "20000000", - "notionalFloor": "10000000", + "notionalCap": "30000000", + "notionalFloor": "18000000", "maintMarginRatio": "0.5", - "cum": "3231712.5" + "cum": "5394150.0" } } ], @@ -2891,14 +3037,14 @@ "currency": "USDT", "minNotional": 0.0, "maxNotional": 5000.0, - "maintenanceMarginRate": 0.0065, + "maintenanceMarginRate": 0.006, "maxLeverage": 50.0, "info": { "bracket": "1", "initialLeverage": "50", "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.0065", + "maintMarginRatio": "0.006", "cum": "0.0" } }, @@ -2915,61 +3061,61 @@ "notionalCap": "50000", "notionalFloor": "5000", "maintMarginRatio": "0.01", - "cum": "17.5" + "cum": "20.0" } }, { "tier": 3.0, "currency": "USDT", "minNotional": 50000.0, - "maxNotional": 200000.0, + "maxNotional": 600000.0, "maintenanceMarginRate": 0.025, "maxLeverage": 20.0, "info": { "bracket": "3", "initialLeverage": "20", - "notionalCap": "200000", + "notionalCap": "600000", "notionalFloor": "50000", "maintMarginRatio": "0.025", - "cum": "767.5" + "cum": "770.0" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 200000.0, - "maxNotional": 400000.0, + "minNotional": 600000.0, + "maxNotional": 1200000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "4", "initialLeverage": "10", - "notionalCap": "400000", - "notionalFloor": "200000", + "notionalCap": "1200000", + "notionalFloor": "600000", "maintMarginRatio": "0.05", - "cum": "5767.5" + "cum": "15770.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 400000.0, - "maxNotional": 1000000.0, + "minNotional": 1200000.0, + "maxNotional": 3200000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "5", "initialLeverage": "5", - "notionalCap": "1000000", - "notionalFloor": "400000", + "notionalCap": "3200000", + "notionalFloor": "1200000", "maintMarginRatio": "0.1", - "cum": "25767.5" + "cum": "75770.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 1000000.0, + "minNotional": 3200000.0, "maxNotional": 5000000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 4.0, @@ -2977,41 +3123,41 @@ "bracket": "6", "initialLeverage": "4", "notionalCap": "5000000", - "notionalFloor": "1000000", + "notionalFloor": "3200000", "maintMarginRatio": "0.125", - "cum": "50767.5" + "cum": "155770.0" } }, { "tier": 7.0, "currency": "USDT", "minNotional": 5000000.0, - "maxNotional": 6000000.0, + "maxNotional": 12000000.0, "maintenanceMarginRate": 0.25, "maxLeverage": 2.0, "info": { "bracket": "7", "initialLeverage": "2", - "notionalCap": "6000000", + "notionalCap": "12000000", "notionalFloor": "5000000", "maintMarginRatio": "0.25", - "cum": "675767.5" + "cum": "780770.0" } }, { "tier": 8.0, "currency": "USDT", - "minNotional": 6000000.0, - "maxNotional": 10000000.0, + "minNotional": 12000000.0, + "maxNotional": 20000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { "bracket": "8", "initialLeverage": "1", - "notionalCap": "10000000", - "notionalFloor": "6000000", + "notionalCap": "20000000", + "notionalFloor": "12000000", "maintMarginRatio": "0.5", - "cum": "2175767.5" + "cum": "3780770.0" } } ], @@ -4762,96 +4908,96 @@ "tier": 1.0, "currency": "USDT", "minNotional": 0.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 8.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, "info": { "bracket": "1", - "initialLeverage": "8", - "notionalCap": "25000", + "initialLeverage": "20", + "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.025", + "maintMarginRatio": "0.02", "cum": "0.0" } }, { "tier": 2.0, "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 6.0, + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 15.0, "info": { "bracket": "2", - "initialLeverage": "6", - "notionalCap": "250000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "625.0" + "initialLeverage": "15", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" } }, { "tier": 3.0, "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 500000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, "info": { "bracket": "3", - "initialLeverage": "5", - "notionalCap": "500000", - "notionalFloor": "250000", - "maintMarginRatio": "0.1", - "cum": "13125.0" + "initialLeverage": "10", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 500000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 4.0, + "minNotional": 100000.0, + "maxNotional": 250000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, "info": { "bracket": "4", - "initialLeverage": "4", - "notionalCap": "1000000", - "notionalFloor": "500000", - "maintMarginRatio": "0.125", - "cum": "25625.0" + "initialLeverage": "5", + "notionalCap": "250000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5650.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 1500000.0, - "maintenanceMarginRate": 0.25, + "minNotional": 250000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, "maxLeverage": 2.0, "info": { "bracket": "5", "initialLeverage": "2", - "notionalCap": "1500000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.25", - "cum": "150625.0" + "notionalCap": "1000000", + "notionalFloor": "250000", + "maintMarginRatio": "0.125", + "cum": "11900.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 1500000.0, - "maxNotional": 2000000.0, + "minNotional": 1000000.0, + "maxNotional": 5000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { "bracket": "6", "initialLeverage": "1", - "notionalCap": "2000000", - "notionalFloor": "1500000", + "notionalCap": "5000000", + "notionalFloor": "1000000", "maintMarginRatio": "0.5", - "cum": "525625.0" + "cum": "386900.0" } } ], @@ -5054,13 +5200,13 @@ "tier": 3.0, "currency": "USDT", "minNotional": 250000.0, - "maxNotional": 1000000.0, + "maxNotional": 3000000.0, "maintenanceMarginRate": 0.01, "maxLeverage": 50.0, "info": { "bracket": "3", "initialLeverage": "50", - "notionalCap": "1000000", + "notionalCap": "3000000", "notionalFloor": "250000", "maintMarginRatio": "0.01", "cum": "1300.0" @@ -5069,55 +5215,55 @@ { "tier": 4.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 10000000.0, + "minNotional": 3000000.0, + "maxNotional": 15000000.0, "maintenanceMarginRate": 0.025, "maxLeverage": 20.0, "info": { "bracket": "4", "initialLeverage": "20", - "notionalCap": "10000000", - "notionalFloor": "1000000", + "notionalCap": "15000000", + "notionalFloor": "3000000", "maintMarginRatio": "0.025", - "cum": "16300.0" + "cum": "46300.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 10000000.0, - "maxNotional": 20000000.0, + "minNotional": 15000000.0, + "maxNotional": 30000000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "5", "initialLeverage": "10", - "notionalCap": "20000000", - "notionalFloor": "10000000", + "notionalCap": "30000000", + "notionalFloor": "15000000", "maintMarginRatio": "0.05", - "cum": "266300.0" + "cum": "421300.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 20000000.0, - "maxNotional": 50000000.0, + "minNotional": 30000000.0, + "maxNotional": 80000000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "6", "initialLeverage": "5", - "notionalCap": "50000000", - "notionalFloor": "20000000", + "notionalCap": "80000000", + "notionalFloor": "30000000", "maintMarginRatio": "0.1", - "cum": "1266300.0" + "cum": "1921300.0" } }, { "tier": 7.0, "currency": "USDT", - "minNotional": 50000000.0, + "minNotional": 80000000.0, "maxNotional": 100000000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 4.0, @@ -5125,9 +5271,9 @@ "bracket": "7", "initialLeverage": "4", "notionalCap": "100000000", - "notionalFloor": "50000000", + "notionalFloor": "80000000", "maintMarginRatio": "0.125", - "cum": "2516300.0" + "cum": "3921300.0" } }, { @@ -5143,7 +5289,7 @@ "notionalCap": "200000000", "notionalFloor": "100000000", "maintMarginRatio": "0.15", - "cum": "5016300.0" + "cum": "6421300.0" } }, { @@ -5159,7 +5305,7 @@ "notionalCap": "300000000", "notionalFloor": "200000000", "maintMarginRatio": "0.25", - "cum": "2.50163E7" + "cum": "2.64213E7" } }, { @@ -5175,7 +5321,7 @@ "notionalCap": "500000000", "notionalFloor": "300000000", "maintMarginRatio": "0.5", - "cum": "1.000163E8" + "cum": "1.014213E8" } } ], @@ -5881,6 +6027,136 @@ } } ], + "CFX/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 50.0, + "info": { + "bracket": "1", + "initialLeverage": "50", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.01", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.015, + "maxLeverage": 25.0, + "info": { + "bracket": "2", + "initialLeverage": "25", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.015", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 300000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, + "info": { + "bracket": "3", + "initialLeverage": "20", + "notionalCap": "300000", + "notionalFloor": "25000", + "maintMarginRatio": "0.02", + "cum": "150.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 300000.0, + "maxNotional": 1200000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "4", + "initialLeverage": "10", + "notionalCap": "1200000", + "notionalFloor": "300000", + "maintMarginRatio": "0.05", + "cum": "9150.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 1200000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "5", + "initialLeverage": "5", + "notionalCap": "3000000", + "notionalFloor": "1200000", + "maintMarginRatio": "0.1", + "cum": "69150.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 3000000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "6", + "initialLeverage": "4", + "notionalCap": "6000000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.125", + "cum": "144150.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 6000000.0, + "maxNotional": 18000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "7", + "initialLeverage": "2", + "notionalCap": "18000000", + "notionalFloor": "6000000", + "maintMarginRatio": "0.25", + "cum": "894150.0" + } + }, + { + "tier": 8.0, + "currency": "USDT", + "minNotional": 18000000.0, + "maxNotional": 30000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "8", + "initialLeverage": "1", + "notionalCap": "30000000", + "notionalFloor": "18000000", + "maintMarginRatio": "0.5", + "cum": "5394150.0" + } + } + ], "CHR/USDT:USDT": [ { "tier": 1.0, @@ -6077,6 +6353,218 @@ } } ], + "CKB/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 15.0, + "info": { + "bracket": "2", + "initialLeverage": "15", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 200000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "200000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 200000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "500000", + "notionalFloor": "200000", + "maintMarginRatio": "0.1", + "cum": "10650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.125", + "cum": "23150.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "148150.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.5", + "cum": "898150.0" + } + } + ], + "COCOS/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 15.0, + "info": { + "bracket": "2", + "initialLeverage": "15", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 100000.0, + "maxNotional": 250000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "250000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 250000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 2.0, + "info": { + "bracket": "5", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "250000", + "maintMarginRatio": "0.125", + "cum": "11900.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "6", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "386900.0" + } + } + ], "COMP/USDT:USDT": [ { "tier": 1.0, @@ -7944,10 +8432,10 @@ "minNotional": 0.0, "maxNotional": 5000.0, "maintenanceMarginRate": 0.02, - "maxLeverage": 20.0, + "maxLeverage": 25.0, "info": { "bracket": "1", - "initialLeverage": "20", + "initialLeverage": "25", "notionalCap": "5000", "notionalFloor": "0", "maintMarginRatio": "0.02", @@ -7960,10 +8448,10 @@ "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, + "maxLeverage": 20.0, "info": { "bracket": "2", - "initialLeverage": "10", + "initialLeverage": "20", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", @@ -7974,13 +8462,13 @@ "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, + "maxNotional": 200000.0, "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, + "maxLeverage": 10.0, "info": { "bracket": "3", - "initialLeverage": "8", - "notionalCap": "100000", + "initialLeverage": "10", + "notionalCap": "200000", "notionalFloor": "25000", "maintMarginRatio": "0.05", "cum": "650.0" @@ -7989,105 +8477,7 @@ { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5650.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11900.0" - } - }, - { - "tier": 6.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 3000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "3000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386900.0" - } - } - ], - "DYDX/USDT:USDT": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 50000.0, - "maintenanceMarginRate": 0.02, - "maxLeverage": 20.0, - "info": { - "bracket": "1", - "initialLeverage": "20", - "notionalCap": "50000", - "notionalFloor": "0", - "maintMarginRatio": "0.02", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 50000.0, - "maxNotional": 150000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, - "info": { - "bracket": "2", - "initialLeverage": "10", - "notionalCap": "150000", - "notionalFloor": "50000", - "maintMarginRatio": "0.025", - "cum": "250.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 150000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, - "info": { - "bracket": "3", - "initialLeverage": "8", - "notionalCap": "250000", - "notionalFloor": "150000", - "maintMarginRatio": "0.05", - "cum": "4000.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 250000.0, + "minNotional": 200000.0, "maxNotional": 500000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, @@ -8095,9 +8485,9 @@ "bracket": "4", "initialLeverage": "5", "notionalCap": "500000", - "notionalFloor": "250000", + "notionalFloor": "200000", "maintMarginRatio": "0.1", - "cum": "16500.0" + "cum": "10650.0" } }, { @@ -8113,39 +8503,169 @@ "notionalCap": "1000000", "notionalFloor": "500000", "maintMarginRatio": "0.125", - "cum": "29000.0" + "cum": "23150.0" } }, { "tier": 6.0, "currency": "USDT", "minNotional": 1000000.0, - "maxNotional": 4000000.0, + "maxNotional": 3000000.0, "maintenanceMarginRate": 0.25, "maxLeverage": 2.0, "info": { "bracket": "6", "initialLeverage": "2", - "notionalCap": "4000000", + "notionalCap": "3000000", "notionalFloor": "1000000", "maintMarginRatio": "0.25", - "cum": "154000.0" + "cum": "148150.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.5", + "cum": "898150.0" + } + } + ], + "DYDX/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 50.0, + "info": { + "bracket": "1", + "initialLeverage": "50", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.01", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 50000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, + "info": { + "bracket": "2", + "initialLeverage": "25", + "notionalCap": "50000", + "notionalFloor": "5000", + "maintMarginRatio": "0.02", + "cum": "50.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 50000.0, + "maxNotional": 400000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "3", + "initialLeverage": "20", + "notionalCap": "400000", + "notionalFloor": "50000", + "maintMarginRatio": "0.025", + "cum": "300.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 400000.0, + "maxNotional": 800000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "4", + "initialLeverage": "10", + "notionalCap": "800000", + "notionalFloor": "400000", + "maintMarginRatio": "0.05", + "cum": "10300.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 800000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "5", + "initialLeverage": "5", + "notionalCap": "2000000", + "notionalFloor": "800000", + "maintMarginRatio": "0.1", + "cum": "50300.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 2000000.0, + "maxNotional": 4000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "6", + "initialLeverage": "4", + "notionalCap": "4000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.125", + "cum": "100300.0" } }, { "tier": 7.0, "currency": "USDT", "minNotional": 4000000.0, - "maxNotional": 8000000.0, + "maxNotional": 12000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "7", + "initialLeverage": "2", + "notionalCap": "12000000", + "notionalFloor": "4000000", + "maintMarginRatio": "0.25", + "cum": "600300.0" + } + }, + { + "tier": 8.0, + "currency": "USDT", + "minNotional": 12000000.0, + "maxNotional": 20000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "7", + "bracket": "8", "initialLeverage": "1", - "notionalCap": "8000000", - "notionalFloor": "4000000", + "notionalCap": "20000000", + "notionalFloor": "12000000", "maintMarginRatio": "0.5", - "cum": "1154000.0" + "cum": "3600300.0" } } ], @@ -9096,13 +9616,13 @@ "tier": 3.0, "currency": "USDT", "minNotional": 250000.0, - "maxNotional": 1000000.0, + "maxNotional": 2000000.0, "maintenanceMarginRate": 0.01, "maxLeverage": 50.0, "info": { "bracket": "3", "initialLeverage": "50", - "notionalCap": "1000000", + "notionalCap": "2000000", "notionalFloor": "250000", "maintMarginRatio": "0.01", "cum": "1025.0" @@ -9111,71 +9631,71 @@ { "tier": 4.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, + "minNotional": 2000000.0, + "maxNotional": 10000000.0, "maintenanceMarginRate": 0.02, "maxLeverage": 20.0, "info": { "bracket": "4", "initialLeverage": "20", - "notionalCap": "5000000", - "notionalFloor": "1000000", + "notionalCap": "10000000", + "notionalFloor": "2000000", "maintMarginRatio": "0.02", - "cum": "11025.0" + "cum": "21025.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 5000000.0, - "maxNotional": 10000000.0, + "minNotional": 10000000.0, + "maxNotional": 25000000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "5", "initialLeverage": "10", - "notionalCap": "10000000", - "notionalFloor": "5000000", + "notionalCap": "25000000", + "notionalFloor": "10000000", "maintMarginRatio": "0.05", - "cum": "161025.0" + "cum": "321025.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 10000000.0, - "maxNotional": 20000000.0, + "minNotional": 25000000.0, + "maxNotional": 50000000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "6", "initialLeverage": "5", - "notionalCap": "20000000", - "notionalFloor": "10000000", + "notionalCap": "50000000", + "notionalFloor": "25000000", "maintMarginRatio": "0.1", - "cum": "661025.0" + "cum": "1571025.0" } }, { "tier": 7.0, "currency": "USDT", - "minNotional": 20000000.0, - "maxNotional": 40000000.0, + "minNotional": 50000000.0, + "maxNotional": 60000000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 4.0, "info": { "bracket": "7", "initialLeverage": "4", - "notionalCap": "40000000", - "notionalFloor": "20000000", + "notionalCap": "60000000", + "notionalFloor": "50000000", "maintMarginRatio": "0.125", - "cum": "1161025.0" + "cum": "2821025.0" } }, { "tier": 8.0, "currency": "USDT", - "minNotional": 40000000.0, + "minNotional": 60000000.0, "maxNotional": 80000000.0, "maintenanceMarginRate": 0.15, "maxLeverage": 3.0, @@ -9183,9 +9703,9 @@ "bracket": "8", "initialLeverage": "3", "notionalCap": "80000000", - "notionalFloor": "40000000", + "notionalFloor": "60000000", "maintMarginRatio": "0.15", - "cum": "2161025.0" + "cum": "4321025.0" } }, { @@ -9201,7 +9721,7 @@ "notionalCap": "150000000", "notionalFloor": "80000000", "maintMarginRatio": "0.25", - "cum": "1.0161025E7" + "cum": "1.2321025E7" } }, { @@ -9217,7 +9737,7 @@ "notionalCap": "300000000", "notionalFloor": "150000000", "maintMarginRatio": "0.5", - "cum": "4.7661025E7" + "cum": "4.9821025E7" } } ], @@ -9342,10 +9862,10 @@ "minNotional": 0.0, "maxNotional": 5000.0, "maintenanceMarginRate": 0.02, - "maxLeverage": 20.0, + "maxLeverage": 25.0, "info": { "bracket": "1", - "initialLeverage": "20", + "initialLeverage": "25", "notionalCap": "5000", "notionalFloor": "0", "maintMarginRatio": "0.02", @@ -9358,10 +9878,10 @@ "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 15.0, + "maxLeverage": 20.0, "info": { "bracket": "2", - "initialLeverage": "15", + "initialLeverage": "20", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", @@ -9372,13 +9892,13 @@ "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, + "maxNotional": 200000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "3", "initialLeverage": "10", - "notionalCap": "100000", + "notionalCap": "200000", "notionalFloor": "25000", "maintMarginRatio": "0.05", "cum": "650.0" @@ -9387,49 +9907,65 @@ { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, + "minNotional": 200000.0, + "maxNotional": 500000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", + "notionalCap": "500000", + "notionalFloor": "200000", "maintMarginRatio": "0.1", - "cum": "5650.0" + "cum": "10650.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, + "minNotional": 500000.0, "maxNotional": 1000000.0, "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maxLeverage": 4.0, "info": { "bracket": "5", - "initialLeverage": "2", + "initialLeverage": "4", "notionalCap": "1000000", - "notionalFloor": "250000", + "notionalFloor": "500000", "maintMarginRatio": "0.125", - "cum": "11900.0" + "cum": "23150.0" } }, { "tier": 6.0, "currency": "USDT", "minNotional": 1000000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "148150.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, "maxNotional": 5000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "1", "notionalCap": "5000000", - "notionalFloor": "1000000", + "notionalFloor": "3000000", "maintMarginRatio": "0.5", - "cum": "386900.0" + "cum": "898150.0" } } ], @@ -9537,14 +10073,14 @@ "currency": "USDT", "minNotional": 0.0, "maxNotional": 50000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 25.0, + "maintenanceMarginRate": 0.006, + "maxLeverage": 50.0, "info": { "bracket": "1", - "initialLeverage": "25", + "initialLeverage": "50", "notionalCap": "50000", "notionalFloor": "0", - "maintMarginRatio": "0.01", + "maintMarginRatio": "0.006", "cum": "0.0" } }, @@ -9553,111 +10089,127 @@ "currency": "USDT", "minNotional": 50000.0, "maxNotional": 250000.0, - "maintenanceMarginRate": 0.02, - "maxLeverage": 20.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 25.0, "info": { "bracket": "2", - "initialLeverage": "20", + "initialLeverage": "25", "notionalCap": "250000", "notionalFloor": "50000", - "maintMarginRatio": "0.02", - "cum": "500.0" + "maintMarginRatio": "0.01", + "cum": "200.0" } }, { "tier": 3.0, "currency": "USDT", "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, + "maxNotional": 600000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, "info": { "bracket": "3", - "initialLeverage": "10", - "notionalCap": "1000000", + "initialLeverage": "20", + "notionalCap": "600000", "notionalFloor": "250000", - "maintMarginRatio": "0.05", - "cum": "8000.0" + "maintMarginRatio": "0.02", + "cum": "2700.0" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 2000000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, + "minNotional": 600000.0, + "maxNotional": 1200000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, "info": { "bracket": "4", - "initialLeverage": "5", - "notionalCap": "2000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.1", - "cum": "58000.0" + "initialLeverage": "10", + "notionalCap": "1200000", + "notionalFloor": "600000", + "maintMarginRatio": "0.05", + "cum": "20700.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 2000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 4.0, + "minNotional": 1200000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, "info": { "bracket": "5", - "initialLeverage": "4", - "notionalCap": "5000000", - "notionalFloor": "2000000", - "maintMarginRatio": "0.125", - "cum": "108000.0" + "initialLeverage": "5", + "notionalCap": "3000000", + "notionalFloor": "1200000", + "maintMarginRatio": "0.1", + "cum": "80700.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 5000000.0, - "maxNotional": 10000000.0, - "maintenanceMarginRate": 0.1665, - "maxLeverage": 3.0, + "minNotional": 3000000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, "info": { "bracket": "6", - "initialLeverage": "3", - "notionalCap": "10000000", - "notionalFloor": "5000000", - "maintMarginRatio": "0.1665", - "cum": "315500.0" + "initialLeverage": "4", + "notionalCap": "6000000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.125", + "cum": "155700.0" } }, { "tier": 7.0, "currency": "USDT", + "minNotional": 6000000.0, + "maxNotional": 10000000.0, + "maintenanceMarginRate": 0.165, + "maxLeverage": 3.0, + "info": { + "bracket": "7", + "initialLeverage": "3", + "notionalCap": "10000000", + "notionalFloor": "6000000", + "maintMarginRatio": "0.165", + "cum": "395700.0" + } + }, + { + "tier": 8.0, + "currency": "USDT", "minNotional": 10000000.0, "maxNotional": 20000000.0, "maintenanceMarginRate": 0.25, "maxLeverage": 2.0, "info": { - "bracket": "7", + "bracket": "8", "initialLeverage": "2", "notionalCap": "20000000", "notionalFloor": "10000000", "maintMarginRatio": "0.25", - "cum": "1150500.0" + "cum": "1245700.0" } }, { - "tier": 8.0, + "tier": 9.0, "currency": "USDT", "minNotional": 20000000.0, "maxNotional": 30000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "8", + "bracket": "9", "initialLeverage": "1", "notionalCap": "30000000", "notionalFloor": "20000000", "maintMarginRatio": "0.5", - "cum": "6150500.0" + "cum": "6245700.0" } } ], @@ -10075,14 +10627,14 @@ "currency": "USDT", "minNotional": 0.0, "maxNotional": 5000.0, - "maintenanceMarginRate": 0.0075, + "maintenanceMarginRate": 0.006, "maxLeverage": 50.0, "info": { "bracket": "1", "initialLeverage": "50", "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.0075", + "maintMarginRatio": "0.006", "cum": "0.0" } }, @@ -10099,103 +10651,103 @@ "notionalCap": "50000", "notionalFloor": "5000", "maintMarginRatio": "0.01", - "cum": "12.5" + "cum": "20.0" } }, { "tier": 3.0, "currency": "USDT", "minNotional": 50000.0, - "maxNotional": 150000.0, + "maxNotional": 400000.0, "maintenanceMarginRate": 0.025, "maxLeverage": 20.0, "info": { "bracket": "3", "initialLeverage": "20", - "notionalCap": "150000", + "notionalCap": "400000", "notionalFloor": "50000", "maintMarginRatio": "0.025", - "cum": "762.5" + "cum": "770.0" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 150000.0, - "maxNotional": 250000.0, + "minNotional": 400000.0, + "maxNotional": 800000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "4", "initialLeverage": "10", - "notionalCap": "250000", - "notionalFloor": "150000", + "notionalCap": "800000", + "notionalFloor": "400000", "maintMarginRatio": "0.05", - "cum": "4512.5" + "cum": "10770.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 500000.0, + "minNotional": 800000.0, + "maxNotional": 2000000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "5", "initialLeverage": "5", - "notionalCap": "500000", - "notionalFloor": "250000", + "notionalCap": "2000000", + "notionalFloor": "800000", "maintMarginRatio": "0.1", - "cum": "17012.5" + "cum": "50770.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 500000.0, - "maxNotional": 1000000.0, + "minNotional": 2000000.0, + "maxNotional": 5000000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 4.0, "info": { "bracket": "6", "initialLeverage": "4", - "notionalCap": "1000000", - "notionalFloor": "500000", + "notionalCap": "5000000", + "notionalFloor": "2000000", "maintMarginRatio": "0.125", - "cum": "29512.5" + "cum": "100770.0" } }, { "tier": 7.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 4000000.0, + "minNotional": 5000000.0, + "maxNotional": 12000000.0, "maintenanceMarginRate": 0.25, "maxLeverage": 2.0, "info": { "bracket": "7", "initialLeverage": "2", - "notionalCap": "4000000", - "notionalFloor": "1000000", + "notionalCap": "12000000", + "notionalFloor": "5000000", "maintMarginRatio": "0.25", - "cum": "154512.5" + "cum": "725770.0" } }, { "tier": 8.0, "currency": "USDT", - "minNotional": 4000000.0, - "maxNotional": 10000000.0, + "minNotional": 12000000.0, + "maxNotional": 20000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { "bracket": "8", "initialLeverage": "1", - "notionalCap": "10000000", - "notionalFloor": "4000000", + "notionalCap": "20000000", + "notionalFloor": "12000000", "maintMarginRatio": "0.5", - "cum": "1154512.5" + "cum": "3725770.0" } } ], @@ -11113,6 +11665,104 @@ } } ], + "GMX/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 15.0, + "info": { + "bracket": "2", + "initialLeverage": "15", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 100000.0, + "maxNotional": 250000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "250000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 250000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 2.0, + "info": { + "bracket": "5", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "250000", + "maintMarginRatio": "0.125", + "cum": "11900.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "6", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "386900.0" + } + } + ], "GRT/USDT:USDT": [ { "tier": 1.0, @@ -11150,13 +11800,13 @@ "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, + "maxNotional": 400000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "3", "initialLeverage": "10", - "notionalCap": "100000", + "notionalCap": "400000", "notionalFloor": "25000", "maintMarginRatio": "0.05", "cum": "700.0" @@ -11165,49 +11815,65 @@ { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, + "minNotional": 400000.0, + "maxNotional": 1000000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", + "notionalCap": "1000000", + "notionalFloor": "400000", "maintMarginRatio": "0.1", - "cum": "5700.0" + "cum": "20700.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, + "minNotional": 1000000.0, + "maxNotional": 2000000.0, "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maxLeverage": 4.0, "info": { "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", + "initialLeverage": "4", + "notionalCap": "2000000", + "notionalFloor": "1000000", "maintMarginRatio": "0.125", - "cum": "11950.0" + "cum": "45700.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, + "minNotional": 2000000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "6000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.25", + "cum": "295700.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 6000000.0, + "maxNotional": 10000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", + "notionalCap": "10000000", + "notionalFloor": "6000000", "maintMarginRatio": "0.5", - "cum": "386950.0" + "cum": "1795700.0" } } ], @@ -11414,10 +12080,10 @@ "minNotional": 0.0, "maxNotional": 5000.0, "maintenanceMarginRate": 0.02, - "maxLeverage": 20.0, + "maxLeverage": 25.0, "info": { "bracket": "1", - "initialLeverage": "20", + "initialLeverage": "25", "notionalCap": "5000", "notionalFloor": "0", "maintMarginRatio": "0.02", @@ -11430,10 +12096,10 @@ "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 15.0, + "maxLeverage": 20.0, "info": { "bracket": "2", - "initialLeverage": "15", + "initialLeverage": "20", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", @@ -11444,13 +12110,13 @@ "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, + "maxNotional": 400000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "3", "initialLeverage": "10", - "notionalCap": "100000", + "notionalCap": "400000", "notionalFloor": "25000", "maintMarginRatio": "0.05", "cum": "650.0" @@ -11459,49 +12125,65 @@ { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, + "minNotional": 400000.0, + "maxNotional": 1000000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", + "notionalCap": "1000000", + "notionalFloor": "400000", "maintMarginRatio": "0.1", - "cum": "5650.0" + "cum": "20650.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, + "minNotional": 1000000.0, + "maxNotional": 2000000.0, "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maxLeverage": 4.0, "info": { "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", + "initialLeverage": "4", + "notionalCap": "2000000", + "notionalFloor": "1000000", "maintMarginRatio": "0.125", - "cum": "11900.0" + "cum": "45650.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, + "minNotional": 2000000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "6000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.25", + "cum": "295650.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 6000000.0, + "maxNotional": 10000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", + "notionalCap": "10000000", + "notionalFloor": "6000000", "maintMarginRatio": "0.5", - "cum": "386900.0" + "cum": "1795650.0" } } ], @@ -12296,10 +12978,10 @@ "minNotional": 0.0, "maxNotional": 5000.0, "maintenanceMarginRate": 0.02, - "maxLeverage": 20.0, + "maxLeverage": 25.0, "info": { "bracket": "1", - "initialLeverage": "20", + "initialLeverage": "25", "notionalCap": "5000", "notionalFloor": "0", "maintMarginRatio": "0.02", @@ -12312,10 +12994,10 @@ "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, + "maxLeverage": 20.0, "info": { "bracket": "2", - "initialLeverage": "10", + "initialLeverage": "20", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", @@ -12326,13 +13008,13 @@ "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, + "maxNotional": 200000.0, "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, + "maxLeverage": 10.0, "info": { "bracket": "3", - "initialLeverage": "8", - "notionalCap": "100000", + "initialLeverage": "10", + "notionalCap": "200000", "notionalFloor": "25000", "maintMarginRatio": "0.05", "cum": "650.0" @@ -12341,33 +13023,33 @@ { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, + "minNotional": 200000.0, + "maxNotional": 500000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", + "notionalCap": "500000", + "notionalFloor": "200000", "maintMarginRatio": "0.1", - "cum": "5650.0" + "cum": "10650.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, + "minNotional": 500000.0, "maxNotional": 1000000.0, "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maxLeverage": 4.0, "info": { "bracket": "5", - "initialLeverage": "2", + "initialLeverage": "4", "notionalCap": "1000000", - "notionalFloor": "250000", + "notionalFloor": "500000", "maintMarginRatio": "0.125", - "cum": "11900.0" + "cum": "23150.0" } }, { @@ -12375,15 +13057,31 @@ "currency": "USDT", "minNotional": 1000000.0, "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "148150.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, + "maxNotional": 5000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "1", - "notionalCap": "3000000", - "notionalFloor": "1000000", + "notionalCap": "5000000", + "notionalFloor": "3000000", "maintMarginRatio": "0.5", - "cum": "386900.0" + "cum": "898150.0" } } ], @@ -12800,80 +13498,112 @@ "tier": 1.0, "currency": "USDT", "minNotional": 0.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 25.0, "info": { "bracket": "1", - "initialLeverage": "20", - "notionalCap": "25000", + "initialLeverage": "25", + "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.025", + "maintMarginRatio": "0.01", "cum": "0.0" } }, { "tier": 2.0, "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, "info": { "bracket": "2", - "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "625.0" + "initialLeverage": "20", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "75.0" } }, { "tier": 3.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, + "minNotional": 25000.0, + "maxNotional": 400000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, "info": { "bracket": "3", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5625.0" + "initialLeverage": "10", + "notionalCap": "400000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "700.0" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 250000.0, + "minNotional": 400000.0, "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, "info": { "bracket": "4", - "initialLeverage": "2", + "initialLeverage": "5", "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11875.0" + "notionalFloor": "400000", + "maintMarginRatio": "0.1", + "cum": "20700.0" } }, { "tier": 5.0, "currency": "USDT", "minNotional": 1000000.0, - "maxNotional": 5000000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "2000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.125", + "cum": "45700.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 2000000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "6000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.25", + "cum": "295700.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 6000000.0, + "maxNotional": 10000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "5", + "bracket": "7", "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", + "notionalCap": "10000000", + "notionalFloor": "6000000", "maintMarginRatio": "0.5", - "cum": "386875.0" + "cum": "1795700.0" } } ], @@ -13177,14 +13907,14 @@ "currency": "USDT", "minNotional": 0.0, "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 20.0, + "maintenanceMarginRate": 0.006, + "maxLeverage": 50.0, "info": { "bracket": "1", - "initialLeverage": "20", + "initialLeverage": "50", "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.01", + "maintMarginRatio": "0.006", "cum": "0.0" } }, @@ -13192,80 +13922,112 @@ "tier": 2.0, "currency": "USDT", "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, + "maxNotional": 50000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 25.0, "info": { "bracket": "2", - "initialLeverage": "10", - "notionalCap": "25000", + "initialLeverage": "25", + "notionalCap": "50000", "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "75.0" + "maintMarginRatio": "0.01", + "cum": "20.0" } }, { "tier": 3.0, "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, + "minNotional": 50000.0, + "maxNotional": 400000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, "info": { "bracket": "3", - "initialLeverage": "8", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "700.0" + "initialLeverage": "20", + "notionalCap": "400000", + "notionalFloor": "50000", + "maintMarginRatio": "0.025", + "cum": "770.0" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, + "minNotional": 400000.0, + "maxNotional": 800000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, "info": { "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5700.0" + "initialLeverage": "10", + "notionalCap": "800000", + "notionalFloor": "400000", + "maintMarginRatio": "0.05", + "cum": "10770.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "minNotional": 800000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, "info": { "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11950.0" + "initialLeverage": "5", + "notionalCap": "2000000", + "notionalFloor": "800000", + "maintMarginRatio": "0.1", + "cum": "50770.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 1000000.0, + "minNotional": 2000000.0, "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "6", + "initialLeverage": "4", + "notionalCap": "5000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.125", + "cum": "100770.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 5000000.0, + "maxNotional": 12000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "7", + "initialLeverage": "2", + "notionalCap": "12000000", + "notionalFloor": "5000000", + "maintMarginRatio": "0.25", + "cum": "725770.0" + } + }, + { + "tier": 8.0, + "currency": "USDT", + "minNotional": 12000000.0, + "maxNotional": 20000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "8", "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", + "notionalCap": "20000000", + "notionalFloor": "12000000", "maintMarginRatio": "0.5", - "cum": "386950.0" + "cum": "3725770.0" } } ], @@ -13921,6 +14683,120 @@ } } ], + "LQTY/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 15.0, + "info": { + "bracket": "2", + "initialLeverage": "15", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 200000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "200000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 200000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "500000", + "notionalFloor": "200000", + "maintMarginRatio": "0.1", + "cum": "10650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.125", + "cum": "23150.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "148150.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.5", + "cum": "898150.0" + } + } + ], "LRC/USDT:USDT": [ { "tier": 1.0, @@ -14772,13 +15648,13 @@ "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, + "maxNotional": 600000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "3", "initialLeverage": "10", - "notionalCap": "100000", + "notionalCap": "600000", "notionalFloor": "25000", "maintMarginRatio": "0.05", "cum": "650.0" @@ -14787,49 +15663,65 @@ { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, + "minNotional": 600000.0, + "maxNotional": 1600000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", + "notionalCap": "1600000", + "notionalFloor": "600000", "maintMarginRatio": "0.1", - "cum": "5650.0" + "cum": "30650.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, + "minNotional": 1600000.0, "maxNotional": 3000000.0, "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maxLeverage": 4.0, "info": { "bracket": "5", - "initialLeverage": "2", + "initialLeverage": "4", "notionalCap": "3000000", - "notionalFloor": "250000", + "notionalFloor": "1600000", "maintMarginRatio": "0.125", - "cum": "11900.0" + "cum": "70650.0" } }, { "tier": 6.0, "currency": "USDT", "minNotional": 3000000.0, - "maxNotional": 8000000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "6000000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.25", + "cum": "445650.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 6000000.0, + "maxNotional": 10000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "1", - "notionalCap": "8000000", - "notionalFloor": "3000000", + "notionalCap": "10000000", + "notionalFloor": "6000000", "maintMarginRatio": "0.5", - "cum": "1136900.0" + "cum": "1945650.0" } } ], @@ -14953,14 +15845,14 @@ "currency": "USDT", "minNotional": 0.0, "maxNotional": 5000.0, - "maintenanceMarginRate": 0.0065, + "maintenanceMarginRate": 0.006, "maxLeverage": 50.0, "info": { "bracket": "1", "initialLeverage": "50", "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.0065", + "maintMarginRatio": "0.006", "cum": "0.0" } }, @@ -14969,14 +15861,14 @@ "currency": "USDT", "minNotional": 5000.0, "maxNotional": 25000.0, - "maintenanceMarginRate": 0.0075, + "maintenanceMarginRate": 0.007, "maxLeverage": 40.0, "info": { "bracket": "2", "initialLeverage": "40", "notionalCap": "25000", "notionalFloor": "5000", - "maintMarginRatio": "0.0075", + "maintMarginRatio": "0.007", "cum": "5.0" } }, @@ -14993,103 +15885,103 @@ "notionalCap": "50000", "notionalFloor": "25000", "maintMarginRatio": "0.01", - "cum": "67.5" + "cum": "80.0" } }, { "tier": 4.0, "currency": "USDT", "minNotional": 50000.0, - "maxNotional": 150000.0, + "maxNotional": 400000.0, "maintenanceMarginRate": 0.025, "maxLeverage": 20.0, "info": { "bracket": "4", "initialLeverage": "20", - "notionalCap": "150000", + "notionalCap": "400000", "notionalFloor": "50000", "maintMarginRatio": "0.025", - "cum": "817.5" + "cum": "830.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 150000.0, - "maxNotional": 250000.0, + "minNotional": 400000.0, + "maxNotional": 800000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "5", "initialLeverage": "10", - "notionalCap": "250000", - "notionalFloor": "150000", + "notionalCap": "800000", + "notionalFloor": "400000", "maintMarginRatio": "0.05", - "cum": "4567.5" + "cum": "10830.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 500000.0, + "minNotional": 800000.0, + "maxNotional": 2000000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "6", "initialLeverage": "5", - "notionalCap": "500000", - "notionalFloor": "250000", + "notionalCap": "2000000", + "notionalFloor": "800000", "maintMarginRatio": "0.1", - "cum": "17067.5" + "cum": "50830.0" } }, { "tier": 7.0, "currency": "USDT", - "minNotional": 500000.0, - "maxNotional": 750000.0, + "minNotional": 2000000.0, + "maxNotional": 5000000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 4.0, "info": { "bracket": "7", "initialLeverage": "4", - "notionalCap": "750000", - "notionalFloor": "500000", + "notionalCap": "5000000", + "notionalFloor": "2000000", "maintMarginRatio": "0.125", - "cum": "29567.5" + "cum": "100830.0" } }, { "tier": 8.0, "currency": "USDT", - "minNotional": 750000.0, - "maxNotional": 3000000.0, + "minNotional": 5000000.0, + "maxNotional": 12000000.0, "maintenanceMarginRate": 0.25, "maxLeverage": 2.0, "info": { "bracket": "8", "initialLeverage": "2", - "notionalCap": "3000000", - "notionalFloor": "750000", + "notionalCap": "12000000", + "notionalFloor": "5000000", "maintMarginRatio": "0.25", - "cum": "123317.5" + "cum": "725830.0" } }, { "tier": 9.0, "currency": "USDT", - "minNotional": 3000000.0, - "maxNotional": 10000000.0, + "minNotional": 12000000.0, + "maxNotional": 20000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { "bracket": "9", "initialLeverage": "1", - "notionalCap": "10000000", - "notionalFloor": "3000000", + "notionalCap": "20000000", + "notionalFloor": "12000000", "maintMarginRatio": "0.5", - "cum": "873317.5" + "cum": "3725830.0" } } ], @@ -15099,14 +15991,14 @@ "currency": "USDT", "minNotional": 0.0, "maxNotional": 5000.0, - "maintenanceMarginRate": 0.02, - "maxLeverage": 20.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 25.0, "info": { "bracket": "1", - "initialLeverage": "20", + "initialLeverage": "25", "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.02", + "maintMarginRatio": "0.01", "cum": "0.0" } }, @@ -15116,78 +16008,94 @@ "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 15.0, + "maxLeverage": 20.0, "info": { "bracket": "2", - "initialLeverage": "15", + "initialLeverage": "20", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", - "cum": "25.0" + "cum": "75.0" } }, { "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, + "maxNotional": 400000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "3", "initialLeverage": "10", - "notionalCap": "100000", + "notionalCap": "400000", "notionalFloor": "25000", "maintMarginRatio": "0.05", - "cum": "650.0" + "cum": "700.0" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, + "minNotional": 400000.0, + "maxNotional": 1000000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", + "notionalCap": "1000000", + "notionalFloor": "400000", "maintMarginRatio": "0.1", - "cum": "5650.0" + "cum": "20700.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, + "minNotional": 1000000.0, + "maxNotional": 2000000.0, "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maxLeverage": 4.0, "info": { "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", + "initialLeverage": "4", + "notionalCap": "2000000", + "notionalFloor": "1000000", "maintMarginRatio": "0.125", - "cum": "11900.0" + "cum": "45700.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, + "minNotional": 2000000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "6000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.25", + "cum": "295700.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 6000000.0, + "maxNotional": 10000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", + "notionalCap": "10000000", + "notionalFloor": "6000000", "maintMarginRatio": "0.5", - "cum": "386900.0" + "cum": "1795700.0" } } ], @@ -15522,13 +16430,13 @@ "tier": 3.0, "currency": "USDT", "minNotional": 150000.0, - "maxNotional": 250000.0, + "maxNotional": 600000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "3", "initialLeverage": "10", - "notionalCap": "250000", + "notionalCap": "600000", "notionalFloor": "150000", "maintMarginRatio": "0.05", "cum": "4500.0" @@ -15537,65 +16445,65 @@ { "tier": 4.0, "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 500000.0, + "minNotional": 600000.0, + "maxNotional": 1600000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "500000", - "notionalFloor": "250000", + "notionalCap": "1600000", + "notionalFloor": "600000", "maintMarginRatio": "0.1", - "cum": "17000.0" + "cum": "34500.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 500000.0, - "maxNotional": 1000000.0, + "minNotional": 1600000.0, + "maxNotional": 2000000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 4.0, "info": { "bracket": "5", "initialLeverage": "4", - "notionalCap": "1000000", - "notionalFloor": "500000", + "notionalCap": "2000000", + "notionalFloor": "1600000", "maintMarginRatio": "0.125", - "cum": "29500.0" + "cum": "74500.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 2000000.0, + "minNotional": 2000000.0, + "maxNotional": 6000000.0, "maintenanceMarginRate": 0.25, "maxLeverage": 2.0, "info": { "bracket": "6", "initialLeverage": "2", - "notionalCap": "2000000", - "notionalFloor": "1000000", + "notionalCap": "6000000", + "notionalFloor": "2000000", "maintMarginRatio": "0.25", - "cum": "154500.0" + "cum": "324500.0" } }, { "tier": 7.0, "currency": "USDT", - "minNotional": 2000000.0, - "maxNotional": 5000000.0, + "minNotional": 6000000.0, + "maxNotional": 10000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { "bracket": "7", "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "2000000", + "notionalCap": "10000000", + "notionalFloor": "6000000", "maintMarginRatio": "0.5", - "cum": "654500.0" + "cum": "1824500.0" } } ], @@ -15605,14 +16513,14 @@ "currency": "USDT", "minNotional": 0.0, "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 25.0, + "maintenanceMarginRate": 0.006, + "maxLeverage": 50.0, "info": { "bracket": "1", - "initialLeverage": "25", + "initialLeverage": "50", "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.01", + "maintMarginRatio": "0.006", "cum": "0.0" } }, @@ -15620,80 +16528,112 @@ "tier": 2.0, "currency": "USDT", "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, + "maxNotional": 50000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 25.0, "info": { "bracket": "2", - "initialLeverage": "20", - "notionalCap": "25000", + "initialLeverage": "25", + "notionalCap": "50000", "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "75.0" + "maintMarginRatio": "0.01", + "cum": "20.0" } }, { "tier": 3.0, "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, + "minNotional": 50000.0, + "maxNotional": 400000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, "info": { "bracket": "3", - "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "700.0" + "initialLeverage": "20", + "notionalCap": "400000", + "notionalFloor": "50000", + "maintMarginRatio": "0.025", + "cum": "770.0" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, + "minNotional": 400000.0, + "maxNotional": 800000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, "info": { "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5700.0" + "initialLeverage": "10", + "notionalCap": "800000", + "notionalFloor": "400000", + "maintMarginRatio": "0.05", + "cum": "10770.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "minNotional": 800000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, "info": { "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11950.0" + "initialLeverage": "5", + "notionalCap": "2000000", + "notionalFloor": "800000", + "maintMarginRatio": "0.1", + "cum": "50770.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 1000000.0, + "minNotional": 2000000.0, "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "6", + "initialLeverage": "4", + "notionalCap": "5000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.125", + "cum": "100770.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 5000000.0, + "maxNotional": 12000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "7", + "initialLeverage": "2", + "notionalCap": "12000000", + "notionalFloor": "5000000", + "maintMarginRatio": "0.25", + "cum": "725770.0" + } + }, + { + "tier": 8.0, + "currency": "USDT", + "minNotional": 12000000.0, + "maxNotional": 20000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "8", "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", + "notionalCap": "20000000", + "notionalFloor": "12000000", "maintMarginRatio": "0.5", - "cum": "386950.0" + "cum": "3725770.0" } } ], @@ -16291,14 +17231,14 @@ "currency": "USDT", "minNotional": 0.0, "maxNotional": 5000.0, - "maintenanceMarginRate": 0.0065, + "maintenanceMarginRate": 0.006, "maxLeverage": 50.0, "info": { "bracket": "1", "initialLeverage": "50", "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.0065", + "maintMarginRatio": "0.006", "cum": "0.0" } }, @@ -16315,61 +17255,61 @@ "notionalCap": "50000", "notionalFloor": "5000", "maintMarginRatio": "0.01", - "cum": "17.5" + "cum": "20.0" } }, { "tier": 3.0, "currency": "USDT", "minNotional": 50000.0, - "maxNotional": 200000.0, + "maxNotional": 400000.0, "maintenanceMarginRate": 0.025, "maxLeverage": 20.0, "info": { "bracket": "3", "initialLeverage": "20", - "notionalCap": "200000", + "notionalCap": "400000", "notionalFloor": "50000", "maintMarginRatio": "0.025", - "cum": "767.5" + "cum": "770.0" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 200000.0, - "maxNotional": 400000.0, + "minNotional": 400000.0, + "maxNotional": 800000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "4", "initialLeverage": "10", - "notionalCap": "400000", - "notionalFloor": "200000", + "notionalCap": "800000", + "notionalFloor": "400000", "maintMarginRatio": "0.05", - "cum": "5767.5" + "cum": "10770.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 400000.0, - "maxNotional": 1000000.0, + "minNotional": 800000.0, + "maxNotional": 2000000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "5", "initialLeverage": "5", - "notionalCap": "1000000", - "notionalFloor": "400000", + "notionalCap": "2000000", + "notionalFloor": "800000", "maintMarginRatio": "0.1", - "cum": "25767.5" + "cum": "50770.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 1000000.0, + "minNotional": 2000000.0, "maxNotional": 5000000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 4.0, @@ -16377,41 +17317,41 @@ "bracket": "6", "initialLeverage": "4", "notionalCap": "5000000", - "notionalFloor": "1000000", + "notionalFloor": "2000000", "maintMarginRatio": "0.125", - "cum": "50767.5" + "cum": "100770.0" } }, { "tier": 7.0, "currency": "USDT", "minNotional": 5000000.0, - "maxNotional": 6000000.0, + "maxNotional": 12000000.0, "maintenanceMarginRate": 0.25, "maxLeverage": 2.0, "info": { "bracket": "7", "initialLeverage": "2", - "notionalCap": "6000000", + "notionalCap": "12000000", "notionalFloor": "5000000", "maintMarginRatio": "0.25", - "cum": "675767.5" + "cum": "725770.0" } }, { "tier": 8.0, "currency": "USDT", - "minNotional": 6000000.0, - "maxNotional": 10000000.0, + "minNotional": 12000000.0, + "maxNotional": 20000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { "bracket": "8", "initialLeverage": "1", - "notionalCap": "10000000", - "notionalFloor": "6000000", + "notionalCap": "20000000", + "notionalFloor": "12000000", "maintMarginRatio": "0.5", - "cum": "2175767.5" + "cum": "3725770.0" } } ], @@ -16513,6 +17453,120 @@ } } ], + "PERP/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 15.0, + "info": { + "bracket": "2", + "initialLeverage": "15", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 200000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "200000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 200000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "500000", + "notionalFloor": "200000", + "maintMarginRatio": "0.1", + "cum": "10650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.125", + "cum": "23150.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "148150.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.5", + "cum": "898150.0" + } + } + ], "PHB/BUSD:BUSD": [ { "tier": 1.0, @@ -16844,13 +17898,13 @@ "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, + "maxNotional": 200000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "3", "initialLeverage": "10", - "notionalCap": "100000", + "notionalCap": "200000", "notionalFloor": "25000", "maintMarginRatio": "0.05", "cum": "700.0" @@ -16859,49 +17913,65 @@ { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, + "minNotional": 200000.0, + "maxNotional": 500000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", + "notionalCap": "500000", + "notionalFloor": "200000", "maintMarginRatio": "0.1", - "cum": "5700.0" + "cum": "10700.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, + "minNotional": 500000.0, "maxNotional": 1000000.0, "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maxLeverage": 4.0, "info": { "bracket": "5", - "initialLeverage": "2", + "initialLeverage": "4", "notionalCap": "1000000", - "notionalFloor": "250000", + "notionalFloor": "500000", "maintMarginRatio": "0.125", - "cum": "11950.0" + "cum": "23200.0" } }, { "tier": 6.0, "currency": "USDT", "minNotional": 1000000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "148200.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, "maxNotional": 5000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "1", "notionalCap": "5000000", - "notionalFloor": "1000000", + "notionalFloor": "3000000", "maintMarginRatio": "0.5", - "cum": "386950.0" + "cum": "898200.0" } } ], @@ -17108,10 +18178,10 @@ "minNotional": 0.0, "maxNotional": 5000.0, "maintenanceMarginRate": 0.02, - "maxLeverage": 15.0, + "maxLeverage": 25.0, "info": { "bracket": "1", - "initialLeverage": "15", + "initialLeverage": "25", "notionalCap": "5000", "notionalFloor": "0", "maintMarginRatio": "0.02", @@ -17124,10 +18194,10 @@ "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, + "maxLeverage": 20.0, "info": { "bracket": "2", - "initialLeverage": "10", + "initialLeverage": "20", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", @@ -17138,13 +18208,13 @@ "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, + "maxNotional": 200000.0, "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, + "maxLeverage": 10.0, "info": { "bracket": "3", - "initialLeverage": "8", - "notionalCap": "100000", + "initialLeverage": "10", + "notionalCap": "200000", "notionalFloor": "25000", "maintMarginRatio": "0.05", "cum": "650.0" @@ -17153,49 +18223,65 @@ { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, + "minNotional": 200000.0, + "maxNotional": 500000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", + "notionalCap": "500000", + "notionalFloor": "200000", "maintMarginRatio": "0.1", - "cum": "5650.0" + "cum": "10650.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, + "minNotional": 500000.0, "maxNotional": 1000000.0, "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maxLeverage": 4.0, "info": { "bracket": "5", - "initialLeverage": "2", + "initialLeverage": "4", "notionalCap": "1000000", - "notionalFloor": "250000", + "notionalFloor": "500000", "maintMarginRatio": "0.125", - "cum": "11900.0" + "cum": "23150.0" } }, { "tier": 6.0, "currency": "USDT", "minNotional": 1000000.0, - "maxNotional": 1500000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "148150.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, + "maxNotional": 5000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "1", - "notionalCap": "1500000", - "notionalFloor": "1000000", + "notionalCap": "5000000", + "notionalFloor": "3000000", "maintMarginRatio": "0.5", - "cum": "386900.0" + "cum": "898150.0" } } ], @@ -17386,10 +18472,10 @@ "minNotional": 0.0, "maxNotional": 5000.0, "maintenanceMarginRate": 0.01, - "maxLeverage": 20.0, + "maxLeverage": 25.0, "info": { "bracket": "1", - "initialLeverage": "20", + "initialLeverage": "25", "notionalCap": "5000", "notionalFloor": "0", "maintMarginRatio": "0.01", @@ -17402,10 +18488,10 @@ "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, + "maxLeverage": 20.0, "info": { "bracket": "2", - "initialLeverage": "10", + "initialLeverage": "20", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", @@ -17416,13 +18502,13 @@ "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, + "maxNotional": 200000.0, "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, + "maxLeverage": 10.0, "info": { "bracket": "3", - "initialLeverage": "8", - "notionalCap": "100000", + "initialLeverage": "10", + "notionalCap": "200000", "notionalFloor": "25000", "maintMarginRatio": "0.05", "cum": "700.0" @@ -17431,49 +18517,65 @@ { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, + "minNotional": 200000.0, + "maxNotional": 500000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", + "notionalCap": "500000", + "notionalFloor": "200000", "maintMarginRatio": "0.1", - "cum": "5700.0" + "cum": "10700.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, + "minNotional": 500000.0, "maxNotional": 1000000.0, "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maxLeverage": 4.0, "info": { "bracket": "5", - "initialLeverage": "2", + "initialLeverage": "4", "notionalCap": "1000000", - "notionalFloor": "250000", + "notionalFloor": "500000", "maintMarginRatio": "0.125", - "cum": "11950.0" + "cum": "23200.0" } }, { "tier": 6.0, "currency": "USDT", "minNotional": 1000000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "148200.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, "maxNotional": 5000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "1", "notionalCap": "5000000", - "notionalFloor": "1000000", + "notionalFloor": "3000000", "maintMarginRatio": "0.5", - "cum": "386950.0" + "cum": "898200.0" } } ], @@ -17484,10 +18586,10 @@ "minNotional": 0.0, "maxNotional": 5000.0, "maintenanceMarginRate": 0.02, - "maxLeverage": 20.0, + "maxLeverage": 25.0, "info": { "bracket": "1", - "initialLeverage": "20", + "initialLeverage": "25", "notionalCap": "5000", "notionalFloor": "0", "maintMarginRatio": "0.02", @@ -17500,10 +18602,10 @@ "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, + "maxLeverage": 20.0, "info": { "bracket": "2", - "initialLeverage": "10", + "initialLeverage": "20", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", @@ -17514,13 +18616,13 @@ "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, + "maxNotional": 300000.0, "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, + "maxLeverage": 10.0, "info": { "bracket": "3", - "initialLeverage": "8", - "notionalCap": "100000", + "initialLeverage": "10", + "notionalCap": "300000", "notionalFloor": "25000", "maintMarginRatio": "0.05", "cum": "650.0" @@ -17529,49 +18631,65 @@ { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, + "minNotional": 300000.0, + "maxNotional": 800000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", + "notionalCap": "800000", + "notionalFloor": "300000", "maintMarginRatio": "0.1", - "cum": "5650.0" + "cum": "15650.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, + "minNotional": 800000.0, "maxNotional": 1000000.0, "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maxLeverage": 4.0, "info": { "bracket": "5", - "initialLeverage": "2", + "initialLeverage": "4", "notionalCap": "1000000", - "notionalFloor": "250000", + "notionalFloor": "800000", "maintMarginRatio": "0.125", - "cum": "11900.0" + "cum": "35650.0" } }, { "tier": 6.0, "currency": "USDT", "minNotional": 1000000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "160650.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, "maxNotional": 5000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "1", "notionalCap": "5000000", - "notionalFloor": "1000000", + "notionalFloor": "3000000", "maintMarginRatio": "0.5", - "cum": "386900.0" + "cum": "910650.0" } } ], @@ -17875,14 +18993,14 @@ "currency": "USDT", "minNotional": 0.0, "maxNotional": 5000.0, - "maintenanceMarginRate": 0.0065, + "maintenanceMarginRate": 0.006, "maxLeverage": 50.0, "info": { "bracket": "1", "initialLeverage": "50", "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.0065", + "maintMarginRatio": "0.006", "cum": "0.0" } }, @@ -17891,14 +19009,14 @@ "currency": "USDT", "minNotional": 5000.0, "maxNotional": 25000.0, - "maintenanceMarginRate": 0.0075, + "maintenanceMarginRate": 0.007, "maxLeverage": 40.0, "info": { "bracket": "2", "initialLeverage": "40", "notionalCap": "25000", "notionalFloor": "5000", - "maintMarginRatio": "0.0075", + "maintMarginRatio": "0.007", "cum": "5.0" } }, @@ -17915,103 +19033,103 @@ "notionalCap": "50000", "notionalFloor": "25000", "maintMarginRatio": "0.01", - "cum": "67.5" + "cum": "80.0" } }, { "tier": 4.0, "currency": "USDT", "minNotional": 50000.0, - "maxNotional": 150000.0, + "maxNotional": 400000.0, "maintenanceMarginRate": 0.025, "maxLeverage": 20.0, "info": { "bracket": "4", "initialLeverage": "20", - "notionalCap": "150000", + "notionalCap": "400000", "notionalFloor": "50000", "maintMarginRatio": "0.025", - "cum": "817.5" + "cum": "830.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 150000.0, - "maxNotional": 250000.0, + "minNotional": 400000.0, + "maxNotional": 800000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "5", "initialLeverage": "10", - "notionalCap": "250000", - "notionalFloor": "150000", + "notionalCap": "800000", + "notionalFloor": "400000", "maintMarginRatio": "0.05", - "cum": "4567.5" + "cum": "10830.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 500000.0, + "minNotional": 800000.0, + "maxNotional": 2000000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "6", "initialLeverage": "5", - "notionalCap": "500000", - "notionalFloor": "250000", + "notionalCap": "2000000", + "notionalFloor": "800000", "maintMarginRatio": "0.1", - "cum": "17067.5" + "cum": "50830.0" } }, { "tier": 7.0, "currency": "USDT", - "minNotional": 500000.0, - "maxNotional": 1000000.0, + "minNotional": 2000000.0, + "maxNotional": 5000000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 4.0, "info": { "bracket": "7", "initialLeverage": "4", - "notionalCap": "1000000", - "notionalFloor": "500000", + "notionalCap": "5000000", + "notionalFloor": "2000000", "maintMarginRatio": "0.125", - "cum": "29567.5" + "cum": "100830.0" } }, { "tier": 8.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, + "minNotional": 5000000.0, + "maxNotional": 12000000.0, "maintenanceMarginRate": 0.25, "maxLeverage": 2.0, "info": { "bracket": "8", "initialLeverage": "2", - "notionalCap": "5000000", - "notionalFloor": "1000000", + "notionalCap": "12000000", + "notionalFloor": "5000000", "maintMarginRatio": "0.25", - "cum": "154567.5" + "cum": "725830.0" } }, { "tier": 9.0, "currency": "USDT", - "minNotional": 5000000.0, - "maxNotional": 10000000.0, + "minNotional": 12000000.0, + "maxNotional": 20000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { "bracket": "9", "initialLeverage": "1", - "notionalCap": "10000000", - "notionalFloor": "5000000", + "notionalCap": "20000000", + "notionalFloor": "12000000", "maintMarginRatio": "0.5", - "cum": "1404567.5" + "cum": "3725830.0" } } ], @@ -18316,10 +19434,10 @@ "minNotional": 0.0, "maxNotional": 5000.0, "maintenanceMarginRate": 0.01, - "maxLeverage": 20.0, + "maxLeverage": 25.0, "info": { "bracket": "1", - "initialLeverage": "20", + "initialLeverage": "25", "notionalCap": "5000", "notionalFloor": "0", "maintMarginRatio": "0.01", @@ -18332,10 +19450,10 @@ "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, + "maxLeverage": 20.0, "info": { "bracket": "2", - "initialLeverage": "10", + "initialLeverage": "20", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", @@ -18346,13 +19464,13 @@ "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, + "maxNotional": 400000.0, "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, + "maxLeverage": 10.0, "info": { "bracket": "3", - "initialLeverage": "8", - "notionalCap": "100000", + "initialLeverage": "10", + "notionalCap": "400000", "notionalFloor": "25000", "maintMarginRatio": "0.05", "cum": "700.0" @@ -18361,49 +19479,65 @@ { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, + "minNotional": 400000.0, + "maxNotional": 1000000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", + "notionalCap": "1000000", + "notionalFloor": "400000", "maintMarginRatio": "0.1", - "cum": "5700.0" + "cum": "20700.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, + "minNotional": 1000000.0, + "maxNotional": 2000000.0, "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maxLeverage": 4.0, "info": { "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", + "initialLeverage": "4", + "notionalCap": "2000000", + "notionalFloor": "1000000", "maintMarginRatio": "0.125", - "cum": "11950.0" + "cum": "45700.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, + "minNotional": 2000000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "6000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.25", + "cum": "295700.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 6000000.0, + "maxNotional": 10000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", + "notionalCap": "10000000", + "notionalFloor": "6000000", "maintMarginRatio": "0.5", - "cum": "386950.0" + "cum": "1795700.0" } } ], @@ -18831,7 +19965,7 @@ } } ], - "STG/USDT:USDT": [ + "SSV/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", @@ -18854,10 +19988,10 @@ "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, + "maxLeverage": 15.0, "info": { "bracket": "2", - "initialLeverage": "10", + "initialLeverage": "15", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", @@ -18868,13 +20002,13 @@ "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, + "maxNotional": 200000.0, "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, + "maxLeverage": 10.0, "info": { "bracket": "3", - "initialLeverage": "8", - "notionalCap": "100000", + "initialLeverage": "10", + "notionalCap": "200000", "notionalFloor": "25000", "maintMarginRatio": "0.05", "cum": "650.0" @@ -18883,33 +20017,33 @@ { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, + "minNotional": 200000.0, + "maxNotional": 500000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", + "notionalCap": "500000", + "notionalFloor": "200000", "maintMarginRatio": "0.1", - "cum": "5650.0" + "cum": "10650.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, + "minNotional": 500000.0, "maxNotional": 1000000.0, "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maxLeverage": 4.0, "info": { "bracket": "5", - "initialLeverage": "2", + "initialLeverage": "4", "notionalCap": "1000000", - "notionalFloor": "250000", + "notionalFloor": "500000", "maintMarginRatio": "0.125", - "cum": "11900.0" + "cum": "23150.0" } }, { @@ -18917,15 +20051,145 @@ "currency": "USDT", "minNotional": 1000000.0, "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "148150.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, + "maxNotional": 5000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.5", + "cum": "898150.0" + } + } + ], + "STG/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, + "info": { + "bracket": "1", + "initialLeverage": "25", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "2", + "initialLeverage": "20", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 200000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "200000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 200000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "500000", + "notionalFloor": "200000", + "maintMarginRatio": "0.1", + "cum": "10650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.125", + "cum": "23150.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", "notionalCap": "3000000", "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "148150.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "3000000", "maintMarginRatio": "0.5", - "cum": "386900.0" + "cum": "898150.0" } } ], @@ -19064,13 +20328,13 @@ "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, + "maxNotional": 300000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "3", "initialLeverage": "10", - "notionalCap": "100000", + "notionalCap": "300000", "notionalFloor": "25000", "maintMarginRatio": "0.05", "cum": "700.0" @@ -19079,49 +20343,179 @@ { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, + "minNotional": 300000.0, + "maxNotional": 800000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", + "notionalCap": "800000", + "notionalFloor": "300000", "maintMarginRatio": "0.1", - "cum": "5700.0" + "cum": "15700.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, + "minNotional": 800000.0, "maxNotional": 1000000.0, "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maxLeverage": 4.0, "info": { "bracket": "5", - "initialLeverage": "2", + "initialLeverage": "4", "notionalCap": "1000000", - "notionalFloor": "250000", + "notionalFloor": "800000", "maintMarginRatio": "0.125", - "cum": "11950.0" + "cum": "35700.0" } }, { "tier": 6.0, "currency": "USDT", "minNotional": 1000000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "160700.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, "maxNotional": 5000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "1", "notionalCap": "5000000", - "notionalFloor": "1000000", + "notionalFloor": "3000000", "maintMarginRatio": "0.5", - "cum": "386950.0" + "cum": "910700.0" + } + } + ], + "STX/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 25.0, + "info": { + "bracket": "1", + "initialLeverage": "25", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.01", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "2", + "initialLeverage": "20", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "75.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 400000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "400000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "700.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 400000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "1000000", + "notionalFloor": "400000", + "maintMarginRatio": "0.1", + "cum": "20700.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "2000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.125", + "cum": "45700.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 2000000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "6000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.25", + "cum": "295700.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 6000000.0, + "maxNotional": 10000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "10000000", + "notionalFloor": "6000000", + "maintMarginRatio": "0.5", + "cum": "1795700.0" } } ], @@ -19941,6 +21335,120 @@ } } ], + "TRU/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 15.0, + "info": { + "bracket": "2", + "initialLeverage": "15", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 200000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "200000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 200000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "500000", + "notionalFloor": "200000", + "maintMarginRatio": "0.1", + "cum": "10650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.125", + "cum": "23150.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "148150.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.5", + "cum": "898150.0" + } + } + ], "TRX/BUSD:BUSD": [ { "tier": 1.0, @@ -20403,14 +21911,14 @@ "currency": "USDT", "minNotional": 0.0, "maxNotional": 5000.0, - "maintenanceMarginRate": 0.0065, - "maxLeverage": 25.0, + "maintenanceMarginRate": 0.006, + "maxLeverage": 50.0, "info": { "bracket": "1", - "initialLeverage": "25", + "initialLeverage": "50", "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.0065", + "maintMarginRatio": "0.006", "cum": "0.0" } }, @@ -20418,96 +21926,242 @@ "tier": 2.0, "currency": "USDT", "minNotional": 5000.0, - "maxNotional": 10000.0, + "maxNotional": 50000.0, "maintenanceMarginRate": 0.01, - "maxLeverage": 20.0, + "maxLeverage": 25.0, "info": { "bracket": "2", - "initialLeverage": "20", - "notionalCap": "10000", + "initialLeverage": "25", + "notionalCap": "50000", "notionalFloor": "5000", "maintMarginRatio": "0.01", - "cum": "17.5" + "cum": "20.0" } }, { "tier": 3.0, "currency": "USDT", - "minNotional": 10000.0, - "maxNotional": 25000.0, + "minNotional": 50000.0, + "maxNotional": 400000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, + "maxLeverage": 20.0, "info": { "bracket": "3", - "initialLeverage": "10", - "notionalCap": "25000", - "notionalFloor": "10000", + "initialLeverage": "20", + "notionalCap": "400000", + "notionalFloor": "50000", "maintMarginRatio": "0.025", - "cum": "167.5" + "cum": "770.0" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, + "minNotional": 400000.0, + "maxNotional": 800000.0, "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, + "maxLeverage": 10.0, "info": { "bracket": "4", - "initialLeverage": "8", - "notionalCap": "100000", - "notionalFloor": "25000", + "initialLeverage": "10", + "notionalCap": "800000", + "notionalFloor": "400000", "maintMarginRatio": "0.05", - "cum": "792.5" + "cum": "10770.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, + "minNotional": 800000.0, + "maxNotional": 2000000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "5", "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", + "notionalCap": "2000000", + "notionalFloor": "800000", "maintMarginRatio": "0.1", - "cum": "5792.5" + "cum": "50770.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 250000.0, + "minNotional": 2000000.0, "maxNotional": 5000000.0, "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maxLeverage": 4.0, "info": { "bracket": "6", - "initialLeverage": "2", + "initialLeverage": "4", "notionalCap": "5000000", - "notionalFloor": "250000", + "notionalFloor": "2000000", "maintMarginRatio": "0.125", - "cum": "12042.5" + "cum": "100770.0" } }, { "tier": 7.0, "currency": "USDT", "minNotional": 5000000.0, - "maxNotional": 8000000.0, + "maxNotional": 12000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "7", + "initialLeverage": "2", + "notionalCap": "12000000", + "notionalFloor": "5000000", + "maintMarginRatio": "0.25", + "cum": "725770.0" + } + }, + { + "tier": 8.0, + "currency": "USDT", + "minNotional": 12000000.0, + "maxNotional": 20000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "7", + "bracket": "8", "initialLeverage": "1", - "notionalCap": "8000000", - "notionalFloor": "5000000", + "notionalCap": "20000000", + "notionalFloor": "12000000", "maintMarginRatio": "0.5", - "cum": "1887042.5" + "cum": "3725770.0" + } + } + ], + "USDC/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.006, + "maxLeverage": 30.0, + "info": { + "bracket": "1", + "initialLeverage": "30", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.006", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 50000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 25.0, + "info": { + "bracket": "2", + "initialLeverage": "25", + "notionalCap": "50000", + "notionalFloor": "5000", + "maintMarginRatio": "0.01", + "cum": "20.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 50000.0, + "maxNotional": 600000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "3", + "initialLeverage": "20", + "notionalCap": "600000", + "notionalFloor": "50000", + "maintMarginRatio": "0.025", + "cum": "770.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 600000.0, + "maxNotional": 1200000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "4", + "initialLeverage": "10", + "notionalCap": "1200000", + "notionalFloor": "600000", + "maintMarginRatio": "0.05", + "cum": "15770.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 1200000.0, + "maxNotional": 3200000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "5", + "initialLeverage": "5", + "notionalCap": "3200000", + "notionalFloor": "1200000", + "maintMarginRatio": "0.1", + "cum": "75770.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 3200000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "6", + "initialLeverage": "4", + "notionalCap": "5000000", + "notionalFloor": "3200000", + "maintMarginRatio": "0.125", + "cum": "155770.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 5000000.0, + "maxNotional": 12000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "7", + "initialLeverage": "2", + "notionalCap": "12000000", + "notionalFloor": "5000000", + "maintMarginRatio": "0.25", + "cum": "780770.0" + } + }, + { + "tier": 8.0, + "currency": "USDT", + "minNotional": 12000000.0, + "maxNotional": 20000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "8", + "initialLeverage": "1", + "notionalCap": "20000000", + "notionalFloor": "12000000", + "maintMarginRatio": "0.5", + "cum": "3780770.0" } } ], @@ -20517,14 +22171,14 @@ "currency": "USDT", "minNotional": 0.0, "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 20.0, + "maintenanceMarginRate": 0.006, + "maxLeverage": 50.0, "info": { "bracket": "1", - "initialLeverage": "20", + "initialLeverage": "50", "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.01", + "maintMarginRatio": "0.006", "cum": "0.0" } }, @@ -20533,63 +22187,63 @@ "currency": "USDT", "minNotional": 5000.0, "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 25.0, "info": { "bracket": "2", - "initialLeverage": "10", + "initialLeverage": "25", "notionalCap": "25000", "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "75.0" + "maintMarginRatio": "0.01", + "cum": "20.0" } }, { "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, + "maxNotional": 200000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, "info": { "bracket": "3", - "initialLeverage": "8", - "notionalCap": "100000", + "initialLeverage": "20", + "notionalCap": "200000", "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "700.0" + "maintMarginRatio": "0.025", + "cum": "395.0" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, + "minNotional": 200000.0, + "maxNotional": 400000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, "info": { "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5700.0" + "initialLeverage": "10", + "notionalCap": "400000", + "notionalFloor": "200000", + "maintMarginRatio": "0.05", + "cum": "5395.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, + "minNotional": 400000.0, "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, "info": { "bracket": "5", - "initialLeverage": "2", + "initialLeverage": "5", "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11950.0" + "notionalFloor": "400000", + "maintMarginRatio": "0.1", + "cum": "25395.0" } }, { @@ -20597,15 +22251,47 @@ "currency": "USDT", "minNotional": 1000000.0, "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "6", + "initialLeverage": "4", + "notionalCap": "5000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.125", + "cum": "50395.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 5000000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "7", + "initialLeverage": "2", + "notionalCap": "6000000", + "notionalFloor": "5000000", + "maintMarginRatio": "0.25", + "cum": "675395.0" + } + }, + { + "tier": 8.0, + "currency": "USDT", + "minNotional": 6000000.0, + "maxNotional": 10000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "8", "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", + "notionalCap": "10000000", + "notionalFloor": "6000000", "maintMarginRatio": "0.5", - "cum": "386950.0" + "cum": "2175395.0" } } ], @@ -20714,10 +22400,10 @@ "minNotional": 0.0, "maxNotional": 5000.0, "maintenanceMarginRate": 0.02, - "maxLeverage": 15.0, + "maxLeverage": 25.0, "info": { "bracket": "1", - "initialLeverage": "15", + "initialLeverage": "25", "notionalCap": "5000", "notionalFloor": "0", "maintMarginRatio": "0.02", @@ -20730,10 +22416,10 @@ "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, + "maxLeverage": 20.0, "info": { "bracket": "2", - "initialLeverage": "10", + "initialLeverage": "20", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", @@ -20744,13 +22430,13 @@ "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 120000.0, + "maxNotional": 200000.0, "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, + "maxLeverage": 10.0, "info": { "bracket": "3", - "initialLeverage": "8", - "notionalCap": "120000", + "initialLeverage": "10", + "notionalCap": "200000", "notionalFloor": "25000", "maintMarginRatio": "0.05", "cum": "650.0" @@ -20759,33 +22445,33 @@ { "tier": 4.0, "currency": "USDT", - "minNotional": 120000.0, - "maxNotional": 300000.0, + "minNotional": 200000.0, + "maxNotional": 500000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "300000", - "notionalFloor": "120000", + "notionalCap": "500000", + "notionalFloor": "200000", "maintMarginRatio": "0.1", - "cum": "6650.0" + "cum": "10650.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 300000.0, + "minNotional": 500000.0, "maxNotional": 1000000.0, "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maxLeverage": 4.0, "info": { "bracket": "5", - "initialLeverage": "2", + "initialLeverage": "4", "notionalCap": "1000000", - "notionalFloor": "300000", + "notionalFloor": "500000", "maintMarginRatio": "0.125", - "cum": "14150.0" + "cum": "23150.0" } }, { @@ -20793,15 +22479,31 @@ "currency": "USDT", "minNotional": 1000000.0, "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "148150.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, + "maxNotional": 5000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "1", - "notionalCap": "3000000", - "notionalFloor": "1000000", + "notionalCap": "5000000", + "notionalFloor": "3000000", "maintMarginRatio": "0.5", - "cum": "389150.0" + "cum": "898150.0" } } ], @@ -20910,10 +22612,10 @@ "minNotional": 0.0, "maxNotional": 5000.0, "maintenanceMarginRate": 0.02, - "maxLeverage": 20.0, + "maxLeverage": 25.0, "info": { "bracket": "1", - "initialLeverage": "20", + "initialLeverage": "25", "notionalCap": "5000", "notionalFloor": "0", "maintMarginRatio": "0.02", @@ -20926,10 +22628,10 @@ "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, + "maxLeverage": 20.0, "info": { "bracket": "2", - "initialLeverage": "10", + "initialLeverage": "20", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", @@ -20940,13 +22642,13 @@ "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, + "maxNotional": 400000.0, "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, + "maxLeverage": 10.0, "info": { "bracket": "3", - "initialLeverage": "8", - "notionalCap": "100000", + "initialLeverage": "10", + "notionalCap": "400000", "notionalFloor": "25000", "maintMarginRatio": "0.05", "cum": "650.0" @@ -20955,49 +22657,65 @@ { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, + "minNotional": 400000.0, + "maxNotional": 1000000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", + "notionalCap": "1000000", + "notionalFloor": "400000", "maintMarginRatio": "0.1", - "cum": "5650.0" + "cum": "20650.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, + "minNotional": 1000000.0, + "maxNotional": 2000000.0, "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maxLeverage": 4.0, "info": { "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", + "initialLeverage": "4", + "notionalCap": "2000000", + "notionalFloor": "1000000", "maintMarginRatio": "0.125", - "cum": "11900.0" + "cum": "45650.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 3000000.0, + "minNotional": 2000000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "6000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.25", + "cum": "295650.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 6000000.0, + "maxNotional": 10000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "1", - "notionalCap": "3000000", - "notionalFloor": "1000000", + "notionalCap": "10000000", + "notionalFloor": "6000000", "maintMarginRatio": "0.5", - "cum": "386900.0" + "cum": "1795650.0" } } ], @@ -21640,13 +23358,13 @@ "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, + "maxNotional": 400000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "3", "initialLeverage": "10", - "notionalCap": "100000", + "notionalCap": "400000", "notionalFloor": "25000", "maintMarginRatio": "0.05", "cum": "700.0" @@ -21655,49 +23373,65 @@ { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, + "minNotional": 400000.0, + "maxNotional": 1000000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", + "notionalCap": "1000000", + "notionalFloor": "400000", "maintMarginRatio": "0.1", - "cum": "5700.0" + "cum": "20700.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, + "minNotional": 1000000.0, + "maxNotional": 2000000.0, "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maxLeverage": 4.0, "info": { "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", + "initialLeverage": "4", + "notionalCap": "2000000", + "notionalFloor": "1000000", "maintMarginRatio": "0.125", - "cum": "11950.0" + "cum": "45700.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, + "minNotional": 2000000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "6000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.25", + "cum": "295700.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 6000000.0, + "maxNotional": 10000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", + "notionalCap": "10000000", + "notionalFloor": "6000000", "maintMarginRatio": "0.5", - "cum": "386950.0" + "cum": "1795700.0" } } ], From ad5afd30478a22d95818c678698fd175ff7a44a5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Mar 2023 08:08:57 +0000 Subject: [PATCH 222/360] Bump uvicorn from 0.20.0 to 0.21.0 Bumps [uvicorn](https://github.com/encode/uvicorn) from 0.20.0 to 0.21.0. - [Release notes](https://github.com/encode/uvicorn/releases) - [Changelog](https://github.com/encode/uvicorn/blob/master/CHANGELOG.md) - [Commits](https://github.com/encode/uvicorn/compare/0.20.0...0.21.0) --- updated-dependencies: - dependency-name: uvicorn dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f5a8f068c..868fc9699 100644 --- a/requirements.txt +++ b/requirements.txt @@ -36,7 +36,7 @@ sdnotify==0.3.2 # API Server fastapi==0.94.0 pydantic==1.10.6 -uvicorn==0.20.0 +uvicorn==0.21.0 pyjwt==2.6.0 aiofiles==23.1.0 psutil==5.9.4 From f3a1177badd09e84c81e0f220b77c36082a656d8 Mon Sep 17 00:00:00 2001 From: initrv Date: Mon, 13 Mar 2023 17:53:35 +0300 Subject: [PATCH 223/360] bring inc back --- freqtrade/freqai/RL/BaseEnvironment.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqai/RL/BaseEnvironment.py b/freqtrade/freqai/RL/BaseEnvironment.py index a9a9a613c..7ac77361c 100644 --- a/freqtrade/freqai/RL/BaseEnvironment.py +++ b/freqtrade/freqai/RL/BaseEnvironment.py @@ -138,7 +138,7 @@ class BaseEnvironment(gym.Env): return [seed] def tensorboard_log(self, metric: str, value: Optional[Union[int, float]] = None, - category: str = "custom"): + inc: Optional[bool] = None, category: str = "custom"): """ Function builds the tensorboard_metrics dictionary to be parsed by the TensorboardCallback. This @@ -155,6 +155,7 @@ class BaseEnvironment(gym.Env): :param metric: metric to be tracked and incremented :param value: `metric` value + :param inc: (deprecated) sets whether the `value` is incremented or not :param category: `metric` category """ increment = True if value is None else False From d723979c4288c35f230734cf03d7c39fb20c539b Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 13 Mar 2023 19:21:53 +0100 Subject: [PATCH 224/360] Move total_trades to explicit variable --- freqtrade/rpc/rpc.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index c68ed2d48..727fc5a37 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -388,12 +388,13 @@ class RPC: Trade.close_date.desc()) output = [trade.to_json() for trade in trades] + total_trades = Trade.get_trades([Trade.is_open.is_(False)]).count() return { "trades": output, "trades_count": len(output), "offset": offset, - "total_trades": Trade.get_trades([Trade.is_open.is_(False)]).count(), + "total_trades": total_trades, } def _rpc_stats(self) -> Dict[str, Any]: From 3d31eca3653c7944eefa04d9284e578d1753498b Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 13 Mar 2023 19:40:48 +0100 Subject: [PATCH 225/360] Update Exception to contain more info part of #8300 --- freqtrade/exchange/exchange.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index c0e07c6d7..24fc8daa1 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1138,7 +1138,10 @@ class Exchange: # Ensure rate is less than stop price if bad_stop_price: raise OperationalException( - 'In stoploss limit order, stop price should be more than limit price') + "In stoploss limit order, stop price should be more than limit price. " + f"Stop price: {stop_price}, Limit price: {limit_rate}, " + f"Limit Price pct: {limit_price_pct}" + ) return limit_rate def _get_stop_params(self, side: BuySell, ordertype: str, stop_price: float) -> Dict: From cf70deaf8d7faed50a8bfbf7c0ca0ff605db4979 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 13 Mar 2023 19:41:39 +0100 Subject: [PATCH 226/360] Disallow negative liquidation prices part of #8300 --- freqtrade/exchange/exchange.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 24fc8daa1..0e0c05cb1 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -2785,7 +2785,7 @@ class Exchange: if is_short else isolated_liq + buffer_amount ) - return isolated_liq + return max(isolated_liq, 0.0) else: return None From 8fd13933c30d53a02e04dfb88fca89b65bba6e78 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 13 Mar 2023 19:50:19 +0100 Subject: [PATCH 227/360] Improve variable naming --- freqtrade/exchange/exchange.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 0e0c05cb1..e2a36d3a3 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -2758,10 +2758,10 @@ class Exchange: raise OperationalException( f"{self.name} does not support {self.margin_mode} {self.trading_mode}") - isolated_liq = None + liquidation_price = None if self._config['dry_run'] or not self.exchange_has("fetchPositions"): - isolated_liq = self.dry_run_liquidation_price( + liquidation_price = self.dry_run_liquidation_price( pair=pair, open_rate=open_rate, is_short=is_short, @@ -2776,16 +2776,16 @@ class Exchange: positions = self.fetch_positions(pair) if len(positions) > 0: pos = positions[0] - isolated_liq = pos['liquidationPrice'] + liquidation_price = pos['liquidationPrice'] - if isolated_liq is not None: - buffer_amount = abs(open_rate - isolated_liq) * self.liquidation_buffer - isolated_liq = ( - isolated_liq - buffer_amount + if liquidation_price is not None: + buffer_amount = abs(open_rate - liquidation_price) * self.liquidation_buffer + liquidation_price_buffer = ( + liquidation_price - buffer_amount if is_short else - isolated_liq + buffer_amount + liquidation_price + buffer_amount ) - return max(isolated_liq, 0.0) + return max(liquidation_price_buffer, 0.0) else: return None From 487469680f1d4120fe1d40cb9d870047425f0559 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 13 Mar 2023 20:13:12 +0100 Subject: [PATCH 228/360] Use correct exception type for ccxt.InvalidOrder --- freqtrade/exchange/exchange.py | 2 +- tests/exchange/test_exchange.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index e2a36d3a3..489dc1b68 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1087,7 +1087,7 @@ class Exchange: f'Tried to {side} amount {amount} at rate {rate}.' f'Message: {e}') from e except ccxt.InvalidOrder as e: - raise ExchangeError( + raise InvalidOrderException( f'Could not create {ordertype} {side} order on market {pair}. ' f'Tried to {side} amount {amount} at rate {rate}. ' f'Message: {e}') from e diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 940319a45..d7f6a8b90 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -12,8 +12,8 @@ from pandas import DataFrame from freqtrade.enums import CandleType, MarginMode, TradingMode from freqtrade.exceptions import (DDosProtection, DependencyException, ExchangeError, - InvalidOrderException, OperationalException, PricingError, - TemporaryError) + InsufficientFundsError, InvalidOrderException, + OperationalException, PricingError, TemporaryError) from freqtrade.exchange import (Binance, Bittrex, Exchange, Kraken, amount_to_precision, date_minus_candles, market_is_active, price_to_precision, timeframe_to_minutes, timeframe_to_msecs, timeframe_to_next_date, @@ -1599,13 +1599,13 @@ def test_sell_prod(default_conf, mocker, exchange_name): assert api_mock.create_order.call_args[0][4] == 200 # test exception handling - with pytest.raises(DependencyException): + with pytest.raises(InsufficientFundsError): api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.create_order(pair='ETH/BTC', ordertype=order_type, side="sell", amount=1, rate=200, leverage=1.0) - with pytest.raises(DependencyException): + with pytest.raises(InvalidOrderException): api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.create_order(pair='ETH/BTC', ordertype='limit', side="sell", amount=1, rate=200, From b23cea6e59ae252dd39222544e739bc953e3ac3d Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 13 Mar 2023 20:16:12 +0100 Subject: [PATCH 229/360] Bump ruff to 0.0.255 --- .pre-commit-config.yaml | 2 +- requirements-dev.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2d1fb20fc..bc2e0bc0d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -30,7 +30,7 @@ repos: - repo: https://github.com/charliermarsh/ruff-pre-commit # Ruff version. - rev: 'v0.0.251' + rev: 'v0.0.255' hooks: - id: ruff diff --git a/requirements-dev.txt b/requirements-dev.txt index 44c1a69fc..6d076777f 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -7,7 +7,7 @@ -r docs/requirements-docs.txt coveralls==3.3.1 -ruff==0.0.254 +ruff==0.0.255 mypy==1.1.1 pre-commit==3.1.1 pytest==7.2.2 From 5c280d5649b431b8ff366c14acca8db6d7b45c32 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 13 Mar 2023 20:28:13 +0100 Subject: [PATCH 230/360] Improve emergency_exit handling --- freqtrade/freqtradebot.py | 20 +++++++++++--------- tests/test_freqtradebot.py | 6 +++--- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 3924b111f..ec61f45b1 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1122,8 +1122,7 @@ class FreqtradeBot(LoggingMixin): 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, stop_price, exit_check=ExitCheckTuple( - exit_type=ExitType.EMERGENCY_EXIT)) + self.emergency_exit(trade, stop_price) except ExchangeError: trade.stoploss_order_id = None @@ -1281,13 +1280,16 @@ class FreqtradeBot(LoggingMixin): if canceled and max_timeouts > 0 and canceled_count >= max_timeouts: logger.warning(f'Emergency exiting trade {trade}, as the exit order ' f'timed out {max_timeouts} times.') - try: - self.execute_trade_exit( - trade, order['price'], - exit_check=ExitCheckTuple(exit_type=ExitType.EMERGENCY_EXIT)) - except DependencyException as exception: - logger.warning( - f'Unable to emergency sell trade {trade.pair}: {exception}') + self.emergency_exit(trade, order['price']) + + def emergency_exit(self, trade: Trade, price: float) -> None: + try: + self.execute_trade_exit( + trade, price, + exit_check=ExitCheckTuple(exit_type=ExitType.EMERGENCY_EXIT)) + except DependencyException as exception: + logger.warning( + f'Unable to emergency exit trade {trade.pair}: {exception}') def replace_order(self, order: Dict, order_obj: Optional[Order], trade: Trade) -> None: """ diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 06832589c..349655243 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2724,21 +2724,21 @@ def test_manage_open_orders_exit_usercustom( assert freqtrade.strategy.check_exit_timeout.call_count == 1 assert freqtrade.strategy.check_entry_timeout.call_count == 0 - # 2nd canceled trade - Fail execute sell + # 2nd canceled trade - Fail execute exit caplog.clear() open_trade_usdt.open_order_id = limit_sell_order_old['id'] mocker.patch('freqtrade.persistence.Trade.get_exit_order_count', return_value=1) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.execute_trade_exit', side_effect=DependencyException) freqtrade.manage_open_orders() - assert log_has_re('Unable to emergency sell .*', caplog) + assert log_has_re('Unable to emergency exit .*', caplog) et_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.execute_trade_exit') caplog.clear() # 2nd canceled trade ... open_trade_usdt.open_order_id = limit_sell_order_old['id'] - # If cancelling fails - no emergency sell! + # If cancelling fails - no emergency exit! with patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_exit', return_value=False): freqtrade.manage_open_orders() assert et_mock.call_count == 0 From 8f29312c9e6f6637628965c28dcb9409f562c29e Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 14 Mar 2023 08:14:01 +0100 Subject: [PATCH 231/360] Minimum re-entry stake should not include stoploss --- freqtrade/freqtradebot.py | 14 +++++++++++--- freqtrade/optimize/backtesting.py | 2 +- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index ec61f45b1..dfac11347 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -586,7 +586,7 @@ class FreqtradeBot(LoggingMixin): min_entry_stake = self.exchange.get_min_pair_stake_amount(trade.pair, current_entry_rate, - self.strategy.stoploss) + 0.0) min_exit_stake = self.exchange.get_min_pair_stake_amount(trade.pair, current_exit_rate, self.strategy.stoploss) @@ -700,7 +700,8 @@ class FreqtradeBot(LoggingMixin): pos_adjust = trade is not None enter_limit_requested, stake_amount, leverage = self.get_valid_enter_price_and_stake( - pair, price, stake_amount, trade_side, enter_tag, trade, order_adjust, leverage_) + pair, price, stake_amount, trade_side, enter_tag, trade, order_adjust, leverage_, + pos_adjust) if not stake_amount: return False @@ -860,7 +861,12 @@ class FreqtradeBot(LoggingMixin): trade: Optional[Trade], order_adjust: bool, leverage_: Optional[float], + pos_adjust: bool, ) -> Tuple[float, float, float]: + """ + Validate and eventually adjust (within limits) limit, amount and leverage + :return: Tuple with (price, amount, leverage) + """ if price: enter_limit_requested = price @@ -906,7 +912,9 @@ class FreqtradeBot(LoggingMixin): # We do however also need min-stake to determine leverage, therefore this is ignored as # edge-case for now. min_stake_amount = self.exchange.get_min_pair_stake_amount( - pair, enter_limit_requested, self.strategy.stoploss, leverage) + pair, enter_limit_requested, + self.strategy.stoploss if not pos_adjust else 0.0, + leverage) max_stake_amount = self.exchange.get_max_pair_stake_amount( pair, enter_limit_requested, leverage) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 83b65d24b..5e1e9b48a 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -749,7 +749,7 @@ class Backtesting: leverage = min(max(leverage, 1.0), max_leverage) min_stake_amount = self.exchange.get_min_pair_stake_amount( - pair, propose_rate, -0.05, leverage=leverage) or 0 + pair, propose_rate, -0.05 if not pos_adjust else 0.0, leverage=leverage) or 0 max_stake_amount = self.exchange.get_max_pair_stake_amount( pair, propose_rate, leverage=leverage) stake_available = self.wallets.get_available_stake_amount() From 7e08e3a59a964801f02f083d71a76b17e3a6dcc6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 15 Mar 2023 07:22:07 +0100 Subject: [PATCH 232/360] Update example to use get_trades_proxy --- docs/strategy-customization.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 8ab0b1464..8b6654c6c 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -1040,11 +1040,10 @@ from datetime import timedelta, datetime, timezone # Within populate indicators (or populate_buy): if self.config['runmode'].value in ('live', 'dry_run'): - # fetch closed trades for the last 2 days - trades = Trade.get_trades([Trade.pair == metadata['pair'], - Trade.open_date > datetime.utcnow() - timedelta(days=2), - Trade.is_open.is_(False), - ]).all() + # fetch closed trades for the last 2 days + trades = Trade.get_trades_proxy( + pair=metadata['pair'], is_open=False, + open_date=datetime.now(timezone.utc) - timedelta(days=2)) # Analyze the conditions you'd like to lock the pair .... will probably be different for every strategy sumprofit = sum(trade.close_profit for trade in trades) if sumprofit < 0: From 95ff59a21c910d13f463ab6c3fe57bff0eb08695 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 15 Mar 2023 07:23:54 +0100 Subject: [PATCH 233/360] Improve documentation for get_trades_proxy --- freqtrade/persistence/trade_model.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 8e8a414c8..19d71aa7b 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -1088,6 +1088,11 @@ class LocalTrade(): In live mode, converts the filter to a database query and returns all rows In Backtest mode, uses filters on Trade.trades to get the result. + :param pair: Filter by pair + :param is_open: Filter by open/closed status + :param open_date: Filter by open_date (filters via trade.open_date > input) + :param close_date: Filter by close_date (filters via trade.close_date > input) + Will implicitly only return closed trades. :return: unsorted List[Trade] """ From 47ab285252996cf7eabd109eba09973272917b26 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 15 Mar 2023 20:49:35 +0100 Subject: [PATCH 234/360] Minor test fix --- tests/rpc/test_rpc_telegram.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 1dc255b3e..59b6df489 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -302,8 +302,7 @@ def test_telegram_status_closed_trade(default_conf, update, mocker, fee) -> None telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) create_mock_trades(fee) - trades = Trade.get_trades([Trade.is_open.is_(False)]) - trade = trades[0] + trade = Trade.get_trades([Trade.is_open.is_(False)]).first() context = MagicMock() context.args = [str(trade.id)] telegram._status(update=update, context=context) From b469addffb09945f50d36eed0a9273b242d9626c Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 15 Mar 2023 21:00:30 +0100 Subject: [PATCH 235/360] remove usage of .query from regular models --- freqtrade/persistence/trade_model.py | 142 +++++++++++++++----------- freqtrade/rpc/rpc.py | 39 ++++--- tests/persistence/test_persistence.py | 8 +- 3 files changed, 114 insertions(+), 75 deletions(-) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 19d71aa7b..a2bbd1ee1 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -7,7 +7,8 @@ from datetime import datetime, timedelta, timezone from math import isclose from typing import Any, ClassVar, Dict, List, Optional, cast -from sqlalchemy import Enum, Float, ForeignKey, Integer, String, UniqueConstraint, desc, func +from sqlalchemy import (Enum, Float, ForeignKey, Integer, Result, Select, String, UniqueConstraint, + desc, func, select) from sqlalchemy.orm import (Mapped, Query, QueryPropertyDescriptor, lazyload, mapped_column, relationship) @@ -1153,7 +1154,9 @@ class LocalTrade(): get open trade count """ if Trade.use_db: - return Trade.query.filter(Trade.is_open.is_(True)).count() + return Trade._session.scalar( + select(func.count(Trade.id)).filter(Trade.is_open.is_(True)) + ) else: return LocalTrade.bt_open_open_trade_count @@ -1287,18 +1290,18 @@ class Trade(ModelBase, LocalTrade): def delete(self) -> None: for order in self.orders: - Order.query.session.delete(order) + Order._session.delete(order) - Trade.query.session.delete(self) + Trade._session.delete(self) Trade.commit() @staticmethod def commit(): - Trade.query.session.commit() + Trade._session.commit() @staticmethod def rollback(): - Trade.query.session.rollback() + Trade._session.rollback() @staticmethod def get_trades_proxy(*, pair: Optional[str] = None, is_open: Optional[bool] = None, @@ -1332,7 +1335,7 @@ class Trade(ModelBase, LocalTrade): ) @staticmethod - def get_trades(trade_filter=None, include_orders: bool = True) -> Query['Trade']: + def get_trades_query(trade_filter=None, include_orders: bool = True) -> Select['Trade']: """ Helper function to query Trades using filters. NOTE: Not supported in Backtesting. @@ -1347,15 +1350,28 @@ class Trade(ModelBase, LocalTrade): if trade_filter is not None: if not isinstance(trade_filter, list): trade_filter = [trade_filter] - this_query = Trade.query.filter(*trade_filter) + this_query = select(Trade).filter(*trade_filter) else: - this_query = Trade.query + this_query = select(Trade) if not include_orders: # Don't load order relations # Consider using noload or raiseload instead of lazyload this_query = this_query.options(lazyload(Trade.orders)) return this_query + @staticmethod + def get_trades(trade_filter=None, include_orders: bool = True) -> Query['Trade']: + """ + Helper function to query Trades using filters. + NOTE: Not supported in Backtesting. + :param trade_filter: Optional filter to apply to trades + Can be either a Filter object, or a List of filters + e.g. `(trade_filter=[Trade.id == trade_id, Trade.is_open.is_(True),])` + e.g. `(trade_filter=Trade.id == trade_id)` + :return: unsorted query object + """ + return Trade._session.execute(Trade.get_trades_query(trade_filter, include_orders)) + @staticmethod def get_open_order_trades() -> List['Trade']: """ @@ -1392,8 +1408,9 @@ class Trade(ModelBase, LocalTrade): Retrieves total realized profit """ if Trade.use_db: - total_profit = Trade.query.with_entities( - func.sum(Trade.close_profit_abs)).filter(Trade.is_open.is_(False)).scalar() + total_profit = Trade._session.scalar( + select(func.sum(Trade.close_profit_abs)).filter(Trade.is_open.is_(False)) + ) else: total_profit = sum( t.close_profit_abs for t in LocalTrade.get_trades_proxy(is_open=False)) @@ -1406,8 +1423,9 @@ class Trade(ModelBase, LocalTrade): in stake currency """ if Trade.use_db: - total_open_stake_amount = Trade.query.with_entities( - func.sum(Trade.stake_amount)).filter(Trade.is_open.is_(True)).scalar() + total_open_stake_amount = Trade._session.scalar( + select(func.sum(Trade.stake_amount)).filter(Trade.is_open.is_(True)) + ) else: total_open_stake_amount = sum( t.stake_amount for t in LocalTrade.get_trades_proxy(is_open=True)) @@ -1423,15 +1441,18 @@ class Trade(ModelBase, LocalTrade): if minutes: start_date = datetime.now(timezone.utc) - timedelta(minutes=minutes) filters.append(Trade.close_date >= start_date) - pair_rates = Trade.query.with_entities( - Trade.pair, - 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.pair) \ - .order_by(desc('profit_sum_abs')) \ - .all() + + pair_rates = Trade._session.execute( + select( + Trade.pair, + 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.pair) + .order_by(desc('profit_sum_abs')) + ).all() + return [ { 'pair': pair, @@ -1456,15 +1477,16 @@ class Trade(ModelBase, LocalTrade): if (pair is not None): filters.append(Trade.pair == pair) - enter_tag_perf = Trade.query.with_entities( - Trade.enter_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.enter_tag) \ - .order_by(desc('profit_sum_abs')) \ - .all() + enter_tag_perf = Trade._session.execute( + select( + Trade.enter_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.enter_tag) + .order_by(desc('profit_sum_abs')) + ).all() return [ { @@ -1488,16 +1510,16 @@ class Trade(ModelBase, LocalTrade): filters: List = [Trade.is_open.is_(False)] if (pair is not None): filters.append(Trade.pair == pair) - - sell_tag_perf = Trade.query.with_entities( - Trade.exit_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.exit_reason) \ - .order_by(desc('profit_sum_abs')) \ - .all() + sell_tag_perf = Trade._session.execute( + select( + Trade.exit_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.exit_reason) + .order_by(desc('profit_sum_abs')) + ).all() return [ { @@ -1521,18 +1543,18 @@ class Trade(ModelBase, LocalTrade): filters: List = [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.enter_tag, - Trade.exit_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() + mix_tag_perf = Trade._session.execute( + select( + Trade.id, + Trade.enter_tag, + Trade.exit_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, enter_tag, exit_reason, profit, profit_abs, count in mix_tag_perf: @@ -1568,11 +1590,15 @@ class Trade(ModelBase, LocalTrade): NOTE: Not supported in Backtesting. :returns: Tuple containing (pair, profit_sum) """ - best_pair = Trade.query.with_entities( - Trade.pair, func.sum(Trade.close_profit).label('profit_sum') - ).filter(Trade.is_open.is_(False) & (Trade.close_date >= start_date)) \ - .group_by(Trade.pair) \ - .order_by(desc('profit_sum')).first() + best_pair = Trade._session.execute( + select( + Trade.pair, + func.sum(Trade.close_profit).label('profit_sum') + ).filter(Trade.is_open.is_(False) & (Trade.close_date >= start_date)) + .group_by(Trade.pair) + .order_by(desc('profit_sum')) + ).first() + return best_pair @staticmethod diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 727fc5a37..db2b35049 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -5,7 +5,7 @@ import logging from abc import abstractmethod from datetime import date, datetime, timedelta, timezone from math import isnan -from typing import Any, Dict, Generator, List, Optional, Tuple, Union +from typing import Any, Dict, Generator, List, Optional, Sequence, Tuple, Union import arrow import psutil @@ -13,6 +13,7 @@ from dateutil.relativedelta import relativedelta from dateutil.tz import tzlocal from numpy import NAN, inf, int64, mean from pandas import DataFrame, NaT +from sqlalchemy import func, select from freqtrade import __version__ from freqtrade.configuration.timerange import TimeRange @@ -339,11 +340,18 @@ class RPC: for day in range(0, timescale): profitday = start_date - time_offset(day) # Only query for necessary columns for performance reasons. - trades = Trade.query.session.query(Trade.close_profit_abs).filter( - Trade.is_open.is_(False), - Trade.close_date >= profitday, - Trade.close_date < (profitday + time_offset(1)) - ).order_by(Trade.close_date).all() + trades = Trade._session.execute( + select(Trade.close_profit_abs) + .filter(Trade.is_open.is_(False), + Trade.close_date >= profitday, + Trade.close_date < (profitday + time_offset(1))) + .order_by(Trade.close_date) + ).all() + # trades = Trade.query.session.query(Trade.close_profit_abs).filter( + # Trade.is_open.is_(False), + # Trade.close_date >= profitday, + # Trade.close_date < (profitday + time_offset(1)) + # ).order_by(Trade.close_date).all() curdayprofit = sum( trade.close_profit_abs for trade in trades if trade.close_profit_abs is not None) @@ -381,14 +389,19 @@ class RPC: """ Returns the X last trades """ order_by: Any = Trade.id if order_by_id else Trade.close_date.desc() if limit: - trades = Trade.get_trades([Trade.is_open.is_(False)]).order_by( - order_by).limit(limit).offset(offset) + trades = Trade._session.execute( + Trade.get_trades_query([Trade.is_open.is_(False)]) + .order_by(order_by) + .limit(limit) + .offset(offset)) else: - trades = Trade.get_trades([Trade.is_open.is_(False)]).order_by( - Trade.close_date.desc()) + trades = Trade._session.execute( + Trade.get_trades_query([Trade.is_open.is_(False)]) + .order_by(Trade.close_date.desc())) output = [trade.to_json() for trade in trades] - total_trades = Trade.get_trades([Trade.is_open.is_(False)]).count() + total_trades = Trade._session.scalar( + select(func.count(Trade.id)).filter(Trade.is_open.is_(False))) return { "trades": output, @@ -436,8 +449,8 @@ class RPC: """ Returns cumulative profit statistics """ trade_filter = ((Trade.is_open.is_(False) & (Trade.close_date >= start_date)) | Trade.is_open.is_(True)) - trades: List[Trade] = Trade.get_trades( - trade_filter, include_orders=False).order_by(Trade.id).all() + trades: Sequence[Trade] = Trade._session.execute(Trade.get_trades_query( + trade_filter, include_orders=False).order_by(Trade.id)).all() profit_all_coin = [] profit_all_ratio = [] diff --git a/tests/persistence/test_persistence.py b/tests/persistence/test_persistence.py index 0598d4134..cd4890db9 100644 --- a/tests/persistence/test_persistence.py +++ b/tests/persistence/test_persistence.py @@ -1793,17 +1793,17 @@ def test_get_trades_proxy(fee, use_db, is_short): @pytest.mark.usefixtures("init_persistence") @pytest.mark.parametrize('is_short', [True, False]) def test_get_trades__query(fee, is_short): - query = Trade.get_trades([]) + query = Trade.get_trades_query([]) # without orders there should be no join issued. - query1 = Trade.get_trades([], include_orders=False) + query1 = Trade.get_trades_query([], include_orders=False) # Empty "with-options -> default - selectin" assert query._with_options == () assert query1._with_options != () create_mock_trades(fee, is_short) - query = Trade.get_trades([]) - query1 = Trade.get_trades([], include_orders=False) + query = Trade.get_trades_query([]) + query1 = Trade.get_trades_query([], include_orders=False) assert query._with_options == () assert query1._with_options != () From d45599ca3b5c64c9a488f8235d2f2284ed400728 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 15 Mar 2023 21:09:25 +0100 Subject: [PATCH 236/360] Fix some type errors --- freqtrade/data/btanalysis.py | 2 +- freqtrade/persistence/trade_model.py | 27 +++++++++++++-------------- freqtrade/rpc/rpc.py | 4 ++-- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 9772506a7..3567f4112 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -373,7 +373,7 @@ def load_trades_from_db(db_url: str, strategy: Optional[str] = None) -> pd.DataF filters = [] if strategy: filters.append(Trade.strategy == strategy) - trades = trade_list_to_dataframe(Trade.get_trades(filters).all()) + trades = trade_list_to_dataframe(list(Trade.get_trades(filters).all())) return trades diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index a2bbd1ee1..58f6c666a 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -7,10 +7,9 @@ from datetime import datetime, timedelta, timezone from math import isclose from typing import Any, ClassVar, Dict, List, Optional, cast -from sqlalchemy import (Enum, Float, ForeignKey, Integer, Result, Select, String, UniqueConstraint, - desc, func, select) -from sqlalchemy.orm import (Mapped, Query, QueryPropertyDescriptor, lazyload, mapped_column, - relationship) +from sqlalchemy import (Enum, Float, ForeignKey, Integer, ScalarResult, Select, String, + UniqueConstraint, desc, func, select) +from sqlalchemy.orm import Mapped, QueryPropertyDescriptor, lazyload, mapped_column, relationship from freqtrade.constants import (DATETIME_PRINT_FORMAT, MATH_CLOSE_PREC, NON_OPEN_EXCHANGE_STATES, BuySell, LongShort) @@ -1154,9 +1153,9 @@ class LocalTrade(): get open trade count """ if Trade.use_db: - return Trade._session.scalar( + return Trade._session.execute( select(func.count(Trade.id)).filter(Trade.is_open.is_(True)) - ) + ).scalar_one() else: return LocalTrade.bt_open_open_trade_count @@ -1335,7 +1334,7 @@ class Trade(ModelBase, LocalTrade): ) @staticmethod - def get_trades_query(trade_filter=None, include_orders: bool = True) -> Select['Trade']: + def get_trades_query(trade_filter=None, include_orders: bool = True) -> Select: """ Helper function to query Trades using filters. NOTE: Not supported in Backtesting. @@ -1360,7 +1359,7 @@ class Trade(ModelBase, LocalTrade): return this_query @staticmethod - def get_trades(trade_filter=None, include_orders: bool = True) -> Query['Trade']: + def get_trades(trade_filter=None, include_orders: bool = True) -> ScalarResult['Trade']: """ Helper function to query Trades using filters. NOTE: Not supported in Backtesting. @@ -1370,7 +1369,7 @@ class Trade(ModelBase, LocalTrade): e.g. `(trade_filter=Trade.id == trade_id)` :return: unsorted query object """ - return Trade._session.execute(Trade.get_trades_query(trade_filter, include_orders)) + return Trade._session.scalars(Trade.get_trades_query(trade_filter, include_orders)) @staticmethod def get_open_order_trades() -> List['Trade']: @@ -1378,7 +1377,7 @@ class Trade(ModelBase, LocalTrade): Returns all open trades NOTE: Not supported in Backtesting. """ - return Trade.get_trades(Trade.open_order_id.isnot(None)).all() + return cast(List[Trade], Trade.get_trades(Trade.open_order_id.isnot(None)).all()) @staticmethod def get_open_trades_without_assigned_fees(): @@ -1408,12 +1407,12 @@ class Trade(ModelBase, LocalTrade): Retrieves total realized profit """ if Trade.use_db: - total_profit = Trade._session.scalar( + total_profit: float = Trade._session.execute( select(func.sum(Trade.close_profit_abs)).filter(Trade.is_open.is_(False)) - ) + ).scalar_one() else: - total_profit = sum( - t.close_profit_abs for t in LocalTrade.get_trades_proxy(is_open=False)) + total_profit = sum(t.close_profit_abs # type: ignore + for t in LocalTrade.get_trades_proxy(is_open=False)) return total_profit or 0 @staticmethod diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index db2b35049..45150423b 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -159,7 +159,7 @@ class RPC: """ # Fetch open trades if trade_ids: - trades: List[Trade] = Trade.get_trades(trade_filter=Trade.id.in_(trade_ids)).all() + trades: Sequence[Trade] = Trade.get_trades(trade_filter=Trade.id.in_(trade_ids)).all() else: trades = Trade.get_open_trades() @@ -449,7 +449,7 @@ class RPC: """ Returns cumulative profit statistics """ trade_filter = ((Trade.is_open.is_(False) & (Trade.close_date >= start_date)) | Trade.is_open.is_(True)) - trades: Sequence[Trade] = Trade._session.execute(Trade.get_trades_query( + trades: Sequence[Trade] = Trade._session.scalars(Trade.get_trades_query( trade_filter, include_orders=False).order_by(Trade.id)).all() profit_all_coin = [] From 8073989c988aa2c07438d994da77893a565e657d Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 15 Mar 2023 21:10:47 +0100 Subject: [PATCH 237/360] Remove more usages of .query --- freqtrade/freqtradebot.py | 2 +- freqtrade/persistence/pairlock_middleware.py | 8 ++++---- freqtrade/rpc/rpc.py | 5 ----- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index dfac11347..1c181cb26 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -819,7 +819,7 @@ class FreqtradeBot(LoggingMixin): trade.orders.append(order_obj) trade.recalc_trade_from_orders() - Trade.query.session.add(trade) + Trade._session.add(trade) Trade.commit() # Updating wallets diff --git a/freqtrade/persistence/pairlock_middleware.py b/freqtrade/persistence/pairlock_middleware.py index 5ed131a9b..08c947a83 100644 --- a/freqtrade/persistence/pairlock_middleware.py +++ b/freqtrade/persistence/pairlock_middleware.py @@ -51,8 +51,8 @@ class PairLocks(): active=True ) if PairLocks.use_db: - PairLock.query.session.add(lock) - PairLock.query.session.commit() + PairLock._session.add(lock) + PairLock._session.commit() else: PairLocks.locks.append(lock) return lock @@ -106,7 +106,7 @@ class PairLocks(): for lock in locks: lock.active = False if PairLocks.use_db: - PairLock.query.session.commit() + PairLock._session.commit() @staticmethod def unlock_reason(reason: str, now: Optional[datetime] = None) -> None: @@ -130,7 +130,7 @@ class PairLocks(): for lock in locks: logger.info(f"Releasing lock for {lock.pair} with reason '{reason}'.") lock.active = False - PairLock.query.session.commit() + PairLock._session.commit() else: # used in backtesting mode; don't show log messages for speed locksb = PairLocks.get_pair_locks(None) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 45150423b..2d29944f8 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -347,11 +347,6 @@ class RPC: Trade.close_date < (profitday + time_offset(1))) .order_by(Trade.close_date) ).all() - # trades = Trade.query.session.query(Trade.close_profit_abs).filter( - # Trade.is_open.is_(False), - # Trade.close_date >= profitday, - # Trade.close_date < (profitday + time_offset(1)) - # ).order_by(Trade.close_date).all() curdayprofit = sum( trade.close_profit_abs for trade in trades if trade.close_profit_abs is not None) From aa54b7770256a72b20ad7e3f548291951dd59ab2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 15 Mar 2023 21:12:06 +0100 Subject: [PATCH 238/360] Rename _session to sessoin --- freqtrade/commands/db_commands.py | 2 +- freqtrade/freqtradebot.py | 2 +- freqtrade/persistence/models.py | 12 ++++---- freqtrade/persistence/pairlock.py | 2 +- freqtrade/persistence/pairlock_middleware.py | 8 +++--- freqtrade/persistence/trade_model.py | 30 ++++++++++---------- freqtrade/rpc/rpc.py | 10 +++---- 7 files changed, 33 insertions(+), 33 deletions(-) diff --git a/freqtrade/commands/db_commands.py b/freqtrade/commands/db_commands.py index c424016b1..b4997582d 100644 --- a/freqtrade/commands/db_commands.py +++ b/freqtrade/commands/db_commands.py @@ -20,7 +20,7 @@ def start_convert_db(args: Dict[str, Any]) -> None: config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) init_db(config['db_url']) - session_target = Trade._session + session_target = Trade.session init_db(config['db_url_from']) logger.info("Starting db migration.") diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 1c181cb26..06c8831f5 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -819,7 +819,7 @@ class FreqtradeBot(LoggingMixin): trade.orders.append(order_obj) trade.recalc_trade_from_orders() - Trade._session.add(trade) + Trade.session.add(trade) Trade.commit() # Updating wallets diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index d718af2f4..f4058b4eb 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -54,12 +54,12 @@ def init_db(db_url: str) -> None: # https://docs.sqlalchemy.org/en/13/orm/contextual.html#thread-local-scope # Scoped sessions proxy requests to the appropriate thread-local session. # We should use the scoped_session object - not a seperately initialized version - Trade._session = scoped_session(sessionmaker(bind=engine, autoflush=False)) - Order._session = Trade._session - PairLock._session = Trade._session - Trade.query = Trade._session.query_property() - Order.query = Trade._session.query_property() - PairLock.query = Trade._session.query_property() + Trade.session = scoped_session(sessionmaker(bind=engine, autoflush=False)) + Order.session = Trade.session + PairLock.session = Trade.session + Trade.query = Trade.session.query_property() + Order.query = Trade.session.query_property() + PairLock.query = Trade.session.query_property() previous_tables = inspect(engine).get_table_names() ModelBase.metadata.create_all(engine) diff --git a/freqtrade/persistence/pairlock.py b/freqtrade/persistence/pairlock.py index 1e5699145..0cd595b66 100644 --- a/freqtrade/persistence/pairlock.py +++ b/freqtrade/persistence/pairlock.py @@ -14,7 +14,7 @@ class PairLock(ModelBase): """ __tablename__ = 'pairlocks' query: ClassVar[QueryPropertyDescriptor] - _session: ClassVar[SessionType] + session: ClassVar[SessionType] id: Mapped[int] = mapped_column(primary_key=True) diff --git a/freqtrade/persistence/pairlock_middleware.py b/freqtrade/persistence/pairlock_middleware.py index 08c947a83..e6860bbe6 100644 --- a/freqtrade/persistence/pairlock_middleware.py +++ b/freqtrade/persistence/pairlock_middleware.py @@ -51,8 +51,8 @@ class PairLocks(): active=True ) if PairLocks.use_db: - PairLock._session.add(lock) - PairLock._session.commit() + PairLock.session.add(lock) + PairLock.session.commit() else: PairLocks.locks.append(lock) return lock @@ -106,7 +106,7 @@ class PairLocks(): for lock in locks: lock.active = False if PairLocks.use_db: - PairLock._session.commit() + PairLock.session.commit() @staticmethod def unlock_reason(reason: str, now: Optional[datetime] = None) -> None: @@ -130,7 +130,7 @@ class PairLocks(): for lock in locks: logger.info(f"Releasing lock for {lock.pair} with reason '{reason}'.") lock.active = False - PairLock._session.commit() + PairLock.session.commit() else: # used in backtesting mode; don't show log messages for speed locksb = PairLocks.get_pair_locks(None) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 58f6c666a..0cc7ad4ff 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -37,7 +37,7 @@ class Order(ModelBase): """ __tablename__ = 'orders' query: ClassVar[QueryPropertyDescriptor] - _session: ClassVar[SessionType] + session: ClassVar[SessionType] # Uniqueness should be ensured over pair, order_id # its likely that order_id is unique per Pair on some exchanges. @@ -1153,7 +1153,7 @@ class LocalTrade(): get open trade count """ if Trade.use_db: - return Trade._session.execute( + return Trade.session.execute( select(func.count(Trade.id)).filter(Trade.is_open.is_(True)) ).scalar_one() else: @@ -1189,7 +1189,7 @@ class Trade(ModelBase, LocalTrade): """ __tablename__ = 'trades' query: ClassVar[QueryPropertyDescriptor] - _session: ClassVar[SessionType] + session: ClassVar[SessionType] use_db: bool = True @@ -1289,18 +1289,18 @@ class Trade(ModelBase, LocalTrade): def delete(self) -> None: for order in self.orders: - Order._session.delete(order) + Order.session.delete(order) - Trade._session.delete(self) + Trade.session.delete(self) Trade.commit() @staticmethod def commit(): - Trade._session.commit() + Trade.session.commit() @staticmethod def rollback(): - Trade._session.rollback() + Trade.session.rollback() @staticmethod def get_trades_proxy(*, pair: Optional[str] = None, is_open: Optional[bool] = None, @@ -1369,7 +1369,7 @@ class Trade(ModelBase, LocalTrade): e.g. `(trade_filter=Trade.id == trade_id)` :return: unsorted query object """ - return Trade._session.scalars(Trade.get_trades_query(trade_filter, include_orders)) + return Trade.session.scalars(Trade.get_trades_query(trade_filter, include_orders)) @staticmethod def get_open_order_trades() -> List['Trade']: @@ -1407,7 +1407,7 @@ class Trade(ModelBase, LocalTrade): Retrieves total realized profit """ if Trade.use_db: - total_profit: float = Trade._session.execute( + total_profit: float = Trade.session.execute( select(func.sum(Trade.close_profit_abs)).filter(Trade.is_open.is_(False)) ).scalar_one() else: @@ -1422,7 +1422,7 @@ class Trade(ModelBase, LocalTrade): in stake currency """ if Trade.use_db: - total_open_stake_amount = Trade._session.scalar( + total_open_stake_amount = Trade.session.scalar( select(func.sum(Trade.stake_amount)).filter(Trade.is_open.is_(True)) ) else: @@ -1441,7 +1441,7 @@ class Trade(ModelBase, LocalTrade): start_date = datetime.now(timezone.utc) - timedelta(minutes=minutes) filters.append(Trade.close_date >= start_date) - pair_rates = Trade._session.execute( + pair_rates = Trade.session.execute( select( Trade.pair, func.sum(Trade.close_profit).label('profit_sum'), @@ -1476,7 +1476,7 @@ class Trade(ModelBase, LocalTrade): if (pair is not None): filters.append(Trade.pair == pair) - enter_tag_perf = Trade._session.execute( + enter_tag_perf = Trade.session.execute( select( Trade.enter_tag, func.sum(Trade.close_profit).label('profit_sum'), @@ -1509,7 +1509,7 @@ class Trade(ModelBase, LocalTrade): filters: List = [Trade.is_open.is_(False)] if (pair is not None): filters.append(Trade.pair == pair) - sell_tag_perf = Trade._session.execute( + sell_tag_perf = Trade.session.execute( select( Trade.exit_reason, func.sum(Trade.close_profit).label('profit_sum'), @@ -1542,7 +1542,7 @@ class Trade(ModelBase, LocalTrade): filters: List = [Trade.is_open.is_(False)] if (pair is not None): filters.append(Trade.pair == pair) - mix_tag_perf = Trade._session.execute( + mix_tag_perf = Trade.session.execute( select( Trade.id, Trade.enter_tag, @@ -1589,7 +1589,7 @@ class Trade(ModelBase, LocalTrade): NOTE: Not supported in Backtesting. :returns: Tuple containing (pair, profit_sum) """ - best_pair = Trade._session.execute( + best_pair = Trade.session.execute( select( Trade.pair, func.sum(Trade.close_profit).label('profit_sum') diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 2d29944f8..f86362841 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -340,7 +340,7 @@ class RPC: for day in range(0, timescale): profitday = start_date - time_offset(day) # Only query for necessary columns for performance reasons. - trades = Trade._session.execute( + trades = Trade.session.execute( select(Trade.close_profit_abs) .filter(Trade.is_open.is_(False), Trade.close_date >= profitday, @@ -384,18 +384,18 @@ class RPC: """ Returns the X last trades """ order_by: Any = Trade.id if order_by_id else Trade.close_date.desc() if limit: - trades = Trade._session.execute( + trades = Trade.session.execute( Trade.get_trades_query([Trade.is_open.is_(False)]) .order_by(order_by) .limit(limit) .offset(offset)) else: - trades = Trade._session.execute( + trades = Trade.session.execute( Trade.get_trades_query([Trade.is_open.is_(False)]) .order_by(Trade.close_date.desc())) output = [trade.to_json() for trade in trades] - total_trades = Trade._session.scalar( + total_trades = Trade.session.scalar( select(func.count(Trade.id)).filter(Trade.is_open.is_(False))) return { @@ -444,7 +444,7 @@ class RPC: """ Returns cumulative profit statistics """ trade_filter = ((Trade.is_open.is_(False) & (Trade.close_date >= start_date)) | Trade.is_open.is_(True)) - trades: Sequence[Trade] = Trade._session.scalars(Trade.get_trades_query( + trades: Sequence[Trade] = Trade.session.scalars(Trade.get_trades_query( trade_filter, include_orders=False).order_by(Trade.id)).all() profit_all_coin = [] From 8865af9104f570401ae1bf1e7b0ca7afe7bdf5aa Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 15 Mar 2023 21:19:36 +0100 Subject: [PATCH 239/360] Remove .query from pairlock --- freqtrade/persistence/pairlock.py | 10 ++++------ freqtrade/persistence/pairlock_middleware.py | 6 ++++-- freqtrade/rpc/rpc.py | 2 +- freqtrade/util/binance_mig.py | 3 ++- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/freqtrade/persistence/pairlock.py b/freqtrade/persistence/pairlock.py index 0cd595b66..76382b849 100644 --- a/freqtrade/persistence/pairlock.py +++ b/freqtrade/persistence/pairlock.py @@ -1,8 +1,8 @@ from datetime import datetime, timezone from typing import Any, ClassVar, Dict, Optional -from sqlalchemy import String, or_ -from sqlalchemy.orm import Mapped, Query, QueryPropertyDescriptor, mapped_column +from sqlalchemy import ScalarResult, String, or_, select +from sqlalchemy.orm import Mapped, QueryPropertyDescriptor, mapped_column from freqtrade.constants import DATETIME_PRINT_FORMAT from freqtrade.persistence.base import ModelBase, SessionType @@ -37,7 +37,7 @@ class PairLock(ModelBase): f'lock_end_time={lock_end_time}, reason={self.reason}, active={self.active})') @staticmethod - def query_pair_locks(pair: Optional[str], now: datetime, side: str = '*') -> Query: + def query_pair_locks(pair: Optional[str], now: datetime, side: str = '*') -> ScalarResult['PairLock']: """ Get all currently active locks for this pair :param pair: Pair to check for. Returns all current locks if pair is empty @@ -53,9 +53,7 @@ class PairLock(ModelBase): else: filters.append(PairLock.side == '*') - return PairLock.query.filter( - *filters - ) + return PairLock.session.scalars(select(PairLock).filter(*filters)) def to_json(self) -> Dict[str, Any]: return { diff --git a/freqtrade/persistence/pairlock_middleware.py b/freqtrade/persistence/pairlock_middleware.py index e6860bbe6..a8950961e 100644 --- a/freqtrade/persistence/pairlock_middleware.py +++ b/freqtrade/persistence/pairlock_middleware.py @@ -2,6 +2,8 @@ import logging from datetime import datetime, timezone from typing import List, Optional +from sqlalchemy import select + from freqtrade.exchange import timeframe_to_next_date from freqtrade.persistence.models import PairLock @@ -126,7 +128,7 @@ class PairLocks(): PairLock.active.is_(True), PairLock.reason == reason ] - locks = PairLock.query.filter(*filters) + locks = PairLock.session.scalars(select(PairLock).filter(*filters)).all() for lock in locks: logger.info(f"Releasing lock for {lock.pair} with reason '{reason}'.") lock.active = False @@ -170,6 +172,6 @@ class PairLocks(): Return all locks, also locks with expired end date """ if PairLocks.use_db: - return PairLock.query.all() + return PairLock.session.scalars(select(PairLock)).all() else: return PairLocks.locks diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index f86362841..6915c2025 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -959,7 +959,7 @@ class RPC: if pair: locks = PairLocks.get_pair_locks(pair) if lockid: - locks = PairLock.query.filter(PairLock.id == lockid).all() + locks = PairLock.session.scalar(select(PairLock).filter(PairLock.id == lockid)).all() for lock in locks: lock.active = False diff --git a/freqtrade/util/binance_mig.py b/freqtrade/util/binance_mig.py index 708bb1db7..37a2d2ef1 100644 --- a/freqtrade/util/binance_mig.py +++ b/freqtrade/util/binance_mig.py @@ -1,6 +1,7 @@ import logging from packaging import version +from sqlalchemy import select from freqtrade.constants import Config from freqtrade.enums.tradingmode import TradingMode @@ -44,7 +45,7 @@ def _migrate_binance_futures_db(config: Config): # Should symbol be migrated too? # order.symbol = new_pair Trade.commit() - pls = PairLock.query.filter(PairLock.pair.notlike('%:%')) + pls = PairLock.session.scalars(select(PairLock).filter(PairLock.pair.notlike('%:%'))).all() for pl in pls: pl.pair = f"{pl.pair}:{config['stake_currency']}" # print(pls) From ae361e1d5d82adc78578f99c716e63f95a642c9d Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 16 Mar 2023 06:44:53 +0100 Subject: [PATCH 240/360] Update more .query usages --- freqtrade/persistence/pairlock.py | 3 ++- freqtrade/persistence/pairlock_middleware.py | 8 ++++---- freqtrade/persistence/trade_model.py | 8 ++++---- freqtrade/rpc/rpc.py | 4 ++-- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/freqtrade/persistence/pairlock.py b/freqtrade/persistence/pairlock.py index 76382b849..af56a392c 100644 --- a/freqtrade/persistence/pairlock.py +++ b/freqtrade/persistence/pairlock.py @@ -37,7 +37,8 @@ class PairLock(ModelBase): f'lock_end_time={lock_end_time}, reason={self.reason}, active={self.active})') @staticmethod - def query_pair_locks(pair: Optional[str], now: datetime, side: str = '*') -> ScalarResult['PairLock']: + def query_pair_locks( + pair: Optional[str], now: datetime, side: str = '*') -> ScalarResult['PairLock']: """ Get all currently active locks for this pair :param pair: Pair to check for. Returns all current locks if pair is empty diff --git a/freqtrade/persistence/pairlock_middleware.py b/freqtrade/persistence/pairlock_middleware.py index a8950961e..0a3d78c4a 100644 --- a/freqtrade/persistence/pairlock_middleware.py +++ b/freqtrade/persistence/pairlock_middleware.py @@ -1,6 +1,6 @@ import logging from datetime import datetime, timezone -from typing import List, Optional +from typing import List, Optional, Sequence from sqlalchemy import select @@ -60,8 +60,8 @@ class PairLocks(): return lock @staticmethod - def get_pair_locks( - pair: Optional[str], now: Optional[datetime] = None, side: str = '*') -> List[PairLock]: + def get_pair_locks(pair: Optional[str], now: Optional[datetime] = None, + side: str = '*') -> Sequence[PairLock]: """ Get all currently active locks for this pair :param pair: Pair to check for. Returns all current locks if pair is empty @@ -167,7 +167,7 @@ class PairLocks(): ) @staticmethod - def get_all_locks() -> List[PairLock]: + def get_all_locks() -> Sequence[PairLock]: """ Return all locks, also locks with expired end date """ diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 0cc7ad4ff..101638828 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -5,7 +5,7 @@ import logging from collections import defaultdict from datetime import datetime, timedelta, timezone from math import isclose -from typing import Any, ClassVar, Dict, List, Optional, cast +from typing import Any, ClassVar, Dict, List, Optional, Sequence, cast from sqlalchemy import (Enum, Float, ForeignKey, Integer, ScalarResult, Select, String, UniqueConstraint, desc, func, select) @@ -263,12 +263,12 @@ class Order(ModelBase): return o @staticmethod - def get_open_orders() -> List['Order']: + def get_open_orders() -> Sequence['Order']: """ Retrieve open orders from the database :return: List of open orders """ - return Order.query.filter(Order.ft_is_open.is_(True)).all() + return Order.session.scalars(select(Order).filter(Order.ft_is_open.is_(True))).all() @staticmethod def order_by_id(order_id: str) -> Optional['Order']: @@ -276,7 +276,7 @@ class Order(ModelBase): Retrieve order based on order_id :return: Order or None """ - return Order.query.filter(Order.order_id == order_id).first() + return Order.session.scalars(select(Order).filter(Order.order_id == order_id)).first() class LocalTrade(): diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 6915c2025..cf1669231 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -954,12 +954,12 @@ class RPC: def _rpc_delete_lock(self, lockid: Optional[int] = None, pair: Optional[str] = None) -> Dict[str, Any]: """ Delete specific lock(s) """ - locks = [] + locks: Sequence[PairLock] = [] if pair: locks = PairLocks.get_pair_locks(pair) if lockid: - locks = PairLock.session.scalar(select(PairLock).filter(PairLock.id == lockid)).all() + locks = PairLock.session.scalars(select(PairLock).filter(PairLock.id == lockid)).all() for lock in locks: lock.active = False From e579ff9532dbcd0c98c462c3dff3c394e8a9fbd4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 16 Mar 2023 06:48:12 +0100 Subject: [PATCH 241/360] Simplify pairlock querying --- freqtrade/commands/db_commands.py | 2 +- freqtrade/persistence/pairlock.py | 4 ++++ freqtrade/persistence/pairlock_middleware.py | 2 +- freqtrade/persistence/trade_model.py | 13 +++++++------ 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/freqtrade/commands/db_commands.py b/freqtrade/commands/db_commands.py index b4997582d..c819ca243 100644 --- a/freqtrade/commands/db_commands.py +++ b/freqtrade/commands/db_commands.py @@ -36,7 +36,7 @@ def start_convert_db(args: Dict[str, Any]) -> None: session_target.commit() - for pairlock in PairLock.query: + for pairlock in PairLock.get_all_locks(): pairlock_count += 1 make_transient(pairlock) session_target.add(pairlock) diff --git a/freqtrade/persistence/pairlock.py b/freqtrade/persistence/pairlock.py index af56a392c..e787b5fa0 100644 --- a/freqtrade/persistence/pairlock.py +++ b/freqtrade/persistence/pairlock.py @@ -56,6 +56,10 @@ class PairLock(ModelBase): return PairLock.session.scalars(select(PairLock).filter(*filters)) + @staticmethod + def get_all_locks() -> ScalarResult['PairLock']: + return PairLock.session.scalars(select(PairLock)) + def to_json(self) -> Dict[str, Any]: return { 'id': self.id, diff --git a/freqtrade/persistence/pairlock_middleware.py b/freqtrade/persistence/pairlock_middleware.py index 0a3d78c4a..29169a50d 100644 --- a/freqtrade/persistence/pairlock_middleware.py +++ b/freqtrade/persistence/pairlock_middleware.py @@ -172,6 +172,6 @@ class PairLocks(): Return all locks, also locks with expired end date """ if PairLocks.use_db: - return PairLock.session.scalars(select(PairLock)).all() + return PairLock.get_all_locks().all() else: return PairLocks.locks diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 101638828..892707810 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -1607,12 +1607,13 @@ class Trade(ModelBase, LocalTrade): NOTE: Not supported in Backtesting. :returns: Tuple containing (pair, profit_sum) """ - trading_volume = Order.query.with_entities( - func.sum(Order.cost).label('volume') - ).filter( - Order.order_filled_date >= start_date, - Order.status == 'closed' - ).scalar() + trading_volume = Trade.session.execute( + select( + func.sum(Order.cost).label('volume') + ).filter( + Order.order_filled_date >= start_date, + Order.status == 'closed' + )).scalar_one() return trading_volume @staticmethod From 6ed337faa38bd1a3c83858f438af3aa1060a446e Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 16 Mar 2023 07:04:15 +0100 Subject: [PATCH 242/360] Update several tests to remove .query --- freqtrade/rpc/rpc.py | 4 ++-- tests/conftest.py | 10 +++++----- tests/persistence/test_migrations.py | 6 +++--- tests/persistence/test_persistence.py | 12 ++++++------ tests/rpc/test_rpc_apiserver.py | 12 ++++++------ 5 files changed, 22 insertions(+), 22 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index cf1669231..eb184c6d6 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -384,13 +384,13 @@ class RPC: """ Returns the X last trades """ order_by: Any = Trade.id if order_by_id else Trade.close_date.desc() if limit: - trades = Trade.session.execute( + trades = Trade.session.scalars( Trade.get_trades_query([Trade.is_open.is_(False)]) .order_by(order_by) .limit(limit) .offset(offset)) else: - trades = Trade.session.execute( + trades = Trade.session.scalars( Trade.get_trades_query([Trade.is_open.is_(False)]) .order_by(Trade.close_date.desc())) diff --git a/tests/conftest.py b/tests/conftest.py index 3c10de4ec..0aa6e70a8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -299,7 +299,7 @@ def create_mock_trades(fee, is_short: Optional[bool] = False, use_db: bool = Tru """ def add_trade(trade): if use_db: - Trade.query.session.add(trade) + Trade.session.add(trade) else: LocalTrade.add_bt_trade(trade) is_short1 = is_short if is_short is not None else True @@ -332,11 +332,11 @@ def create_mock_trades_with_leverage(fee, use_db: bool = True): Create some fake trades ... """ if use_db: - Trade.query.session.rollback() + Trade.session.rollback() def add_trade(trade): if use_db: - Trade.query.session.add(trade) + Trade.session.add(trade) else: LocalTrade.add_bt_trade(trade) @@ -366,7 +366,7 @@ def create_mock_trades_with_leverage(fee, use_db: bool = True): add_trade(trade) if use_db: - Trade.query.session.flush() + Trade.session.flush() def create_mock_trades_usdt(fee, is_short: Optional[bool] = False, use_db: bool = True): @@ -375,7 +375,7 @@ def create_mock_trades_usdt(fee, is_short: Optional[bool] = False, use_db: bool """ def add_trade(trade): if use_db: - Trade.query.session.add(trade) + Trade.session.add(trade) else: LocalTrade.add_bt_trade(trade) diff --git a/tests/persistence/test_migrations.py b/tests/persistence/test_migrations.py index 2a6959d58..5254164c1 100644 --- a/tests/persistence/test_migrations.py +++ b/tests/persistence/test_migrations.py @@ -21,8 +21,8 @@ spot, margin, futures = TradingMode.SPOT, TradingMode.MARGIN, TradingMode.FUTURE def test_init_create_session(default_conf): # Check if init create a session init_db(default_conf['db_url']) - assert hasattr(Trade, '_session') - assert 'scoped_session' in type(Trade._session).__name__ + assert hasattr(Trade, 'session') + assert 'scoped_session' in type(Trade.session).__name__ def test_init_custom_db_url(default_conf, tmpdir): @@ -34,7 +34,7 @@ def test_init_custom_db_url(default_conf, tmpdir): init_db(default_conf['db_url']) assert Path(filename).is_file() - r = Trade._session.execute(text("PRAGMA journal_mode")) + r = Trade.session.execute(text("PRAGMA journal_mode")) assert r.first() == ('wal',) diff --git a/tests/persistence/test_persistence.py b/tests/persistence/test_persistence.py index cd4890db9..24ab75a2d 100644 --- a/tests/persistence/test_persistence.py +++ b/tests/persistence/test_persistence.py @@ -1494,7 +1494,7 @@ def test_stoploss_reinitialization(default_conf, fee): assert trade.stop_loss_pct == -0.05 assert trade.initial_stop_loss == 0.95 assert trade.initial_stop_loss_pct == -0.05 - Trade.query.session.add(trade) + Trade.session.add(trade) Trade.commit() # Lower stoploss @@ -1556,7 +1556,7 @@ def test_stoploss_reinitialization_leverage(default_conf, fee): assert trade.stop_loss_pct == -0.1 assert trade.initial_stop_loss == 0.98 assert trade.initial_stop_loss_pct == -0.1 - Trade.query.session.add(trade) + Trade.session.add(trade) Trade.commit() # Lower stoploss @@ -1618,7 +1618,7 @@ def test_stoploss_reinitialization_short(default_conf, fee): assert trade.stop_loss_pct == -0.1 assert trade.initial_stop_loss == 1.02 assert trade.initial_stop_loss_pct == -0.1 - Trade.query.session.add(trade) + Trade.session.add(trade) Trade.commit() # Lower stoploss Trade.stoploss_reinitialization(-0.15) @@ -2443,8 +2443,8 @@ def test_order_to_ccxt(limit_buy_order_open): order = Order.parse_from_ccxt_object(limit_buy_order_open, 'mocked', 'buy') order.ft_trade_id = 1 - order.query.session.add(order) - Order.query.session.commit() + order.session.add(order) + Order.session.commit() order_resp = Order.order_by_id(limit_buy_order_open['id']) assert order_resp @@ -2546,7 +2546,7 @@ def test_recalc_trade_from_orders_dca(data) -> None: leverage=1.0, trading_mode=TradingMode.SPOT ) - Trade.query.session.add(trade) + Trade.session.add(trade) for idx, (order, result) in enumerate(data['orders']): amount = order[1] diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 9c2c3ee3a..5211946ef 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -652,7 +652,7 @@ def test_api_trade_single(botclient, mocker, fee, ticker, markets, is_short): assert_response(rc, 404) assert rc.json()['detail'] == 'Trade not found.' - Trade.query.session.rollback() + Trade.rollback() create_mock_trades(fee, is_short=is_short) rc = client_get(client, f"{BASE_URI}/trade/3") @@ -943,7 +943,7 @@ def test_api_performance(botclient, fee): ) trade.close_profit = trade.calc_profit_ratio(trade.close_rate) trade.close_profit_abs = trade.calc_profit(trade.close_rate) - Trade.query.session.add(trade) + Trade.session.add(trade) trade = Trade( pair='XRP/ETH', @@ -960,7 +960,7 @@ def test_api_performance(botclient, fee): trade.close_profit = trade.calc_profit_ratio(trade.close_rate) trade.close_profit_abs = trade.calc_profit(trade.close_rate) - Trade.query.session.add(trade) + Trade.session.add(trade) Trade.commit() rc = client_get(client, f"{BASE_URI}/performance") @@ -1290,7 +1290,7 @@ def test_api_forceexit(botclient, mocker, ticker, fee, markets): data={"tradeid": "1"}) assert_response(rc, 502) assert rc.json() == {"error": "Error querying /api/v1/forceexit: invalid argument"} - Trade.query.session.rollback() + Trade.rollback() create_mock_trades(fee) trade = Trade.get_trades([Trade.id == 5]).first() @@ -1299,7 +1299,7 @@ def test_api_forceexit(botclient, mocker, ticker, fee, markets): data={"tradeid": "5", "ordertype": "market", "amount": 23}) assert_response(rc) assert rc.json() == {'result': 'Created sell order for trade 5.'} - Trade.query.session.rollback() + Trade.rollback() trade = Trade.get_trades([Trade.id == 5]).first() assert pytest.approx(trade.amount) == 100 @@ -1309,7 +1309,7 @@ def test_api_forceexit(botclient, mocker, ticker, fee, markets): data={"tradeid": "5"}) assert_response(rc) assert rc.json() == {'result': 'Created sell order for trade 5.'} - Trade.query.session.rollback() + Trade.rollback() trade = Trade.get_trades([Trade.id == 5]).first() assert trade.is_open is False From 9d6e973e5b91a8c816557f2ad21ca883292527b0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 16 Mar 2023 07:25:04 +0100 Subject: [PATCH 243/360] remove .query from most tests --- tests/persistence/test_migrations.py | 9 +- tests/persistence/test_persistence.py | 7 +- tests/plugins/test_pairlocks.py | 6 +- tests/plugins/test_protections.py | 2 +- tests/rpc/test_rpc.py | 9 +- tests/rpc/test_rpc_apiserver.py | 11 +- tests/rpc/test_rpc_telegram.py | 7 +- tests/test_freqtradebot.py | 220 +++++++++++++------------- tests/test_integration.py | 7 +- 9 files changed, 144 insertions(+), 134 deletions(-) diff --git a/tests/persistence/test_migrations.py b/tests/persistence/test_migrations.py index 5254164c1..053d6da12 100644 --- a/tests/persistence/test_migrations.py +++ b/tests/persistence/test_migrations.py @@ -4,7 +4,7 @@ from pathlib import Path from unittest.mock import MagicMock import pytest -from sqlalchemy import create_engine, text +from sqlalchemy import create_engine, select, text from freqtrade.constants import DEFAULT_DB_PROD_URL from freqtrade.enums import TradingMode @@ -235,8 +235,9 @@ def test_migrate_new(mocker, default_conf, fee, caplog): # Run init to test migration init_db(default_conf['db_url']) - assert len(Trade.query.filter(Trade.id == 1).all()) == 1 - trade = Trade.query.filter(Trade.id == 1).first() + trades = Trade.session.scalars(select(Trade).filter(Trade.id == 1)).all() + assert len(trades) == 1 + trade = trades[0] assert trade.fee_open == fee.return_value assert trade.fee_close == fee.return_value assert trade.open_rate_requested is None @@ -404,7 +405,7 @@ def test_migrate_pairlocks(mocker, default_conf, fee, caplog): init_db(default_conf['db_url']) - assert len(PairLock.query.all()) == 2 + assert len(PairLock.get_all_locks().all()) == 2 assert len(PairLock.query.filter(PairLock.pair == '*').all()) == 1 pairlocks = PairLock.query.filter(PairLock.pair == 'ETH/BTC').all() assert len(pairlocks) == 1 diff --git a/tests/persistence/test_persistence.py b/tests/persistence/test_persistence.py index 24ab75a2d..78e97e2e6 100644 --- a/tests/persistence/test_persistence.py +++ b/tests/persistence/test_persistence.py @@ -4,6 +4,7 @@ from types import FunctionType import arrow import pytest +from sqlalchemy import select from freqtrade.constants import DATETIME_PRINT_FORMAT from freqtrade.enums import TradingMode @@ -2575,11 +2576,11 @@ def test_recalc_trade_from_orders_dca(data) -> None: trade.recalc_trade_from_orders() Trade.commit() - orders1 = Order.query.all() + orders1 = Order.session.scalars(select(Order)).all() assert orders1 assert len(orders1) == idx + 1 - trade = Trade.query.first() + trade = Trade.session.scalars(select(Trade)).first() assert trade assert len(trade.orders) == idx + 1 if idx < len(data) - 1: @@ -2596,6 +2597,6 @@ def test_recalc_trade_from_orders_dca(data) -> None: assert pytest.approx(trade.close_profit_abs) == data['end_profit'] assert pytest.approx(trade.close_profit) == data['end_profit_ratio'] assert not trade.is_open - trade = Trade.query.first() + trade = Trade.session.scalars(select(Trade)).first() assert trade assert trade.open_order_id is None diff --git a/tests/plugins/test_pairlocks.py b/tests/plugins/test_pairlocks.py index 0ba9bb746..39bde3cda 100644 --- a/tests/plugins/test_pairlocks.py +++ b/tests/plugins/test_pairlocks.py @@ -14,7 +14,7 @@ def test_PairLocks(use_db): PairLocks.use_db = use_db # No lock should be present if use_db: - assert len(PairLock.query.all()) == 0 + assert len(PairLock.get_all_locks().all()) == 0 assert PairLocks.use_db == use_db @@ -88,13 +88,13 @@ def test_PairLocks(use_db): if use_db: locks = PairLocks.get_all_locks() - locks_db = PairLock.query.all() + locks_db = PairLock.get_all_locks().all() assert len(locks) == len(locks_db) assert len(locks_db) > 0 else: # Nothing was pushed to the database assert len(PairLocks.get_all_locks()) > 0 - assert len(PairLock.query.all()) == 0 + assert len(PairLock.get_all_locks().all()) == 0 # Reset use-db variable PairLocks.reset_locks() PairLocks.use_db = True diff --git a/tests/plugins/test_protections.py b/tests/plugins/test_protections.py index 2bbdf3d4f..5e6128c73 100644 --- a/tests/plugins/test_protections.py +++ b/tests/plugins/test_protections.py @@ -74,7 +74,7 @@ def generate_mock_trade(pair: str, fee: float, is_open: bool, trade.close(close_price) trade.exit_reason = exit_reason - Trade.query.session.add(trade) + Trade.session.add(trade) Trade.commit() return trade diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 1a1802c68..7d829bdb6 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -4,6 +4,7 @@ from unittest.mock import ANY, MagicMock, PropertyMock import pytest from numpy import isnan +from sqlalchemy import select from freqtrade.edge import PairInfo from freqtrade.enums import SignalDirection, State, TradingMode @@ -354,7 +355,7 @@ def test_rpc_delete_trade(mocker, default_conf, fee, markets, caplog, is_short): with pytest.raises(RPCException, match='invalid argument'): rpc._rpc_delete('200') - trades = Trade.query.all() + trades = Trade.session.scalars(select(Trade)).all() trades[1].stoploss_order_id = '1234' trades[2].stoploss_order_id = '1234' assert len(trades) > 2 @@ -717,7 +718,7 @@ def test_rpc_force_exit(default_conf, ticker, fee, mocker) -> None: mocker.patch(f'{EXMS}._dry_is_price_crossed', MagicMock(return_value=False)) freqtradebot.enter_positions() # make an limit-buy open trade - trade = Trade.query.filter(Trade.id == '3').first() + trade = Trade.session.scalars(select(Trade).filter(Trade.id == '3')).first() filled_amount = trade.amount / 2 # Fetch order - it's open first, and closed after cancel_order is called. mocker.patch( @@ -753,7 +754,7 @@ def test_rpc_force_exit(default_conf, ticker, fee, mocker) -> None: freqtradebot.config['max_open_trades'] = 3 freqtradebot.enter_positions() - trade = Trade.query.filter(Trade.id == '2').first() + trade = Trade.session.scalars(select(Trade).filter(Trade.id == '2')).first() amount = trade.amount # make an limit-buy open trade, if there is no 'filled', don't sell it mocker.patch( @@ -771,7 +772,7 @@ def test_rpc_force_exit(default_conf, ticker, fee, mocker) -> None: assert cancel_order_mock.call_count == 2 assert trade.amount == amount - trade = Trade.query.filter(Trade.id == '3').first() + trade = Trade.session.scalars(select(Trade).filter(Trade.id == '3')).first() # make an limit-sell open trade mocker.patch( diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 5211946ef..97319b78b 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -14,6 +14,7 @@ from fastapi import FastAPI, WebSocketDisconnect from fastapi.exceptions import HTTPException from fastapi.testclient import TestClient from requests.auth import _basic_auth_str +from sqlalchemy import select from freqtrade.__init__ import __version__ from freqtrade.enums import CandleType, RunMode, State, TradingMode @@ -624,7 +625,7 @@ def test_api_trades(botclient, mocker, fee, markets, is_short): assert rc.json()['offset'] == 0 create_mock_trades(fee, is_short=is_short) - Trade.query.session.flush() + Trade.session.flush() rc = client_get(client, f"{BASE_URI}/trades") assert_response(rc) @@ -677,7 +678,7 @@ def test_api_delete_trade(botclient, mocker, fee, markets, is_short): create_mock_trades(fee, is_short=is_short) ftbot.strategy.order_types['stoploss_on_exchange'] = True - trades = Trade.query.all() + trades = Trade.session.scalars(select(Trade)).all() trades[1].stoploss_order_id = '1234' Trade.commit() assert len(trades) > 2 @@ -685,7 +686,7 @@ def test_api_delete_trade(botclient, mocker, fee, markets, is_short): rc = client_delete(client, f"{BASE_URI}/trades/1") assert_response(rc) assert rc.json()['result_msg'] == 'Deleted trade 1. Closed 1 open orders.' - assert len(trades) - 1 == len(Trade.query.all()) + assert len(trades) - 1 == len(Trade.session.scalars(select(Trade)).all()) assert cancel_mock.call_count == 1 cancel_mock.reset_mock() @@ -694,11 +695,11 @@ def test_api_delete_trade(botclient, mocker, fee, markets, is_short): assert_response(rc, 502) assert cancel_mock.call_count == 0 - assert len(trades) - 1 == len(Trade.query.all()) + assert len(trades) - 1 == len(Trade.session.scalars(select(Trade)).all()) rc = client_delete(client, f"{BASE_URI}/trades/2") assert_response(rc) assert rc.json()['result_msg'] == 'Deleted trade 2. Closed 2 open orders.' - assert len(trades) - 2 == len(Trade.query.all()) + assert len(trades) - 2 == len(Trade.session.scalars(select(Trade)).all()) assert stoploss_mock.call_count == 1 rc = client_delete(client, f"{BASE_URI}/trades/502") diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 59b6df489..b1859f581 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -14,6 +14,7 @@ import arrow import pytest import time_machine from pandas import DataFrame +from sqlalchemy import select from telegram import Chat, Message, ReplyKeyboardMarkup, Update from telegram.error import BadRequest, NetworkError, TelegramError @@ -692,7 +693,7 @@ def test_profit_handle(default_conf_usdt, update, ticker_usdt, ticker_sell_up, f # Create some test data freqtradebot.enter_positions() - trade = Trade.query.first() + trade = Trade.session.scalars(select(Trade)).first() context = MagicMock() # Test with invalid 2nd argument (should silently pass) @@ -945,7 +946,7 @@ def test_telegram_forceexit_handle(default_conf, update, ticker, fee, # Create some test data freqtradebot.enter_positions() - trade = Trade.query.first() + trade = Trade.session.scalars(select(Trade)).first() assert trade # Increase the price and sell it @@ -1020,7 +1021,7 @@ def test_telegram_force_exit_down_handle(default_conf, update, ticker, fee, fetch_ticker=ticker_sell_down ) - trade = Trade.query.first() + trade = Trade.session.scalars(select(Trade)).first() assert trade # /forceexit 1 diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 349655243..49d00a860 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -10,6 +10,7 @@ from unittest.mock import ANY, MagicMock, PropertyMock, patch import arrow import pytest from pandas import DataFrame +from sqlalchemy import select from freqtrade.constants import CANCEL_REASON, UNLIMITED_STAKE_AMOUNT from freqtrade.enums import (CandleType, ExitCheckTuple, ExitType, RPCMessageType, RunMode, @@ -247,7 +248,7 @@ def test_edge_overrides_stoploss(limit_order, fee, caplog, mocker, patch_get_signal(freqtrade) freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) freqtrade.enter_positions() - trade = Trade.query.first() + trade = Trade.session.scalars(select(Trade)).first() caplog.clear() ############################################# ticker_val.update({ @@ -278,7 +279,7 @@ def test_total_open_trades_stakes(mocker, default_conf_usdt, ticker_usdt, fee) - freqtrade = FreqtradeBot(default_conf_usdt) patch_get_signal(freqtrade) freqtrade.enter_positions() - trade = Trade.query.first() + trade = Trade.session.scalars(select(Trade)).first() assert trade is not None assert trade.stake_amount == 60.0 @@ -286,7 +287,7 @@ def test_total_open_trades_stakes(mocker, default_conf_usdt, ticker_usdt, fee) - assert trade.open_date is not None freqtrade.enter_positions() - trade = Trade.query.order_by(Trade.id.desc()).first() + trade = Trade.session.scalars(select(Trade).order_by(Trade.id.desc())).first() assert trade is not None assert trade.stake_amount == 60.0 @@ -317,7 +318,7 @@ def test_create_trade(default_conf_usdt, ticker_usdt, limit_order, patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) freqtrade.create_trade('ETH/USDT') - trade = Trade.query.first() + trade = Trade.session.scalars(select(Trade)).first() trade.is_short = is_short assert trade is not None assert pytest.approx(trade.stake_amount) == 60.0 @@ -568,12 +569,12 @@ def test_process_trade_creation(default_conf_usdt, ticker_usdt, limit_order, lim freqtrade = FreqtradeBot(default_conf_usdt) patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) - trades = Trade.query.filter(Trade.is_open.is_(True)).all() + trades = Trade.get_open_trades() assert not trades freqtrade.process() - trades = Trade.query.filter(Trade.is_open.is_(True)).all() + trades = Trade.get_open_trades() assert len(trades) == 1 trade = trades[0] assert trade is not None @@ -640,11 +641,11 @@ def test_process_trade_handling(default_conf_usdt, ticker_usdt, limit_buy_order_ freqtrade = FreqtradeBot(default_conf_usdt) patch_get_signal(freqtrade) - trades = Trade.query.filter(Trade.is_open.is_(True)).all() + trades = Trade.get_open_trades() assert not trades freqtrade.process() - trades = Trade.query.filter(Trade.is_open.is_(True)).all() + trades = Trade.get_open_trades() assert len(trades) == 1 # Nothing happened ... @@ -671,7 +672,7 @@ def test_process_trade_no_whitelist_pair(default_conf_usdt, ticker_usdt, limit_b assert pair not in default_conf_usdt['exchange']['pair_whitelist'] # create open trade not in whitelist - Trade.query.session.add(Trade( + Trade.session.add(Trade( pair=pair, stake_amount=0.001, fee_open=fee.return_value, @@ -681,7 +682,7 @@ def test_process_trade_no_whitelist_pair(default_conf_usdt, ticker_usdt, limit_b open_rate=0.01, exchange='binance', )) - Trade.query.session.add(Trade( + Trade.session.add(Trade( pair='ETH/USDT', stake_amount=0.001, fee_open=fee.return_value, @@ -838,7 +839,7 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order, # Should create an open trade with an open order id # As the order is not fulfilled yet - trade = Trade.query.first() + trade = Trade.session.scalars(select(Trade)).first() trade.is_short = is_short assert trade assert trade.is_open is True @@ -865,7 +866,7 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order, mocker.patch(f'{EXMS}.create_order', MagicMock(return_value=order)) assert freqtrade.execute_entry(pair, stake_amount, is_short=is_short) - trade = Trade.query.all()[2] + trade = Trade.session.scalars(select(Trade)).all()[2] trade.is_short = is_short assert trade assert trade.open_order_id is None @@ -883,7 +884,7 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order, order['id'] = '555' mocker.patch(f'{EXMS}.create_order', MagicMock(return_value=order)) assert freqtrade.execute_entry(pair, stake_amount) - trade = Trade.query.all()[3] + trade = Trade.session.scalars(select(Trade)).all()[3] trade.is_short = is_short assert trade assert trade.open_order_id is None @@ -896,7 +897,7 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order, freqtrade.strategy.custom_stake_amount = lambda **kwargs: 150.0 assert freqtrade.execute_entry(pair, stake_amount, is_short=is_short) - trade = Trade.query.all()[4] + trade = Trade.session.scalars(select(Trade)).all()[4] trade.is_short = is_short assert trade assert pytest.approx(trade.stake_amount) == 150 @@ -905,7 +906,7 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order, order['id'] = '557' freqtrade.strategy.custom_stake_amount = lambda **kwargs: 20 / 0 assert freqtrade.execute_entry(pair, stake_amount, is_short=is_short) - trade = Trade.query.all()[5] + trade = Trade.session.scalars(select(Trade)).all()[5] trade.is_short = is_short assert trade assert pytest.approx(trade.stake_amount) == 2.0 @@ -934,7 +935,7 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order, order['id'] = '5566' freqtrade.strategy.custom_entry_price = lambda **kwargs: 0.508 assert freqtrade.execute_entry(pair, stake_amount, is_short=is_short) - trade = Trade.query.all()[6] + trade = Trade.session.scalars(select(Trade)).all()[6] trade.is_short = is_short assert trade assert trade.open_rate_requested == 0.508 @@ -951,7 +952,7 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order, ) assert freqtrade.execute_entry(pair, stake_amount, is_short=is_short) - trade = Trade.query.all()[7] + trade = Trade.session.scalars(select(Trade)).all()[7] trade.is_short = is_short assert trade assert trade.open_rate_requested == 10 @@ -961,7 +962,7 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order, order['id'] = '5568' freqtrade.strategy.custom_entry_price = lambda **kwargs: "string price" assert freqtrade.execute_entry(pair, stake_amount, is_short=is_short) - trade = Trade.query.all()[8] + trade = Trade.session.scalars(select(Trade)).all()[8] # Trade(id=9, pair=ETH/USDT, amount=0.20000000, is_short=False, # leverage=1.0, open_rate=10.00000000, open_since=...) # Trade(id=9, pair=ETH/USDT, amount=0.60000000, is_short=True, @@ -982,7 +983,7 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order, freqtrade.exchange.get_max_pair_stake_amount = MagicMock(return_value=500) assert freqtrade.execute_entry(pair, 2000, is_short=is_short) - trade = Trade.query.all()[9] + trade = Trade.session.scalars(select(Trade)).all()[9] trade.is_short = is_short assert pytest.approx(trade.stake_amount) == 500 @@ -991,7 +992,7 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order, freqtrade.strategy.leverage.reset_mock() assert freqtrade.execute_entry(pair, 200, leverage_=3) assert freqtrade.strategy.leverage.call_count == 0 - trade = Trade.query.all()[10] + trade = Trade.session.scalars(select(Trade)).all()[10] assert trade.leverage == 1 if trading_mode == 'spot' else 3 @@ -1053,7 +1054,7 @@ def test_execute_entry_min_leverage(mocker, default_conf_usdt, fee, limit_order, freqtrade.strategy.leverage = MagicMock(return_value=5.0) assert freqtrade.execute_entry(pair, stake_amount, is_short=is_short) - trade = Trade.query.first() + trade = Trade.session.scalars(select(Trade)).first() assert trade.leverage == 5.0 # assert trade.stake_amount == 2 @@ -1158,7 +1159,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_ # as a trade actually happened caplog.clear() freqtrade.enter_positions() - trade = Trade.query.first() + trade = Trade.session.scalars(select(Trade)).first() trade.is_short = is_short trade.is_open = True trade.open_order_id = None @@ -1271,7 +1272,7 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf_usdt, fee, caplog, patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) freqtrade.enter_positions() - trade = Trade.query.first() + trade = Trade.session.scalars(select(Trade)).first() trade.is_short = is_short trade.is_open = True trade.open_order_id = None @@ -1316,7 +1317,7 @@ def test_create_stoploss_order_invalid_order( freqtrade.strategy.order_types['stoploss_on_exchange'] = True freqtrade.enter_positions() - trade = Trade.query.first() + trade = Trade.session.scalars(select(Trade)).first() trade.is_short = is_short caplog.clear() freqtrade.create_stoploss_order(trade, 200) @@ -1367,7 +1368,7 @@ def test_create_stoploss_order_insufficient_funds( freqtrade.strategy.order_types['stoploss_on_exchange'] = True freqtrade.enter_positions() - trade = Trade.query.first() + trade = Trade.session.scalars(select(Trade)).first() trade.is_short = is_short caplog.clear() freqtrade.create_stoploss_order(trade, 200) @@ -1435,7 +1436,7 @@ def test_handle_stoploss_on_exchange_trailing( patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) freqtrade.enter_positions() - trade = Trade.query.first() + trade = Trade.session.scalars(select(Trade)).first() trade.is_short = is_short trade.is_open = True trade.open_order_id = None @@ -1554,7 +1555,7 @@ def test_handle_stoploss_on_exchange_trailing_error( freqtrade.strategy.order_types['stoploss_on_exchange_interval'] = 60 patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) freqtrade.enter_positions() - trade = Trade.query.first() + trade = Trade.session.scalars(select(Trade)).first() trade.is_short = is_short trade.is_open = True trade.open_order_id = None @@ -1668,7 +1669,7 @@ def test_handle_stoploss_on_exchange_custom_stop( patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) freqtrade.enter_positions() - trade = Trade.query.first() + trade = Trade.session.scalars(select(Trade)).first() trade.is_short = is_short trade.is_open = True trade.open_order_id = None @@ -1796,7 +1797,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, limit_orde freqtrade.active_pair_whitelist = freqtrade.edge.adjust(freqtrade.active_pair_whitelist) freqtrade.enter_positions() - trade = Trade.query.first() + trade = Trade.session.scalars(select(Trade)).first() trade.is_open = True trade.open_order_id = None trade.stoploss_order_id = 100 @@ -2162,7 +2163,7 @@ def test_handle_trade( freqtrade.enter_positions() - trade = Trade.query.first() + trade = Trade.session.scalars(select(Trade)).first() trade.is_short = is_short assert trade @@ -2217,7 +2218,7 @@ def test_handle_overlapping_signals( freqtrade.enter_positions() # Buy and Sell triggering, so doing nothing ... - trades = Trade.query.all() + trades = Trade.session.scalars(select(Trade)).all() nb_trades = len(trades) assert nb_trades == 0 @@ -2225,7 +2226,7 @@ def test_handle_overlapping_signals( # Buy is triggering, so buying ... patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) freqtrade.enter_positions() - trades = Trade.query.all() + trades = Trade.session.scalars(select(Trade)).all() for trade in trades: trade.is_short = is_short nb_trades = len(trades) @@ -2235,7 +2236,7 @@ def test_handle_overlapping_signals( # Buy and Sell are not triggering, so doing nothing ... patch_get_signal(freqtrade, enter_long=False) assert freqtrade.handle_trade(trades[0]) is False - trades = Trade.query.all() + trades = Trade.session.scalars(select(Trade)).all() for trade in trades: trade.is_short = is_short nb_trades = len(trades) @@ -2248,7 +2249,7 @@ def test_handle_overlapping_signals( else: patch_get_signal(freqtrade, enter_long=True, exit_long=True) assert freqtrade.handle_trade(trades[0]) is False - trades = Trade.query.all() + trades = Trade.session.scalars(select(Trade)).all() for trade in trades: trade.is_short = is_short nb_trades = len(trades) @@ -2260,7 +2261,7 @@ def test_handle_overlapping_signals( patch_get_signal(freqtrade, enter_long=False, exit_short=True) else: patch_get_signal(freqtrade, enter_long=False, exit_long=True) - trades = Trade.query.all() + trades = Trade.session.scalars(select(Trade)).all() for trade in trades: trade.is_short = is_short assert freqtrade.handle_trade(trades[0]) is True @@ -2291,7 +2292,7 @@ def test_handle_trade_roi(default_conf_usdt, ticker_usdt, limit_order_open, fee, freqtrade.enter_positions() - trade = Trade.query.first() + trade = Trade.session.scalars(select(Trade)).first() trade.is_short = is_short trade.is_open = True @@ -2333,7 +2334,7 @@ def test_handle_trade_use_exit_signal( freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) freqtrade.enter_positions() - trade = Trade.query.first() + trade = Trade.session.scalars(select(Trade)).first() trade.is_short = is_short trade.is_open = True @@ -2370,7 +2371,7 @@ def test_close_trade( # Create trade and sell it freqtrade.enter_positions() - trade = Trade.query.first() + trade = Trade.session.scalars(select(Trade)).first() trade.is_short = is_short assert trade @@ -2427,7 +2428,7 @@ def test_manage_open_orders_entry_usercustom( open_trade.is_short = is_short open_trade.orders[0].side = 'sell' if is_short else 'buy' open_trade.orders[0].ft_order_side = 'sell' if is_short else 'buy' - Trade.query.session.add(open_trade) + Trade.session.add(open_trade) Trade.commit() # Ensure default is to return empty (so not mocked yet) @@ -2438,7 +2439,8 @@ def test_manage_open_orders_entry_usercustom( freqtrade.strategy.check_entry_timeout = MagicMock(return_value=False) freqtrade.manage_open_orders() assert cancel_order_mock.call_count == 0 - trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all() + trades = Trade.session.scalars( + select(Trade).filter(Trade.open_order_id.is_(open_trade.open_order_id))).all() nb_trades = len(trades) assert nb_trades == 1 assert freqtrade.strategy.check_entry_timeout.call_count == 1 @@ -2446,7 +2448,8 @@ def test_manage_open_orders_entry_usercustom( freqtrade.manage_open_orders() assert cancel_order_mock.call_count == 0 - trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all() + trades = Trade.session.scalars( + select(Trade).filter(Trade.open_order_id.is_(open_trade.open_order_id))).all() nb_trades = len(trades) assert nb_trades == 1 assert freqtrade.strategy.check_entry_timeout.call_count == 1 @@ -2456,7 +2459,8 @@ def test_manage_open_orders_entry_usercustom( freqtrade.manage_open_orders() assert cancel_order_wr_mock.call_count == 1 assert rpc_mock.call_count == 2 - trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all() + trades = Trade.session.scalars( + select(Trade).filter(Trade.open_order_id.is_(open_trade.open_order_id))).all() nb_trades = len(trades) assert nb_trades == 0 assert freqtrade.strategy.check_entry_timeout.call_count == 1 @@ -2486,7 +2490,7 @@ def test_manage_open_orders_entry( freqtrade = FreqtradeBot(default_conf_usdt) open_trade.is_short = is_short - Trade.query.session.add(open_trade) + Trade.session.add(open_trade) Trade.commit() freqtrade.strategy.check_entry_timeout = MagicMock(return_value=False) @@ -2524,7 +2528,7 @@ def test_adjust_entry_cancel( ) open_trade.is_short = is_short - Trade.query.session.add(open_trade) + Trade.session.add(open_trade) Trade.commit() # Timeout to not interfere @@ -2535,7 +2539,7 @@ def test_adjust_entry_cancel( freqtrade.manage_open_orders() trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all() assert len(trades) == 0 - assert len(Order.query.all()) == 0 + assert len(Order.session.scalars(select(Order)).all()) == 0 assert log_has_re( f"{'Sell' if is_short else 'Buy'} order user requested order cancel*", caplog) assert log_has_re( @@ -2565,7 +2569,7 @@ def test_adjust_entry_maintain_replace( ) open_trade.is_short = is_short - Trade.query.session.add(open_trade) + Trade.session.add(open_trade) Trade.commit() # Timeout to not interfere @@ -2586,7 +2590,7 @@ def test_adjust_entry_maintain_replace( freqtrade.manage_open_orders() trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all() assert len(trades) == 1 - nb_all_orders = len(Order.query.all()) + nb_all_orders = len(Order.session.scalars(select(Order)).all()) assert nb_all_orders == 2 # New order seems to be in closed status? # nb_open_orders = len(Order.get_open_orders()) @@ -2618,7 +2622,7 @@ def test_check_handle_cancelled_buy( freqtrade = FreqtradeBot(default_conf_usdt) open_trade.orders = [] open_trade.is_short = is_short - Trade.query.session.add(open_trade) + Trade.session.add(open_trade) Trade.commit() # check it does cancel buy orders over the time limit @@ -2649,7 +2653,7 @@ def test_manage_open_orders_buy_exception( freqtrade = FreqtradeBot(default_conf_usdt) open_trade.is_short = is_short - Trade.query.session.add(open_trade) + Trade.session.add(open_trade) Trade.commit() # check it does cancel buy orders over the time limit @@ -2691,7 +2695,7 @@ def test_manage_open_orders_exit_usercustom( open_trade_usdt.close_date = arrow.utcnow().shift(minutes=-601).datetime open_trade_usdt.close_profit_abs = 0.001 - Trade.query.session.add(open_trade_usdt) + Trade.session.add(open_trade_usdt) Trade.commit() # Ensure default is false freqtrade.manage_open_orders() @@ -2771,7 +2775,7 @@ def test_manage_open_orders_exit( open_trade_usdt.close_profit_abs = 0.001 open_trade_usdt.is_short = is_short - Trade.query.session.add(open_trade_usdt) + Trade.session.add(open_trade_usdt) Trade.commit() freqtrade.strategy.check_exit_timeout = MagicMock(return_value=False) @@ -2811,7 +2815,7 @@ def test_check_handle_cancelled_exit( open_trade_usdt.close_date = arrow.utcnow().shift(minutes=-601).datetime open_trade_usdt.is_short = is_short - Trade.query.session.add(open_trade_usdt) + Trade.session.add(open_trade_usdt) Trade.commit() # check it does cancel sell orders over the time limit @@ -2848,7 +2852,7 @@ def test_manage_open_orders_partial( ) freqtrade = FreqtradeBot(default_conf_usdt) prior_stake = open_trade.stake_amount - Trade.query.session.add(open_trade) + Trade.session.add(open_trade) Trade.commit() # check it does cancel buy orders over the time limit @@ -2893,7 +2897,7 @@ def test_manage_open_orders_partial_fee( open_trade.fee_open = fee() open_trade.fee_close = fee() - Trade.query.session.add(open_trade) + Trade.session.add(open_trade) Trade.commit() # cancelling a half-filled order should update the amount to the bought amount # and apply fees if necessary. @@ -2943,7 +2947,7 @@ def test_manage_open_orders_partial_except( open_trade.fee_open = fee() open_trade.fee_close = fee() - Trade.query.session.add(open_trade) + Trade.session.add(open_trade) Trade.commit() # cancelling a half-filled order should update the amount to the bought amount # and apply fees if necessary. @@ -2982,7 +2986,7 @@ def test_manage_open_orders_exception(default_conf_usdt, ticker_usdt, open_trade ) freqtrade = FreqtradeBot(default_conf_usdt) - Trade.query.session.add(open_trade_usdt) + Trade.session.add(open_trade_usdt) Trade.commit() caplog.clear() @@ -3011,7 +3015,7 @@ def test_handle_cancel_enter(mocker, caplog, default_conf_usdt, limit_order, is_ freqtrade._notify_enter_cancel = MagicMock() trade = mock_trade_usdt_4(fee, is_short) - Trade.query.session.add(trade) + Trade.session.add(trade) Trade.commit() l_order['filled'] = 0.0 @@ -3061,7 +3065,7 @@ def test_handle_cancel_enter_exchanges(mocker, caplog, default_conf_usdt, is_sho reason = CANCEL_REASON['TIMEOUT'] trade = mock_trade_usdt_4(fee, is_short) - Trade.query.session.add(trade) + Trade.session.add(trade) Trade.commit() assert freqtrade.handle_cancel_enter(trade, limit_buy_order_canceled_empty, reason) assert cancel_order_mock.call_count == 0 @@ -3095,7 +3099,7 @@ def test_handle_cancel_enter_corder_empty(mocker, default_conf_usdt, limit_order freqtrade = FreqtradeBot(default_conf_usdt) freqtrade._notify_enter_cancel = MagicMock() trade = mock_trade_usdt_4(fee, is_short) - Trade.query.session.add(trade) + Trade.session.add(trade) Trade.commit() l_order['filled'] = 0.0 l_order['status'] = 'open' @@ -3261,7 +3265,7 @@ def test_execute_trade_exit_up(default_conf_usdt, ticker_usdt, fee, ticker_usdt_ freqtrade.enter_positions() rpc_mock.reset_mock() - trade = Trade.query.first() + trade = Trade.session.scalars(select(Trade)).first() assert trade.is_short == is_short assert trade assert freqtrade.strategy.confirm_trade_exit.call_count == 0 @@ -3342,7 +3346,7 @@ def test_execute_trade_exit_down(default_conf_usdt, ticker_usdt, fee, ticker_usd # Create some test data freqtrade.enter_positions() - trade = Trade.query.first() + trade = Trade.session.scalars(select(Trade)).first() trade.is_short = is_short assert trade @@ -3415,7 +3419,7 @@ def test_execute_trade_exit_custom_exit_price( freqtrade.enter_positions() rpc_mock.reset_mock() - trade = Trade.query.first() + trade = Trade.session.scalars(select(Trade)).first() trade.is_short = is_short assert trade assert freqtrade.strategy.confirm_trade_exit.call_count == 0 @@ -3492,7 +3496,7 @@ def test_execute_trade_exit_down_stoploss_on_exchange_dry_run( # Create some test data freqtrade.enter_positions() - trade = Trade.query.first() + trade = Trade.session.scalars(select(Trade)).first() assert trade.is_short == is_short assert trade @@ -3566,7 +3570,7 @@ def test_execute_trade_exit_sloe_cancel_exception( patch_get_signal(freqtrade) freqtrade.enter_positions() - trade = Trade.query.first() + trade = Trade.session.scalars(select(Trade)).first() PairLock.session = MagicMock() freqtrade.config['dry_run'] = False @@ -3611,7 +3615,7 @@ def test_execute_trade_exit_with_stoploss_on_exchange( # Create some test data freqtrade.enter_positions() - trade = Trade.query.first() + trade = Trade.session.scalars(select(Trade)).first() trade.is_short = is_short assert trade trades = [trade] @@ -3631,7 +3635,7 @@ def test_execute_trade_exit_with_stoploss_on_exchange( exit_check=ExitCheckTuple(exit_type=ExitType.STOP_LOSS) ) - trade = Trade.query.first() + trade = Trade.session.scalars(select(Trade)).first() trade.is_short = is_short assert trade assert cancel_order.call_count == 1 @@ -3669,7 +3673,7 @@ def test_may_execute_trade_exit_after_stoploss_on_exchange_hit( # Create some test data freqtrade.enter_positions() freqtrade.manage_open_orders() - trade = Trade.query.first() + trade = Trade.session.scalars(select(Trade)).first() trades = [trade] assert trade.stoploss_order_id is None @@ -3755,7 +3759,7 @@ def test_execute_trade_exit_market_order( # Create some test data freqtrade.enter_positions() - trade = Trade.query.first() + trade = Trade.session.scalars(select(Trade)).first() trade.is_short = is_short assert trade @@ -3830,7 +3834,7 @@ def test_execute_trade_exit_insufficient_funds_error(default_conf_usdt, ticker_u # Create some test data freqtrade.enter_positions() - trade = Trade.query.first() + trade = Trade.session.scalars(select(Trade)).first() trade.is_short = is_short assert trade @@ -3898,7 +3902,7 @@ def test_exit_profit_only( exit_type=ExitType.NONE)) freqtrade.enter_positions() - trade = Trade.query.first() + trade = Trade.session.scalars(select(Trade)).first() assert trade.is_short == is_short oobj = Order.parse_from_ccxt_object(limit_order[eside], limit_order[eside]['symbol'], eside) trade.update_order(limit_order[eside]) @@ -3941,7 +3945,7 @@ def test_sell_not_enough_balance(default_conf_usdt, limit_order, limit_order_ope freqtrade.enter_positions() - trade = Trade.query.first() + trade = Trade.session.scalars(select(Trade)).first() amnt = trade.amount oobj = Order.parse_from_ccxt_object(limit_order['buy'], limit_order['buy']['symbol'], 'buy') @@ -4009,7 +4013,7 @@ def test_locked_pairs(default_conf_usdt, ticker_usdt, fee, # Create some test data freqtrade.enter_positions() - trade = Trade.query.first() + trade = Trade.session.scalars(select(Trade)).first() trade.is_short = is_short assert trade @@ -4064,7 +4068,7 @@ def test_ignore_roi_if_entry_signal(default_conf_usdt, limit_order, limit_order_ freqtrade.enter_positions() - trade = Trade.query.first() + trade = Trade.session.scalars(select(Trade)).first() trade.is_short = is_short oobj = Order.parse_from_ccxt_object( limit_order[eside], limit_order[eside]['symbol'], eside) @@ -4114,7 +4118,7 @@ def test_trailing_stop_loss(default_conf_usdt, limit_order_open, freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) freqtrade.enter_positions() - trade = Trade.query.first() + trade = Trade.session.scalars(select(Trade)).first() assert trade.is_short == is_short assert freqtrade.handle_trade(trade) is False @@ -4189,7 +4193,7 @@ def test_trailing_stop_loss_positive( freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) freqtrade.enter_positions() - trade = Trade.query.first() + trade = Trade.session.scalars(select(Trade)).first() assert trade.is_short == is_short oobj = Order.parse_from_ccxt_object(limit_order[eside], limit_order[eside]['symbol'], eside) trade.update_order(limit_order[eside]) @@ -4286,7 +4290,7 @@ def test_disable_ignore_roi_if_entry_signal(default_conf_usdt, limit_order, limi freqtrade.enter_positions() - trade = Trade.query.first() + trade = Trade.session.scalars(select(Trade)).first() trade.is_short = is_short oobj = Order.parse_from_ccxt_object( @@ -4752,7 +4756,7 @@ def test_order_book_depth_of_market( patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) freqtrade.enter_positions() - trade = Trade.query.first() + trade = Trade.session.scalars(select(Trade)).first() if is_high_delta: assert trade is None else: @@ -4763,7 +4767,7 @@ def test_order_book_depth_of_market( assert trade.open_date is not None assert trade.exchange == 'binance' - assert len(Trade.query.all()) == 1 + assert len(Trade.session.scalars(select(Trade)).all()) == 1 # Simulate fulfilled LIMIT_BUY order for trade oobj = Order.parse_from_ccxt_object( @@ -4860,7 +4864,7 @@ def test_order_book_exit_pricing( freqtrade.enter_positions() - trade = Trade.query.first() + trade = Trade.session.scalars(select(Trade)).first() assert trade time.sleep(0.01) # Race condition fix @@ -4932,7 +4936,7 @@ def test_sync_wallet_dry_run(mocker, default_conf_usdt, ticker_usdt, fee, limit_ n = bot.enter_positions() assert n == 2 - trades = Trade.query.all() + trades = Trade.session.scalars(select(Trade)).all() assert len(trades) == 2 bot.config['max_open_trades'] = 3 @@ -4965,7 +4969,7 @@ def test_cancel_all_open_orders(mocker, default_conf_usdt, fee, limit_order, lim freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) create_mock_trades(fee, is_short=is_short) - trades = Trade.query.all() + trades = Trade.session.scalars(select(Trade)).all() assert len(trades) == MOCK_TRADE_COUNT freqtrade.cancel_all_open_orders() assert buy_mock.call_count == buy_calls @@ -4981,7 +4985,7 @@ def test_check_for_open_trades(mocker, default_conf_usdt, fee, is_short): assert freqtrade.rpc.send_msg.call_count == 0 create_mock_trades(fee, is_short) - trade = Trade.query.first() + trade = Trade.session.scalars(select(Trade)).first() trade.is_short = is_short trade.is_open = True @@ -5149,7 +5153,7 @@ def test_reupdate_enter_order_fees(mocker, default_conf_usdt, fee, caplog, is_sh exchange='binance', is_short=is_short ) - Trade.query.session.add(trade) + Trade.session.add(trade) freqtrade.handle_insufficient_funds(trade) # assert log_has_re(r"Trying to reupdate buy fees for .*", caplog) @@ -5546,10 +5550,10 @@ def test_position_adjust(mocker, default_conf_usdt, fee) -> None: assert freqtrade.execute_entry(pair, stake_amount) # Should create an closed trade with an no open order id # Order is filled and trade is open - orders = Order.query.all() + orders = Order.session.scalars(select(Order)).all() assert orders assert len(orders) == 1 - trade = Trade.query.first() + trade = Trade.session.scalars(select(Trade)).first() assert trade assert trade.is_open is True assert trade.open_order_id is None @@ -5559,7 +5563,7 @@ def test_position_adjust(mocker, default_conf_usdt, fee) -> None: # Assume it does nothing since order is closed and trade is open freqtrade.update_trades_without_assigned_fees() - trade = Trade.query.first() + trade = Trade.session.scalars(select(Trade)).first() assert trade assert trade.is_open is True assert trade.open_order_id is None @@ -5569,7 +5573,7 @@ def test_position_adjust(mocker, default_conf_usdt, fee) -> None: freqtrade.manage_open_orders() - trade = Trade.query.first() + trade = Trade.session.scalars(select(Trade)).first() assert trade assert trade.is_open is True assert trade.open_order_id is None @@ -5595,10 +5599,10 @@ def test_position_adjust(mocker, default_conf_usdt, fee) -> None: mocker.patch(f'{EXMS}.fetch_order_or_stoploss_order', MagicMock(return_value=open_dca_order_1)) assert freqtrade.execute_entry(pair, stake_amount, trade=trade) - orders = Order.query.all() + orders = Order.session.scalars(select(Order)).all() assert orders assert len(orders) == 2 - trade = Trade.query.first() + trade = Trade.session.scalars(select(Trade)).first() assert trade assert trade.open_order_id == '651' assert trade.open_rate == 11 @@ -5628,14 +5632,14 @@ def test_position_adjust(mocker, default_conf_usdt, fee) -> None: mocker.patch(f'{EXMS}.fetch_order_or_stoploss_order', fetch_order_mm) freqtrade.update_trades_without_assigned_fees() - orders = Order.query.all() + orders = Order.session.scalars(select(Order)).all() assert orders assert len(orders) == 2 # Assert that the trade is found as open and without fees trades: List[Trade] = Trade.get_open_trades_without_assigned_fees() assert len(trades) == 1 # Assert trade is as expected - trade = Trade.query.first() + trade = Trade.session.scalars(select(Trade)).first() assert trade assert trade.open_order_id == '651' assert trade.open_rate == 11 @@ -5672,14 +5676,14 @@ def test_position_adjust(mocker, default_conf_usdt, fee) -> None: freqtrade.manage_open_orders() # Assert trade is as expected (averaged dca) - trade = Trade.query.first() + trade = Trade.session.scalars(select(Trade)).first() assert trade assert trade.open_order_id is None assert pytest.approx(trade.open_rate) == 9.90909090909 assert trade.amount == 22 assert pytest.approx(trade.stake_amount) == 218 - orders = Order.query.all() + orders = Order.session.scalars(select(Order)).all() assert orders assert len(orders) == 2 @@ -5714,14 +5718,14 @@ def test_position_adjust(mocker, default_conf_usdt, fee) -> None: assert freqtrade.execute_entry(pair, stake_amount, trade=trade) # Assert trade is as expected (averaged dca) - trade = Trade.query.first() + trade = Trade.session.scalars(select(Trade)).first() assert trade assert trade.open_order_id is None assert pytest.approx(trade.open_rate) == 8.729729729729 assert trade.amount == 37 assert trade.stake_amount == 323 - orders = Order.query.all() + orders = Order.session.scalars(select(Order)).all() assert orders assert len(orders) == 3 @@ -5752,7 +5756,7 @@ def test_position_adjust(mocker, default_conf_usdt, fee) -> None: sub_trade_amt=15) # Assert trade is as expected (averaged dca) - trade = Trade.query.first() + trade = Trade.session.scalars(select(Trade)).first() assert trade assert trade.open_order_id is None assert trade.is_open @@ -5760,7 +5764,7 @@ def test_position_adjust(mocker, default_conf_usdt, fee) -> None: assert trade.stake_amount == 192.05405405405406 assert pytest.approx(trade.open_rate) == 8.729729729729 - orders = Order.query.all() + orders = Order.session.scalars(select(Order)).all() assert orders assert len(orders) == 4 @@ -5825,10 +5829,10 @@ def test_position_adjust2(mocker, default_conf_usdt, fee) -> None: assert freqtrade.execute_entry(pair, amount) # Should create an closed trade with an no open order id # Order is filled and trade is open - orders = Order.query.all() + orders = Order.session.scalars(select(Order)).all() assert orders assert len(orders) == 1 - trade = Trade.query.first() + trade = Trade.session.scalars(select(Trade)).first() assert trade assert trade.is_open is True assert trade.open_order_id is None @@ -5838,7 +5842,7 @@ def test_position_adjust2(mocker, default_conf_usdt, fee) -> None: # Assume it does nothing since order is closed and trade is open freqtrade.update_trades_without_assigned_fees() - trade = Trade.query.first() + trade = Trade.session.scalars(select(Trade)).first() assert trade assert trade.is_open is True assert trade.open_order_id is None @@ -5848,7 +5852,7 @@ def test_position_adjust2(mocker, default_conf_usdt, fee) -> None: freqtrade.manage_open_orders() - trade = Trade.query.first() + trade = Trade.session.scalars(select(Trade)).first() assert trade assert trade.is_open is True assert trade.open_order_id is None @@ -5884,7 +5888,7 @@ def test_position_adjust2(mocker, default_conf_usdt, fee) -> None: assert len(trades) == 1 # Assert trade is as expected (averaged dca) - trade = Trade.query.first() + trade = Trade.session.scalars(select(Trade)).first() assert trade assert trade.open_order_id is None assert trade.amount == 50 @@ -5893,7 +5897,7 @@ def test_position_adjust2(mocker, default_conf_usdt, fee) -> None: assert pytest.approx(trade.realized_profit) == -152.375 assert pytest.approx(trade.close_profit_abs) == -152.375 - orders = Order.query.all() + orders = Order.session.scalars(select(Order)).all() assert orders assert len(orders) == 2 # Make sure the closed order is found as the second order. @@ -5926,7 +5930,7 @@ def test_position_adjust2(mocker, default_conf_usdt, fee) -> None: sub_trade_amt=amount) # Assert trade is as expected (averaged dca) - trade = Trade.query.first() + trade = Trade.session.scalars(select(Trade)).first() assert trade assert trade.open_order_id is None assert trade.amount == 50 @@ -5935,7 +5939,7 @@ def test_position_adjust2(mocker, default_conf_usdt, fee) -> None: # Trade fully realized assert pytest.approx(trade.realized_profit) == 94.25 assert pytest.approx(trade.close_profit_abs) == 94.25 - orders = Order.query.all() + orders = Order.session.scalars(select(Order)).all() assert orders assert len(orders) == 3 @@ -6020,11 +6024,11 @@ def test_position_adjust3(mocker, default_conf_usdt, fee, data) -> None: exit_check=ExitCheckTuple(exit_type=ExitType.PARTIAL_EXIT), sub_trade_amt=amount) - orders1 = Order.query.all() + orders1 = Order.session.scalars(select(Order)).all() assert orders1 assert len(orders1) == idx + 1 - trade = Trade.query.first() + trade = Trade.session.scalars(select(Trade)).first() assert trade if idx < len(data) - 1: assert trade.is_open is True @@ -6039,7 +6043,7 @@ def test_position_adjust3(mocker, default_conf_usdt, fee, data) -> None: order_obj = trade.select_order(order[0], False) assert order_obj.order_id == f'60{idx}' - trade = Trade.query.first() + trade = Trade.session.scalars(select(Trade)).first() assert trade assert trade.open_order_id is None assert trade.is_open is False diff --git a/tests/test_integration.py b/tests/test_integration.py index 4c57c5669..922285309 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -1,6 +1,7 @@ from unittest.mock import MagicMock import pytest +from sqlalchemy import select from freqtrade.enums import ExitCheckTuple, ExitType, TradingMode from freqtrade.persistence import Trade @@ -91,7 +92,7 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee, assert freqtrade.strategy.confirm_trade_exit.call_count == 0 wallets_mock.reset_mock() - trades = Trade.query.all() + trades = Trade.session.scalars(select(Trade)).all() # Make sure stoploss-order is open and trade is bought (since we mock update_trade_state) for trade in trades: stoploss_order_closed['id'] = '3' @@ -179,13 +180,13 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, mocker, balance_rati n = freqtrade.enter_positions() assert n == 4 - trades = Trade.query.all() + trades = Trade.session.scalars(select(Trade)).all() assert len(trades) == 4 assert freqtrade.wallets.get_trade_stake_amount('XRP/BTC') == result1 rpc._rpc_force_entry('TKN/BTC', None) - trades = Trade.query.all() + trades = Trade.session.scalars(select(Trade)).all() assert len(trades) == 5 for trade in trades: From 4cfbc55d3418d05449a439d754976fd001f900c4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 16 Mar 2023 18:07:06 +0100 Subject: [PATCH 244/360] Update remaining tests to get rid of .query --- tests/persistence/test_migrations.py | 4 ++-- tests/persistence/test_persistence.py | 1 + tests/plugins/test_pairlist.py | 4 ++-- tests/plugins/test_pairlocks.py | 4 ++-- tests/test_freqtradebot.py | 27 ++++++++++++++++++--------- 5 files changed, 25 insertions(+), 15 deletions(-) diff --git a/tests/persistence/test_migrations.py b/tests/persistence/test_migrations.py index 053d6da12..854d39994 100644 --- a/tests/persistence/test_migrations.py +++ b/tests/persistence/test_migrations.py @@ -406,8 +406,8 @@ def test_migrate_pairlocks(mocker, default_conf, fee, caplog): init_db(default_conf['db_url']) assert len(PairLock.get_all_locks().all()) == 2 - assert len(PairLock.query.filter(PairLock.pair == '*').all()) == 1 - pairlocks = PairLock.query.filter(PairLock.pair == 'ETH/BTC').all() + assert len(PairLock.session.scalars(select(PairLock).filter(PairLock.pair == '*')).all()) == 1 + pairlocks = PairLock.session.scalars(select(PairLock).filter(PairLock.pair == 'ETH/BTC')).all() assert len(pairlocks) == 1 pairlocks[0].pair == 'ETH/BTC' pairlocks[0].side == '*' diff --git a/tests/persistence/test_persistence.py b/tests/persistence/test_persistence.py index 78e97e2e6..db882d56d 100644 --- a/tests/persistence/test_persistence.py +++ b/tests/persistence/test_persistence.py @@ -2017,6 +2017,7 @@ def test_Trade_object_idem(): 'get_open_trades_without_assigned_fees', 'get_open_order_trades', 'get_trades', + 'get_trades_query', 'get_exit_reason_performance', 'get_enter_tag_performance', 'get_mix_tag_performance', diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index 18ee365e2..bc8fe84f1 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -711,8 +711,8 @@ def test_PrecisionFilter_error(mocker, whitelist_conf) -> None: def test_PerformanceFilter_error(mocker, whitelist_conf, caplog) -> None: whitelist_conf['pairlists'] = [{"method": "StaticPairList"}, {"method": "PerformanceFilter"}] - if hasattr(Trade, 'query'): - del Trade.query + if hasattr(Trade, 'session'): + del Trade.session mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=True)) exchange = get_patched_exchange(mocker, whitelist_conf) pm = PairListManager(exchange, whitelist_conf, MagicMock()) diff --git a/tests/plugins/test_pairlocks.py b/tests/plugins/test_pairlocks.py index 39bde3cda..6b7112f98 100644 --- a/tests/plugins/test_pairlocks.py +++ b/tests/plugins/test_pairlocks.py @@ -107,7 +107,7 @@ def test_PairLocks_getlongestlock(use_db): # No lock should be present PairLocks.use_db = use_db if use_db: - assert len(PairLock.query.all()) == 0 + assert len(PairLock.get_all_locks().all()) == 0 assert PairLocks.use_db == use_db @@ -139,7 +139,7 @@ def test_PairLocks_reason(use_db): PairLocks.use_db = use_db # No lock should be present if use_db: - assert len(PairLock.query.all()) == 0 + assert len(PairLock.get_all_locks().all()) == 0 assert PairLocks.use_db == use_db diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 49d00a860..cea70ec48 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2499,7 +2499,8 @@ def test_manage_open_orders_entry( freqtrade.manage_open_orders() assert cancel_order_mock.call_count == 1 assert rpc_mock.call_count == 2 - trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all() + trades = Trade.session.scalars( + select(Trade).filter(Trade.open_order_id.is_(open_trade.open_order_id))).all() nb_trades = len(trades) assert nb_trades == 0 # Custom user buy-timeout is never called @@ -2537,7 +2538,8 @@ def test_adjust_entry_cancel( # check that order is cancelled freqtrade.strategy.adjust_entry_price = MagicMock(return_value=None) freqtrade.manage_open_orders() - trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all() + trades = Trade.session.scalars( + select(Trade).filter(Trade.open_order_id.is_(open_trade.open_order_id))).all() assert len(trades) == 0 assert len(Order.session.scalars(select(Order)).all()) == 0 assert log_has_re( @@ -2578,7 +2580,8 @@ def test_adjust_entry_maintain_replace( # Check that order is maintained freqtrade.strategy.adjust_entry_price = MagicMock(return_value=old_order['price']) freqtrade.manage_open_orders() - trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all() + trades = Trade.session.scalars( + select(Trade).filter(Trade.open_order_id.is_(open_trade.open_order_id))).all() assert len(trades) == 1 assert len(Order.get_open_orders()) == 1 # Entry adjustment is called @@ -2588,7 +2591,8 @@ def test_adjust_entry_maintain_replace( freqtrade.get_valid_enter_price_and_stake = MagicMock(return_value={100, 10, 1}) freqtrade.strategy.adjust_entry_price = MagicMock(return_value=1234) freqtrade.manage_open_orders() - trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all() + trades = Trade.session.scalars( + select(Trade).filter(Trade.open_order_id.is_(open_trade.open_order_id))).all() assert len(trades) == 1 nb_all_orders = len(Order.session.scalars(select(Order)).all()) assert nb_all_orders == 2 @@ -2629,7 +2633,8 @@ def test_check_handle_cancelled_buy( freqtrade.manage_open_orders() assert cancel_order_mock.call_count == 0 assert rpc_mock.call_count == 2 - trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all() + trades = Trade.session.scalars( + select(Trade).filter(Trade.open_order_id.is_(open_trade.open_order_id))).all() assert len(trades) == 0 assert log_has_re( f"{'Sell' if is_short else 'Buy'} order cancelled on exchange for Trade.*", caplog) @@ -2660,7 +2665,8 @@ def test_manage_open_orders_buy_exception( freqtrade.manage_open_orders() assert cancel_order_mock.call_count == 0 assert rpc_mock.call_count == 1 - trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all() + trades = Trade.session.scalars( + select(Trade).filter(Trade.open_order_id.is_(open_trade.open_order_id))).all() nb_trades = len(trades) assert nb_trades == 1 @@ -2860,7 +2866,8 @@ def test_manage_open_orders_partial( freqtrade.manage_open_orders() assert cancel_order_mock.call_count == 1 assert rpc_mock.call_count == 3 - trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all() + trades = Trade.session.scalars( + select(Trade).filter(Trade.open_order_id.is_(open_trade.open_order_id))).all() assert len(trades) == 1 assert trades[0].amount == 23.0 assert trades[0].stake_amount == open_trade.open_rate * trades[0].amount / leverage @@ -2907,7 +2914,8 @@ def test_manage_open_orders_partial_fee( assert cancel_order_mock.call_count == 1 assert rpc_mock.call_count == 3 - trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all() + trades = Trade.session.scalars( + select(Trade).filter(Trade.open_order_id.is_(open_trade.open_order_id))).all() assert len(trades) == 1 # Verify that trade has been updated assert trades[0].amount == (limit_buy_order_old_partial['amount'] - @@ -2957,7 +2965,8 @@ def test_manage_open_orders_partial_except( assert cancel_order_mock.call_count == 1 assert rpc_mock.call_count == 3 - trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all() + trades = Trade.session.scalars( + select(Trade).filter(Trade.open_order_id.is_(open_trade.open_order_id))).all() assert len(trades) == 1 # Verify that trade has been updated From b7709126f9430a87bfcd515f4f37b6fa11e4f17c Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 16 Mar 2023 18:07:22 +0100 Subject: [PATCH 245/360] remove .query completely --- freqtrade/commands/db_commands.py | 8 ++++---- freqtrade/persistence/models.py | 3 --- freqtrade/persistence/pairlock.py | 3 +-- freqtrade/persistence/trade_model.py | 4 +--- 4 files changed, 6 insertions(+), 12 deletions(-) diff --git a/freqtrade/commands/db_commands.py b/freqtrade/commands/db_commands.py index c819ca243..d83605c6f 100644 --- a/freqtrade/commands/db_commands.py +++ b/freqtrade/commands/db_commands.py @@ -1,7 +1,7 @@ import logging from typing import Any, Dict -from sqlalchemy import func +from sqlalchemy import func, select from freqtrade.configuration.config_setup import setup_utils_configuration from freqtrade.enums import RunMode @@ -43,9 +43,9 @@ def start_convert_db(args: Dict[str, Any]) -> None: session_target.commit() # Update sequences - max_trade_id = session_target.query(func.max(Trade.id)).scalar() - max_order_id = session_target.query(func.max(Order.id)).scalar() - max_pairlock_id = session_target.query(func.max(PairLock.id)).scalar() + max_trade_id = session_target.scalar(select(func.max(Trade.id))) + max_order_id = session_target.scalar(select(func.max(Order.id))) + max_pairlock_id = session_target.scalar(select(func.max(PairLock.id))) set_sequence_ids(session_target.get_bind(), trade_id=max_trade_id, diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index f4058b4eb..eee07e61c 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -57,9 +57,6 @@ def init_db(db_url: str) -> None: Trade.session = scoped_session(sessionmaker(bind=engine, autoflush=False)) Order.session = Trade.session PairLock.session = Trade.session - Trade.query = Trade.session.query_property() - Order.query = Trade.session.query_property() - PairLock.query = Trade.session.query_property() previous_tables = inspect(engine).get_table_names() ModelBase.metadata.create_all(engine) diff --git a/freqtrade/persistence/pairlock.py b/freqtrade/persistence/pairlock.py index e787b5fa0..1b254c2b2 100644 --- a/freqtrade/persistence/pairlock.py +++ b/freqtrade/persistence/pairlock.py @@ -2,7 +2,7 @@ from datetime import datetime, timezone from typing import Any, ClassVar, Dict, Optional from sqlalchemy import ScalarResult, String, or_, select -from sqlalchemy.orm import Mapped, QueryPropertyDescriptor, mapped_column +from sqlalchemy.orm import Mapped, mapped_column from freqtrade.constants import DATETIME_PRINT_FORMAT from freqtrade.persistence.base import ModelBase, SessionType @@ -13,7 +13,6 @@ class PairLock(ModelBase): Pair Locks database model. """ __tablename__ = 'pairlocks' - query: ClassVar[QueryPropertyDescriptor] session: ClassVar[SessionType] id: Mapped[int] = mapped_column(primary_key=True) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 892707810..27be0d726 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -9,7 +9,7 @@ from typing import Any, ClassVar, Dict, List, Optional, Sequence, cast from sqlalchemy import (Enum, Float, ForeignKey, Integer, ScalarResult, Select, String, UniqueConstraint, desc, func, select) -from sqlalchemy.orm import Mapped, QueryPropertyDescriptor, lazyload, mapped_column, relationship +from sqlalchemy.orm import Mapped, lazyload, mapped_column, relationship from freqtrade.constants import (DATETIME_PRINT_FORMAT, MATH_CLOSE_PREC, NON_OPEN_EXCHANGE_STATES, BuySell, LongShort) @@ -36,7 +36,6 @@ class Order(ModelBase): Mirrors CCXT Order structure """ __tablename__ = 'orders' - query: ClassVar[QueryPropertyDescriptor] session: ClassVar[SessionType] # Uniqueness should be ensured over pair, order_id @@ -1188,7 +1187,6 @@ class Trade(ModelBase, LocalTrade): Note: Fields must be aligned with LocalTrade class """ __tablename__ = 'trades' - query: ClassVar[QueryPropertyDescriptor] session: ClassVar[SessionType] use_db: bool = True From e3e4fbd5ba397558bdd90f985759583e71026f61 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 16 Mar 2023 19:24:37 +0100 Subject: [PATCH 246/360] Minor test fix --- tests/rpc/test_rpc_telegram.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 1dc255b3e..59b6df489 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -302,8 +302,7 @@ def test_telegram_status_closed_trade(default_conf, update, mocker, fee) -> None telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) create_mock_trades(fee) - trades = Trade.get_trades([Trade.is_open.is_(False)]) - trade = trades[0] + trade = Trade.get_trades([Trade.is_open.is_(False)]).first() context = MagicMock() context.args = [str(trade.id)] telegram._status(update=update, context=context) From 774eacc561915ff5fdb78d1552089158766a13eb Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 14 Mar 2023 20:27:14 +0100 Subject: [PATCH 247/360] Attempt push to ghcr.io --- .github/workflows/ci.yml | 14 +++++++++++--- build_helpers/publish_docker_arm64.sh | 14 ++++++++++++++ 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7e0483c3d..a1fa487a5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,8 @@ on: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true - +permissions: + repository-projects: read jobs: build_linux: @@ -321,7 +322,6 @@ jobs: build_linux_online: # Run pytest with "live" checks runs-on: ubuntu-22.04 - # permissions: steps: - uses: actions/checkout@v3 @@ -472,6 +472,8 @@ jobs: build_helpers/publish_docker_multi.sh deploy_arm: + permissions: + packages: write needs: [ deploy ] # Only run on 64bit machines runs-on: [self-hosted, linux, ARM64] @@ -492,9 +494,15 @@ jobs: run: | echo "${DOCKER_PASSWORD}" | docker login --username ${DOCKER_USERNAME} --password-stdin + - name: GHCR.io login + env: + GHCR_USERNAME: ${{ github.actor }} + GHCR_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + run: | + echo "${GHCR_PASSWORD}" | docker login ghcr.io --username ${GHCR_USERNAME} --password-stdin + - name: Build and test and push docker images env: - IMAGE_NAME: freqtradeorg/freqtrade BRANCH_NAME: ${{ steps.extract_branch.outputs.branch }} run: | build_helpers/publish_docker_arm64.sh diff --git a/build_helpers/publish_docker_arm64.sh b/build_helpers/publish_docker_arm64.sh index f3cedff2e..c362ee825 100755 --- a/build_helpers/publish_docker_arm64.sh +++ b/build_helpers/publish_docker_arm64.sh @@ -3,6 +3,8 @@ # Use BuildKit, otherwise building on ARM fails export DOCKER_BUILDKIT=1 +IMAGE_NAME=freqtradeorg/freqtrade +GHCR_IMAGE_NAME=ghcr.io/freqtrade/freqtrade # Replace / with _ to create a valid tag TAG=$(echo "${BRANCH_NAME}" | sed -e "s/\//_/g") TAG_PLOT=${TAG}_plot @@ -82,6 +84,18 @@ docker manifest push -p ${IMAGE_NAME}:${TAG_FREQAI} docker manifest create ${IMAGE_NAME}:${TAG_FREQAI_RL} ${CACHE_IMAGE}:${TAG_FREQAI_RL} ${CACHE_IMAGE}:${TAG_FREQAI_RL_ARM} docker manifest push -p ${IMAGE_NAME}:${TAG_FREQAI_RL} +# Retag images for GHCR +docker tag ${IMAGE_NAME}:${TAG} ${GHCR_IMAGE_NAME}:${TAG} +docker tag ${IMAGE_NAME}:${TAG_PLOT} ${GHCR_IMAGE_NAME}:${TAG_PLOT} +docker tag ${IMAGE_NAME}:${TAG_FREQAI} ${GHCR_IMAGE_NAME}:${TAG_FREQAI} +docker tag ${IMAGE_NAME}:${TAG_FREQAI_RL} ${GHCR_IMAGE_NAME}:${TAG_FREQAI_RL} + +# Push GHCR iamges +docker push ${GHCR_IMAGE_NAME}:${TAG} +docker push ${GHCR_IMAGE_NAME}:${TAG_PLOT} +docker push ${GHCR_IMAGE_NAME}:${TAG_FREQAI} +docker push ${GHCR_IMAGE_NAME}:${TAG_FREQAI_RL} + # Tag as latest for develop builds if [ "${TAG}" = "develop" ]; then echo 'Tagging image as latest' From db0f449d93d5b79b0b3d5230f8a56ba145d19256 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 15 Mar 2023 06:46:31 +0100 Subject: [PATCH 248/360] Use docker manifest for GHCR builds --- build_helpers/publish_docker_arm64.sh | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/build_helpers/publish_docker_arm64.sh b/build_helpers/publish_docker_arm64.sh index c362ee825..e29cd695d 100755 --- a/build_helpers/publish_docker_arm64.sh +++ b/build_helpers/publish_docker_arm64.sh @@ -84,23 +84,27 @@ docker manifest push -p ${IMAGE_NAME}:${TAG_FREQAI} docker manifest create ${IMAGE_NAME}:${TAG_FREQAI_RL} ${CACHE_IMAGE}:${TAG_FREQAI_RL} ${CACHE_IMAGE}:${TAG_FREQAI_RL_ARM} docker manifest push -p ${IMAGE_NAME}:${TAG_FREQAI_RL} -# Retag images for GHCR -docker tag ${IMAGE_NAME}:${TAG} ${GHCR_IMAGE_NAME}:${TAG} -docker tag ${IMAGE_NAME}:${TAG_PLOT} ${GHCR_IMAGE_NAME}:${TAG_PLOT} -docker tag ${IMAGE_NAME}:${TAG_FREQAI} ${GHCR_IMAGE_NAME}:${TAG_FREQAI} -docker tag ${IMAGE_NAME}:${TAG_FREQAI_RL} ${GHCR_IMAGE_NAME}:${TAG_FREQAI_RL} +# Recreate multiarch images for GHCR +docker manifest create ${GHCR_IMAGE_NAME}:${TAG} ${CACHE_IMAGE}:${TAG} ${CACHE_IMAGE}:${TAG_ARM} ${IMAGE_NAME}:${TAG_PI} +docker manifest push -p ${GHCR_IMAGE_NAME}:${TAG} -# Push GHCR iamges -docker push ${GHCR_IMAGE_NAME}:${TAG} -docker push ${GHCR_IMAGE_NAME}:${TAG_PLOT} -docker push ${GHCR_IMAGE_NAME}:${TAG_FREQAI} -docker push ${GHCR_IMAGE_NAME}:${TAG_FREQAI_RL} +docker manifest create ${GHCR_IMAGE_NAME}:${TAG_PLOT} ${CACHE_IMAGE}:${TAG_PLOT} ${CACHE_IMAGE}:${TAG_PLOT_ARM} +docker manifest push -p ${GHCR_IMAGE_NAME}:${TAG_PLOT} + +docker manifest create ${GHCR_IMAGE_NAME}:${TAG_FREQAI} ${CACHE_IMAGE}:${TAG_FREQAI} ${CACHE_IMAGE}:${TAG_FREQAI_ARM} +docker manifest push -p ${GHCR_IMAGE_NAME}:${TAG_FREQAI} + +docker manifest create ${GHCR_IMAGE_NAME}:${TAG_FREQAI_RL} ${CACHE_IMAGE}:${TAG_FREQAI_RL} ${CACHE_IMAGE}:${TAG_FREQAI_RL_ARM} +docker manifest push -p ${GHCR_IMAGE_NAME}:${TAG_FREQAI_RL} # Tag as latest for develop builds if [ "${TAG}" = "develop" ]; then echo 'Tagging image as latest' docker manifest create ${IMAGE_NAME}:latest ${CACHE_IMAGE}:${TAG_ARM} ${IMAGE_NAME}:${TAG_PI} ${CACHE_IMAGE}:${TAG} docker manifest push -p ${IMAGE_NAME}:latest + + docker manifest create ${GHCR_IMAGE_NAME}:latest ${CACHE_IMAGE}:${TAG_ARM} ${IMAGE_NAME}:${TAG_PI} ${CACHE_IMAGE}:${TAG} + docker manifest push -p ${GHCR_IMAGE_NAME}:latest fi docker images From 0d3de0701256b351d66b53a4001b2fae4d69ab7f Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 15 Mar 2023 19:06:08 +0100 Subject: [PATCH 249/360] use Crane to move images around --- .github/workflows/ci.yml | 15 ++++++++------- build_helpers/publish_docker_arm64.sh | 27 ++++++++++++--------------- build_helpers/publish_docker_multi.sh | 3 ++- 3 files changed, 22 insertions(+), 23 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a1fa487a5..f09e5feac 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -466,7 +466,6 @@ jobs: - name: Build and test and push docker images env: - IMAGE_NAME: freqtradeorg/freqtrade BRANCH_NAME: ${{ steps.extract_branch.outputs.branch }} run: | build_helpers/publish_docker_multi.sh @@ -494,16 +493,18 @@ jobs: run: | echo "${DOCKER_PASSWORD}" | docker login --username ${DOCKER_USERNAME} --password-stdin - - name: GHCR.io login - env: - GHCR_USERNAME: ${{ github.actor }} - GHCR_PASSWORD: ${{ secrets.GITHUB_TOKEN }} - run: | - echo "${GHCR_PASSWORD}" | docker login ghcr.io --username ${GHCR_USERNAME} --password-stdin + # - name: GHCR.io login + # env: + # GHCR_USERNAME: ${{ github.actor }} + # GHCR_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + # run: | + # echo "${GHCR_PASSWORD}" | docker login ghcr.io --username ${GHCR_USERNAME} --password-stdin - name: Build and test and push docker images env: BRANCH_NAME: ${{ steps.extract_branch.outputs.branch }} + GHCR_USERNAME: ${{ github.actor }} + GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | build_helpers/publish_docker_arm64.sh diff --git a/build_helpers/publish_docker_arm64.sh b/build_helpers/publish_docker_arm64.sh index e29cd695d..3de113d37 100755 --- a/build_helpers/publish_docker_arm64.sh +++ b/build_helpers/publish_docker_arm64.sh @@ -4,7 +4,9 @@ export DOCKER_BUILDKIT=1 IMAGE_NAME=freqtradeorg/freqtrade +CACHE_IMAGE=freqtradeorg/freqtrade_cache GHCR_IMAGE_NAME=ghcr.io/freqtrade/freqtrade + # Replace / with _ to create a valid tag TAG=$(echo "${BRANCH_NAME}" | sed -e "s/\//_/g") TAG_PLOT=${TAG}_plot @@ -16,7 +18,6 @@ TAG_ARM=${TAG}_arm TAG_PLOT_ARM=${TAG_PLOT}_arm TAG_FREQAI_ARM=${TAG_FREQAI}_arm TAG_FREQAI_RL_ARM=${TAG_FREQAI_RL}_arm -CACHE_IMAGE=freqtradeorg/freqtrade_cache echo "Running for ${TAG}" @@ -40,13 +41,13 @@ if [ $? -ne 0 ]; then echo "failed building multiarch images" return 1 fi -# Tag image for upload and next build step -docker tag freqtrade:$TAG_ARM ${CACHE_IMAGE}:$TAG_ARM docker build --cache-from freqtrade:${TAG_ARM} --build-arg sourceimage=${CACHE_IMAGE} --build-arg sourcetag=${TAG_ARM} -t freqtrade:${TAG_PLOT_ARM} -f docker/Dockerfile.plot . docker build --cache-from freqtrade:${TAG_ARM} --build-arg sourceimage=${CACHE_IMAGE} --build-arg sourcetag=${TAG_ARM} -t freqtrade:${TAG_FREQAI_ARM} -f docker/Dockerfile.freqai . docker build --cache-from freqtrade:${TAG_ARM} --build-arg sourceimage=${CACHE_IMAGE} --build-arg sourcetag=${TAG_ARM} -t freqtrade:${TAG_FREQAI_RL_ARM} -f docker/Dockerfile.freqai_rl . +# Tag image for upload and next build step +docker tag freqtrade:$TAG_ARM ${CACHE_IMAGE}:$TAG_ARM docker tag freqtrade:$TAG_PLOT_ARM ${CACHE_IMAGE}:$TAG_PLOT_ARM docker tag freqtrade:$TAG_FREQAI_ARM ${CACHE_IMAGE}:$TAG_FREQAI_ARM docker tag freqtrade:$TAG_FREQAI_RL_ARM ${CACHE_IMAGE}:$TAG_FREQAI_RL_ARM @@ -61,7 +62,6 @@ fi docker images -# docker push ${IMAGE_NAME} docker push ${CACHE_IMAGE}:$TAG_PLOT_ARM docker push ${CACHE_IMAGE}:$TAG_FREQAI_ARM docker push ${CACHE_IMAGE}:$TAG_FREQAI_RL_ARM @@ -84,18 +84,16 @@ docker manifest push -p ${IMAGE_NAME}:${TAG_FREQAI} docker manifest create ${IMAGE_NAME}:${TAG_FREQAI_RL} ${CACHE_IMAGE}:${TAG_FREQAI_RL} ${CACHE_IMAGE}:${TAG_FREQAI_RL_ARM} docker manifest push -p ${IMAGE_NAME}:${TAG_FREQAI_RL} -# Recreate multiarch images for GHCR -docker manifest create ${GHCR_IMAGE_NAME}:${TAG} ${CACHE_IMAGE}:${TAG} ${CACHE_IMAGE}:${TAG_ARM} ${IMAGE_NAME}:${TAG_PI} -docker manifest push -p ${GHCR_IMAGE_NAME}:${TAG} +# copy images to ghcr.io -docker manifest create ${GHCR_IMAGE_NAME}:${TAG_PLOT} ${CACHE_IMAGE}:${TAG_PLOT} ${CACHE_IMAGE}:${TAG_PLOT_ARM} -docker manifest push -p ${GHCR_IMAGE_NAME}:${TAG_PLOT} +alias crane="docker run --rm -v $(pwd)/crane:/home/nonroot/.docker/ gcr.io/go-containerregistry/crane" -docker manifest create ${GHCR_IMAGE_NAME}:${TAG_FREQAI} ${CACHE_IMAGE}:${TAG_FREQAI} ${CACHE_IMAGE}:${TAG_FREQAI_ARM} -docker manifest push -p ${GHCR_IMAGE_NAME}:${TAG_FREQAI} +echo "${GHCR_TOKEN}" | crane auth login ghcr.io -u ${GHCR_USER} --password-stdin -docker manifest create ${GHCR_IMAGE_NAME}:${TAG_FREQAI_RL} ${CACHE_IMAGE}:${TAG_FREQAI_RL} ${CACHE_IMAGE}:${TAG_FREQAI_RL_ARM} -docker manifest push -p ${GHCR_IMAGE_NAME}:${TAG_FREQAI_RL} +crane copy ${IMAGE_NAME}:${TAG} ${GHCR_IMAGE_NAME}:${TAG} +crane copy ${IMAGE_NAME}:${TAG_PLOT} ${GHCR_IMAGE_NAME}:${TAG_PLOT} +crane copy ${IMAGE_NAME}:${TAG_FREQAI} ${GHCR_IMAGE_NAME}:${TAG_FREQAI} +crane copy ${IMAGE_NAME}:${TAG_FREQAI_RL} ${GHCR_IMAGE_NAME}:${TAG_FREQAI_RL} # Tag as latest for develop builds if [ "${TAG}" = "develop" ]; then @@ -103,8 +101,7 @@ if [ "${TAG}" = "develop" ]; then docker manifest create ${IMAGE_NAME}:latest ${CACHE_IMAGE}:${TAG_ARM} ${IMAGE_NAME}:${TAG_PI} ${CACHE_IMAGE}:${TAG} docker manifest push -p ${IMAGE_NAME}:latest - docker manifest create ${GHCR_IMAGE_NAME}:latest ${CACHE_IMAGE}:${TAG_ARM} ${IMAGE_NAME}:${TAG_PI} ${CACHE_IMAGE}:${TAG} - docker manifest push -p ${GHCR_IMAGE_NAME}:latest + crane copy ${IMAGE_NAME}:latest ${GHCR_IMAGE_NAME}:latest fi docker images diff --git a/build_helpers/publish_docker_multi.sh b/build_helpers/publish_docker_multi.sh index 3e5e61564..27fa06b95 100755 --- a/build_helpers/publish_docker_multi.sh +++ b/build_helpers/publish_docker_multi.sh @@ -2,6 +2,8 @@ # The below assumes a correctly setup docker buildx environment +IMAGE_NAME=freqtradeorg/freqtrade +CACHE_IMAGE=freqtradeorg/freqtrade_cache # Replace / with _ to create a valid tag TAG=$(echo "${BRANCH_NAME}" | sed -e "s/\//_/g") TAG_PLOT=${TAG}_plot @@ -11,7 +13,6 @@ TAG_PI="${TAG}_pi" PI_PLATFORM="linux/arm/v7" echo "Running for ${TAG}" -CACHE_IMAGE=freqtradeorg/freqtrade_cache CACHE_TAG=${CACHE_IMAGE}:${TAG_PI}_cache # Add commit and commit_message to docker container From 628f6b8b7cdc4c73d89d3aec678366f021761cb4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 15 Mar 2023 20:41:08 +0100 Subject: [PATCH 250/360] Fix crane docker permissions --- .github/workflows/ci.yml | 7 ------- build_helpers/publish_docker_arm64.sh | 7 +++++-- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f09e5feac..663cfb1be 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -493,13 +493,6 @@ jobs: run: | echo "${DOCKER_PASSWORD}" | docker login --username ${DOCKER_USERNAME} --password-stdin - # - name: GHCR.io login - # env: - # GHCR_USERNAME: ${{ github.actor }} - # GHCR_PASSWORD: ${{ secrets.GITHUB_TOKEN }} - # run: | - # echo "${GHCR_PASSWORD}" | docker login ghcr.io --username ${GHCR_USERNAME} --password-stdin - - name: Build and test and push docker images env: BRANCH_NAME: ${{ steps.extract_branch.outputs.branch }} diff --git a/build_helpers/publish_docker_arm64.sh b/build_helpers/publish_docker_arm64.sh index 3de113d37..ce0fab6ec 100755 --- a/build_helpers/publish_docker_arm64.sh +++ b/build_helpers/publish_docker_arm64.sh @@ -86,9 +86,11 @@ docker manifest push -p ${IMAGE_NAME}:${TAG_FREQAI_RL} # copy images to ghcr.io -alias crane="docker run --rm -v $(pwd)/crane:/home/nonroot/.docker/ gcr.io/go-containerregistry/crane" +alias crane="docker run --rm --i -v $(pwd)/.crane:/home/nonroot/.docker/ gcr.io/go-containerregistry/crane" +mkdir .crane +chmod a+rwx .crane -echo "${GHCR_TOKEN}" | crane auth login ghcr.io -u ${GHCR_USER} --password-stdin +echo "${GHCR_TOKEN}" | crane auth login ghcr.io -u "${GHCR_USERNAME}" --password-stdin crane copy ${IMAGE_NAME}:${TAG} ${GHCR_IMAGE_NAME}:${TAG} crane copy ${IMAGE_NAME}:${TAG_PLOT} ${GHCR_IMAGE_NAME}:${TAG_PLOT} @@ -105,6 +107,7 @@ if [ "${TAG}" = "develop" ]; then fi docker images +rm -rf .crane # Cleanup old images from arm64 node. docker image prune -a --force --filter "until=24h" From 764d5507a3914d5f109ffd98c3c21fb6e6449aa5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 16 Mar 2023 19:33:56 +0100 Subject: [PATCH 251/360] Fix typo in docker param --- build_helpers/publish_docker_arm64.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build_helpers/publish_docker_arm64.sh b/build_helpers/publish_docker_arm64.sh index ce0fab6ec..696f5bc48 100755 --- a/build_helpers/publish_docker_arm64.sh +++ b/build_helpers/publish_docker_arm64.sh @@ -86,7 +86,7 @@ docker manifest push -p ${IMAGE_NAME}:${TAG_FREQAI_RL} # copy images to ghcr.io -alias crane="docker run --rm --i -v $(pwd)/.crane:/home/nonroot/.docker/ gcr.io/go-containerregistry/crane" +alias crane="docker run --rm -i -v $(pwd)/.crane:/home/nonroot/.docker/ gcr.io/go-containerregistry/crane" mkdir .crane chmod a+rwx .crane From 9044052b4e1eded666da0b1ad00e50fa5e7ef311 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 17 Mar 2023 06:46:55 +0100 Subject: [PATCH 252/360] Fix exceptions when training fails --- freqtrade/freqai/freqai_interface.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/freqtrade/freqai/freqai_interface.py b/freqtrade/freqai/freqai_interface.py index 884849446..07c357de3 100644 --- a/freqtrade/freqai/freqai_interface.py +++ b/freqtrade/freqai/freqai_interface.py @@ -104,6 +104,7 @@ class IFreqaiModel(ABC): self.data_provider: Optional[DataProvider] = None self.max_system_threads = max(int(psutil.cpu_count() * 2 - 2), 1) self.can_short = True # overridden in start() with strategy.can_short + self.model: Any = None record_params(config, self.full_path) @@ -338,13 +339,14 @@ class IFreqaiModel(ABC): except Exception as msg: logger.warning( f"Training {pair} raised exception {msg.__class__.__name__}. " - f"Message: {msg}, skipping.") + f"Message: {msg}, skipping.", exc_info=True) + self.model = None self.dd.pair_dict[pair]["trained_timestamp"] = int( tr_train.stopts) - if self.plot_features: + if self.plot_features and self.model is not None: plot_feature_importance(self.model, pair, dk, self.plot_features) - if self.save_backtest_models: + if self.save_backtest_models and self.model is not None: logger.info('Saving backtest model to disk.') self.dd.save_data(self.model, pair, dk) else: From 477dc504250f7e305e592664d1a30932e36cfdd8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 18 Mar 2023 16:32:07 +0000 Subject: [PATCH 253/360] Add pair output to "tossed" messages --- freqtrade/freqai/data_kitchen.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/freqai/data_kitchen.py b/freqtrade/freqai/data_kitchen.py index 66923b5c2..52d487b08 100644 --- a/freqtrade/freqai/data_kitchen.py +++ b/freqtrade/freqai/data_kitchen.py @@ -251,7 +251,7 @@ class FreqaiDataKitchen: (drop_index == 0) & (drop_index_labels == 0) ] logger.info( - f"dropped {len(unfiltered_df) - len(filtered_df)} training points" + f"{self.pair}: dropped {len(unfiltered_df) - len(filtered_df)} training points" f" due to NaNs in populated dataset {len(unfiltered_df)}." ) if (1 - len(filtered_df) / len(unfiltered_df)) > 0.1 and self.live: @@ -675,7 +675,7 @@ class FreqaiDataKitchen: ] logger.info( - f"SVM tossed {len(y_pred) - kept_points.sum()}" + f"{self.pair}: SVM tossed {len(y_pred) - kept_points.sum()}" f" test points from {len(y_pred)} total points." ) @@ -949,7 +949,7 @@ class FreqaiDataKitchen: if (len(do_predict) - do_predict.sum()) > 0: logger.info( - f"DI tossed {len(do_predict) - do_predict.sum()} predictions for " + f"{self.pair}: DI tossed {len(do_predict) - do_predict.sum()} predictions for " "being too far from training data." ) From 818d2bf92afcb6d41f3c91d3b81fb045dcfd470b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 18 Mar 2023 17:02:17 +0100 Subject: [PATCH 254/360] Fix stoploss on exchange value in /show_config call --- freqtrade/rpc/rpc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index eb184c6d6..6b82efe71 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -123,7 +123,7 @@ class RPC: if config['max_open_trades'] != float('inf') else -1), 'minimal_roi': config['minimal_roi'].copy() if 'minimal_roi' in config else {}, 'stoploss': config.get('stoploss'), - 'stoploss_on_exchange': config.get('stoploss_on_exchange', False), + 'stoploss_on_exchange': config.get('order_types', {}).get('stoploss_on_exchange', False), 'trailing_stop': config.get('trailing_stop'), 'trailing_stop_positive': config.get('trailing_stop_positive'), 'trailing_stop_positive_offset': config.get('trailing_stop_positive_offset'), From d808dd49e81978f84a3ff40bf0cce215b40455ca Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 18 Mar 2023 19:28:13 +0100 Subject: [PATCH 255/360] Fix ruff violation --- freqtrade/rpc/rpc.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 6b82efe71..c6a6f5cae 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -123,7 +123,8 @@ class RPC: if config['max_open_trades'] != float('inf') else -1), 'minimal_roi': config['minimal_roi'].copy() if 'minimal_roi' in config else {}, 'stoploss': config.get('stoploss'), - 'stoploss_on_exchange': config.get('order_types', {}).get('stoploss_on_exchange', False), + 'stoploss_on_exchange': config.get('order_types', + {}).get('stoploss_on_exchange', False), 'trailing_stop': config.get('trailing_stop'), 'trailing_stop_positive': config.get('trailing_stop_positive'), 'trailing_stop_positive_offset': config.get('trailing_stop_positive_offset'), From b0a7b64d444ddffdb47ec11166a3126f54f2d5da Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 17 Mar 2023 20:41:11 +0100 Subject: [PATCH 256/360] Close sessions after telegram calls --- freqtrade/rpc/telegram.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 0c0b24f00..fda791313 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -83,6 +83,8 @@ def authorized_only(command_handler: Callable[..., None]) -> Callable[..., Any]: self._send_msg(str(e)) except BaseException: logger.exception('Exception occurred within Telegram module') + finally: + Trade.session.remove() return wrapper From 62c8dd98d57c8d0ece15ff2a31f83a68f0f2d34d Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 17 Mar 2023 20:44:00 +0100 Subject: [PATCH 257/360] Use combination of thread-local and asyncio-aware session context --- freqtrade/persistence/models.py | 25 ++++++++++++++++++++++--- freqtrade/rpc/api_server/deps.py | 15 ++++++++++++--- tests/rpc/test_rpc_telegram.py | 6 ++++-- 3 files changed, 38 insertions(+), 8 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index eee07e61c..2315c0acc 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -2,7 +2,9 @@ This module contains the class to persist trades into SQLite """ import logging -from typing import Any, Dict +import threading +from contextvars import ContextVar +from typing import Any, Dict, Final, Optional from sqlalchemy import create_engine, inspect from sqlalchemy.exc import NoSuchModuleError @@ -19,6 +21,22 @@ from freqtrade.persistence.trade_model import Order, Trade logger = logging.getLogger(__name__) +REQUEST_ID_CTX_KEY: Final[str] = 'request_id' +_request_id_ctx_var: ContextVar[Optional[str]] = ContextVar(REQUEST_ID_CTX_KEY, default=None) + + +def get_request_or_thread_id() -> Optional[str]: + """ + Helper method to get either async context (for fastapi requests), or thread id + """ + id = _request_id_ctx_var.get() + if id is None: + # when not in request context - use thread id + id = str(threading.current_thread().ident) + + return id + + _SQL_DOCS_URL = 'http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls' @@ -53,8 +71,9 @@ def init_db(db_url: str) -> None: # https://docs.sqlalchemy.org/en/13/orm/contextual.html#thread-local-scope # Scoped sessions proxy requests to the appropriate thread-local session. - # We should use the scoped_session object - not a seperately initialized version - Trade.session = scoped_session(sessionmaker(bind=engine, autoflush=False)) + # Since we also use fastAPI, we need to make it aware of the request id, too + Trade.session = scoped_session(sessionmaker( + bind=engine, autoflush=False), scopefunc=get_request_or_thread_id) Order.session = Trade.session PairLock.session = Trade.session diff --git a/freqtrade/rpc/api_server/deps.py b/freqtrade/rpc/api_server/deps.py index aed97367b..eb41d728d 100644 --- a/freqtrade/rpc/api_server/deps.py +++ b/freqtrade/rpc/api_server/deps.py @@ -1,9 +1,11 @@ from typing import Any, Dict, Iterator, Optional +from uuid import uuid4 from fastapi import Depends from freqtrade.enums import RunMode from freqtrade.persistence import Trade +from freqtrade.persistence.models import _request_id_ctx_var from freqtrade.rpc.rpc import RPC, RPCException from .webserver import ApiServer @@ -15,12 +17,19 @@ def get_rpc_optional() -> Optional[RPC]: return None -def get_rpc() -> Optional[Iterator[RPC]]: +async def get_rpc() -> Optional[Iterator[RPC]]: + _rpc = get_rpc_optional() if _rpc: + request_id = str(uuid4()) + ctx_token = _request_id_ctx_var.set(request_id) Trade.rollback() - yield _rpc - Trade.rollback() + try: + yield _rpc + finally: + Trade.session.remove() + _request_id_ctx_var.reset(ctx_token) + else: raise RPCException('Bot is not in the correct state') diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index b1859f581..521e3b66d 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -674,8 +674,9 @@ def test_monthly_handle(default_conf_usdt, update, ticker, fee, mocker, time_mac assert str('Monthly Profit over the last 6 months:') in msg_mock.call_args_list[0][0][0] -def test_profit_handle(default_conf_usdt, update, ticker_usdt, ticker_sell_up, fee, - limit_sell_order_usdt, mocker) -> None: +def test_telegram_profit_handle( + default_conf_usdt, update, ticker_usdt, ticker_sell_up, fee, + limit_sell_order_usdt, mocker) -> None: mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=1.1) mocker.patch.multiple( EXMS, @@ -710,6 +711,7 @@ def test_profit_handle(default_conf_usdt, update, ticker_usdt, ticker_sell_up, f # Update the ticker with a market going up mocker.patch(f'{EXMS}.fetch_ticker', ticker_sell_up) # Simulate fulfilled LIMIT_SELL order for trade + trade = Trade.session.scalars(select(Trade)).first() oobj = Order.parse_from_ccxt_object( limit_sell_order_usdt, limit_sell_order_usdt['symbol'], 'sell') trade.orders.append(oobj) From b1f88e88612285c5ccaa219c6d9366a575c30de9 Mon Sep 17 00:00:00 2001 From: hippocritical Date: Sat, 18 Mar 2023 20:02:55 +0100 Subject: [PATCH 258/360] fixed typo from trades to trade --- freqtrade/strategy/strategyupdater.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/strategy/strategyupdater.py b/freqtrade/strategy/strategyupdater.py index bc692b71c..2669dcc4a 100644 --- a/freqtrade/strategy/strategyupdater.py +++ b/freqtrade/strategy/strategyupdater.py @@ -181,7 +181,7 @@ class NameUpdater(ast_comments.NodeTransformer): def visit_Attribute(self, node): if ( isinstance(node.value, ast_comments.Name) - and node.value.id == 'trades' + and node.value.id == 'trade' and node.attr == 'nr_of_successful_buys' ): node.attr = 'nr_of_successful_entries' From bf3f2e4de4cdaf52f41e55ef65ab997a5c29e50d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 19 Mar 2023 11:16:54 +0100 Subject: [PATCH 259/360] Fix failing test --- tests/test_strategy_updater.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_strategy_updater.py b/tests/test_strategy_updater.py index d3bdd27b5..597d49fda 100644 --- a/tests/test_strategy_updater.py +++ b/tests/test_strategy_updater.py @@ -124,11 +124,11 @@ def test_strategy_updater_method_params(default_conf, caplog) -> None: instance_strategy_updater = StrategyUpdater() modified_code = instance_strategy_updater.update_code(""" def confirm_trade_exit(sell_reason: str): - nr_orders = trades.nr_of_successful_buys + nr_orders = trade.nr_of_successful_buys pass """) assert "exit_reason" in modified_code - assert "nr_orders = trades.nr_of_successful_entries" in modified_code + assert "nr_orders = trade.nr_of_successful_entries" in modified_code def test_strategy_updater_dicts(default_conf, caplog) -> None: From f5f151fcc54b2255e4dc167276500fd937b54f8c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 19 Mar 2023 15:06:56 +0100 Subject: [PATCH 260/360] Fix typing error --- freqtrade/rpc/api_server/deps.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/api_server/deps.py b/freqtrade/rpc/api_server/deps.py index eb41d728d..f5b1bcd74 100644 --- a/freqtrade/rpc/api_server/deps.py +++ b/freqtrade/rpc/api_server/deps.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, Iterator, Optional +from typing import Any, AsyncIterator, Dict, Optional from uuid import uuid4 from fastapi import Depends @@ -17,7 +17,7 @@ def get_rpc_optional() -> Optional[RPC]: return None -async def get_rpc() -> Optional[Iterator[RPC]]: +async def get_rpc() -> Optional[AsyncIterator[RPC]]: _rpc = get_rpc_optional() if _rpc: From 9ccc3e52ec0ae5da850452a6d703d47aa1a4ccba Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 19 Mar 2023 15:30:27 +0100 Subject: [PATCH 261/360] Simplify time in force code structure --- freqtrade/exchange/exchange.py | 4 +--- freqtrade/exchange/gate.py | 3 +-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 489dc1b68..e5f897c2a 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -60,7 +60,6 @@ class Exchange: _ft_has_default: Dict = { "stoploss_on_exchange": False, "order_time_in_force": ["GTC"], - "time_in_force_parameter": "timeInForce", "ohlcv_params": {}, "ohlcv_candle_limit": 500, "ohlcv_has_history": True, # Some exchanges (Kraken) don't provide history via ohlcv @@ -1034,8 +1033,7 @@ class Exchange: ) -> Dict: params = self._params.copy() if time_in_force != 'GTC' and ordertype != 'market': - param = self._ft_has.get('time_in_force_parameter', '') - params.update({param: time_in_force.upper()}) + params.update({'timeInForce': time_in_force.upper()}) if reduceOnly: params.update({'reduceOnly': True}) return params diff --git a/freqtrade/exchange/gate.py b/freqtrade/exchange/gate.py index 03b568460..bf6d5b59c 100644 --- a/freqtrade/exchange/gate.py +++ b/freqtrade/exchange/gate.py @@ -75,8 +75,7 @@ class Gate(Exchange): ) if ordertype == 'market' and self.trading_mode == TradingMode.FUTURES: params['type'] = 'market' - param = self._ft_has.get('time_in_force_parameter', '') - params.update({param: 'IOC'}) + params.update({'timeInForce': 'IOC'}) return params def get_trades_for_order(self, order_id: str, pair: str, since: datetime, From 3d91dd8a982df6c2768d63178e4fcd0be590d2ce Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 19 Mar 2023 15:36:35 +0100 Subject: [PATCH 262/360] Support post-only orders for Binance spot closes #8044 --- freqtrade/exchange/binance.py | 24 +++++++++++++++++++++++- tests/exchange/test_binance.py | 13 +++++++++++++ tests/exchange/test_exchange.py | 2 +- 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 9580bc690..a89c02631 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -7,6 +7,7 @@ from typing import Dict, List, Optional, Tuple import arrow import ccxt +from freqtrade.constants import BuySell from freqtrade.enums import CandleType, MarginMode, PriceType, TradingMode from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError from freqtrade.exchange import Exchange @@ -23,7 +24,7 @@ class Binance(Exchange): _ft_has: Dict = { "stoploss_on_exchange": True, "stoploss_order_types": {"limit": "stop_loss_limit"}, - "order_time_in_force": ['GTC', 'FOK', 'IOC'], + "order_time_in_force": ["GTC", "FOK", "IOC", "PO"], "ohlcv_candle_limit": 1000, "trades_pagination": "id", "trades_pagination_arg": "fromId", @@ -31,6 +32,7 @@ class Binance(Exchange): } _ft_has_futures: Dict = { "stoploss_order_types": {"limit": "stop", "market": "stop_market"}, + "order_time_in_force": ["GTC", "FOK", "IOC"], "tickers_have_price": False, "floor_leverage": True, "stop_price_type_field": "workingType", @@ -47,6 +49,26 @@ class Binance(Exchange): (TradingMode.FUTURES, MarginMode.ISOLATED) ] + def _get_params( + self, + side: BuySell, + ordertype: str, + leverage: float, + reduceOnly: bool, + time_in_force: str = 'GTC', + ) -> Dict: + params = super()._get_params(side, ordertype, leverage, reduceOnly, time_in_force) + if ( + time_in_force == 'PO' + and ordertype != 'market' + and self.trading_mode == TradingMode.SPOT + # Only spot can do post only orders + ): + params.pop('timeInForce') + params['postOnly'] = True + + return params + def get_tickers(self, symbols: Optional[List[str]] = None, cached: bool = False) -> Tickers: tickers = super().get_tickers(symbols=symbols, cached=cached) if self.trading_mode == TradingMode.FUTURES: diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index 616910682..ba786bb3b 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -11,6 +11,19 @@ from tests.conftest import EXMS, get_mock_coro, get_patched_exchange, log_has_re from tests.exchange.test_exchange import ccxt_exceptionhandlers +@pytest.mark.parametrize('side,type,time_in_force,expected', [ + ('buy', 'limit', 'gtc', {'timeInForce': 'GTC'}), + ('buy', 'limit', 'IOC', {'timeInForce': 'IOC'}), + ('buy', 'market', 'IOC', {}), + ('buy', 'limit', 'PO', {'postOnly': True}), + ('sell', 'limit', 'PO', {'postOnly': True}), + ('sell', 'market', 'PO', {}), + ]) +def test__get_params_binance(default_conf, mocker, side, type, time_in_force, expected): + exchange = get_patched_exchange(mocker, default_conf, id='binance') + assert exchange._get_params(side, type, 1, False, time_in_force) == expected + + @pytest.mark.parametrize('trademode', [TradingMode.FUTURES, TradingMode.SPOT]) @pytest.mark.parametrize('limitratio,expected,side', [ (None, 220 * 0.99, "sell"), diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index d7f6a8b90..7c48f1c9d 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -3387,7 +3387,7 @@ def test_merge_ft_has_dict(default_conf, mocker): ex = Binance(default_conf) assert ex._ft_has != Exchange._ft_has_default assert ex.get_option('stoploss_on_exchange') - assert ex.get_option('order_time_in_force') == ['GTC', 'FOK', 'IOC'] + assert ex.get_option('order_time_in_force') == ['GTC', 'FOK', 'IOC', 'PO'] assert ex.get_option('trades_pagination') == 'id' assert ex.get_option('trades_pagination_arg') == 'fromId' From 236499a195d61bb2f3e54917a42b4a37af612ea5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 19 Mar 2023 15:47:42 +0100 Subject: [PATCH 263/360] Reorder push logic for ghcr --- build_helpers/publish_docker_arm64.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build_helpers/publish_docker_arm64.sh b/build_helpers/publish_docker_arm64.sh index 696f5bc48..a6ecdbee6 100755 --- a/build_helpers/publish_docker_arm64.sh +++ b/build_helpers/publish_docker_arm64.sh @@ -92,10 +92,10 @@ chmod a+rwx .crane echo "${GHCR_TOKEN}" | crane auth login ghcr.io -u "${GHCR_USERNAME}" --password-stdin -crane copy ${IMAGE_NAME}:${TAG} ${GHCR_IMAGE_NAME}:${TAG} -crane copy ${IMAGE_NAME}:${TAG_PLOT} ${GHCR_IMAGE_NAME}:${TAG_PLOT} -crane copy ${IMAGE_NAME}:${TAG_FREQAI} ${GHCR_IMAGE_NAME}:${TAG_FREQAI} crane copy ${IMAGE_NAME}:${TAG_FREQAI_RL} ${GHCR_IMAGE_NAME}:${TAG_FREQAI_RL} +crane copy ${IMAGE_NAME}:${TAG_FREQAI} ${GHCR_IMAGE_NAME}:${TAG_FREQAI} +crane copy ${IMAGE_NAME}:${TAG_PLOT} ${GHCR_IMAGE_NAME}:${TAG_PLOT} +crane copy ${IMAGE_NAME}:${TAG} ${GHCR_IMAGE_NAME}:${TAG} # Tag as latest for develop builds if [ "${TAG}" = "develop" ]; then From 222ecdecd29120e07ae1b8dcebea0e6525206b77 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 19 Mar 2023 17:50:08 +0100 Subject: [PATCH 264/360] Improve code quality --- freqtrade/commands/analyze_commands.py | 4 ++-- freqtrade/optimize/hyperopt_tools.py | 3 +-- freqtrade/rpc/telegram.py | 4 ++-- freqtrade/rpc/webhook.py | 2 +- tests/data/test_btanalysis.py | 2 +- tests/data/test_history.py | 2 +- tests/optimize/test_optimize_reports.py | 4 ++-- tests/rpc/test_rpc_telegram.py | 8 ++++---- tests/strategy/test_strategy_loading.py | 2 +- 9 files changed, 15 insertions(+), 16 deletions(-) diff --git a/freqtrade/commands/analyze_commands.py b/freqtrade/commands/analyze_commands.py index 20afa7ffd..e928ccad7 100644 --- a/freqtrade/commands/analyze_commands.py +++ b/freqtrade/commands/analyze_commands.py @@ -40,8 +40,8 @@ def setup_analyze_configuration(args: Dict[str, Any], method: RunMode) -> Dict[s if (not Path(signals_file).exists()): raise OperationalException( - (f"Cannot find latest backtest signals file: {signals_file}." - "Run backtesting with `--export signals`.") + f"Cannot find latest backtest signals file: {signals_file}." + "Run backtesting with `--export signals`." ) return config diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index cf0650f7d..93efbf644 100644 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -1,4 +1,3 @@ -import io import logging from copy import deepcopy from datetime import datetime, timezone @@ -464,7 +463,7 @@ class HyperoptTools(): return try: - io.open(csv_file, 'w+').close() + Path(csv_file).open('w+').close() except IOError: logger.error(f"Failed to create CSV file: {csv_file}") return diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 0c0b24f00..cb219eef1 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -1340,7 +1340,7 @@ class Telegram(RPCHandler): message = tabulate({k: [v] for k, v in counts.items()}, headers=['current', 'max', 'total stake'], tablefmt='simple') - message = "
{}
".format(message) + message = f"
{message}
" logger.debug(message) self._send_msg(message, parse_mode=ParseMode.HTML, reload_able=True, callback_path="update_count", @@ -1642,7 +1642,7 @@ class Telegram(RPCHandler): ]) else: reply_markup = InlineKeyboardMarkup([[]]) - msg += "\nUpdated: {}".format(datetime.now().ctime()) + msg += f"\nUpdated: {datetime.now().ctime()}" if not query.message: return chat_id = query.message.chat_id diff --git a/freqtrade/rpc/webhook.py b/freqtrade/rpc/webhook.py index 0967de70d..118ebed88 100644 --- a/freqtrade/rpc/webhook.py +++ b/freqtrade/rpc/webhook.py @@ -113,7 +113,7 @@ class Webhook(RPCHandler): response = post(self._url, data=payload['data'], headers={'Content-Type': 'text/plain'}) else: - raise NotImplementedError('Unknown format: {}'.format(self._format)) + raise NotImplementedError(f'Unknown format: {self._format}') # Throw a RequestException if the post was not successful response.raise_for_status() diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index 345e3c299..2c5515f7c 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -98,7 +98,7 @@ def test_load_backtest_data_new_format(testdatadir): assert bt_data.equals(bt_data3) with pytest.raises(ValueError, match=r"File .* does not exist\."): - load_backtest_data(str("filename") + "nofile") + load_backtest_data("filename" + "nofile") with pytest.raises(ValueError, match=r"Unknown dataformat."): load_backtest_data(testdatadir / "backtest_results" / LAST_BT_RESULT_FN) diff --git a/tests/data/test_history.py b/tests/data/test_history.py index c967f0c89..24ad8bcc9 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -409,7 +409,7 @@ def test_init_with_refresh(default_conf, mocker) -> None: def test_file_dump_json_tofile(testdatadir) -> None: - file = testdatadir / 'test_{id}.json'.format(id=str(uuid.uuid4())) + file = testdatadir / f'test_{uuid.uuid4()}.json' data = {'bar': 'foo'} # check the file we will create does not exist diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index f71e6c492..0cc32baaf 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -236,7 +236,7 @@ def test_store_backtest_candles(testdatadir, mocker): assert dump_mock.call_count == 1 assert isinstance(dump_mock.call_args_list[0][0][0], Path) - assert str(dump_mock.call_args_list[0][0][0]).endswith(str('_signals.pkl')) + assert str(dump_mock.call_args_list[0][0][0]).endswith('_signals.pkl') dump_mock.reset_mock() # mock file exporting @@ -245,7 +245,7 @@ def test_store_backtest_candles(testdatadir, mocker): assert dump_mock.call_count == 1 assert isinstance(dump_mock.call_args_list[0][0][0], Path) # result will be testdatadir / testresult-_signals.pkl - assert str(dump_mock.call_args_list[0][0][0]).endswith(str('_signals.pkl')) + assert str(dump_mock.call_args_list[0][0][0]).endswith('_signals.pkl') dump_mock.reset_mock() diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index b1859f581..7dabe9dfe 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -652,7 +652,7 @@ def test_monthly_handle(default_conf_usdt, update, ticker, fee, mocker, time_mac # The one-digit months should contain a zero, Eg: September 2021 = "2021-09" # Since we loaded the last 12 months, any month should appear - assert str('-09') in msg_mock.call_args_list[0][0][0] + assert '-09' in msg_mock.call_args_list[0][0][0] # Try invalid data msg_mock.reset_mock() @@ -671,7 +671,7 @@ def test_monthly_handle(default_conf_usdt, update, ticker, fee, mocker, time_mac context = MagicMock() context.args = ["february"] telegram._monthly(update=update, context=context) - assert str('Monthly Profit over the last 6 months:') in msg_mock.call_args_list[0][0][0] + assert 'Monthly Profit over the last 6 months:' in msg_mock.call_args_list[0][0][0] def test_profit_handle(default_conf_usdt, update, ticker_usdt, ticker_sell_up, fee, @@ -1730,14 +1730,14 @@ def test_version_handle(default_conf, update, mocker) -> None: telegram._version(update=update, context=MagicMock()) assert msg_mock.call_count == 1 - assert '*Version:* `{}`'.format(__version__) in msg_mock.call_args_list[0][0][0] + assert f'*Version:* `{__version__}`' in msg_mock.call_args_list[0][0][0] msg_mock.reset_mock() freqtradebot.strategy.version = lambda: '1.1.1' telegram._version(update=update, context=MagicMock()) assert msg_mock.call_count == 1 - assert '*Version:* `{}`'.format(__version__) in msg_mock.call_args_list[0][0][0] + assert f'*Version:* `{__version__}`' in msg_mock.call_args_list[0][0][0] assert '*Strategy version: * `1.1.1`' in msg_mock.call_args_list[0][0][0] diff --git a/tests/strategy/test_strategy_loading.py b/tests/strategy/test_strategy_loading.py index 98185e152..4cdb35936 100644 --- a/tests/strategy/test_strategy_loading.py +++ b/tests/strategy/test_strategy_loading.py @@ -69,7 +69,7 @@ def test_load_strategy(default_conf, dataframe_1m): def test_load_strategy_base64(dataframe_1m, caplog, default_conf): filepath = Path(__file__).parents[2] / 'freqtrade/templates/sample_strategy.py' encoded_string = urlsafe_b64encode(filepath.read_bytes()).decode("utf-8") - default_conf.update({'strategy': 'SampleStrategy:{}'.format(encoded_string)}) + default_conf.update({'strategy': f'SampleStrategy:{encoded_string}'}) strategy = StrategyResolver.load_strategy(default_conf) assert 'rsi' in strategy.advise_indicators(dataframe_1m, {'pair': 'ETH/BTC'}) From c92f28bf6f6e04dee47615d7022da3e901332a2b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 19 Mar 2023 17:57:56 +0100 Subject: [PATCH 265/360] ruff: Activate UP ruleset --- freqtrade/configuration/config_validation.py | 5 +---- freqtrade/freqai/RL/TensorboardCallback.py | 2 +- freqtrade/optimize/hyperopt_tools.py | 2 +- freqtrade/vendor/qtpylib/indicators.py | 8 -------- pyproject.toml | 8 ++++++-- 5 files changed, 9 insertions(+), 16 deletions(-) diff --git a/freqtrade/configuration/config_validation.py b/freqtrade/configuration/config_validation.py index 606f081ef..0ee48cf91 100644 --- a/freqtrade/configuration/config_validation.py +++ b/freqtrade/configuration/config_validation.py @@ -27,10 +27,7 @@ def _extend_validator(validator_class): if 'default' in subschema: instance.setdefault(prop, subschema['default']) - for error in validate_properties( - validator, properties, instance, schema, - ): - yield error + yield from validate_properties(validator, properties, instance, schema) return validators.extend( validator_class, {'properties': set_defaults} diff --git a/freqtrade/freqai/RL/TensorboardCallback.py b/freqtrade/freqai/RL/TensorboardCallback.py index 1828319cd..7f8c76956 100644 --- a/freqtrade/freqai/RL/TensorboardCallback.py +++ b/freqtrade/freqai/RL/TensorboardCallback.py @@ -13,7 +13,7 @@ class TensorboardCallback(BaseCallback): episodic summary reports. """ def __init__(self, verbose=1, actions: Type[Enum] = BaseActions): - super(TensorboardCallback, self).__init__(verbose) + super().__init__(verbose) self.model: Any = None self.logger = None # type: Any self.training_env: BaseEnvironment = None # type: ignore diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index 93efbf644..e2133a956 100644 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -464,7 +464,7 @@ class HyperoptTools(): try: Path(csv_file).open('w+').close() - except IOError: + except OSError: logger.error(f"Failed to create CSV file: {csv_file}") return diff --git a/freqtrade/vendor/qtpylib/indicators.py b/freqtrade/vendor/qtpylib/indicators.py index 3da4f038d..63797d462 100644 --- a/freqtrade/vendor/qtpylib/indicators.py +++ b/freqtrade/vendor/qtpylib/indicators.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -# # QTPyLib: Quantitative Trading Python Library # https://github.com/ranaroussi/qtpylib # @@ -18,7 +16,6 @@ # limitations under the License. # -import sys import warnings from datetime import datetime, timedelta @@ -27,11 +24,6 @@ import pandas as pd from pandas.core.base import PandasObject -# ============================================= -# check min, python version -if sys.version_info < (3, 4): - raise SystemError("QTPyLib requires Python version >= 3.4") - # ============================================= warnings.simplefilter(action="ignore", category=RuntimeWarning) diff --git a/pyproject.toml b/pyproject.toml index 71687961d..81ab50d40 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,15 +68,19 @@ target-version = "py38" extend-select = [ "C90", # mccabe # "N", # pep8-naming - # "UP", # pyupgrade + "UP", # pyupgrade "TID", # flake8-tidy-imports # "EXE", # flake8-executable "YTT", # flake8-2020 + # "S", # flake8-bandit # "DTZ", # flake8-datetimez # "RSE", # flake8-raise # "TCH", # flake8-type-checking - "PTH", # flake8-use-pathlib + "PTH", # flake8-use-pathlib ] [tool.ruff.mccabe] max-complexity = 12 + +[tool.ruff.per-file-ignores] +"tests/*" = ["S"] From ce3efa8f00aac5fe7c733271cdcf3c27e0ab0d09 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 19 Mar 2023 18:05:08 +0100 Subject: [PATCH 266/360] Remove pointless asserts --- freqtrade/optimize/backtesting.py | 4 ---- pyproject.toml | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 5e1e9b48a..315b3b9db 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -442,10 +442,6 @@ class Backtesting: # Worst case: price ticks tiny bit above open and dives down. stop_rate = row[OPEN_IDX] * (1 - side_1 * abs( (trade.stop_loss_pct or 0.0) / leverage)) - if is_short: - assert stop_rate > row[LOW_IDX] - else: - assert stop_rate < row[HIGH_IDX] # Limit lower-end to candle low to avoid exits below the low. # This still remains "worst case" - but "worst realistic case". diff --git a/pyproject.toml b/pyproject.toml index 81ab50d40..c3ca9e1b0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,7 +68,7 @@ target-version = "py38" extend-select = [ "C90", # mccabe # "N", # pep8-naming - "UP", # pyupgrade + "UP", # pyupgrade "TID", # flake8-tidy-imports # "EXE", # flake8-executable "YTT", # flake8-2020 From a2ce288241b8a77523b9608c1418fb306a8d95c0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 7 Jun 2022 21:05:15 +0200 Subject: [PATCH 267/360] Add okx stoploss on exchange (non-working for futures). --- freqtrade/exchange/exchange.py | 1 + freqtrade/exchange/okx.py | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index e5f897c2a..728e997f1 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1193,6 +1193,7 @@ class Exchange: try: params = self._get_stop_params(side=side, ordertype=ordertype, stop_price=stop_price_norm) + # TODO: reduceOnly is invalid for OKX stop orders if self.trading_mode == TradingMode.FUTURES: params['reduceOnly'] = True if 'stoploss_price_type' in order_types and 'stop_price_type_field' in self._ft_has: diff --git a/freqtrade/exchange/okx.py b/freqtrade/exchange/okx.py index e7d658d24..048d4cad5 100644 --- a/freqtrade/exchange/okx.py +++ b/freqtrade/exchange/okx.py @@ -24,6 +24,8 @@ class Okx(Exchange): "ohlcv_candle_limit": 100, # Warning, special case with data prior to X months "mark_ohlcv_timeframe": "4h", "funding_fee_timeframe": "8h", + "stoploss_order_types": {"limit": "limit"}, + "stoploss_on_exchange": True, } _ft_has_futures: Dict = { "tickers_have_quoteVolume": False, @@ -157,3 +159,26 @@ class Okx(Exchange): pair_tiers = self._leverage_tiers[pair] return pair_tiers[-1]['maxNotional'] / leverage + + def _get_stop_params(self, side: BuySell, ordertype: str, stop_price: float) -> Dict: + + params = super()._get_stop_params(side, ordertype, stop_price) + if self.trading_mode == TradingMode.FUTURES and self.margin_mode: + params['tdMode'] = self.margin_mode.value + params['posSide'] = self._get_posSide(side, True) + return params + + def fetch_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict: + # TODO: This does not work until the algo-order is actually triggered! + return self.fetch_order( + order_id=order_id, + pair=pair, + params={'stop': True} + ) + + def cancel_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict: + return self.cancel_order( + order_id=order_id, + pair=pair, + params={'stop': True} + ) From df20757d2116a52265edfbe4624ed545eddb9a23 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 8 Nov 2022 19:58:39 +0100 Subject: [PATCH 268/360] OKX stop: implement proper stoploss fetching --- freqtrade/exchange/okx.py | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/freqtrade/exchange/okx.py b/freqtrade/exchange/okx.py index 048d4cad5..8199bd0ea 100644 --- a/freqtrade/exchange/okx.py +++ b/freqtrade/exchange/okx.py @@ -6,7 +6,8 @@ import ccxt from freqtrade.constants import BuySell from freqtrade.enums import CandleType, MarginMode, TradingMode from freqtrade.enums.pricetype import PriceType -from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError +from freqtrade.exceptions import (DDosProtection, OperationalException, RetryableOrderError, + TemporaryError) from freqtrade.exchange import Exchange, date_minus_candles from freqtrade.exchange.common import retrier @@ -169,12 +170,23 @@ class Okx(Exchange): return params def fetch_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict: - # TODO: This does not work until the algo-order is actually triggered! - return self.fetch_order( - order_id=order_id, - pair=pair, - params={'stop': True} - ) + params1 = {'stop': True, 'ordType': 'trigger'} + for method in (self._api.fetch_open_orders, self._api.fetch_closed_orders, + self._api.fetch_canceled_orders): + try: + orders = method(pair, params=params1) + orders_f = [order for order in orders if order['id'] == order_id] + if orders_f: + order = orders_f[0] + if (order['status'] == 'closed' + and order.get('info', {}).get('ordId') is not None): + # Once a order triggered, we fetch the regular followup order. + return self.fetch_order(order['info']['ordId'], pair) + return order + except ccxt.BaseError: + logger.exception() + raise RetryableOrderError( + f'StoplossOrder not found (pair: {pair} id: {order_id}).') def cancel_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict: return self.cancel_order( From 6c5dc7e0a9b8643dca66e0b1df79696a3eb7cd90 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 8 Nov 2022 20:24:26 +0100 Subject: [PATCH 269/360] OKX: improve stop order handling --- freqtrade/exchange/okx.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/freqtrade/exchange/okx.py b/freqtrade/exchange/okx.py index 8199bd0ea..4ff7f283b 100644 --- a/freqtrade/exchange/okx.py +++ b/freqtrade/exchange/okx.py @@ -1,5 +1,5 @@ import logging -from typing import Dict, List, Optional, Tuple +from typing import Any, Dict, List, Optional, Tuple import ccxt @@ -10,6 +10,7 @@ from freqtrade.exceptions import (DDosProtection, OperationalException, Retryabl TemporaryError) from freqtrade.exchange import Exchange, date_minus_candles from freqtrade.exchange.common import retrier +from freqtrade.misc import safe_value_fallback2 logger = logging.getLogger(__name__) @@ -179,15 +180,26 @@ class Okx(Exchange): if orders_f: order = orders_f[0] if (order['status'] == 'closed' - and order.get('info', {}).get('ordId') is not None): + and (real_order_id := order.get('info', {}).get('ordId')) is not None): # Once a order triggered, we fetch the regular followup order. - return self.fetch_order(order['info']['ordId'], pair) + order_reg = self.fetch_order(real_order_id, pair) + self._log_exchange_response('fetch_stoploss_order1', order_reg) + order_reg['id_stop'] = order_reg['id'] + order_reg['id'] = order_id + order_reg['type'] = 'stop' + order_reg['status_stop'] = 'triggered' + return order_reg return order except ccxt.BaseError: logger.exception() raise RetryableOrderError( f'StoplossOrder not found (pair: {pair} id: {order_id}).') + def get_order_id_conditional(self, order: Dict[str, Any]) -> str: + if order['type'] == 'stop': + return safe_value_fallback2(order, order, 'id_stop', 'id') + return order['id'] + def cancel_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict: return self.cancel_order( order_id=order_id, From d84ece7258a0082628f4459c278851fcac7374c9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 9 Nov 2022 20:17:10 +0100 Subject: [PATCH 270/360] Use conditional orders for stop orders --- freqtrade/exchange/okx.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/freqtrade/exchange/okx.py b/freqtrade/exchange/okx.py index 4ff7f283b..5acf039cb 100644 --- a/freqtrade/exchange/okx.py +++ b/freqtrade/exchange/okx.py @@ -164,14 +164,17 @@ class Okx(Exchange): def _get_stop_params(self, side: BuySell, ordertype: str, stop_price: float) -> Dict: - params = super()._get_stop_params(side, ordertype, stop_price) + params = self._params.copy() + # Verify if stopPrice works for your exchange! + params.update({'stopLossPrice': stop_price}) + if self.trading_mode == TradingMode.FUTURES and self.margin_mode: params['tdMode'] = self.margin_mode.value params['posSide'] = self._get_posSide(side, True) return params def fetch_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict: - params1 = {'stop': True, 'ordType': 'trigger'} + params1 = {'stop': True, 'ordType': 'conditional'} for method in (self._api.fetch_open_orders, self._api.fetch_closed_orders, self._api.fetch_canceled_orders): try: @@ -204,5 +207,5 @@ class Okx(Exchange): return self.cancel_order( order_id=order_id, pair=pair, - params={'stop': True} + params={'ordType': 'conditional'} ) From 224f289ec8fc6d91ee32e2a3d90f4749d4da9ca0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 12 Mar 2023 15:19:56 +0100 Subject: [PATCH 271/360] OKX Stop: Add some more okx specific logic --- freqtrade/exchange/exchange.py | 1 - freqtrade/exchange/okx.py | 31 +++++++++++++++++++++++++++++-- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 728e997f1..e5f897c2a 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1193,7 +1193,6 @@ class Exchange: try: params = self._get_stop_params(side=side, ordertype=ordertype, stop_price=stop_price_norm) - # TODO: reduceOnly is invalid for OKX stop orders if self.trading_mode == TradingMode.FUTURES: params['reduceOnly'] = True if 'stoploss_price_type' in order_types and 'stop_price_type_field' in self._ft_has: diff --git a/freqtrade/exchange/okx.py b/freqtrade/exchange/okx.py index 5acf039cb..5acfe7fcc 100644 --- a/freqtrade/exchange/okx.py +++ b/freqtrade/exchange/okx.py @@ -173,7 +173,30 @@ class Okx(Exchange): params['posSide'] = self._get_posSide(side, True) return params + def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool: + """ + OKX uses non-default stoploss price naming. + """ + if not self._ft_has.get('stoploss_on_exchange'): + raise OperationalException(f"stoploss is not implemented for {self.name}.") + + return ( + order.get('stopLossPrice', None) is None + or ((side == "sell" and stop_loss > float(order['stopLossPrice'])) or + (side == "buy" and stop_loss < float(order['stopLossPrice']))) + ) + def fetch_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict: + if self._config['dry_run']: + return self.fetch_dry_run_order(order_id) + + try: + params1 = {'stop': True} + order_reg = self._api.fetch_order(order_id, pair, params=params1) + self._log_exchange_response('fetch_stoploss_order1', order_reg) + return order_reg + except ccxt.OrderNotFound: + pass params1 = {'stop': True, 'ordType': 'conditional'} for method in (self._api.fetch_open_orders, self._api.fetch_closed_orders, self._api.fetch_canceled_orders): @@ -192,9 +215,10 @@ class Okx(Exchange): order_reg['type'] = 'stop' order_reg['status_stop'] = 'triggered' return order_reg + order['type'] = 'stoploss' return order except ccxt.BaseError: - logger.exception() + pass raise RetryableOrderError( f'StoplossOrder not found (pair: {pair} id: {order_id}).') @@ -204,8 +228,11 @@ class Okx(Exchange): return order['id'] def cancel_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict: + params1 = {'stop': True} + # 'ordType': 'conditional' + # return self.cancel_order( order_id=order_id, pair=pair, - params={'ordType': 'conditional'} + params=params1, ) From a7c7f720c0791d3ea2cdd5655626a8eab827813c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 19 Mar 2023 20:03:34 +0100 Subject: [PATCH 272/360] Add test for okx fetch_stop --- freqtrade/exchange/okx.py | 2 +- tests/exchange/test_okx.py | 54 +++++++++++++++++++++++++++++++++++++- 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/okx.py b/freqtrade/exchange/okx.py index 5acfe7fcc..7de110acf 100644 --- a/freqtrade/exchange/okx.py +++ b/freqtrade/exchange/okx.py @@ -212,7 +212,7 @@ class Okx(Exchange): self._log_exchange_response('fetch_stoploss_order1', order_reg) order_reg['id_stop'] = order_reg['id'] order_reg['id'] = order_id - order_reg['type'] = 'stop' + order_reg['type'] = 'stoploss' order_reg['status_stop'] = 'triggered' return order_reg order['type'] = 'stoploss' diff --git a/tests/exchange/test_okx.py b/tests/exchange/test_okx.py index fce77f4c7..30e23619b 100644 --- a/tests/exchange/test_okx.py +++ b/tests/exchange/test_okx.py @@ -2,11 +2,13 @@ from datetime import datetime, timedelta, timezone from pathlib import Path from unittest.mock import MagicMock, PropertyMock +import ccxt import pytest from freqtrade.enums import CandleType, MarginMode, TradingMode +from freqtrade.exceptions import RetryableOrderError from freqtrade.exchange.exchange import timeframe_to_minutes -from tests.conftest import get_mock_coro, get_patched_exchange, log_has +from tests.conftest import EXMS, get_mock_coro, get_patched_exchange, log_has from tests.exchange.test_exchange import ccxt_exceptionhandlers @@ -476,3 +478,53 @@ def test_load_leverage_tiers_okx(default_conf, mocker, markets, tmpdir, caplog, exchange.load_leverage_tiers() assert log_has(logmsg, caplog) + + +@pytest.mark.usefixtures("init_persistence") +def test_fetch_stoploss_order_okx(default_conf, mocker): + default_conf['dry_run'] = False + api_mock = MagicMock() + api_mock.fetch_order = MagicMock() + + exchange = get_patched_exchange(mocker, default_conf, api_mock, id='okx') + + exchange.fetch_stoploss_order('1234', 'ETH/BTC') + assert api_mock.fetch_order.call_count == 1 + assert api_mock.fetch_order.call_args_list[0][0][0] == '1234' + assert api_mock.fetch_order.call_args_list[0][0][1] == 'ETH/BTC' + assert api_mock.fetch_order.call_args_list[0][1]['params'] == {'stop': True} + + api_mock.fetch_order = MagicMock(side_effect=ccxt.OrderNotFound) + api_mock.fetch_open_orders = MagicMock(return_value=[]) + api_mock.fetch_closed_orders = MagicMock(return_value=[]) + api_mock.fetch_canceled_orders = MagicMock(creturn_value=[]) + + with pytest.raises(RetryableOrderError): + exchange.fetch_stoploss_order('1234', 'ETH/BTC') + assert api_mock.fetch_order.call_count == 1 + assert api_mock.fetch_open_orders.call_count == 1 + assert api_mock.fetch_closed_orders.call_count == 1 + assert api_mock.fetch_canceled_orders.call_count == 1 + + api_mock.fetch_order.reset_mock() + api_mock.fetch_open_orders.reset_mock() + api_mock.fetch_closed_orders.reset_mock() + api_mock.fetch_canceled_orders.reset_mock() + + api_mock.fetch_closed_orders = MagicMock(return_value=[ + { + 'id': '1234', + 'status': 'closed', + 'info': {'ordId': '123455'} + } + ]) + mocker.patch(f"{EXMS}.fetch_order", MagicMock(return_value={'id': '123455'})) + resp = exchange.fetch_stoploss_order('1234', 'ETH/BTC') + assert api_mock.fetch_order.call_count == 1 + assert api_mock.fetch_open_orders.call_count == 1 + assert api_mock.fetch_closed_orders.call_count == 1 + assert api_mock.fetch_canceled_orders.call_count == 0 + + assert resp['id'] == '1234' + assert resp['id_stop'] == '123455' + assert resp['type'] == 'stoploss' From fb0e824a83fb953b4d6d61a215df72e1cd6b74b9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Mar 2023 03:56:45 +0000 Subject: [PATCH 273/360] Bump nbconvert from 7.2.9 to 7.2.10 Bumps [nbconvert](https://github.com/jupyter/nbconvert) from 7.2.9 to 7.2.10. - [Release notes](https://github.com/jupyter/nbconvert/releases) - [Changelog](https://github.com/jupyter/nbconvert/blob/main/CHANGELOG.md) - [Commits](https://github.com/jupyter/nbconvert/compare/v7.2.9...v7.2.10) --- updated-dependencies: - dependency-name: nbconvert dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 6d076777f..0bdb6702e 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -22,7 +22,7 @@ time-machine==2.9.0 httpx==0.23.3 # Convert jupyter notebooks to markdown documents -nbconvert==7.2.9 +nbconvert==7.2.10 # mypy types types-cachetools==5.3.0.4 From 5ade5777e8cbef0619c3d5b33041401b082be93a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Mar 2023 03:56:49 +0000 Subject: [PATCH 274/360] Bump filelock from 3.9.0 to 3.10.0 Bumps [filelock](https://github.com/tox-dev/py-filelock) from 3.9.0 to 3.10.0. - [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.9.0...3.10.0) --- updated-dependencies: - dependency-name: filelock dependency-type: direct:production update-type: version-update:semver-minor ... 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 904b5d661..4d86da2b6 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -5,5 +5,5 @@ scipy==1.10.1 scikit-learn==1.1.3 scikit-optimize==0.9.0 -filelock==3.9.0 +filelock==3.10.0 progressbar2==4.2.0 From 47e84ad106a34e3049a8a5412d2fb7155cff5a44 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Mar 2023 03:56:54 +0000 Subject: [PATCH 275/360] Bump python-rapidjson from 1.9 to 1.10 Bumps [python-rapidjson](https://github.com/python-rapidjson/python-rapidjson) from 1.9 to 1.10. - [Release notes](https://github.com/python-rapidjson/python-rapidjson/releases) - [Changelog](https://github.com/python-rapidjson/python-rapidjson/blob/master/CHANGES.rst) - [Commits](https://github.com/python-rapidjson/python-rapidjson/compare/v1.9...v1.10) --- updated-dependencies: - dependency-name: python-rapidjson dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 9e17424f5..311285217 100644 --- a/requirements.txt +++ b/requirements.txt @@ -26,7 +26,7 @@ pyarrow==11.0.0; platform_machine != 'armv7l' py_find_1st==1.1.5 # Load ticker files 30% faster -python-rapidjson==1.9 +python-rapidjson==1.10 # Properly format api responses orjson==3.8.7 From a43502093dc18897ba4dfd2110cb5b556f1a3482 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Mar 2023 03:57:07 +0000 Subject: [PATCH 276/360] Bump sqlalchemy from 2.0.5.post1 to 2.0.7 Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 2.0.5.post1 to 2.0.7. - [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases) - [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/main/CHANGES.rst) - [Commits](https://github.com/sqlalchemy/sqlalchemy/commits) --- updated-dependencies: - dependency-name: sqlalchemy dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 9e17424f5..36861d029 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ pandas-ta==0.3.14b ccxt==2.9.12 cryptography==39.0.2 aiohttp==3.8.4 -SQLAlchemy==2.0.5.post1 +SQLAlchemy==2.0.7 python-telegram-bot==13.15 arrow==1.2.3 cachetools==4.2.2 From 7d1559f319ae3987fa155e6b85cd1224f11e452c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Mar 2023 03:57:13 +0000 Subject: [PATCH 277/360] Bump mkdocs-material from 9.1.2 to 9.1.3 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 9.1.2 to 9.1.3. - [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/9.1.2...9.1.3) --- 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 d384a7ec5..110373844 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,6 +1,6 @@ markdown==3.3.7 mkdocs==1.4.2 -mkdocs-material==9.1.2 +mkdocs-material==9.1.3 mdx_truly_sane_lists==1.3 pymdown-extensions==9.10 jinja2==3.1.2 From fc7c8cce3cbfea9c905ecc8a503011031f7839c4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Mar 2023 03:57:28 +0000 Subject: [PATCH 278/360] Bump uvicorn from 0.21.0 to 0.21.1 Bumps [uvicorn](https://github.com/encode/uvicorn) from 0.21.0 to 0.21.1. - [Release notes](https://github.com/encode/uvicorn/releases) - [Changelog](https://github.com/encode/uvicorn/blob/master/CHANGELOG.md) - [Commits](https://github.com/encode/uvicorn/compare/0.21.0...0.21.1) --- updated-dependencies: - dependency-name: uvicorn 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 9e17424f5..4eb05db12 100644 --- a/requirements.txt +++ b/requirements.txt @@ -36,7 +36,7 @@ sdnotify==0.3.2 # API Server fastapi==0.94.0 pydantic==1.10.6 -uvicorn==0.21.0 +uvicorn==0.21.1 pyjwt==2.6.0 aiofiles==23.1.0 psutil==5.9.4 From 4543a1fe02a31904afca5016035f89571f5ba3da Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Mar 2023 03:57:33 +0000 Subject: [PATCH 279/360] Bump pre-commit from 3.1.1 to 3.2.0 Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 3.1.1 to 3.2.0. - [Release notes](https://github.com/pre-commit/pre-commit/releases) - [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md) - [Commits](https://github.com/pre-commit/pre-commit/compare/v3.1.1...v3.2.0) --- updated-dependencies: - dependency-name: pre-commit dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 6d076777f..c0dc815e2 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -9,7 +9,7 @@ coveralls==3.3.1 ruff==0.0.255 mypy==1.1.1 -pre-commit==3.1.1 +pre-commit==3.2.0 pytest==7.2.2 pytest-asyncio==0.20.3 pytest-cov==4.0.0 From 29b9be9bd0c14199aaf0c36a59207c06a6c3c79c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Mar 2023 03:57:47 +0000 Subject: [PATCH 280/360] Bump ruff from 0.0.255 to 0.0.257 Bumps [ruff](https://github.com/charliermarsh/ruff) from 0.0.255 to 0.0.257. - [Release notes](https://github.com/charliermarsh/ruff/releases) - [Changelog](https://github.com/charliermarsh/ruff/blob/main/BREAKING_CHANGES.md) - [Commits](https://github.com/charliermarsh/ruff/compare/v0.0.255...v0.0.257) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 6d076777f..8780a6d22 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -7,7 +7,7 @@ -r docs/requirements-docs.txt coveralls==3.3.1 -ruff==0.0.255 +ruff==0.0.257 mypy==1.1.1 pre-commit==3.1.1 pytest==7.2.2 From c78342b1943cf58bc2e78a94165d5281d9205cfa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Mar 2023 03:58:15 +0000 Subject: [PATCH 281/360] Bump pypa/gh-action-pypi-publish from 1.7.1 to 1.8.1 Bumps [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) from 1.7.1 to 1.8.1. - [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases) - [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/v1.7.1...v1.8.1) --- updated-dependencies: - dependency-name: pypa/gh-action-pypi-publish dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 663cfb1be..904387fb2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -425,7 +425,7 @@ jobs: python setup.py sdist bdist_wheel - name: Publish to PyPI (Test) - uses: pypa/gh-action-pypi-publish@v1.7.1 + uses: pypa/gh-action-pypi-publish@v1.8.1 if: (github.event_name == 'release') with: user: __token__ @@ -433,7 +433,7 @@ jobs: repository_url: https://test.pypi.org/legacy/ - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@v1.7.1 + uses: pypa/gh-action-pypi-publish@v1.8.1 if: (github.event_name == 'release') with: user: __token__ From dcca51985d89d5b39e0497dd75249b2387639b2a Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 20 Mar 2023 06:27:39 +0100 Subject: [PATCH 282/360] sqlalchemy - pre-commit --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bc2e0bc0d..ca3da8e90 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,7 +18,7 @@ repos: - types-requests==2.28.11.15 - types-tabulate==0.9.0.1 - types-python-dateutil==2.8.19.10 - - SQLAlchemy==2.0.5.post1 + - SQLAlchemy==2.0.7 # stages: [push] - repo: https://github.com/pycqa/isort From 2de5a59d890e2345eb1a935554b3453ab4047416 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 20 Mar 2023 06:38:42 +0100 Subject: [PATCH 283/360] Add test for dry-run fetching --- tests/exchange/test_okx.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/exchange/test_okx.py b/tests/exchange/test_okx.py index 30e23619b..2f862adda 100644 --- a/tests/exchange/test_okx.py +++ b/tests/exchange/test_okx.py @@ -528,3 +528,19 @@ def test_fetch_stoploss_order_okx(default_conf, mocker): assert resp['id'] == '1234' assert resp['id_stop'] == '123455' assert resp['type'] == 'stoploss' + + default_conf['dry_run'] = True + exchange = get_patched_exchange(mocker, default_conf, api_mock, id='okx') + dro_mock = mocker.patch(f"{EXMS}.fetch_dry_run_order", MagicMock(return_value={'id': '123455'})) + + api_mock.fetch_order.reset_mock() + api_mock.fetch_open_orders.reset_mock() + api_mock.fetch_closed_orders.reset_mock() + api_mock.fetch_canceled_orders.reset_mock() + resp = exchange.fetch_stoploss_order('1234', 'ETH/BTC') + + assert api_mock.fetch_order.call_count == 0 + assert api_mock.fetch_open_orders.call_count == 0 + assert api_mock.fetch_closed_orders.call_count == 0 + assert api_mock.fetch_canceled_orders.call_count == 0 + assert dro_mock.call_count == 1 From 4690244673175e708ed097ca22edcc4e9e432281 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 20 Mar 2023 06:40:57 +0100 Subject: [PATCH 284/360] Enable okx stop-price types --- tests/exchange/test_exchange.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 7c48f1c9d..6e15abaf4 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1039,9 +1039,9 @@ def test_validate_ordertypes(default_conf, mocker): ('bybit', 'last', True), ('bybit', 'mark', True), ('bybit', 'index', True), - # ('okx', 'last', True), - # ('okx', 'mark', True), - # ('okx', 'index', True), + ('okx', 'last', True), + ('okx', 'mark', True), + ('okx', 'index', True), ('gate', 'last', True), ('gate', 'mark', True), ('gate', 'index', True), From 54d8aa7782160d5cf0da4074bf90e134205885fb Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 20 Mar 2023 06:46:00 +0100 Subject: [PATCH 285/360] Test stoploss_adjust okx --- tests/exchange/test_okx.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/exchange/test_okx.py b/tests/exchange/test_okx.py index 2f862adda..3b97e03f4 100644 --- a/tests/exchange/test_okx.py +++ b/tests/exchange/test_okx.py @@ -544,3 +544,18 @@ def test_fetch_stoploss_order_okx(default_conf, mocker): assert api_mock.fetch_closed_orders.call_count == 0 assert api_mock.fetch_canceled_orders.call_count == 0 assert dro_mock.call_count == 1 + + +@pytest.mark.parametrize('sl1,sl2,sl3,side', [ + (1501, 1499, 1501, "sell"), + (1499, 1501, 1499, "buy") +]) +def test_stoploss_adjust_okx(mocker, default_conf, sl1, sl2, sl3, side): + exchange = get_patched_exchange(mocker, default_conf, id='okx') + order = { + 'type': 'stoploss', + 'price': 1500, + 'stopLossPrice': 1500, + } + assert exchange.stoploss_adjust(sl1, order, side=side) + assert not exchange.stoploss_adjust(sl2, order, side=side) From 8d649988ca4cdb1eb558545c8025bfedd6accb41 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Mar 2023 05:47:47 +0000 Subject: [PATCH 286/360] Bump fastapi from 0.94.0 to 0.95.0 Bumps [fastapi](https://github.com/tiangolo/fastapi) from 0.94.0 to 0.95.0. - [Release notes](https://github.com/tiangolo/fastapi/releases) - [Commits](https://github.com/tiangolo/fastapi/compare/0.94.0...0.95.0) --- updated-dependencies: - dependency-name: fastapi dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 664d1752e..201c46106 100644 --- a/requirements.txt +++ b/requirements.txt @@ -34,7 +34,7 @@ orjson==3.8.7 sdnotify==0.3.2 # API Server -fastapi==0.94.0 +fastapi==0.95.0 pydantic==1.10.6 uvicorn==0.21.1 pyjwt==2.6.0 From 3175121030b477514eb603a7ec2ebea5e5673e34 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Mar 2023 05:47:55 +0000 Subject: [PATCH 287/360] Bump ast-comments from 1.0.0 to 1.0.1 Bumps [ast-comments](https://github.com/t3rn0/ast-comments) from 1.0.0 to 1.0.1. - [Release notes](https://github.com/t3rn0/ast-comments/releases) - [Commits](https://github.com/t3rn0/ast-comments/compare/1.0.0...1.0.1) --- updated-dependencies: - dependency-name: ast-comments 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 664d1752e..4251c879a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -56,4 +56,4 @@ schedule==1.1.0 websockets==10.4 janus==1.0.0 -ast-comments==1.0.0 +ast-comments==1.0.1 From cb1f971d4bb1bd9559529b6c5734acfc55a773a6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Mar 2023 06:39:13 +0000 Subject: [PATCH 288/360] Bump ccxt from 2.9.12 to 3.0.23 Bumps [ccxt](https://github.com/ccxt/ccxt) from 2.9.12 to 3.0.23. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/CHANGELOG.md) - [Commits](https://github.com/ccxt/ccxt/compare/2.9.12...3.0.23) --- updated-dependencies: - dependency-name: ccxt dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index de38d50c3..6f16c71d0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.24.2 pandas==1.5.3 pandas-ta==0.3.14b -ccxt==2.9.12 +ccxt==3.0.23 cryptography==39.0.2 aiohttp==3.8.4 SQLAlchemy==2.0.7 From a4e4310d400b73c15236ebaab36dfa7876dc9330 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Mar 2023 07:11:18 +0000 Subject: [PATCH 289/360] Bump pytest-asyncio from 0.20.3 to 0.21.0 Bumps [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio) from 0.20.3 to 0.21.0. - [Release notes](https://github.com/pytest-dev/pytest-asyncio/releases) - [Commits](https://github.com/pytest-dev/pytest-asyncio/compare/v0.20.3...v0.21.0) --- updated-dependencies: - dependency-name: pytest-asyncio dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 25e26f47c..8312e2820 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -11,7 +11,7 @@ ruff==0.0.257 mypy==1.1.1 pre-commit==3.2.0 pytest==7.2.2 -pytest-asyncio==0.20.3 +pytest-asyncio==0.21.0 pytest-cov==4.0.0 pytest-mock==3.10.0 pytest-random-order==1.1.0 From 4f4bfdac4d2491277decca93cdd30aec9ec29a1a Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 20 Mar 2023 09:00:00 +0100 Subject: [PATCH 290/360] Adjustments to okx stoploss --- freqtrade/exchange/okx.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/okx.py b/freqtrade/exchange/okx.py index 7de110acf..3110d8189 100644 --- a/freqtrade/exchange/okx.py +++ b/freqtrade/exchange/okx.py @@ -32,7 +32,7 @@ class Okx(Exchange): _ft_has_futures: Dict = { "tickers_have_quoteVolume": False, "fee_cost_in_contracts": True, - "stop_price_type_field": "tpTriggerPxType", + "stop_price_type_field": "slTriggerPxType", "stop_price_type_value_mapping": { PriceType.LAST: "last", PriceType.MARK: "index", @@ -193,7 +193,7 @@ class Okx(Exchange): try: params1 = {'stop': True} order_reg = self._api.fetch_order(order_id, pair, params=params1) - self._log_exchange_response('fetch_stoploss_order1', order_reg) + self._log_exchange_response('fetch_stoploss_order', order_reg) return order_reg except ccxt.OrderNotFound: pass From 639987cbabc8ae969f280639ffa856af99402064 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 20 Mar 2023 18:19:17 +0100 Subject: [PATCH 291/360] Prevent parameter reuse --- freqtrade/exchange/okx.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/okx.py b/freqtrade/exchange/okx.py index 3110d8189..162630ea5 100644 --- a/freqtrade/exchange/okx.py +++ b/freqtrade/exchange/okx.py @@ -197,11 +197,11 @@ class Okx(Exchange): return order_reg except ccxt.OrderNotFound: pass - params1 = {'stop': True, 'ordType': 'conditional'} + params2 = {'stop': True, 'ordType': 'conditional'} for method in (self._api.fetch_open_orders, self._api.fetch_closed_orders, self._api.fetch_canceled_orders): try: - orders = method(pair, params=params1) + orders = method(pair, params=params2) orders_f = [order for order in orders if order['id'] == order_id] if orders_f: order = orders_f[0] From 97c420b2df8c2fe49c5c14b264836b1af58111f7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 20 Mar 2023 19:27:48 +0100 Subject: [PATCH 292/360] Add explicit test for okx lev_prep --- freqtrade/exchange/okx.py | 1 - tests/exchange/test_okx.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/okx.py b/freqtrade/exchange/okx.py index 162630ea5..1b9134be3 100644 --- a/freqtrade/exchange/okx.py +++ b/freqtrade/exchange/okx.py @@ -128,7 +128,6 @@ class Okx(Exchange): def _lev_prep(self, pair: str, leverage: float, side: BuySell): if self.trading_mode != TradingMode.SPOT and self.margin_mode is not None: try: - # TODO-lev: Test me properly (check mgnMode passed) res = self._api.set_leverage( leverage=leverage, symbol=pair, diff --git a/tests/exchange/test_okx.py b/tests/exchange/test_okx.py index 3b97e03f4..7a3fa22f0 100644 --- a/tests/exchange/test_okx.py +++ b/tests/exchange/test_okx.py @@ -480,6 +480,38 @@ def test_load_leverage_tiers_okx(default_conf, mocker, markets, tmpdir, caplog, assert log_has(logmsg, caplog) +def test__set_leverage_okx(mocker, default_conf): + + api_mock = MagicMock() + api_mock.set_leverage = MagicMock() + type(api_mock).has = PropertyMock(return_value={'setLeverage': True}) + default_conf['dry_run'] = False + default_conf['trading_mode'] = TradingMode.FUTURES + default_conf['margin_mode'] = MarginMode.ISOLATED + + exchange = get_patched_exchange(mocker, default_conf, api_mock, id="okx") + exchange._lev_prep('BTC/USDT:USDT', 3.2, 'buy') + assert api_mock.set_leverage.call_count == 1 + # Leverage is rounded to 3. + assert api_mock.set_leverage.call_args_list[0][1]['leverage'] == 3.2 + assert api_mock.set_leverage.call_args_list[0][1]['symbol'] == 'BTC/USDT:USDT' + assert api_mock.set_leverage.call_args_list[0][1]['params'] == { + 'mgnMode': 'isolated', + 'posSide': 'net'} + + ccxt_exceptionhandlers( + mocker, + default_conf, + api_mock, + "okx", + "_lev_prep", + "set_leverage", + pair="XRP/USDT:USDT", + leverage=5.0, + side='buy' + ) + + @pytest.mark.usefixtures("init_persistence") def test_fetch_stoploss_order_okx(default_conf, mocker): default_conf['dry_run'] = False From e16db814fad1b55c168965027185b68a6f94b083 Mon Sep 17 00:00:00 2001 From: Joe Schr <8218910+TheJoeSchr@users.noreply.github.com> Date: Tue, 21 Mar 2023 17:52:14 +0100 Subject: [PATCH 293/360] featherdatahandler: implement trades_store/_trades_load --- freqtrade/data/history/featherdatahandler.py | 26 ++--- tests/data/test_datahandler.py | 108 +++++++++++++++---- tests/testdata/XRP_ETH-trades.feather | Bin 0 -> 374570 bytes 3 files changed, 102 insertions(+), 32 deletions(-) create mode 100644 tests/testdata/XRP_ETH-trades.feather diff --git a/freqtrade/data/history/featherdatahandler.py b/freqtrade/data/history/featherdatahandler.py index 22a6805e7..87c1d0886 100644 --- a/freqtrade/data/history/featherdatahandler.py +++ b/freqtrade/data/history/featherdatahandler.py @@ -4,8 +4,9 @@ from typing import Optional from pandas import DataFrame, read_feather, to_datetime from freqtrade.configuration import TimeRange -from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, TradeList +from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, TradeList, DEFAULT_TRADES_COLUMNS from freqtrade.enums import CandleType +from freqtrade.data.converter import trades_dict_to_list from .idatahandler import IDataHandler @@ -29,7 +30,8 @@ class FeatherDataHandler(IDataHandler): :param candle_type: Any of the enum CandleType (must match trading mode!) :return: None """ - filename = self._pair_data_filename(self._datadir, pair, timeframe, candle_type) + filename = self._pair_data_filename( + self._datadir, pair, timeframe, candle_type) self.create_dir_if_needed(filename) data.reset_index(drop=True).loc[:, self._columns].to_feather( @@ -92,12 +94,11 @@ class FeatherDataHandler(IDataHandler): :param data: List of Lists containing trade data, column sequence as in DEFAULT_TRADES_COLUMNS """ - # filename = self._pair_trades_filename(self._datadir, pair) + filename = self._pair_trades_filename(self._datadir, pair) + self.create_dir_if_needed(filename) - raise NotImplementedError() - # array = pa.array(data) - # array - # feather.write_feather(data, filename) + tradesdata = DataFrame(data, columns=DEFAULT_TRADES_COLUMNS) + tradesdata.to_feather(filename, compression_level=9, compression='lz4') def trades_append(self, pair: str, data: TradeList): """ @@ -116,14 +117,13 @@ class FeatherDataHandler(IDataHandler): :param timerange: Timerange to load trades for - currently not implemented :return: List of trades """ - raise NotImplementedError() - # filename = self._pair_trades_filename(self._datadir, pair) - # tradesdata = misc.file_load_json(filename) + filename = self._pair_trades_filename(self._datadir, pair) + if not filename.exists(): + return [] - # if not tradesdata: - # return [] + tradesdata = read_feather(filename) - # return tradesdata + return tradesdata.values.tolist() @classmethod def _get_file_extension(cls): diff --git a/tests/data/test_datahandler.py b/tests/data/test_datahandler.py index f673ede6e..2c5241367 100644 --- a/tests/data/test_datahandler.py +++ b/tests/data/test_datahandler.py @@ -20,25 +20,31 @@ from tests.conftest import log_has, log_has_re def test_datahandler_ohlcv_get_pairs(testdatadir): - pairs = JsonDataHandler.ohlcv_get_pairs(testdatadir, '5m', candle_type=CandleType.SPOT) + pairs = JsonDataHandler.ohlcv_get_pairs( + testdatadir, '5m', candle_type=CandleType.SPOT) # Convert to set to avoid failures due to sorting assert set(pairs) == {'UNITTEST/BTC', 'XLM/BTC', 'ETH/BTC', 'TRX/BTC', 'LTC/BTC', 'XMR/BTC', 'ZEC/BTC', 'ADA/BTC', 'ETC/BTC', 'NXT/BTC', 'DASH/BTC', 'XRP/ETH'} - pairs = JsonGzDataHandler.ohlcv_get_pairs(testdatadir, '8m', candle_type=CandleType.SPOT) + pairs = JsonGzDataHandler.ohlcv_get_pairs( + testdatadir, '8m', candle_type=CandleType.SPOT) assert set(pairs) == {'UNITTEST/BTC'} - pairs = HDF5DataHandler.ohlcv_get_pairs(testdatadir, '5m', candle_type=CandleType.SPOT) + pairs = HDF5DataHandler.ohlcv_get_pairs( + testdatadir, '5m', candle_type=CandleType.SPOT) assert set(pairs) == {'UNITTEST/BTC'} - pairs = JsonDataHandler.ohlcv_get_pairs(testdatadir, '1h', candle_type=CandleType.MARK) + pairs = JsonDataHandler.ohlcv_get_pairs( + testdatadir, '1h', candle_type=CandleType.MARK) assert set(pairs) == {'UNITTEST/USDT:USDT', 'XRP/USDT:USDT'} - pairs = JsonGzDataHandler.ohlcv_get_pairs(testdatadir, '1h', candle_type=CandleType.FUTURES) + pairs = JsonGzDataHandler.ohlcv_get_pairs( + testdatadir, '1h', candle_type=CandleType.FUTURES) assert set(pairs) == {'XRP/USDT:USDT'} - pairs = HDF5DataHandler.ohlcv_get_pairs(testdatadir, '1h', candle_type=CandleType.MARK) + pairs = HDF5DataHandler.ohlcv_get_pairs( + testdatadir, '1h', candle_type=CandleType.MARK) assert set(pairs) == {'UNITTEST/USDT:USDT'} @@ -79,7 +85,8 @@ def test_rebuild_pair_from_filename(input, expected): def test_datahandler_ohlcv_get_available_data(testdatadir): - paircombs = JsonDataHandler.ohlcv_get_available_data(testdatadir, TradingMode.SPOT) + paircombs = JsonDataHandler.ohlcv_get_available_data( + testdatadir, TradingMode.SPOT) # Convert to set to avoid failures due to sorting assert set(paircombs) == { ('UNITTEST/BTC', '5m', CandleType.SPOT), @@ -101,7 +108,8 @@ def test_datahandler_ohlcv_get_available_data(testdatadir): ('NOPAIR/XXX', '4m', CandleType.SPOT), } - paircombs = JsonDataHandler.ohlcv_get_available_data(testdatadir, TradingMode.FUTURES) + paircombs = JsonDataHandler.ohlcv_get_available_data( + testdatadir, TradingMode.FUTURES) # Convert to set to avoid failures due to sorting assert set(paircombs) == { ('UNITTEST/USDT:USDT', '1h', 'mark'), @@ -112,9 +120,11 @@ def test_datahandler_ohlcv_get_available_data(testdatadir): ('XRP/USDT:USDT', '8h', 'funding_rate'), } - paircombs = JsonGzDataHandler.ohlcv_get_available_data(testdatadir, TradingMode.SPOT) + paircombs = JsonGzDataHandler.ohlcv_get_available_data( + testdatadir, TradingMode.SPOT) assert set(paircombs) == {('UNITTEST/BTC', '8m', CandleType.SPOT)} - paircombs = HDF5DataHandler.ohlcv_get_available_data(testdatadir, TradingMode.SPOT) + paircombs = HDF5DataHandler.ohlcv_get_available_data( + testdatadir, TradingMode.SPOT) assert set(paircombs) == {('UNITTEST/BTC', '5m', CandleType.SPOT)} @@ -252,7 +262,7 @@ def test_datahandler__check_empty_df(testdatadir, caplog): assert log_has_re(expected_text, caplog) -@pytest.mark.parametrize('datahandler', ['feather', 'parquet']) +@pytest.mark.parametrize('datahandler', ['parquet']) def test_datahandler_trades_not_supported(datahandler, testdatadir, ): dh = get_datahandler(testdatadir, datahandler) with pytest.raises(NotImplementedError): @@ -406,18 +416,21 @@ def test_hdf5datahandler_ohlcv_load_and_resave( assert not ohlcv[ohlcv['date'] < startdt].empty - timerange = TimeRange.parse_timerange(f"{startdt.replace('-', '')}-{enddt.replace('-', '')}") + timerange = TimeRange.parse_timerange( + f"{startdt.replace('-', '')}-{enddt.replace('-', '')}") # Call private function to ensure timerange is filtered in hdf5 ohlcv = dh._ohlcv_load(pair, timeframe, timerange, candle_type=candle_type) - ohlcv1 = dh1._ohlcv_load('UNITTEST/NEW', timeframe, timerange, candle_type=candle_type) + ohlcv1 = dh1._ohlcv_load('UNITTEST/NEW', timeframe, + timerange, candle_type=candle_type) assert len(ohlcv) == len(ohlcv1) assert ohlcv.equals(ohlcv1) assert ohlcv[ohlcv['date'] < startdt].empty assert ohlcv[ohlcv['date'] > enddt].empty # Try loading inexisting file - ohlcv = dh.ohlcv_load('UNITTEST/NONEXIST', timeframe, candle_type=candle_type) + ohlcv = dh.ohlcv_load('UNITTEST/NONEXIST', timeframe, + candle_type=candle_type) assert ohlcv.empty @@ -452,7 +465,8 @@ def test_generic_datahandler_ohlcv_load_and_resave( # Get data to test dh = get_datahandler(testdatadir, datahandler) - file = tmpdir2 / f"UNITTEST_NEW-{timeframe}{candle_append}.{dh._get_file_extension()}" + file = tmpdir2 / \ + f"UNITTEST_NEW-{timeframe}{candle_append}.{dh._get_file_extension()}" assert not file.is_file() dh1 = get_datahandler(tmpdir1, datahandler) @@ -461,11 +475,14 @@ def test_generic_datahandler_ohlcv_load_and_resave( assert not ohlcv[ohlcv['date'] < startdt].empty - timerange = TimeRange.parse_timerange(f"{startdt.replace('-', '')}-{enddt.replace('-', '')}") + timerange = TimeRange.parse_timerange( + f"{startdt.replace('-', '')}-{enddt.replace('-', '')}") - ohlcv = dhbase.ohlcv_load(pair, timeframe, timerange=timerange, candle_type=candle_type) + ohlcv = dhbase.ohlcv_load( + pair, timeframe, timerange=timerange, candle_type=candle_type) if datahandler == 'hdf5': - ohlcv1 = dh1._ohlcv_load('UNITTEST/NEW', timeframe, timerange, candle_type=candle_type) + ohlcv1 = dh1._ohlcv_load( + 'UNITTEST/NEW', timeframe, timerange, candle_type=candle_type) if candle_type == 'mark': ohlcv1['volume'] = 0.0 else: @@ -478,7 +495,8 @@ def test_generic_datahandler_ohlcv_load_and_resave( assert ohlcv[ohlcv['date'] > enddt].empty # Try loading inexisting file - ohlcv = dh.ohlcv_load('UNITTEST/NONEXIST', timeframe, candle_type=candle_type) + ohlcv = dh.ohlcv_load('UNITTEST/NONEXIST', timeframe, + candle_type=candle_type) assert ohlcv.empty @@ -496,6 +514,58 @@ def test_hdf5datahandler_ohlcv_purge(mocker, testdatadir): assert unlinkmock.call_count == 2 +def test_featherdatahandler_trades_load(testdatadir): + dh = get_datahandler(testdatadir, 'feather') + trades = dh.trades_load('XRP/ETH') + assert isinstance(trades, list) + assert trades[0][0] == 1570752011620 + assert trades[-1][-1] == 0.1986231 + + trades1 = dh.trades_load('UNITTEST/NONEXIST') + assert trades1 == [] + + +def test_featherdatahandler_trades_store(testdatadir, tmpdir): + tmpdir1 = Path(tmpdir) + dh = get_datahandler(testdatadir, 'feather') + trades = dh.trades_load('XRP/ETH') + + dh1 = get_datahandler(tmpdir1, 'feather') + dh1.trades_store('XRP/NEW', trades) + file = tmpdir1 / 'XRP_NEW-trades.feather' + assert file.is_file() + # Load trades back + trades_new = dh1.trades_load('XRP/NEW') + + assert len(trades_new) == len(trades) + assert trades[0][0] == trades_new[0][0] + assert trades[0][1] == trades_new[0][1] + # assert trades[0][2] == trades_new[0][2] # This is nan - so comparison does not make sense + assert trades[0][3] == trades_new[0][3] + assert trades[0][4] == trades_new[0][4] + assert trades[0][5] == trades_new[0][5] + assert trades[0][6] == trades_new[0][6] + assert trades[-1][0] == trades_new[-1][0] + assert trades[-1][1] == trades_new[-1][1] + # assert trades[-1][2] == trades_new[-1][2] # This is nan - so comparison does not make sense + assert trades[-1][3] == trades_new[-1][3] + assert trades[-1][4] == trades_new[-1][4] + assert trades[-1][5] == trades_new[-1][5] + assert trades[-1][6] == trades_new[-1][6] + + +def test_featherdatahandler_trades_purge(mocker, testdatadir): + mocker.patch.object(Path, "exists", MagicMock(return_value=False)) + unlinkmock = mocker.patch.object(Path, "unlink", MagicMock()) + dh = get_datahandler(testdatadir, 'feather') + assert not dh.trades_purge('UNITTEST/NONEXIST') + assert unlinkmock.call_count == 0 + + mocker.patch.object(Path, "exists", MagicMock(return_value=True)) + assert dh.trades_purge('UNITTEST/NONEXIST') + assert unlinkmock.call_count == 1 + + def test_gethandlerclass(): cl = get_datahandlerclass('json') assert cl == JsonDataHandler diff --git a/tests/testdata/XRP_ETH-trades.feather b/tests/testdata/XRP_ETH-trades.feather new file mode 100644 index 0000000000000000000000000000000000000000..68e1c8467193123cc540f7b794d14aa151a2168f GIT binary patch literal 374570 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π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 literal 0 HcmV?d00001 From 0128b63c1c44a339ac8b92ccac7322c6a1e885c9 Mon Sep 17 00:00:00 2001 From: Joe Schr <8218910+TheJoeSchr@users.noreply.github.com> Date: Tue, 21 Mar 2023 19:12:25 +0100 Subject: [PATCH 294/360] add 'feather' to AVAILABLE_DATAHANDLERS_TRADES --- freqtrade/constants.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 46e9b5cd4..ebb946221 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -36,9 +36,10 @@ AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'ProducerPairList', ' 'AgeFilter', 'OffsetFilter', 'PerformanceFilter', 'PrecisionFilter', 'PriceFilter', 'RangeStabilityFilter', 'ShuffleFilter', 'SpreadFilter', 'VolatilityFilter'] -AVAILABLE_PROTECTIONS = ['CooldownPeriod', 'LowProfitPairs', 'MaxDrawdown', 'StoplossGuard'] -AVAILABLE_DATAHANDLERS_TRADES = ['json', 'jsongz', 'hdf5'] -AVAILABLE_DATAHANDLERS = AVAILABLE_DATAHANDLERS_TRADES + ['feather', 'parquet'] +AVAILABLE_PROTECTIONS = ['CooldownPeriod', + 'LowProfitPairs', 'MaxDrawdown', 'StoplossGuard'] +AVAILABLE_DATAHANDLERS_TRADES = ['json', 'jsongz', 'hdf5', 'feather'] +AVAILABLE_DATAHANDLERS = AVAILABLE_DATAHANDLERS_TRADES + ['parquet'] BACKTEST_BREAKDOWNS = ['day', 'week', 'month'] BACKTEST_CACHE_AGE = ['none', 'day', 'week', 'month'] BACKTEST_CACHE_DEFAULT = 'day' From 36c45fd14f8daa25240b585757af38cfc3663564 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 21 Mar 2023 19:14:09 +0100 Subject: [PATCH 295/360] Remove unused argument from set_leverage --- freqtrade/exchange/exchange.py | 1 - freqtrade/exchange/kraken.py | 1 - tests/exchange/test_binance.py | 1 - tests/exchange/test_exchange.py | 23 ----------------------- 4 files changed, 26 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index e5f897c2a..f620b5bc1 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -2527,7 +2527,6 @@ class Exchange: self, leverage: float, pair: Optional[str] = None, - trading_mode: Optional[TradingMode] = None, accept_fail: bool = False, ): """ diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 8a4f7f7e0..b1a19fa69 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -158,7 +158,6 @@ class Kraken(Exchange): self, leverage: float, pair: Optional[str] = None, - trading_mode: Optional[TradingMode] = None, accept_fail: bool = False, ): """ diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index ba786bb3b..8ada089bd 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -555,7 +555,6 @@ def test__set_leverage_binance(mocker, default_conf): "set_leverage", pair="XRP/USDT", leverage=5.0, - trading_mode=TradingMode.FUTURES ) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 6e15abaf4..586f023b4 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -3868,29 +3868,6 @@ def test_get_stake_amount_considering_leverage( stake_amount, leverage) == min_stake_with_lev -@pytest.mark.parametrize("exchange_name,trading_mode", [ - ("binance", TradingMode.FUTURES), -]) -def test__set_leverage(mocker, default_conf, exchange_name, trading_mode): - - api_mock = MagicMock() - api_mock.set_leverage = MagicMock() - type(api_mock).has = PropertyMock(return_value={'setLeverage': True}) - default_conf['dry_run'] = False - - ccxt_exceptionhandlers( - mocker, - default_conf, - api_mock, - exchange_name, - "_set_leverage", - "set_leverage", - pair="XRP/USDT", - leverage=5.0, - trading_mode=trading_mode - ) - - @pytest.mark.parametrize("margin_mode", [ (MarginMode.CROSS), (MarginMode.ISOLATED) From ebebcb886c0a3f6ac98460fbf073052c96a9f25e Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 21 Mar 2023 19:28:26 +0100 Subject: [PATCH 296/360] Move build-system to the top of pyproject.toml --- pyproject.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c3ca9e1b0..baf707c68 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,7 @@ +[build-system] +requires = ["setuptools >= 46.4.0", "wheel"] +build-backend = "setuptools.build_meta" + [tool.black] line-length = 100 exclude = ''' @@ -48,10 +52,6 @@ ignore_errors = true module = "telegram.*" implicit_optional = true -[build-system] -requires = ["setuptools >= 46.4.0", "wheel"] -build-backend = "setuptools.build_meta" - [tool.pyright] include = ["freqtrade"] exclude = [ From 8cf3e9f91b50fd7d615822e87a51aa997548dd59 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 21 Mar 2023 19:29:27 +0100 Subject: [PATCH 297/360] Accept "insufficient funds" error on set_leverage from stop calls closes #8341 --- freqtrade/exchange/bybit.py | 2 +- freqtrade/exchange/exchange.py | 10 +++++----- freqtrade/exchange/okx.py | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/freqtrade/exchange/bybit.py b/freqtrade/exchange/bybit.py index 6f841b608..a4b070741 100644 --- a/freqtrade/exchange/bybit.py +++ b/freqtrade/exchange/bybit.py @@ -114,7 +114,7 @@ class Bybit(Exchange): data = [[x['timestamp'], x['fundingRate'], 0, 0, 0, 0] for x in data] return data - def _lev_prep(self, pair: str, leverage: float, side: BuySell): + def _lev_prep(self, pair: str, leverage: float, side: BuySell, accept_fail: bool = False): if self.trading_mode != TradingMode.SPOT: params = {'leverage': leverage} self.set_margin_mode(pair, self.margin_mode, accept_fail=True, params=params) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index f620b5bc1..99551b054 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1018,10 +1018,10 @@ class Exchange: # Order handling - def _lev_prep(self, pair: str, leverage: float, side: BuySell): + def _lev_prep(self, pair: str, leverage: float, side: BuySell, accept_fail: bool = False): if self.trading_mode != TradingMode.SPOT: - self.set_margin_mode(pair, self.margin_mode) - self._set_leverage(leverage, pair) + self.set_margin_mode(pair, self.margin_mode, accept_fail) + self._set_leverage(leverage, pair, accept_fail) def _get_params( self, @@ -1202,7 +1202,7 @@ class Exchange: amount = self.amount_to_precision(pair, self._amount_to_contracts(pair, amount)) - self._lev_prep(pair, leverage, side) + self._lev_prep(pair, leverage, side, accept_fail=True) order = self._api.create_order(symbol=pair, type=ordertype, side=side, amount=amount, price=limit_rate, params=params) self._log_exchange_response('create_stoploss_order', order) @@ -2544,7 +2544,7 @@ class Exchange: self._log_exchange_response('set_leverage', res) except ccxt.DDoSProtection as e: raise DDosProtection(e) from e - except ccxt.BadRequest as e: + except (ccxt.BadRequest, ccxt.InsufficientFunds) as e: if not accept_fail: raise TemporaryError( f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e diff --git a/freqtrade/exchange/okx.py b/freqtrade/exchange/okx.py index 1b9134be3..a4fcaeca0 100644 --- a/freqtrade/exchange/okx.py +++ b/freqtrade/exchange/okx.py @@ -125,7 +125,7 @@ class Okx(Exchange): return params @retrier - def _lev_prep(self, pair: str, leverage: float, side: BuySell): + def _lev_prep(self, pair: str, leverage: float, side: BuySell, accept_fail: bool = False): if self.trading_mode != TradingMode.SPOT and self.margin_mode is not None: try: res = self._api.set_leverage( From bdf19f1d6622272c4f607080eea347854e13ac02 Mon Sep 17 00:00:00 2001 From: Robert Caulk Date: Tue, 21 Mar 2023 22:44:56 +0100 Subject: [PATCH 298/360] Update freqai_interface.py --- freqtrade/freqai/freqai_interface.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqai/freqai_interface.py b/freqtrade/freqai/freqai_interface.py index 07c357de3..8e842b8f2 100644 --- a/freqtrade/freqai/freqai_interface.py +++ b/freqtrade/freqai/freqai_interface.py @@ -105,6 +105,10 @@ class IFreqaiModel(ABC): self.max_system_threads = max(int(psutil.cpu_count() * 2 - 2), 1) self.can_short = True # overridden in start() with strategy.can_short self.model: Any = None + if self.ft_params.get('principal_component_analysis', False) and self.continual_learning: + self.ft_params.update({'principal_component_analysis': False}) + logger.warning('User tried to use PCA with continual learning. Deactivating PCA.') + record_params(config, self.full_path) @@ -154,8 +158,7 @@ class IFreqaiModel(ABC): dk = self.start_backtesting(dataframe, metadata, self.dk, strategy) dataframe = dk.remove_features_from_df(dk.return_dataframe) else: - logger.info( - "Backtesting using historic predictions (live models)") + logger.info("Backtesting using historic predictions (live models)") dk = self.start_backtesting_from_historic_predictions( dataframe, metadata, self.dk) dataframe = dk.return_dataframe From 150c5510c74390641fd723f6a70b5f67f53ec9cf Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 22 Mar 2023 19:46:07 +0100 Subject: [PATCH 299/360] Don''t fully fail bot when invalid price value is reached closes #8300 --- freqtrade/exchange/exchange.py | 6 +++++- freqtrade/freqtradebot.py | 14 +++++++++----- tests/exchange/test_binance.py | 4 ++-- tests/exchange/test_huobi.py | 6 +++--- tests/exchange/test_kucoin.py | 6 +++--- 5 files changed, 22 insertions(+), 14 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 99551b054..104eaa221 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1135,7 +1135,11 @@ class Exchange: "sell" else (stop_price >= limit_rate)) # Ensure rate is less than stop price if bad_stop_price: - raise OperationalException( + # This can for example happen if the stop / liquidation price is set to 0 + # Which is possible if a market-order closes right away. + # The InvalidOrderException will bubble up to exit_positions, where it will be + # handled gracefully. + raise InvalidOrderException( "In stoploss limit order, stop price should be more than limit price. " f"Stop price: {stop_price}, Limit price: {limit_rate}, " f"Limit Price pct: {limit_price_pct}" diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 06c8831f5..00bfc1ee2 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1021,12 +1021,16 @@ class FreqtradeBot(LoggingMixin): trades_closed = 0 for trade in trades: try: + try: + if (self.strategy.order_types.get('stoploss_on_exchange') and + self.handle_stoploss_on_exchange(trade)): + trades_closed += 1 + Trade.commit() + continue - if (self.strategy.order_types.get('stoploss_on_exchange') and - self.handle_stoploss_on_exchange(trade)): - trades_closed += 1 - Trade.commit() - continue + except InvalidOrderException as exception: + logger.warning( + f'Unable to handle stoploss on exchange for {trade.pair}: {exception}') # 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 diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index 8ada089bd..273860e15 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -52,7 +52,7 @@ def test_create_stoploss_order_binance(default_conf, mocker, limitratio, expecte exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') - with pytest.raises(OperationalException): + with pytest.raises(InvalidOrderException): order = exchange.create_stoploss( pair='ETH/BTC', amount=1, @@ -131,7 +131,7 @@ def test_create_stoploss_order_dry_run_binance(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') - with pytest.raises(OperationalException): + with pytest.raises(InvalidOrderException): order = exchange.create_stoploss( pair='ETH/BTC', amount=1, diff --git a/tests/exchange/test_huobi.py b/tests/exchange/test_huobi.py index 5e4fd7316..85d2ced9d 100644 --- a/tests/exchange/test_huobi.py +++ b/tests/exchange/test_huobi.py @@ -4,7 +4,7 @@ from unittest.mock import MagicMock import ccxt import pytest -from freqtrade.exceptions import DependencyException, InvalidOrderException, OperationalException +from freqtrade.exceptions import DependencyException, InvalidOrderException from tests.conftest import EXMS, get_patched_exchange from tests.exchange.test_exchange import ccxt_exceptionhandlers @@ -31,7 +31,7 @@ def test_create_stoploss_order_huobi(default_conf, mocker, limitratio, expected, exchange = get_patched_exchange(mocker, default_conf, api_mock, 'huobi') - with pytest.raises(OperationalException): + with pytest.raises(InvalidOrderException): order = exchange.create_stoploss(pair='ETH/BTC', amount=1, stop_price=190, order_types={'stoploss_on_exchange_limit_ratio': 1.05}, side=side, @@ -84,7 +84,7 @@ def test_create_stoploss_order_dry_run_huobi(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf, api_mock, 'huobi') - with pytest.raises(OperationalException): + with pytest.raises(InvalidOrderException): order = exchange.create_stoploss(pair='ETH/BTC', amount=1, stop_price=190, order_types={'stoploss_on_exchange_limit_ratio': 1.05}, side='sell', leverage=1.0) diff --git a/tests/exchange/test_kucoin.py b/tests/exchange/test_kucoin.py index e0bb32b7c..07f3fb6a3 100644 --- a/tests/exchange/test_kucoin.py +++ b/tests/exchange/test_kucoin.py @@ -4,7 +4,7 @@ from unittest.mock import MagicMock import ccxt import pytest -from freqtrade.exceptions import DependencyException, InvalidOrderException, OperationalException +from freqtrade.exceptions import DependencyException, InvalidOrderException from tests.conftest import EXMS, get_patched_exchange from tests.exchange.test_exchange import ccxt_exceptionhandlers @@ -31,7 +31,7 @@ def test_create_stoploss_order_kucoin(default_conf, mocker, limitratio, expected exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kucoin') if order_type == 'limit': - with pytest.raises(OperationalException): + with pytest.raises(InvalidOrderException): order = exchange.create_stoploss(pair='ETH/BTC', amount=1, stop_price=190, order_types={ 'stoploss': order_type, @@ -92,7 +92,7 @@ def test_stoploss_order_dry_run_kucoin(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kucoin') - with pytest.raises(OperationalException): + with pytest.raises(InvalidOrderException): order = exchange.create_stoploss(pair='ETH/BTC', amount=1, stop_price=190, order_types={'stoploss': 'limit', 'stoploss_on_exchange_limit_ratio': 1.05}, From 469166636c9794afb309875d87be66e76518430e Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 24 Mar 2023 07:27:45 +0100 Subject: [PATCH 300/360] Set initial stoploss when creating the order This ensures that a trade never has "None" as stoploss --- freqtrade/freqtradebot.py | 3 +++ tests/rpc/test_rpc.py | 11 ----------- tests/test_integration.py | 18 +++++++++--------- 3 files changed, 12 insertions(+), 20 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 00bfc1ee2..9d402b6a6 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -810,6 +810,9 @@ class FreqtradeBot(LoggingMixin): precision_mode=self.exchange.precisionMode, contract_size=self.exchange.get_contract_size(pair), ) + stoploss = self.strategy.stoploss if not self.edge else self.edge.get_stoploss(pair) + trade.adjust_stop_loss(trade.open_rate, stoploss, initial=True) + else: # This is additional buy, we reset fee_open_currency so timeout checking can work trade.is_open = True diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 7d829bdb6..4e2dc94ae 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -125,17 +125,6 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'profit_pct': 0.0, 'profit_abs': 0.0, 'total_profit_abs': 0.0, - 'stop_loss_abs': 0.0, - 'stop_loss_pct': None, - 'stop_loss_ratio': None, - 'stoploss_current_dist': -1.099e-05, - 'stoploss_current_dist_ratio': -1.0, - 'stoploss_current_dist_pct': pytest.approx(-100.0), - 'stoploss_entry_dist': -0.0010025, - 'stoploss_entry_dist_ratio': -1.0, - 'initial_stop_loss_abs': 0.0, - 'initial_stop_loss_pct': None, - 'initial_stop_loss_ratio': None, 'open_order': '(limit buy rem=91.07468123)', }) response_unfilled['orders'][0].update({ diff --git a/tests/test_integration.py b/tests/test_integration.py index 922285309..5cbedd818 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -386,12 +386,12 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker) assert trade.open_order_id is not None assert pytest.approx(trade.stake_amount) == 60 assert trade.open_rate == 1.96 - assert trade.stop_loss_pct is None - assert trade.stop_loss == 0.0 + assert trade.stop_loss_pct == -0.1 + assert pytest.approx(trade.stop_loss) == trade.open_rate * (1 - 0.1 / leverage) + assert pytest.approx(trade.initial_stop_loss) == trade.open_rate * (1 - 0.1 / leverage) + assert trade.initial_stop_loss_pct == -0.1 assert trade.leverage == leverage assert trade.stake_amount == 60 - assert trade.initial_stop_loss == 0.0 - assert trade.initial_stop_loss_pct is None # No adjustment freqtrade.process() trade = Trade.get_trades().first() @@ -407,11 +407,11 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker) assert trade.open_order_id is not None # Open rate is not adjusted yet assert trade.open_rate == 1.96 - assert trade.stop_loss_pct is None - assert trade.stop_loss == 0.0 + assert trade.stop_loss_pct == -0.1 + assert pytest.approx(trade.stop_loss) == trade.open_rate * (1 - 0.1 / leverage) + assert pytest.approx(trade.initial_stop_loss) == trade.open_rate * (1 - 0.1 / leverage) assert trade.stake_amount == 60 - assert trade.initial_stop_loss == 0.0 - assert trade.initial_stop_loss_pct is None + assert trade.initial_stop_loss_pct == -0.1 # Fill order mocker.patch(f'{EXMS}._dry_is_price_crossed', return_value=True) @@ -424,7 +424,7 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker) assert pytest.approx(trade.stake_amount) == 60 assert trade.stop_loss_pct == -0.1 assert pytest.approx(trade.stop_loss) == 1.99 * (1 - 0.1 / leverage) - assert pytest.approx(trade.initial_stop_loss) == 1.99 * (1 - 0.1 / leverage) + assert pytest.approx(trade.initial_stop_loss) == 1.96 * (1 - 0.1 / leverage) assert trade.initial_stop_loss_pct == -0.1 # 2nd order - not filling From b317524ed70883a5d440827aeb52434b57a1602e Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 24 Mar 2023 20:27:45 +0100 Subject: [PATCH 301/360] protect adjust_trade_position from crashing in case of unsafe code --- freqtrade/freqtradebot.py | 2 +- freqtrade/optimize/backtesting.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 9d402b6a6..623d39c09 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -594,7 +594,7 @@ class FreqtradeBot(LoggingMixin): stake_available = self.wallets.get_available_stake_amount() logger.debug(f"Calling adjust_trade_position for pair {trade.pair}") stake_amount = strategy_safe_wrapper(self.strategy.adjust_trade_position, - default_retval=None)( + default_retval=None, supress_error=True)( trade=trade, current_time=datetime.now(timezone.utc), current_rate=current_entry_rate, current_profit=current_entry_profit, min_stake=min_entry_stake, diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 315b3b9db..fe6667ad9 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -522,7 +522,7 @@ class Backtesting: max_stake = self.exchange.get_max_pair_stake_amount(trade.pair, current_rate) stake_available = self.wallets.get_available_stake_amount() stake_amount = strategy_safe_wrapper(self.strategy.adjust_trade_position, - default_retval=None)( + default_retval=None, supress_error=True)( trade=trade, # type: ignore[arg-type] current_time=current_date, current_rate=current_rate, current_profit=current_profit, min_stake=min_stake, From 0ece73578c67c64bf83147bc87f9e36e6f502aae Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Sep 2022 19:51:42 +0200 Subject: [PATCH 302/360] Add typedDict for RPC messages Currently not fully functional. --- freqtrade/freqtradebot.py | 10 ++-- freqtrade/rpc/api_server/webserver.py | 5 +- freqtrade/rpc/rpc.py | 3 +- freqtrade/rpc/rpc_manager.py | 5 +- freqtrade/rpc/rpc_types.py | 82 +++++++++++++++++++++++++++ freqtrade/rpc/telegram.py | 3 +- freqtrade/rpc/webhook.py | 5 +- 7 files changed, 101 insertions(+), 12 deletions(-) create mode 100644 freqtrade/rpc/rpc_types.py diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 623d39c09..28dd1fae5 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -30,6 +30,7 @@ from freqtrade.plugins.protectionmanager import ProtectionManager from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.rpc import RPCManager from freqtrade.rpc.external_message_consumer import ExternalMessageConsumer +from freqtrade.rpc.rpc_types import RPCBuyMsg, RPCCancelMsg, RPCSellCancelMsg, RPCSellMsg from freqtrade.strategy.interface import IStrategy from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.util import FtPrecise @@ -957,7 +958,7 @@ class FreqtradeBot(LoggingMixin): current_rate = self.exchange.get_rate( trade.pair, side='entry', is_short=trade.is_short, refresh=False) - msg = { + msg: RPCBuyMsg = { 'trade_id': trade.id, 'type': msg_type, 'buy_tag': trade.enter_tag, @@ -989,7 +990,7 @@ class FreqtradeBot(LoggingMixin): current_rate = self.exchange.get_rate( trade.pair, side='entry', is_short=trade.is_short, refresh=False) - msg = { + msg: RPCCancelMsg = { 'trade_id': trade.id, 'type': RPCMessageType.ENTRY_CANCEL, 'buy_tag': trade.enter_tag, @@ -1001,6 +1002,7 @@ class FreqtradeBot(LoggingMixin): 'limit': trade.open_rate, 'order_type': order_type, 'stake_amount': trade.stake_amount, + 'open_rate': trade.open_rate, 'stake_currency': self.config['stake_currency'], 'fiat_currency': self.config.get('fiat_display_currency', None), 'amount': trade.amount, @@ -1666,7 +1668,7 @@ class FreqtradeBot(LoggingMixin): amount = trade.amount gain = "profit" if profit_ratio > 0 else "loss" - msg = { + msg: RPCSellMsg = { 'type': (RPCMessageType.EXIT_FILL if fill else RPCMessageType.EXIT), 'trade_id': trade.id, @@ -1722,7 +1724,7 @@ class FreqtradeBot(LoggingMixin): profit_ratio = trade.calc_profit_ratio(profit_rate) gain = "profit" if profit_ratio > 0 else "loss" - msg = { + msg: RPCSellCancelMsg = { 'type': RPCMessageType.EXIT_CANCEL, 'trade_id': trade.id, 'exchange': trade.exchange.capitalize(), diff --git a/freqtrade/rpc/api_server/webserver.py b/freqtrade/rpc/api_server/webserver.py index b53662451..2413e5264 100644 --- a/freqtrade/rpc/api_server/webserver.py +++ b/freqtrade/rpc/api_server/webserver.py @@ -1,6 +1,6 @@ import logging from ipaddress import IPv4Address -from typing import Any, Dict, Optional +from typing import Any, Optional import orjson import uvicorn @@ -13,6 +13,7 @@ from freqtrade.exceptions import OperationalException from freqtrade.rpc.api_server.uvicorn_threaded import UvicornServer from freqtrade.rpc.api_server.ws.message_stream import MessageStream from freqtrade.rpc.rpc import RPC, RPCException, RPCHandler +from freqtrade.rpc.rpc_types import RPCSendMsg logger = logging.getLogger(__name__) @@ -108,7 +109,7 @@ class ApiServer(RPCHandler): cls._has_rpc = False cls._rpc = None - def send_msg(self, msg: Dict[str, Any]) -> None: + def send_msg(self, msg: RPCSendMsg) -> None: """ Publish the message to the message stream """ diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index c6a6f5cae..2b5eb107c 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -30,6 +30,7 @@ from freqtrade.persistence import Order, PairLocks, Trade from freqtrade.persistence.models import PairLock from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist from freqtrade.rpc.fiat_convert import CryptoToFiatConverter +from freqtrade.rpc.rpc_types import RPCSendMsg from freqtrade.wallets import PositionWallet, Wallet @@ -79,7 +80,7 @@ class RPCHandler: """ Cleanup pending module resources """ @abstractmethod - def send_msg(self, msg: Dict[str, str]) -> None: + def send_msg(self, msg: RPCSendMsg) -> None: """ Sends a message to all registered rpc modules """ diff --git a/freqtrade/rpc/rpc_manager.py b/freqtrade/rpc/rpc_manager.py index c4d4fa2dd..e4c925995 100644 --- a/freqtrade/rpc/rpc_manager.py +++ b/freqtrade/rpc/rpc_manager.py @@ -3,11 +3,12 @@ This module contains class to manage RPC communications (Telegram, API, ...) """ import logging from collections import deque -from typing import Any, Dict, List +from typing import List from freqtrade.constants import Config from freqtrade.enums import NO_ECHO_MESSAGES, RPCMessageType from freqtrade.rpc import RPC, RPCHandler +from freqtrade.rpc.rpc_types import RPCSendMsg logger = logging.getLogger(__name__) @@ -58,7 +59,7 @@ class RPCManager: mod.cleanup() del mod - def send_msg(self, msg: Dict[str, Any]) -> None: + def send_msg(self, msg: RPCSendMsg) -> None: """ Send given message to all registered rpc modules. A message consists of one or more key value pairs of strings. diff --git a/freqtrade/rpc/rpc_types.py b/freqtrade/rpc/rpc_types.py new file mode 100644 index 000000000..0fb5a6bfa --- /dev/null +++ b/freqtrade/rpc/rpc_types.py @@ -0,0 +1,82 @@ +from datetime import datetime +from typing import Optional, TypedDict, Union + +from freqtrade.enums import RPCMessageType + + +class RPCSendMsgBase(TypedDict): + type: RPCMessageType + + +class RPCStatusMsg(RPCSendMsgBase): + """Used for Status, Startup and Warning messages""" + status: str + + +class RPCProtectionMsg(RPCSendMsgBase): + id: int + pair: str + base_currency: Optional[str] + lock_time: str + lock_timestamp: int + lock_end_time: str + lock_end_timestamp: int + reason: str + side: str + active: bool + + +class RPCBuyMsg(RPCSendMsgBase): + trade_id: str + buy_tag: str + enter_tag: str + exchange: str + pair: str + leverage: float + direction: str + limit: float + open_rate: float + order_type: Optional[str] # TODO: why optional?? + stake_amount: float + stake_currency: str + fiat_currency: Optional[str] + amount: float + open_date: datetime + current_rate: float + sub_trade: bool + + +class RPCCancelMsg(RPCBuyMsg): + reason: str + + +class RPCSellMsg(RPCBuyMsg): + cumulative_profit: float + gain: str # Literal["profit", "loss"] + close_rate: float + profit_amount: float + profit_ratio: float + sell_reason: str + exit_reason: str + close_date: datetime + current_rate: Optional[float] + + +class RPCSellCancelMsg(RPCBuyMsg): + reason: str + gain: str # Literal["profit", "loss"] + profit_amount: float + profit_ratio: float + sell_reason: str + exit_reason: str + close_date: datetime + + +RPCSendMsg = Union[ + RPCStatusMsg, + RPCProtectionMsg, + RPCBuyMsg, + RPCCancelMsg, + RPCSellMsg, + RPCSellCancelMsg + ] diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 962c5e058..c1365702d 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -30,6 +30,7 @@ from freqtrade.exceptions import OperationalException from freqtrade.misc import chunks, plural, round_coin_value from freqtrade.persistence import Trade from freqtrade.rpc import RPC, RPCException, RPCHandler +from freqtrade.rpc.rpc_types import RPCSendMsg logger = logging.getLogger(__name__) @@ -429,7 +430,7 @@ class Telegram(RPCHandler): return None return message - def send_msg(self, msg: Dict[str, Any]) -> None: + def send_msg(self, msg: RPCSendMsg) -> None: """ Send a message to telegram channel """ default_noti = 'on' diff --git a/freqtrade/rpc/webhook.py b/freqtrade/rpc/webhook.py index 118ebed88..14b881126 100644 --- a/freqtrade/rpc/webhook.py +++ b/freqtrade/rpc/webhook.py @@ -10,6 +10,7 @@ from requests import RequestException, post from freqtrade.constants import Config from freqtrade.enums import RPCMessageType from freqtrade.rpc import RPC, RPCHandler +from freqtrade.rpc.rpc_types import RPCSendMsg logger = logging.getLogger(__name__) @@ -41,7 +42,7 @@ class Webhook(RPCHandler): """ pass - def _get_value_dict(self, msg: Dict[str, Any]) -> Optional[Dict[str, Any]]: + def _get_value_dict(self, msg: RPCSendMsg) -> Optional[Dict[str, Any]]: whconfig = self._config['webhook'] # Deprecated 2022.10 - only keep generic method. if msg['type'] in [RPCMessageType.ENTRY]: @@ -75,7 +76,7 @@ class Webhook(RPCHandler): return None return valuedict - def send_msg(self, msg: Dict[str, Any]) -> None: + def send_msg(self, msg: RPCSendMsg) -> None: """ Send a message to telegram channel """ try: From 70ad7b42b1b0492fe79f28ffc4ce855d318c8adc Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 6 Mar 2023 19:23:05 +0100 Subject: [PATCH 303/360] Improve msg typing --- freqtrade/rpc/api_server/webserver.py | 2 +- freqtrade/rpc/rpc_types.py | 21 +++++++++++---------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/freqtrade/rpc/api_server/webserver.py b/freqtrade/rpc/api_server/webserver.py index 2413e5264..8030e303b 100644 --- a/freqtrade/rpc/api_server/webserver.py +++ b/freqtrade/rpc/api_server/webserver.py @@ -1,6 +1,6 @@ import logging from ipaddress import IPv4Address -from typing import Any, Optional +from typing import Any, Dict, Optional import orjson import uvicorn diff --git a/freqtrade/rpc/rpc_types.py b/freqtrade/rpc/rpc_types.py index 0fb5a6bfa..bde985548 100644 --- a/freqtrade/rpc/rpc_types.py +++ b/freqtrade/rpc/rpc_types.py @@ -27,12 +27,12 @@ class RPCProtectionMsg(RPCSendMsgBase): class RPCBuyMsg(RPCSendMsgBase): - trade_id: str - buy_tag: str - enter_tag: str + trade_id: int + buy_tag: Optional[str] + enter_tag: Optional[str] exchange: str pair: str - leverage: float + leverage: Optional[float] direction: str limit: float open_rate: float @@ -42,7 +42,7 @@ class RPCBuyMsg(RPCSendMsgBase): fiat_currency: Optional[str] amount: float open_date: datetime - current_rate: float + current_rate: Optional[float] sub_trade: bool @@ -56,10 +56,11 @@ class RPCSellMsg(RPCBuyMsg): close_rate: float profit_amount: float profit_ratio: float - sell_reason: str - exit_reason: str + sell_reason: Optional[str] + exit_reason: Optional[str] close_date: datetime - current_rate: Optional[float] + # current_rate: Optional[float] + order_rate: Optional[float] class RPCSellCancelMsg(RPCBuyMsg): @@ -67,8 +68,8 @@ class RPCSellCancelMsg(RPCBuyMsg): gain: str # Literal["profit", "loss"] profit_amount: float profit_ratio: float - sell_reason: str - exit_reason: str + sell_reason: Optional[str] + exit_reason: Optional[str] close_date: datetime From 245ae99273dcc7ff591c37d830b1b8462b1efde6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 6 Mar 2023 19:44:23 +0100 Subject: [PATCH 304/360] Further typing ... --- freqtrade/freqtradebot.py | 15 ++++++++++----- freqtrade/rpc/rpc_types.py | 17 +++++++++++++++-- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 28dd1fae5..7db9c1e30 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -30,7 +30,8 @@ from freqtrade.plugins.protectionmanager import ProtectionManager from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.rpc import RPCManager from freqtrade.rpc.external_message_consumer import ExternalMessageConsumer -from freqtrade.rpc.rpc_types import RPCBuyMsg, RPCCancelMsg, RPCSellCancelMsg, RPCSellMsg +from freqtrade.rpc.rpc_types import (RPCBuyMsg, RPCCancelMsg, RPCProtectionMsg, RPCSellCancelMsg, + RPCSellMsg) from freqtrade.strategy.interface import IStrategy from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.util import FtPrecise @@ -1853,14 +1854,18 @@ class FreqtradeBot(LoggingMixin): self.strategy.lock_pair(pair, datetime.now(timezone.utc), reason='Auto lock') prot_trig = self.protections.stop_per_pair(pair, side=side) if prot_trig: - msg = {'type': RPCMessageType.PROTECTION_TRIGGER, } - msg.update(prot_trig.to_json()) + msg: RPCProtectionMsg = { + 'type': RPCMessageType.PROTECTION_TRIGGER, + **prot_trig.to_json() + } self.rpc.send_msg(msg) prot_trig_glb = self.protections.global_stop(side=side) if prot_trig_glb: - msg = {'type': RPCMessageType.PROTECTION_TRIGGER_GLOBAL, } - msg.update(prot_trig_glb.to_json()) + msg = { + 'type': RPCMessageType.PROTECTION_TRIGGER_GLOBAL, + **prot_trig_glb.to_json() + } self.rpc.send_msg(msg) def apply_fee_conditional(self, trade: Trade, trade_base_currency: str, diff --git a/freqtrade/rpc/rpc_types.py b/freqtrade/rpc/rpc_types.py index bde985548..b81591954 100644 --- a/freqtrade/rpc/rpc_types.py +++ b/freqtrade/rpc/rpc_types.py @@ -1,19 +1,22 @@ from datetime import datetime -from typing import Optional, TypedDict, Union +from typing import List, Literal, Optional, TypedDict, Union from freqtrade.enums import RPCMessageType class RPCSendMsgBase(TypedDict): - type: RPCMessageType + pass + # ty1pe: Literal[RPCMessageType] class RPCStatusMsg(RPCSendMsgBase): """Used for Status, Startup and Warning messages""" + type: Literal[RPCMessageType.STATUS] status: str class RPCProtectionMsg(RPCSendMsgBase): + type: Literal[RPCMessageType.PROTECTION_TRIGGER, RPCMessageType.PROTECTION_TRIGGER_GLOBAL] id: int pair: str base_currency: Optional[str] @@ -26,7 +29,13 @@ class RPCProtectionMsg(RPCSendMsgBase): active: bool +class RPCWhitelistMsg(RPCSendMsgBase): + type: Literal[RPCMessageType.WHITELIST] + data: List[str] + + class RPCBuyMsg(RPCSendMsgBase): + type: Literal[RPCMessageType.ENTRY, RPCMessageType.ENTRY_FILL] trade_id: int buy_tag: Optional[str] enter_tag: Optional[str] @@ -47,10 +56,12 @@ class RPCBuyMsg(RPCSendMsgBase): class RPCCancelMsg(RPCBuyMsg): + type: Literal[RPCMessageType.ENTRY_CANCEL] reason: str class RPCSellMsg(RPCBuyMsg): + type: Literal[RPCMessageType.EXIT, RPCMessageType.EXIT_FILL] cumulative_profit: float gain: str # Literal["profit", "loss"] close_rate: float @@ -64,6 +75,7 @@ class RPCSellMsg(RPCBuyMsg): class RPCSellCancelMsg(RPCBuyMsg): + type: Literal[RPCMessageType.EXIT_CANCEL] reason: str gain: str # Literal["profit", "loss"] profit_amount: float @@ -76,6 +88,7 @@ class RPCSellCancelMsg(RPCBuyMsg): RPCSendMsg = Union[ RPCStatusMsg, RPCProtectionMsg, + RPCWhitelistMsg, RPCBuyMsg, RPCCancelMsg, RPCSellMsg, From 76d289f0cecb504c18e678cf06c93cbefab5ca0d Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 24 Mar 2023 20:35:01 +0100 Subject: [PATCH 305/360] Don't overwrite types --- freqtrade/rpc/rpc_types.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/freqtrade/rpc/rpc_types.py b/freqtrade/rpc/rpc_types.py index b81591954..a43cfd34e 100644 --- a/freqtrade/rpc/rpc_types.py +++ b/freqtrade/rpc/rpc_types.py @@ -34,8 +34,7 @@ class RPCWhitelistMsg(RPCSendMsgBase): data: List[str] -class RPCBuyMsg(RPCSendMsgBase): - type: Literal[RPCMessageType.ENTRY, RPCMessageType.ENTRY_FILL] +class __RPCBuyMsgBase(RPCSendMsgBase): trade_id: int buy_tag: Optional[str] enter_tag: Optional[str] @@ -55,12 +54,16 @@ class RPCBuyMsg(RPCSendMsgBase): sub_trade: bool -class RPCCancelMsg(RPCBuyMsg): +class RPCBuyMsg(__RPCBuyMsgBase): + type: Literal[RPCMessageType.ENTRY, RPCMessageType.ENTRY_FILL] + + +class RPCCancelMsg(__RPCBuyMsgBase): type: Literal[RPCMessageType.ENTRY_CANCEL] reason: str -class RPCSellMsg(RPCBuyMsg): +class RPCSellMsg(__RPCBuyMsgBase): type: Literal[RPCMessageType.EXIT, RPCMessageType.EXIT_FILL] cumulative_profit: float gain: str # Literal["profit", "loss"] @@ -74,7 +77,7 @@ class RPCSellMsg(RPCBuyMsg): order_rate: Optional[float] -class RPCSellCancelMsg(RPCBuyMsg): +class RPCSellCancelMsg(__RPCBuyMsgBase): type: Literal[RPCMessageType.EXIT_CANCEL] reason: str gain: str # Literal["profit", "loss"] From e8cffeeffd9972f97769406ffce6498875b089ba Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 24 Mar 2023 20:36:29 +0100 Subject: [PATCH 306/360] Update RPCStatusMessage type --- freqtrade/rpc/rpc_types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/rpc_types.py b/freqtrade/rpc/rpc_types.py index a43cfd34e..5ddaa0c17 100644 --- a/freqtrade/rpc/rpc_types.py +++ b/freqtrade/rpc/rpc_types.py @@ -11,7 +11,7 @@ class RPCSendMsgBase(TypedDict): class RPCStatusMsg(RPCSendMsgBase): """Used for Status, Startup and Warning messages""" - type: Literal[RPCMessageType.STATUS] + type: Literal[RPCMessageType.STATUS, RPCMessageType.STARTUP, RPCMessageType.WARNING] status: str From 8928d3616a81a45d8c7084150f67beded55337a7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 24 Mar 2023 20:47:17 +0100 Subject: [PATCH 307/360] Improve msgtypes --- freqtrade/freqtradebot.py | 4 ++++ freqtrade/rpc/rpc_types.py | 8 ++++++++ freqtrade/rpc/telegram.py | 2 +- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 7db9c1e30..3aa1075d8 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -973,6 +973,7 @@ class FreqtradeBot(LoggingMixin): 'order_type': order_type, 'stake_amount': trade.stake_amount, 'stake_currency': self.config['stake_currency'], + 'base_currency': self.exchange.get_pair_base_currency(trade.pair), 'fiat_currency': self.config.get('fiat_display_currency', None), 'amount': order.safe_amount_after_fee if fill else (order.amount or trade.amount), 'open_date': trade.open_date or datetime.utcnow(), @@ -1005,6 +1006,7 @@ class FreqtradeBot(LoggingMixin): 'stake_amount': trade.stake_amount, 'open_rate': trade.open_rate, 'stake_currency': self.config['stake_currency'], + 'base_currency': self.exchange.get_pair_base_currency(trade.pair), 'fiat_currency': self.config.get('fiat_display_currency', None), 'amount': trade.amount, 'open_date': trade.open_date, @@ -1695,6 +1697,7 @@ class FreqtradeBot(LoggingMixin): 'close_date': trade.close_date or datetime.utcnow(), 'stake_amount': trade.stake_amount, 'stake_currency': self.config['stake_currency'], + 'base_currency': self.exchange.get_pair_base_currency(trade.pair), 'fiat_currency': self.config.get('fiat_display_currency'), 'sub_trade': sub_trade, 'cumulative_profit': trade.realized_profit, @@ -1747,6 +1750,7 @@ class FreqtradeBot(LoggingMixin): 'open_date': trade.open_date, 'close_date': trade.close_date or datetime.now(timezone.utc), 'stake_currency': self.config['stake_currency'], + 'base_currency': self.exchange.get_pair_base_currency(trade.pair), 'fiat_currency': self.config.get('fiat_display_currency', None), 'reason': reason, 'sub_trade': sub_trade, diff --git a/freqtrade/rpc/rpc_types.py b/freqtrade/rpc/rpc_types.py index 5ddaa0c17..5a3549aa7 100644 --- a/freqtrade/rpc/rpc_types.py +++ b/freqtrade/rpc/rpc_types.py @@ -15,6 +15,12 @@ class RPCStatusMsg(RPCSendMsgBase): status: str +class RPCStrategyMsg(RPCSendMsgBase): + """Used for Status, Startup and Warning messages""" + type: Literal[RPCMessageType.STRATEGY_MSG] + msg: str + + class RPCProtectionMsg(RPCSendMsgBase): type: Literal[RPCMessageType.PROTECTION_TRIGGER, RPCMessageType.PROTECTION_TRIGGER_GLOBAL] id: int @@ -40,6 +46,7 @@ class __RPCBuyMsgBase(RPCSendMsgBase): enter_tag: Optional[str] exchange: str pair: str + base_currency: str leverage: Optional[float] direction: str limit: float @@ -90,6 +97,7 @@ class RPCSellCancelMsg(__RPCBuyMsgBase): RPCSendMsg = Union[ RPCStatusMsg, + RPCStrategyMsg, RPCProtectionMsg, RPCWhitelistMsg, RPCBuyMsg, diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index c1365702d..6f34b7325 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -437,7 +437,7 @@ class Telegram(RPCHandler): msg_type = msg['type'] noti = '' - if msg_type == RPCMessageType.EXIT: + if msg['type'] == RPCMessageType.EXIT: sell_noti = self._config['telegram'] \ .get('notification_settings', {}).get(str(msg_type), {}) # For backward compatibility sell still can be string From ad58bac810bf5cfd2fbcd9556a192f0b1dc0d429 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 24 Mar 2023 20:54:28 +0100 Subject: [PATCH 308/360] Type WS messagetypes --- freqtrade/data/dataprovider.py | 6 +++--- freqtrade/rpc/rpc_types.py | 25 +++++++++++++++++++++++-- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index 3991432a4..d05ee5db7 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -21,6 +21,7 @@ from freqtrade.exchange import Exchange, timeframe_to_seconds from freqtrade.exchange.types import OrderBook from freqtrade.misc import append_candles_to_dataframe from freqtrade.rpc import RPCManager +from freqtrade.rpc.rpc_types import RPCAnalyzedDFMsg from freqtrade.util import PeriodicCache @@ -118,8 +119,7 @@ class DataProvider: :param new_candle: This is a new candle """ if self.__rpc: - self.__rpc.send_msg( - { + msg: RPCAnalyzedDFMsg = { 'type': RPCMessageType.ANALYZED_DF, 'data': { 'key': pair_key, @@ -127,7 +127,7 @@ class DataProvider: 'la': datetime.now(timezone.utc) } } - ) + self.__rpc.send_msg(msg) if new_candle: self.__rpc.send_msg({ 'type': RPCMessageType.NEW_CANDLE, diff --git a/freqtrade/rpc/rpc_types.py b/freqtrade/rpc/rpc_types.py index 5a3549aa7..a290ee78c 100644 --- a/freqtrade/rpc/rpc_types.py +++ b/freqtrade/rpc/rpc_types.py @@ -1,6 +1,7 @@ from datetime import datetime -from typing import List, Literal, Optional, TypedDict, Union +from typing import Any, List, Literal, Optional, TypedDict, Union +from freqtrade.constants import PairWithTimeframe from freqtrade.enums import RPCMessageType @@ -95,6 +96,24 @@ class RPCSellCancelMsg(__RPCBuyMsgBase): close_date: datetime +class __AnalyzedDFData(TypedDict): + key: PairWithTimeframe + df: Any + la: datetime + + +class RPCAnalyzedDFMsg(RPCSendMsgBase): + """New Analyzed dataframe message""" + type: Literal[RPCMessageType.ANALYZED_DF] + data: __AnalyzedDFData + + +class RPCNewCandleMsg(RPCSendMsgBase): + """New candle ping message, issued once per new candle/pair""" + type: Literal[RPCMessageType.NEW_CANDLE] + data: PairWithTimeframe + + RPCSendMsg = Union[ RPCStatusMsg, RPCStrategyMsg, @@ -103,5 +122,7 @@ RPCSendMsg = Union[ RPCBuyMsg, RPCCancelMsg, RPCSellMsg, - RPCSellCancelMsg + RPCSellCancelMsg, + RPCAnalyzedDFMsg, + RPCNewCandleMsg ] From 281dd7785ebf190460bdf483f0839799bcf55d58 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 24 Mar 2023 20:56:18 +0100 Subject: [PATCH 309/360] Fix some remaining type errors --- freqtrade/freqtradebot.py | 7 +++---- freqtrade/rpc/telegram.py | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 3aa1075d8..1f7b4493c 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -948,7 +948,6 @@ class FreqtradeBot(LoggingMixin): """ Sends rpc notification when a entry order occurred. """ - msg_type = RPCMessageType.ENTRY_FILL if fill else RPCMessageType.ENTRY open_rate = order.safe_price if open_rate is None: @@ -961,7 +960,7 @@ class FreqtradeBot(LoggingMixin): msg: RPCBuyMsg = { 'trade_id': trade.id, - 'type': msg_type, + 'type': RPCMessageType.ENTRY_FILL if fill else RPCMessageType.ENTRY, 'buy_tag': trade.enter_tag, 'enter_tag': trade.enter_tag, 'exchange': trade.exchange.capitalize(), @@ -1860,7 +1859,7 @@ class FreqtradeBot(LoggingMixin): if prot_trig: msg: RPCProtectionMsg = { 'type': RPCMessageType.PROTECTION_TRIGGER, - **prot_trig.to_json() + **prot_trig.to_json() # type: ignore } self.rpc.send_msg(msg) @@ -1868,7 +1867,7 @@ class FreqtradeBot(LoggingMixin): if prot_trig_glb: msg = { 'type': RPCMessageType.PROTECTION_TRIGGER_GLOBAL, - **prot_trig_glb.to_json() + **prot_trig_glb.to_json() # type: ignore } self.rpc.send_msg(msg) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 6f34b7325..d79d8ea76 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -454,7 +454,7 @@ class Telegram(RPCHandler): # Notification disabled return - message = self.compose_message(deepcopy(msg), msg_type) + message = self.compose_message(deepcopy(msg), msg_type) # type: ignore if message: self._send_msg(message, disable_notification=(noti == 'silent')) From cbdd86d7775ac767b45c0ce4673cb192eeb30fad Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 24 Mar 2023 21:05:10 +0100 Subject: [PATCH 310/360] Fix test failures due to additional field --- freqtrade/rpc/rpc_types.py | 4 ++-- tests/test_freqtradebot.py | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/rpc_types.py b/freqtrade/rpc/rpc_types.py index a290ee78c..3277a2d6e 100644 --- a/freqtrade/rpc/rpc_types.py +++ b/freqtrade/rpc/rpc_types.py @@ -96,7 +96,7 @@ class RPCSellCancelMsg(__RPCBuyMsgBase): close_date: datetime -class __AnalyzedDFData(TypedDict): +class _AnalyzedDFData(TypedDict): key: PairWithTimeframe df: Any la: datetime @@ -105,7 +105,7 @@ class __AnalyzedDFData(TypedDict): class RPCAnalyzedDFMsg(RPCSendMsgBase): """New Analyzed dataframe message""" type: Literal[RPCMessageType.ANALYZED_DF] - data: __AnalyzedDFData + data: _AnalyzedDFData class RPCNewCandleMsg(RPCSendMsgBase): diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index cea70ec48..da98fed94 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -3326,6 +3326,7 @@ def test_execute_trade_exit_up(default_conf_usdt, ticker_usdt, fee, ticker_usdt_ 'profit_ratio': 0.00493809 if is_short else 0.09451372, 'stake_currency': 'USDT', 'fiat_currency': 'USD', + 'base_currency': 'ETH', 'sell_reason': ExitType.ROI.value, 'exit_reason': ExitType.ROI.value, 'open_date': ANY, @@ -3389,6 +3390,7 @@ def test_execute_trade_exit_down(default_conf_usdt, ticker_usdt, fee, ticker_usd 'profit_amount': -5.65990099 if is_short else -0.00075, 'profit_ratio': -0.0945681 if is_short else -1.247e-05, 'stake_currency': 'USDT', + 'base_currency': 'ETH', 'fiat_currency': 'USD', 'sell_reason': ExitType.STOP_LOSS.value, 'exit_reason': ExitType.STOP_LOSS.value, @@ -3474,6 +3476,7 @@ def test_execute_trade_exit_custom_exit_price( 'profit_amount': pytest.approx(profit_amount), 'profit_ratio': profit_ratio, 'stake_currency': 'USDT', + 'base_currency': 'ETH', 'fiat_currency': 'USD', 'sell_reason': 'foo', 'exit_reason': 'foo', @@ -3547,6 +3550,7 @@ def test_execute_trade_exit_down_stoploss_on_exchange_dry_run( 'profit_ratio': -0.00501253 if is_short else -0.01493766, 'stake_currency': 'USDT', 'fiat_currency': 'USD', + 'base_currency': 'ETH', 'sell_reason': ExitType.STOP_LOSS.value, 'exit_reason': ExitType.STOP_LOSS.value, 'open_date': ANY, @@ -3811,6 +3815,7 @@ def test_execute_trade_exit_market_order( 'profit_amount': pytest.approx(profit_amount), 'profit_ratio': profit_ratio, 'stake_currency': 'USDT', + 'base_currency': 'ETH', 'fiat_currency': 'USD', 'sell_reason': ExitType.ROI.value, 'exit_reason': ExitType.ROI.value, From c0a57d352f7f8058cb20c4b60b4b5779ca9b1a45 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 Mar 2023 08:16:07 +0100 Subject: [PATCH 311/360] send base_currency with messages that need it. --- freqtrade/freqtradebot.py | 2 ++ freqtrade/rpc/rpc_manager.py | 4 ---- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 1f7b4493c..1f072571d 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1859,6 +1859,7 @@ class FreqtradeBot(LoggingMixin): if prot_trig: msg: RPCProtectionMsg = { 'type': RPCMessageType.PROTECTION_TRIGGER, + 'base_currency': self.exchange.get_pair_base_currency(prot_trig.pair), **prot_trig.to_json() # type: ignore } self.rpc.send_msg(msg) @@ -1867,6 +1868,7 @@ class FreqtradeBot(LoggingMixin): if prot_trig_glb: msg = { 'type': RPCMessageType.PROTECTION_TRIGGER_GLOBAL, + 'base_currency': self.exchange.get_pair_base_currency(prot_trig_glb.pair), **prot_trig_glb.to_json() # type: ignore } self.rpc.send_msg(msg) diff --git a/freqtrade/rpc/rpc_manager.py b/freqtrade/rpc/rpc_manager.py index e4c925995..1972ad6e5 100644 --- a/freqtrade/rpc/rpc_manager.py +++ b/freqtrade/rpc/rpc_manager.py @@ -70,10 +70,6 @@ class RPCManager: """ if msg.get('type') not in NO_ECHO_MESSAGES: logger.info('Sending rpc message: %s', msg) - if 'pair' in msg: - msg.update({ - 'base_currency': self._rpc._freqtrade.exchange.get_pair_base_currency(msg['pair']) - }) for mod in self.registered_modules: logger.debug('Forwarding message to rpc.%s', mod.name) try: From 79a2de7a64a7a737bf6bdc593a187dd422453bb2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 Mar 2023 08:31:35 +0100 Subject: [PATCH 312/360] Reduce impact of short outages --- tests/exchange/test_ccxt_compat.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index 872cf5059..4a65b16d7 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -37,7 +37,7 @@ EXCHANGES = { 'stake_currency': 'USDT', 'use_ci_proxy': True, 'hasQuoteVolume': True, - 'timeframe': '5m', + 'timeframe': '1h', 'futures': True, 'futures_pair': 'BTC/USDT:USDT', 'hasQuoteVolumeFutures': True, @@ -66,7 +66,7 @@ EXCHANGES = { 'pair': 'BTC/USDT', 'stake_currency': 'USDT', 'hasQuoteVolume': True, - 'timeframe': '5m', + 'timeframe': '1h', 'futures': False, 'sample_order': [{ "symbol": "SOLUSDT", @@ -91,7 +91,7 @@ EXCHANGES = { 'pair': 'BTC/USDT', 'stake_currency': 'USDT', 'hasQuoteVolume': True, - 'timeframe': '5m', + 'timeframe': '1h', 'leverage_tiers_public': False, 'leverage_in_spot_market': True, }, @@ -99,7 +99,7 @@ EXCHANGES = { 'pair': 'XRP/USDT', 'stake_currency': 'USDT', 'hasQuoteVolume': True, - 'timeframe': '5m', + 'timeframe': '1h', 'leverage_tiers_public': False, 'leverage_in_spot_market': True, 'sample_order': [ @@ -141,7 +141,7 @@ EXCHANGES = { 'pair': 'BTC/USDT', 'stake_currency': 'USDT', 'hasQuoteVolume': True, - 'timeframe': '5m', + 'timeframe': '1h', 'futures': True, 'futures_pair': 'BTC/USDT:USDT', 'hasQuoteVolumeFutures': True, @@ -215,7 +215,7 @@ EXCHANGES = { 'pair': 'BTC/USDT', 'stake_currency': 'USDT', 'hasQuoteVolume': True, - 'timeframe': '5m', + 'timeframe': '1h', 'futures': True, 'futures_pair': 'BTC/USDT:USDT', 'hasQuoteVolumeFutures': False, @@ -226,7 +226,7 @@ EXCHANGES = { 'pair': 'BTC/USDT', 'stake_currency': 'USDT', 'hasQuoteVolume': True, - 'timeframe': '5m', + 'timeframe': '1h', 'futures_pair': 'BTC/USDT:USDT', 'futures': True, 'leverage_tiers_public': True, @@ -253,14 +253,14 @@ EXCHANGES = { 'pair': 'ETH/BTC', 'stake_currency': 'BTC', 'hasQuoteVolume': True, - 'timeframe': '5m', + 'timeframe': '1h', 'futures': False, }, 'bitvavo': { 'pair': 'BTC/EUR', 'stake_currency': 'EUR', 'hasQuoteVolume': True, - 'timeframe': '5m', + 'timeframe': '1h', 'leverage_tiers_public': False, 'leverage_in_spot_market': False, }, From 56170dba19b5d46fc5884e17d786b0f17fb0feda Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 Mar 2023 08:44:35 +0100 Subject: [PATCH 313/360] use github to download guess instead of gnu.org gnu.org seems down rn (dns does no longer resolve), and doesn't have good uptime history --- build_helpers/install_ta-lib.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build_helpers/install_ta-lib.sh b/build_helpers/install_ta-lib.sh index 079d578b4..005d9abca 100755 --- a/build_helpers/install_ta-lib.sh +++ b/build_helpers/install_ta-lib.sh @@ -8,8 +8,8 @@ if [ -n "$2" ] || [ ! -f "${INSTALL_LOC}/lib/libta_lib.a" ]; then tar zxvf ta-lib-0.4.0-src.tar.gz cd ta-lib \ && sed -i.bak "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h \ - && curl 'http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.guess;hb=HEAD' -o config.guess \ - && curl 'http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.sub;hb=HEAD' -o config.sub \ + && curl 'https://raw.githubusercontent.com/gcc-mirror/gcc/master/config.guess' -o config.guess \ + && curl 'https://raw.githubusercontent.com/gcc-mirror/gcc/master/config.sub' -o config.sub \ && ./configure --prefix=${INSTALL_LOC}/ \ && make if [ $? -ne 0 ]; then From cdd44a40058bd8bce80519d8f79007ff1034fb3a Mon Sep 17 00:00:00 2001 From: linquanisaac Date: Sat, 25 Mar 2023 17:19:58 +0800 Subject: [PATCH 314/360] docs(protections): fix typo --- docs/includes/protections.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/includes/protections.md b/docs/includes/protections.md index e0ad8189f..12af081c0 100644 --- a/docs/includes/protections.md +++ b/docs/includes/protections.md @@ -149,7 +149,7 @@ The below example assumes a timeframe of 1 hour: * Locks each pair after selling for an additional 5 candles (`CooldownPeriod`), giving other pairs a chance to get filled. * Stops trading for 4 hours (`4 * 1h candles`) if the last 2 days (`48 * 1h candles`) had 20 trades, which caused a max-drawdown of more than 20%. (`MaxDrawdown`). * Stops trading if more than 4 stoploss occur for all pairs within a 1 day (`24 * 1h candles`) limit (`StoplossGuard`). -* Locks all pairs that had 4 Trades within the last 6 hours (`6 * 1h candles`) with a combined profit ratio of below 0.02 (<2%) (`LowProfitPairs`). +* Locks all pairs that had 2 Trades within the last 6 hours (`6 * 1h candles`) with a combined profit ratio of below 0.02 (<2%) (`LowProfitPairs`). * Locks all pairs for 2 candles that had a profit of below 0.01 (<1%) within the last 24h (`24 * 1h candles`), a minimum of 4 trades. ``` python From 9c6a49436bd17752a8c0921c801c642116658c95 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 Mar 2023 11:42:19 +0100 Subject: [PATCH 315/360] Export amount/price precisions per trade --- freqtrade/persistence/trade_model.py | 3 + tests/persistence/test_persistence.py | 256 ++++++++++++++------------ tests/rpc/test_rpc.py | 3 + 3 files changed, 141 insertions(+), 121 deletions(-) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 27be0d726..54ff1313b 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -560,6 +560,9 @@ class LocalTrade(): 'trading_mode': self.trading_mode, 'funding_fees': self.funding_fees, 'open_order_id': self.open_order_id, + 'amount_precision': self.amount_precision, + 'price_precision': self.price_precision, + 'precision_mode': self.precision_mode, 'orders': orders, } diff --git a/tests/persistence/test_persistence.py b/tests/persistence/test_persistence.py index db882d56d..23ec6d4fb 100644 --- a/tests/persistence/test_persistence.py +++ b/tests/persistence/test_persistence.py @@ -1330,71 +1330,78 @@ def test_to_json(fee): open_rate=0.123, exchange='binance', enter_tag=None, - open_order_id='dry_run_buy_12345' + open_order_id='dry_run_buy_12345', + precision_mode=1, + amount_precision=8.0, + price_precision=7.0, ) result = trade.to_json() assert isinstance(result, dict) - assert result == {'trade_id': None, - 'pair': 'ADA/USDT', - 'base_currency': 'ADA', - 'quote_currency': 'USDT', - 'is_open': None, - 'open_date': trade.open_date.strftime(DATETIME_PRINT_FORMAT), - 'open_timestamp': int(trade.open_date.timestamp() * 1000), - 'open_order_id': 'dry_run_buy_12345', - 'close_date': None, - 'close_timestamp': None, - 'open_rate': 0.123, - 'open_rate_requested': None, - 'open_trade_value': 15.1668225, - 'fee_close': 0.0025, - 'fee_close_cost': None, - 'fee_close_currency': None, - 'fee_open': 0.0025, - 'fee_open_cost': None, - 'fee_open_currency': None, - 'close_rate': None, - 'close_rate_requested': None, - 'amount': 123.0, - 'amount_requested': 123.0, - 'stake_amount': 0.001, - 'max_stake_amount': None, - 'trade_duration': None, - 'trade_duration_s': None, - 'realized_profit': 0.0, - 'realized_profit_ratio': None, - 'close_profit': None, - 'close_profit_pct': None, - 'close_profit_abs': None, - 'profit_ratio': None, - 'profit_pct': None, - 'profit_abs': None, - 'exit_reason': None, - 'exit_order_status': None, - 'stop_loss_abs': None, - 'stop_loss_ratio': None, - 'stop_loss_pct': None, - 'stoploss_order_id': None, - 'stoploss_last_update': None, - 'stoploss_last_update_timestamp': None, - 'initial_stop_loss_abs': None, - 'initial_stop_loss_pct': None, - 'initial_stop_loss_ratio': None, - 'min_rate': None, - 'max_rate': None, - 'strategy': None, - 'enter_tag': None, - 'timeframe': None, - 'exchange': 'binance', - 'leverage': None, - 'interest_rate': None, - 'liquidation_price': None, - 'is_short': None, - 'trading_mode': None, - 'funding_fees': None, - 'orders': [], - } + assert result == { + 'trade_id': None, + 'pair': 'ADA/USDT', + 'base_currency': 'ADA', + 'quote_currency': 'USDT', + 'is_open': None, + 'open_date': trade.open_date.strftime(DATETIME_PRINT_FORMAT), + 'open_timestamp': int(trade.open_date.timestamp() * 1000), + 'open_order_id': 'dry_run_buy_12345', + 'close_date': None, + 'close_timestamp': None, + 'open_rate': 0.123, + 'open_rate_requested': None, + 'open_trade_value': 15.1668225, + 'fee_close': 0.0025, + 'fee_close_cost': None, + 'fee_close_currency': None, + 'fee_open': 0.0025, + 'fee_open_cost': None, + 'fee_open_currency': None, + 'close_rate': None, + 'close_rate_requested': None, + 'amount': 123.0, + 'amount_requested': 123.0, + 'stake_amount': 0.001, + 'max_stake_amount': None, + 'trade_duration': None, + 'trade_duration_s': None, + 'realized_profit': 0.0, + 'realized_profit_ratio': None, + 'close_profit': None, + 'close_profit_pct': None, + 'close_profit_abs': None, + 'profit_ratio': None, + 'profit_pct': None, + 'profit_abs': None, + 'exit_reason': None, + 'exit_order_status': None, + 'stop_loss_abs': None, + 'stop_loss_ratio': None, + 'stop_loss_pct': None, + 'stoploss_order_id': None, + 'stoploss_last_update': None, + 'stoploss_last_update_timestamp': None, + 'initial_stop_loss_abs': None, + 'initial_stop_loss_pct': None, + 'initial_stop_loss_ratio': None, + 'min_rate': None, + 'max_rate': None, + 'strategy': None, + 'enter_tag': None, + 'timeframe': None, + 'exchange': 'binance', + 'leverage': None, + 'interest_rate': None, + 'liquidation_price': None, + 'is_short': None, + 'trading_mode': None, + 'funding_fees': None, + 'amount_precision': 8.0, + 'price_precision': 7.0, + 'precision_mode': 1, + 'orders': [], + } # Simulate dry_run entries trade = Trade( @@ -1410,70 +1417,77 @@ def test_to_json(fee): close_rate=0.125, enter_tag='buys_signal_001', exchange='binance', + precision_mode=2, + amount_precision=7.0, + price_precision=8.0, ) result = trade.to_json() assert isinstance(result, dict) - assert result == {'trade_id': None, - 'pair': 'XRP/BTC', - 'base_currency': 'XRP', - 'quote_currency': 'BTC', - 'open_date': trade.open_date.strftime(DATETIME_PRINT_FORMAT), - 'open_timestamp': int(trade.open_date.timestamp() * 1000), - 'close_date': trade.close_date.strftime(DATETIME_PRINT_FORMAT), - 'close_timestamp': int(trade.close_date.timestamp() * 1000), - 'open_rate': 0.123, - 'close_rate': 0.125, - 'amount': 100.0, - 'amount_requested': 101.0, - 'stake_amount': 0.001, - 'max_stake_amount': None, - 'trade_duration': 60, - 'trade_duration_s': 3600, - 'stop_loss_abs': None, - 'stop_loss_pct': None, - 'stop_loss_ratio': None, - 'stoploss_order_id': None, - 'stoploss_last_update': None, - 'stoploss_last_update_timestamp': None, - 'initial_stop_loss_abs': None, - 'initial_stop_loss_pct': None, - 'initial_stop_loss_ratio': None, - 'realized_profit': 0.0, - 'realized_profit_ratio': None, - 'close_profit': None, - 'close_profit_pct': None, - 'close_profit_abs': None, - 'profit_ratio': None, - 'profit_pct': None, - 'profit_abs': None, - 'close_rate_requested': None, - 'fee_close': 0.0025, - 'fee_close_cost': None, - 'fee_close_currency': None, - 'fee_open': 0.0025, - 'fee_open_cost': None, - 'fee_open_currency': None, - 'is_open': None, - 'max_rate': None, - 'min_rate': None, - 'open_order_id': None, - 'open_rate_requested': None, - 'open_trade_value': 12.33075, - 'exit_reason': None, - 'exit_order_status': None, - 'strategy': None, - 'enter_tag': 'buys_signal_001', - 'timeframe': None, - 'exchange': 'binance', - 'leverage': None, - 'interest_rate': None, - 'liquidation_price': None, - 'is_short': None, - 'trading_mode': None, - 'funding_fees': None, - 'orders': [], - } + assert result == { + 'trade_id': None, + 'pair': 'XRP/BTC', + 'base_currency': 'XRP', + 'quote_currency': 'BTC', + 'open_date': trade.open_date.strftime(DATETIME_PRINT_FORMAT), + 'open_timestamp': int(trade.open_date.timestamp() * 1000), + 'close_date': trade.close_date.strftime(DATETIME_PRINT_FORMAT), + 'close_timestamp': int(trade.close_date.timestamp() * 1000), + 'open_rate': 0.123, + 'close_rate': 0.125, + 'amount': 100.0, + 'amount_requested': 101.0, + 'stake_amount': 0.001, + 'max_stake_amount': None, + 'trade_duration': 60, + 'trade_duration_s': 3600, + 'stop_loss_abs': None, + 'stop_loss_pct': None, + 'stop_loss_ratio': None, + 'stoploss_order_id': None, + 'stoploss_last_update': None, + 'stoploss_last_update_timestamp': None, + 'initial_stop_loss_abs': None, + 'initial_stop_loss_pct': None, + 'initial_stop_loss_ratio': None, + 'realized_profit': 0.0, + 'realized_profit_ratio': None, + 'close_profit': None, + 'close_profit_pct': None, + 'close_profit_abs': None, + 'profit_ratio': None, + 'profit_pct': None, + 'profit_abs': None, + 'close_rate_requested': None, + 'fee_close': 0.0025, + 'fee_close_cost': None, + 'fee_close_currency': None, + 'fee_open': 0.0025, + 'fee_open_cost': None, + 'fee_open_currency': None, + 'is_open': None, + 'max_rate': None, + 'min_rate': None, + 'open_order_id': None, + 'open_rate_requested': None, + 'open_trade_value': 12.33075, + 'exit_reason': None, + 'exit_order_status': None, + 'strategy': None, + 'enter_tag': 'buys_signal_001', + 'timeframe': None, + 'exchange': 'binance', + 'leverage': None, + 'interest_rate': None, + 'liquidation_price': None, + 'is_short': None, + 'trading_mode': None, + 'funding_fees': None, + 'amount_precision': 7.0, + 'price_precision': 8.0, + 'precision_mode': 2, + 'orders': [], + } def test_stoploss_reinitialization(default_conf, fee): diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 4e2dc94ae..ff08a0564 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -88,6 +88,9 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'is_short': False, 'funding_fees': 0.0, 'trading_mode': TradingMode.SPOT, + 'amount_precision': 8.0, + 'price_precision': 8.0, + 'precision_mode': 2, 'orders': [{ 'amount': 91.07468123, 'average': 1.098e-05, 'safe_price': 1.098e-05, 'cost': 0.0009999999999054, 'filled': 91.07468123, 'ft_order_side': 'buy', From f7c1ee6d3e1aa9f40ba5329d70cbe3e291c77581 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 Mar 2023 11:55:47 +0100 Subject: [PATCH 316/360] add precision values to api schema --- freqtrade/rpc/api_server/api_schemas.py | 4 ++++ tests/rpc/test_rpc_apiserver.py | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 18621ccbd..7497b27f1 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -276,6 +276,10 @@ class TradeSchema(BaseModel): funding_fees: Optional[float] trading_mode: Optional[TradingMode] + amount_precision: Optional[float] + price_precision: Optional[float] + precision_mode: Optional[int] + class OpenTradeSchema(TradeSchema): stoploss_current_dist: Optional[float] diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 97319b78b..bf9d6cc3b 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -1066,6 +1066,9 @@ def test_api_status(botclient, mocker, ticker, fee, markets, is_short, 'liquidation_price': None, 'funding_fees': None, 'trading_mode': ANY, + 'amount_precision': None, + 'price_precision': None, + 'precision_mode': None, 'orders': [ANY], } @@ -1271,6 +1274,9 @@ def test_api_force_entry(botclient, mocker, fee, endpoint): 'liquidation_price': None, 'funding_fees': None, 'trading_mode': 'spot', + 'amount_precision': None, + 'price_precision': None, + 'precision_mode': None, 'orders': [], } From 68154a1f52c002944132242a089de29f27b0196e Mon Sep 17 00:00:00 2001 From: robcaulk Date: Sat, 25 Mar 2023 11:57:52 +0100 Subject: [PATCH 317/360] document why users cant arbitrarily change parameter spaces... --- docs/freqai-running.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/freqai-running.md b/docs/freqai-running.md index 1eaee1bf2..f3ccc546f 100644 --- a/docs/freqai-running.md +++ b/docs/freqai-running.md @@ -128,6 +128,9 @@ The FreqAI specific parameter `label_period_candles` defines the offset (number You can choose to adopt a continual learning scheme by setting `"continual_learning": true` in the config. By enabling `continual_learning`, after training an initial model from scratch, subsequent trainings will start from the final model state of the preceding training. This gives the new model a "memory" of the previous state. By default, this is set to `False` which means that all new models are trained from scratch, without input from previous models. +???+ danger "Continual learning enforces a constant parameter space" + Since `continual_learning` means that the model parameter space *cannot* change between trainings, `principal_component_analysis` is automatically disabled when `continual_learning` is enabled. Hint: PCA changes the parameter space and the number of features, learn more about PCA [here](freqai-feature-engineering.md#data-dimensionality-reduction-with-principal-component-analysis). + ## Hyperopt You can hyperopt using the same command as for [typical Freqtrade hyperopt](hyperopt.md): From d9c8b322ce45c88abeb23ffc63ad815e10c72e74 Mon Sep 17 00:00:00 2001 From: Robert Caulk Date: Sat, 25 Mar 2023 13:37:07 +0100 Subject: [PATCH 318/360] Update freqai_interface.py --- freqtrade/freqai/freqai_interface.py | 1 - 1 file changed, 1 deletion(-) diff --git a/freqtrade/freqai/freqai_interface.py b/freqtrade/freqai/freqai_interface.py index 8e842b8f2..b657bd811 100644 --- a/freqtrade/freqai/freqai_interface.py +++ b/freqtrade/freqai/freqai_interface.py @@ -109,7 +109,6 @@ class IFreqaiModel(ABC): self.ft_params.update({'principal_component_analysis': False}) logger.warning('User tried to use PCA with continual learning. Deactivating PCA.') - record_params(config, self.full_path) def __getstate__(self): From 486d8a48a05befc9dee60da922f319b51aebc651 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 Mar 2023 19:36:28 +0100 Subject: [PATCH 319/360] Fix docs (buffer_train_data_candles is an integer, not a boolean) closes #8384 --- docs/freqai-parameter-table.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/freqai-parameter-table.md b/docs/freqai-parameter-table.md index f67ea8541..9822a895a 100644 --- a/docs/freqai-parameter-table.md +++ b/docs/freqai-parameter-table.md @@ -46,7 +46,7 @@ Mandatory parameters are marked as **Required** and have to be set in one of the | `outlier_protection_percentage` | Enable to prevent outlier detection methods from discarding too much data. If more than `outlier_protection_percentage` % of points are detected as outliers by the SVM or DBSCAN, FreqAI will log a warning message and ignore outlier detection, i.e., the original dataset will be kept intact. If the outlier protection is triggered, no predictions will be made based on the training dataset.
**Datatype:** Float.
Default: `30`. | `reverse_train_test_order` | Split the feature dataset (see below) and use the latest data split for training and test on historical split of the data. This allows the model to be trained up to the most recent data point, while avoiding overfitting. However, you should be careful to understand the unorthodox nature of this parameter before employing it.
**Datatype:** Boolean.
Default: `False` (no reversal). | `shuffle_after_split` | Split the data into train and test sets, and then shuffle both sets individually.
**Datatype:** Boolean.
Default: `False`. -| `buffer_train_data_candles` | Cut `buffer_train_data_candles` off the beginning and end of the training data *after* the indicators were populated. The main example use is when predicting maxima and minima, the argrelextrema function cannot know the maxima/minima at the edges of the timerange. To improve model accuracy, it is best to compute argrelextrema on the full timerange and then use this function to cut off the edges (buffer) by the kernel. In another case, if the targets are set to a shifted price movement, this buffer is unnecessary because the shifted candles at the end of the timerange will be NaN and FreqAI will automatically cut those off of the training dataset.
**Datatype:** Boolean.
Default: `False`. +| `buffer_train_data_candles` | Cut `buffer_train_data_candles` off the beginning and end of the training data *after* the indicators were populated. The main example use is when predicting maxima and minima, the argrelextrema function cannot know the maxima/minima at the edges of the timerange. To improve model accuracy, it is best to compute argrelextrema on the full timerange and then use this function to cut off the edges (buffer) by the kernel. In another case, if the targets are set to a shifted price movement, this buffer is unnecessary because the shifted candles at the end of the timerange will be NaN and FreqAI will automatically cut those off of the training dataset.
**Datatype:** Integer.
Default: `0`. ### Data split parameters From 298f5685ee3f3c902243efe0e953fb83076bffa6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 Mar 2023 20:06:21 +0100 Subject: [PATCH 320/360] Reuse existing "cancel_stoploss" call --- freqtrade/freqtradebot.py | 12 ++++-------- tests/test_freqtradebot.py | 16 +++++++++------- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 623d39c09..4482f37bf 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -854,7 +854,8 @@ class FreqtradeBot(LoggingMixin): # Reset stoploss order id. trade.stoploss_order_id = None except InvalidOrderException: - logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}") + logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id} " + f"for pair {trade.pair}") return trade def get_valid_enter_price_and_stake( @@ -1239,13 +1240,8 @@ class FreqtradeBot(LoggingMixin): # 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}") + + self.cancel_stoploss_on_exchange(trade) # Create new stoploss order if not self.create_stoploss_order(trade=trade, stop_price=stoploss_norm): diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index cea70ec48..ff10cd2f0 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1440,11 +1440,11 @@ def test_handle_stoploss_on_exchange_trailing( trade.is_short = is_short trade.is_open = True trade.open_order_id = None - trade.stoploss_order_id = 100 + trade.stoploss_order_id = '100' trade.stoploss_last_update = arrow.utcnow().shift(minutes=-20).datetime stoploss_order_hanging = MagicMock(return_value={ - 'id': 100, + 'id': '100', 'status': 'open', 'type': 'stop_loss_limit', 'price': hang_price, @@ -1483,13 +1483,14 @@ def test_handle_stoploss_on_exchange_trailing( assert freqtrade.handle_trade(trade) is False assert trade.stop_loss == stop_price[1] + trade.stoploss_order_id = '100' # setting stoploss_on_exchange_interval to 0 seconds freqtrade.strategy.order_types['stoploss_on_exchange_interval'] = 0 assert freqtrade.handle_stoploss_on_exchange(trade) is False - cancel_order_mock.assert_called_once_with(100, 'ETH/USDT') + cancel_order_mock.assert_called_once_with('100', 'ETH/USDT') stoploss_order_mock.assert_called_once_with( amount=pytest.approx(amt), pair='ETH/USDT', @@ -1673,11 +1674,11 @@ def test_handle_stoploss_on_exchange_custom_stop( trade.is_short = is_short trade.is_open = True trade.open_order_id = None - trade.stoploss_order_id = 100 + trade.stoploss_order_id = '100' trade.stoploss_last_update = arrow.utcnow().shift(minutes=-601).datetime stoploss_order_hanging = MagicMock(return_value={ - 'id': 100, + 'id': '100', 'status': 'open', 'type': 'stop_loss_limit', 'price': 3, @@ -1706,6 +1707,7 @@ def test_handle_stoploss_on_exchange_custom_stop( stoploss_order_mock = MagicMock(return_value={'id': 'so1'}) mocker.patch(f'{EXMS}.cancel_stoploss_order', cancel_order_mock) mocker.patch(f'{EXMS}.create_stoploss', stoploss_order_mock) + trade.stoploss_order_id = '100' # stoploss should not be updated as the interval is 60 seconds assert freqtrade.handle_trade(trade) is False @@ -1722,7 +1724,7 @@ def test_handle_stoploss_on_exchange_custom_stop( assert freqtrade.handle_stoploss_on_exchange(trade) is False - cancel_order_mock.assert_called_once_with(100, 'ETH/USDT') + cancel_order_mock.assert_called_once_with('100', 'ETH/USDT') # Long uses modified ask - offset, short modified bid + offset stoploss_order_mock.assert_called_once_with( amount=pytest.approx(trade.amount), @@ -3588,7 +3590,7 @@ def test_execute_trade_exit_sloe_cancel_exception( freqtrade.execute_trade_exit(trade=trade, limit=1234, exit_check=ExitCheckTuple(exit_type=ExitType.STOP_LOSS)) assert create_order_mock.call_count == 2 - assert log_has('Could not cancel stoploss order abcd', caplog) + assert log_has('Could not cancel stoploss order abcd for pair ETH/USDT', caplog) @pytest.mark.parametrize("is_short", [False, True]) From ee205ddc862ee0105e570deed47b728272d21521 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 Mar 2023 20:26:56 +0100 Subject: [PATCH 321/360] Improve trade.from_json when stops are used --- freqtrade/persistence/trade_model.py | 6 ++++-- tests/persistence/test_trade_fromjson.py | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 54ff1313b..17117d436 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -1663,8 +1663,10 @@ class Trade(ModelBase, LocalTrade): stop_loss=data["stop_loss_abs"], stop_loss_pct=data["stop_loss_ratio"], stoploss_order_id=data["stoploss_order_id"], - stoploss_last_update=(datetime.fromtimestamp(data["stoploss_last_update"] // 1000, - tz=timezone.utc) if data["stoploss_last_update"] else None), + stoploss_last_update=( + datetime.fromtimestamp(data["stoploss_last_update_timestamp"] // 1000, + tz=timezone.utc) + if data["stoploss_last_update_timestamp"] else None), initial_stop_loss=data["initial_stop_loss_abs"], initial_stop_loss_pct=data["initial_stop_loss_ratio"], min_rate=data["min_rate"], diff --git a/tests/persistence/test_trade_fromjson.py b/tests/persistence/test_trade_fromjson.py index 529008e02..22053463d 100644 --- a/tests/persistence/test_trade_fromjson.py +++ b/tests/persistence/test_trade_fromjson.py @@ -50,8 +50,8 @@ def test_trade_fromjson(): "stop_loss_ratio": -0.216, "stop_loss_pct": -21.6, "stoploss_order_id": null, - "stoploss_last_update": null, - "stoploss_last_update_timestamp": null, + "stoploss_last_update": "2022-10-18 09:13:42", + "stoploss_last_update_timestamp": 1666077222000, "initial_stop_loss_abs": 0.1981, "initial_stop_loss_ratio": -0.216, "initial_stop_loss_pct": -21.6, From 86aef7cf9da6eec4c4bdb621bba33c7a38d7eeb5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 26 Mar 2023 11:21:18 +0200 Subject: [PATCH 322/360] Add current_time to bot_loop_start callbak --- docs/strategy-callbacks.md | 3 ++- freqtrade/freqtradebot.py | 3 ++- freqtrade/optimize/backtesting.py | 3 ++- freqtrade/plot/plotting.py | 3 ++- freqtrade/strategy/interface.py | 3 ++- .../strategy_subtemplates/strategy_methods_advanced.j2 | 3 ++- 6 files changed, 12 insertions(+), 6 deletions(-) diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index 64b6bd551..bdc809a22 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -62,11 +62,12 @@ class AwesomeStrategy(IStrategy): # ... populate_* methods - def bot_loop_start(self, **kwargs) -> None: + def bot_loop_start(self, current_time: datetime, **kwargs) -> None: """ Called at the start of the bot iteration (one loop). Might be used to perform pair-independent tasks (e.g. gather some remote resource for comparison) + :param current_time: datetime object, containing the current datetime :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. """ if self.config['runmode'].value in ('live', 'dry_run'): diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index cec7176f6..21d23e49b 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -212,7 +212,8 @@ class FreqtradeBot(LoggingMixin): 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)() + strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)( + current_time=datetime.now(timezone.utc)) self.strategy.analyze(self.active_pair_whitelist) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index ccb027317..699e3c3cb 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -1158,7 +1158,8 @@ class Backtesting: while current_time <= end_date: open_trade_count_start = LocalTrade.bt_open_open_trade_count self.check_abort() - strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)() + strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)( + current_time=current_time) for i, pair in enumerate(data): row_index = indexes[pair] row = self.validate_row(data, pair, row_index, current_time) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 1b2ee44da..e415c4911 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -1,4 +1,5 @@ import logging +from datetime import datetime, timezone from pathlib import Path from typing import Dict, List, Optional @@ -635,7 +636,7 @@ def load_and_plot_trades(config: Config): exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config) IStrategy.dp = DataProvider(config, exchange) strategy.ft_bot_start() - strategy.bot_loop_start() + strategy.bot_loop_start(datetime.now(timezone.utc)) plot_elements = init_plotscript(config, list(exchange.markets), strategy.startup_candle_count) timerange = plot_elements['timerange'] trades = plot_elements['trades'] diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 96b2ac8ce..6d4a3036f 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -251,11 +251,12 @@ class IStrategy(ABC, HyperStrategyMixin): """ pass - def bot_loop_start(self, **kwargs) -> None: + def bot_loop_start(self, current_time: datetime, **kwargs) -> None: """ Called at the start of the bot iteration (one loop). Might be used to perform pair-independent tasks (e.g. gather some remote resource for comparison) + :param current_time: datetime object, containing the current datetime :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. """ pass diff --git a/freqtrade/templates/strategy_subtemplates/strategy_methods_advanced.j2 b/freqtrade/templates/strategy_subtemplates/strategy_methods_advanced.j2 index 488ca2fd7..bfbb20ec1 100644 --- a/freqtrade/templates/strategy_subtemplates/strategy_methods_advanced.j2 +++ b/freqtrade/templates/strategy_subtemplates/strategy_methods_advanced.j2 @@ -1,5 +1,5 @@ -def bot_loop_start(self, **kwargs) -> None: +def bot_loop_start(self, current_time: datetime, **kwargs) -> None: """ Called at the start of the bot iteration (one loop). Might be used to perform pair-independent tasks @@ -8,6 +8,7 @@ def bot_loop_start(self, **kwargs) -> None: For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ When not implemented by a strategy, this simply does nothing. + :param current_time: datetime object, containing the current datetime :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. """ pass From 7cdcd97c266662be3e0be79e9e8f39b4e8e81d7f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 26 Mar 2023 11:30:44 +0200 Subject: [PATCH 323/360] Update tests for new logic. --- tests/optimize/test_backtesting.py | 6 ++++-- tests/optimize/test_hyperopt.py | 9 ++++++--- tests/strategy/strats/hyperoptable_strategy.py | 2 ++ 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 8dee45b6d..9dbda51b0 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -344,7 +344,7 @@ def test_backtest_abort(default_conf, mocker, testdatadir) -> None: assert backtesting.progress.progress == 0 -def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None: +def test_backtesting_start(default_conf, mocker, caplog) -> None: def get_timerange(input1): return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59) @@ -367,6 +367,7 @@ def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None: backtesting = Backtesting(default_conf) backtesting._set_strategy(backtesting.strategylist[0]) backtesting.strategy.bot_loop_start = MagicMock() + backtesting.strategy.bot_start = MagicMock() backtesting.start() # check the logs, that will contain the backtest result exists = [ @@ -376,7 +377,8 @@ def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None: for line in exists: assert log_has(line, caplog) assert backtesting.strategy.dp._pairlists is not None - assert backtesting.strategy.bot_loop_start.call_count == 1 + assert backtesting.strategy.bot_start.call_count == 1 + assert backtesting.strategy.bot_loop_start.call_count == 0 assert sbs.call_count == 1 assert sbc.call_count == 1 diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 998798580..786720030 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -872,7 +872,8 @@ def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir, fee) -> None: hyperopt.backtesting.exchange.get_max_leverage = MagicMock(return_value=1.0) assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto) assert isinstance(hyperopt.backtesting.strategy.buy_rsi, IntParameter) - assert hyperopt.backtesting.strategy.bot_loop_started is True + assert hyperopt.backtesting.strategy.bot_started is True + assert hyperopt.backtesting.strategy.bot_loop_started is False assert hyperopt.backtesting.strategy.buy_rsi.in_space is True assert hyperopt.backtesting.strategy.buy_rsi.value == 35 @@ -922,7 +923,8 @@ def test_in_strategy_auto_hyperopt_with_parallel(mocker, hyperopt_conf, tmpdir, assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto) assert isinstance(hyperopt.backtesting.strategy.buy_rsi, IntParameter) - assert hyperopt.backtesting.strategy.bot_loop_started is True + assert hyperopt.backtesting.strategy.bot_started is True + assert hyperopt.backtesting.strategy.bot_loop_started is False assert hyperopt.backtesting.strategy.buy_rsi.in_space is True assert hyperopt.backtesting.strategy.buy_rsi.value == 35 @@ -959,7 +961,8 @@ def test_in_strategy_auto_hyperopt_per_epoch(mocker, hyperopt_conf, tmpdir, fee) hyperopt.backtesting.exchange.get_max_leverage = MagicMock(return_value=1.0) assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto) assert isinstance(hyperopt.backtesting.strategy.buy_rsi, IntParameter) - assert hyperopt.backtesting.strategy.bot_loop_started is True + assert hyperopt.backtesting.strategy.bot_loop_started is False + assert hyperopt.backtesting.strategy.bot_started is True assert hyperopt.backtesting.strategy.buy_rsi.in_space is True assert hyperopt.backtesting.strategy.buy_rsi.value == 35 diff --git a/tests/strategy/strats/hyperoptable_strategy.py b/tests/strategy/strats/hyperoptable_strategy.py index eadbc533f..d05e8ead2 100644 --- a/tests/strategy/strats/hyperoptable_strategy.py +++ b/tests/strategy/strats/hyperoptable_strategy.py @@ -50,6 +50,7 @@ class HyperoptableStrategy(StrategyTestV3): return prot bot_loop_started = False + bot_started = False def bot_loop_start(self): self.bot_loop_started = True @@ -58,6 +59,7 @@ class HyperoptableStrategy(StrategyTestV3): """ Parameters can also be defined here ... """ + self.bot_started = True self.buy_rsi = IntParameter([0, 50], default=30, space='buy') def informative_pairs(self): From 16057da6cc5f7c418df8c521a7d6dc11f1271267 Mon Sep 17 00:00:00 2001 From: escanoro <128816061+escanoro@users.noreply.github.com> Date: Sun, 26 Mar 2023 14:09:41 +0200 Subject: [PATCH 324/360] typo: above should be below --- docs/freqai-reinforcement-learning.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/freqai-reinforcement-learning.md b/docs/freqai-reinforcement-learning.md index ed6a41825..f5679a4ba 100644 --- a/docs/freqai-reinforcement-learning.md +++ b/docs/freqai-reinforcement-learning.md @@ -55,7 +55,7 @@ where `ReinforcementLearner` will use the templated `ReinforcementLearner` from dataframe["&-action"] = 0 ``` -Most of the function remains the same as for typical Regressors, however, the function above shows how the strategy must pass the raw price data to the agent so that it has access to raw OHLCV in the training environment: +Most of the function remains the same as for typical Regressors, however, the function below shows how the strategy must pass the raw price data to the agent so that it has access to raw OHLCV in the training environment: ```python def feature_engineering_standard(self, dataframe, **kwargs): From 444d18aa39cd7d143a152fa7d17ac79be3726a19 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 26 Mar 2023 14:30:29 +0200 Subject: [PATCH 325/360] Revert binance PO fix, since ccxt has fixed this bug. --- freqtrade/exchange/binance.py | 21 --------------------- tests/exchange/test_binance.py | 4 ++-- 2 files changed, 2 insertions(+), 23 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index a89c02631..7ac496f62 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -7,7 +7,6 @@ from typing import Dict, List, Optional, Tuple import arrow import ccxt -from freqtrade.constants import BuySell from freqtrade.enums import CandleType, MarginMode, PriceType, TradingMode from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError from freqtrade.exchange import Exchange @@ -49,26 +48,6 @@ class Binance(Exchange): (TradingMode.FUTURES, MarginMode.ISOLATED) ] - def _get_params( - self, - side: BuySell, - ordertype: str, - leverage: float, - reduceOnly: bool, - time_in_force: str = 'GTC', - ) -> Dict: - params = super()._get_params(side, ordertype, leverage, reduceOnly, time_in_force) - if ( - time_in_force == 'PO' - and ordertype != 'market' - and self.trading_mode == TradingMode.SPOT - # Only spot can do post only orders - ): - params.pop('timeInForce') - params['postOnly'] = True - - return params - def get_tickers(self, symbols: Optional[List[str]] = None, cached: bool = False) -> Tickers: tickers = super().get_tickers(symbols=symbols, cached=cached) if self.trading_mode == TradingMode.FUTURES: diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index 273860e15..fda33b859 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -15,8 +15,8 @@ from tests.exchange.test_exchange import ccxt_exceptionhandlers ('buy', 'limit', 'gtc', {'timeInForce': 'GTC'}), ('buy', 'limit', 'IOC', {'timeInForce': 'IOC'}), ('buy', 'market', 'IOC', {}), - ('buy', 'limit', 'PO', {'postOnly': True}), - ('sell', 'limit', 'PO', {'postOnly': True}), + ('buy', 'limit', 'PO', {'timeInForce': 'PO'}), + ('sell', 'limit', 'PO', {'timeInForce': 'PO'}), ('sell', 'market', 'PO', {}), ]) def test__get_params_binance(default_conf, mocker, side, type, time_in_force, expected): From fb1541bdf632c5c9b6f8e96907fbd1986ff0bff7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 26 Mar 2023 15:46:20 +0200 Subject: [PATCH 326/360] Explicitly close loop in async tests --- freqtrade/exchange/exchange.py | 2 ++ tests/exchange/test_exchange.py | 21 +++++++++++++-------- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 104eaa221..92eb4b58a 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -205,6 +205,8 @@ class Exchange: and self._api_async.session): logger.debug("Closing async ccxt session.") self.loop.run_until_complete(self._api_async.close()) + if self.loop and not self.loop.is_closed(): + self.loop.close() def validate_config(self, config): # Check if timeframe is available diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 586f023b4..5350f4e3e 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -113,18 +113,21 @@ async def async_ccxt_exception(mocker, default_conf, api_mock, fun, mock_ccxt_fu exchange = get_patched_exchange(mocker, default_conf, api_mock) await getattr(exchange, fun)(**kwargs) assert api_mock.__dict__[mock_ccxt_fun].call_count == retries + exchange.close() with pytest.raises(TemporaryError): api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.NetworkError("DeadBeef")) exchange = get_patched_exchange(mocker, default_conf, api_mock) await getattr(exchange, fun)(**kwargs) assert api_mock.__dict__[mock_ccxt_fun].call_count == retries + exchange.close() with pytest.raises(OperationalException): api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.BaseError("DeadBeef")) exchange = get_patched_exchange(mocker, default_conf, api_mock) await getattr(exchange, fun)(**kwargs) assert api_mock.__dict__[mock_ccxt_fun].call_count == 1 + exchange.close() def test_init(default_conf, mocker, caplog): @@ -2248,7 +2251,6 @@ def test_refresh_latest_ohlcv_cache(mocker, default_conf, candle_type, time_mach assert res[pair2].at[0, 'open'] -@pytest.mark.asyncio @pytest.mark.parametrize("exchange_name", EXCHANGES) async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_name): ohlcv = [ @@ -2277,7 +2279,7 @@ async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_ assert res[3] == ohlcv assert exchange._api_async.fetch_ohlcv.call_count == 1 assert not log_has(f"Using cached candle (OHLCV) data for {pair} ...", caplog) - + exchange.close() # exchange = Exchange(default_conf) await async_ccxt_exception(mocker, default_conf, MagicMock(), "_async_get_candle_history", "fetch_ohlcv", @@ -2292,15 +2294,17 @@ async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_ await exchange._async_get_candle_history(pair, "5m", CandleType.SPOT, (arrow.utcnow().int_timestamp - 2000) * 1000) + exchange.close() + with pytest.raises(OperationalException, match=r'Exchange.* does not support fetching ' r'historical candle \(OHLCV\) data\..*'): api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.NotSupported("Not supported")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) await exchange._async_get_candle_history(pair, "5m", CandleType.SPOT, (arrow.utcnow().int_timestamp - 2000) * 1000) + exchange.close() -@pytest.mark.asyncio async def test__async_kucoin_get_candle_history(default_conf, mocker, caplog): from freqtrade.exchange.common import _reset_logging_mixin _reset_logging_mixin() @@ -2341,9 +2345,9 @@ async def test__async_kucoin_get_candle_history(default_conf, mocker, caplog): # Expect the "returned exception" message 12 times (4 retries * 3 (loop)) assert num_log_has_re(msg, caplog) == 12 assert num_log_has_re(msg2, caplog) == 9 + exchange.close() -@pytest.mark.asyncio async def test__async_get_candle_history_empty(default_conf, mocker, caplog): """ Test empty exchange result """ ohlcv = [] @@ -2363,6 +2367,7 @@ async def test__async_get_candle_history_empty(default_conf, mocker, caplog): assert res[2] == CandleType.SPOT assert res[3] == ohlcv assert exchange._api_async.fetch_ohlcv.call_count == 1 + exchange.close() def test_refresh_latest_ohlcv_inv_result(default_conf, mocker, caplog): @@ -2757,7 +2762,6 @@ async def test___async_get_candle_history_sort(default_conf, mocker, exchange_na assert res_ohlcv[9][5] == 2.31452783 -@pytest.mark.asyncio @pytest.mark.parametrize("exchange_name", EXCHANGES) async def test__async_fetch_trades(default_conf, mocker, caplog, exchange_name, fetch_trades_result): @@ -2785,8 +2789,8 @@ async def test__async_fetch_trades(default_conf, mocker, caplog, exchange_name, assert exchange._api_async.fetch_trades.call_args[1]['limit'] == 1000 assert exchange._api_async.fetch_trades.call_args[1]['params'] == {'from': '123'} assert log_has_re(f"Fetching trades for pair {pair}, params: .*", caplog) + exchange.close() - exchange = Exchange(default_conf) await async_ccxt_exception(mocker, default_conf, MagicMock(), "_async_fetch_trades", "fetch_trades", pair='ABCD/BTC', since=None) @@ -2796,15 +2800,16 @@ async def test__async_fetch_trades(default_conf, mocker, caplog, exchange_name, api_mock.fetch_trades = MagicMock(side_effect=ccxt.BaseError("Unknown error")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) await exchange._async_fetch_trades(pair, since=(arrow.utcnow().int_timestamp - 2000) * 1000) + exchange.close() with pytest.raises(OperationalException, match=r'Exchange.* does not support fetching ' r'historical trade data\..*'): api_mock.fetch_trades = MagicMock(side_effect=ccxt.NotSupported("Not supported")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) await exchange._async_fetch_trades(pair, since=(arrow.utcnow().int_timestamp - 2000) * 1000) + exchange.close() -@pytest.mark.asyncio @pytest.mark.parametrize("exchange_name", EXCHANGES) async def test__async_fetch_trades_contract_size(default_conf, mocker, caplog, exchange_name, fetch_trades_result): @@ -2839,6 +2844,7 @@ async def test__async_fetch_trades_contract_size(default_conf, mocker, caplog, e pair = 'ETH/USDT:USDT' res = await exchange._async_fetch_trades(pair, since=None, params=None) assert res[0][5] == 300 + exchange.close() @pytest.mark.asyncio @@ -4807,7 +4813,6 @@ def test_load_leverage_tiers(mocker, default_conf, leverage_tiers, exchange_name ) -@pytest.mark.asyncio @pytest.mark.parametrize('exchange_name', EXCHANGES) async def test_get_market_leverage_tiers(mocker, default_conf, exchange_name): default_conf['exchange']['name'] = exchange_name From b09fb5826f9404bd60963736307b6efa3649461f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 26 Mar 2023 16:17:10 +0200 Subject: [PATCH 327/360] don't use "can_short" in backtesting to determine application of leverage --- freqtrade/optimize/backtesting.py | 2 +- tests/optimize/test_backtest_detail.py | 4 +++- tests/optimize/test_backtesting_adjust_position.py | 5 +++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 702b4fb85..cd77c75a5 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -739,7 +739,7 @@ class Backtesting: proposed_leverage=1.0, max_leverage=max_leverage, side=direction, entry_tag=entry_tag, - ) if self._can_short else 1.0 + ) if self.trading_mode != TradingMode.SPOT else 1.0 # Cap leverage between 1.0 and max_leverage. leverage = min(max(leverage, 1.0), max_leverage) diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index 2cb42c003..dd9e42971 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -5,7 +5,7 @@ from unittest.mock import MagicMock import pytest from freqtrade.data.history import get_timerange -from freqtrade.enums import ExitType +from freqtrade.enums import ExitType, TradingMode from freqtrade.optimize.backtesting import Backtesting from freqtrade.persistence.trade_model import LocalTrade from tests.conftest import EXMS, patch_exchange @@ -925,11 +925,13 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data: BTContainer) mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001) mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf')) mocker.patch(f"{EXMS}.get_max_leverage", return_value=100) + mocker.patch(f"{EXMS}.calculate_funding_fees", return_value=0) patch_exchange(mocker) frame = _build_backtest_dataframe(data.data) backtesting = Backtesting(default_conf) # TODO: Should we initialize this properly?? backtesting._can_short = True + backtesting.trading_mode = TradingMode.MARGIN backtesting._set_strategy(backtesting.strategylist[0]) backtesting.required_startup = 0 backtesting.strategy.advise_entry = lambda a, m: frame diff --git a/tests/optimize/test_backtesting_adjust_position.py b/tests/optimize/test_backtesting_adjust_position.py index 9fc726bd1..0d57ff89a 100644 --- a/tests/optimize/test_backtesting_adjust_position.py +++ b/tests/optimize/test_backtesting_adjust_position.py @@ -10,7 +10,7 @@ from arrow import Arrow from freqtrade.configuration import TimeRange from freqtrade.data import history from freqtrade.data.history import get_timerange -from freqtrade.enums import ExitType +from freqtrade.enums import ExitType, TradingMode from freqtrade.optimize.backtesting import Backtesting from tests.conftest import EXMS, patch_exchange @@ -108,9 +108,10 @@ def test_backtest_position_adjustment_detailed(default_conf, fee, mocker, levera default_conf.update({ "stake_amount": 100.0, "dry_run_wallet": 1000.0, - "strategy": "StrategyTestV3" + "strategy": "StrategyTestV3", }) backtesting = Backtesting(default_conf) + backtesting.trading_mode = TradingMode.FUTURES backtesting._can_short = True backtesting._set_strategy(backtesting.strategylist[0]) pair = 'XRP/USDT' From c14ac8a205e53bf8f6ecbb16292efae96101331b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 26 Mar 2023 16:46:41 +0200 Subject: [PATCH 328/360] Properly handle non-replaced first entry orders --- freqtrade/optimize/backtesting.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index cd77c75a5..31e133515 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -1029,6 +1029,9 @@ class Backtesting: requested_stake=( order.safe_remaining * order.ft_price / trade.leverage), direction='short' if trade.is_short else 'long') + # Delete trade if no successful entries happened (if placing the new order failed) + if trade.open_order_id is None and trade.nr_of_successful_entries == 0 : + return True self.replaced_entry_orders += 1 else: # assumption: there can't be multiple open entry orders at any given time From 1c9abd9e3579097bb6c3c323e2d765afe551f351 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 26 Mar 2023 17:27:52 +0200 Subject: [PATCH 329/360] Properly respect can_short flag in backtesting closes #8387 --- freqtrade/optimize/backtesting.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 31e133515..a701b04e4 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -203,6 +203,8 @@ class Backtesting: # since a "perfect" stoploss-exit is assumed anyway # And the regular "stoploss" function would not apply to that case self.strategy.order_types['stoploss_on_exchange'] = False + # Update can_short flag + self._can_short = self.trading_mode != TradingMode.SPOT and strategy.can_short self.strategy.ft_bot_start() From 80a27bc0db800d336b036411cccda7df97000a84 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 26 Mar 2023 18:18:52 +0200 Subject: [PATCH 330/360] Fix random uvicorn error --- freqtrade/optimize/backtesting.py | 2 +- tests/rpc/test_rpc_apiserver.py | 14 ++------------ 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index a701b04e4..c7b2a0d3c 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -1032,7 +1032,7 @@ class Backtesting: order.safe_remaining * order.ft_price / trade.leverage), direction='short' if trade.is_short else 'long') # Delete trade if no successful entries happened (if placing the new order failed) - if trade.open_order_id is None and trade.nr_of_successful_entries == 0 : + if trade.open_order_id is None and trade.nr_of_successful_entries == 0: return True self.replaced_entry_orders += 1 else: diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index bf9d6cc3b..31075e514 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -1,6 +1,7 @@ """ Unit test file for rpc/api_server.py """ +import asyncio import logging import time from datetime import datetime, timedelta, timezone @@ -299,10 +300,6 @@ def test_api_UvicornServer(mocker): s = UvicornServer(uvicorn.Config(MagicMock(), port=8080, host='127.0.0.1')) assert thread_mock.call_count == 0 - s.install_signal_handlers() - # Original implementation starts a thread - make sure that's not the case - assert thread_mock.call_count == 0 - # Fake started to avoid sleeping forever s.started = True s.run_in_thread() @@ -318,10 +315,6 @@ def test_api_UvicornServer_run(mocker): s = UvicornServer(uvicorn.Config(MagicMock(), port=8080, host='127.0.0.1')) assert serve_mock.call_count == 0 - s.install_signal_handlers() - # Original implementation starts a thread - make sure that's not the case - assert serve_mock.call_count == 0 - # Fake started to avoid sleeping forever s.started = True s.run() @@ -331,13 +324,10 @@ def test_api_UvicornServer_run(mocker): def test_api_UvicornServer_run_no_uvloop(mocker, import_fails): serve_mock = mocker.patch('freqtrade.rpc.api_server.uvicorn_threaded.UvicornServer.serve', get_mock_coro(None)) + asyncio.set_event_loop(asyncio.new_event_loop()) s = UvicornServer(uvicorn.Config(MagicMock(), port=8080, host='127.0.0.1')) assert serve_mock.call_count == 0 - s.install_signal_handlers() - # Original implementation starts a thread - make sure that's not the case - assert serve_mock.call_count == 0 - # Fake started to avoid sleeping forever s.started = True s.run() From 72284317c2cfefe6b22558b169a328016c426e17 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 26 Mar 2023 18:21:21 +0200 Subject: [PATCH 331/360] Fix failing backtest test --- tests/optimize/test_backtest_detail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index dd9e42971..158dd04dc 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -930,9 +930,9 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data: BTContainer) frame = _build_backtest_dataframe(data.data) backtesting = Backtesting(default_conf) # TODO: Should we initialize this properly?? - backtesting._can_short = True backtesting.trading_mode = TradingMode.MARGIN backtesting._set_strategy(backtesting.strategylist[0]) + backtesting._can_short = True backtesting.required_startup = 0 backtesting.strategy.advise_entry = lambda a, m: frame backtesting.strategy.advise_exit = lambda a, m: frame From 2f8f60373e6331c6ad0519e70816b1af3ad8f057 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Mar 2023 03:56:49 +0000 Subject: [PATCH 332/360] Bump ccxt from 3.0.23 to 3.0.36 Bumps [ccxt](https://github.com/ccxt/ccxt) from 3.0.23 to 3.0.36. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/CHANGELOG.md) - [Commits](https://github.com/ccxt/ccxt/compare/3.0.23...3.0.36) --- 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 ad34883ae..7e7463d9f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.24.2 pandas==1.5.3 pandas-ta==0.3.14b -ccxt==3.0.23 +ccxt==3.0.36 cryptography==39.0.2 aiohttp==3.8.4 SQLAlchemy==2.0.7 From 1b3d9efedd9a9c4a2bc367cd3fd1d4d2734c8848 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Mar 2023 03:56:55 +0000 Subject: [PATCH 333/360] Bump types-requests from 2.28.11.15 to 2.28.11.16 Bumps [types-requests](https://github.com/python/typeshed) from 2.28.11.15 to 2.28.11.16. - [Release notes](https://github.com/python/typeshed/releases) - [Commits](https://github.com/python/typeshed/commits) --- updated-dependencies: - dependency-name: types-requests dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 8312e2820..a9458b51d 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -27,6 +27,6 @@ nbconvert==7.2.10 # mypy types types-cachetools==5.3.0.4 types-filelock==3.2.7 -types-requests==2.28.11.15 +types-requests==2.28.11.16 types-tabulate==0.9.0.1 types-python-dateutil==2.8.19.10 From 75c31cc8cc966af3964e419f8cea0ba42a32a720 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Mar 2023 03:57:14 +0000 Subject: [PATCH 334/360] Bump ruff from 0.0.257 to 0.0.259 Bumps [ruff](https://github.com/charliermarsh/ruff) from 0.0.257 to 0.0.259. - [Release notes](https://github.com/charliermarsh/ruff/releases) - [Changelog](https://github.com/charliermarsh/ruff/blob/main/BREAKING_CHANGES.md) - [Commits](https://github.com/charliermarsh/ruff/compare/v0.0.257...v0.0.259) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 8312e2820..02d384889 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -7,7 +7,7 @@ -r docs/requirements-docs.txt coveralls==3.3.1 -ruff==0.0.257 +ruff==0.0.259 mypy==1.1.1 pre-commit==3.2.0 pytest==7.2.2 From b72f61080b98abfa8933686f42c59c8131ce097c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Mar 2023 03:57:46 +0000 Subject: [PATCH 335/360] Bump orjson from 3.8.7 to 3.8.8 Bumps [orjson](https://github.com/ijl/orjson) from 3.8.7 to 3.8.8. - [Release notes](https://github.com/ijl/orjson/releases) - [Changelog](https://github.com/ijl/orjson/blob/master/CHANGELOG.md) - [Commits](https://github.com/ijl/orjson/compare/3.8.7...3.8.8) --- updated-dependencies: - dependency-name: orjson 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 ad34883ae..80fc1f776 100644 --- a/requirements.txt +++ b/requirements.txt @@ -28,7 +28,7 @@ py_find_1st==1.1.5 # Load ticker files 30% faster python-rapidjson==1.10 # Properly format api responses -orjson==3.8.7 +orjson==3.8.8 # Notify systemd sdnotify==0.3.2 From d13ea71a58744eb2bf4e2ae39bd5bc3763cf19e1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Mar 2023 03:57:55 +0000 Subject: [PATCH 336/360] Bump mkdocs-material from 9.1.3 to 9.1.4 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 9.1.3 to 9.1.4. - [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/9.1.3...9.1.4) --- 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 110373844..7f4215aef 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,6 +1,6 @@ markdown==3.3.7 mkdocs==1.4.2 -mkdocs-material==9.1.3 +mkdocs-material==9.1.4 mdx_truly_sane_lists==1.3 pymdown-extensions==9.10 jinja2==3.1.2 From 8955e091750cf805f5e01b3ec56ebb6c42d2f1cb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Mar 2023 03:58:00 +0000 Subject: [PATCH 337/360] Bump filelock from 3.10.0 to 3.10.6 Bumps [filelock](https://github.com/tox-dev/py-filelock) from 3.10.0 to 3.10.6. - [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.10.0...3.10.6) --- 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 4d86da2b6..2c7c27d98 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -5,5 +5,5 @@ scipy==1.10.1 scikit-learn==1.1.3 scikit-optimize==0.9.0 -filelock==3.10.0 +filelock==3.10.6 progressbar2==4.2.0 From 7e11bce4f44a8272832095ea21b6306346f6010f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Mar 2023 03:58:02 +0000 Subject: [PATCH 338/360] Bump pypa/gh-action-pypi-publish from 1.8.1 to 1.8.3 Bumps [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) from 1.8.1 to 1.8.3. - [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases) - [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/v1.8.1...v1.8.3) --- updated-dependencies: - dependency-name: pypa/gh-action-pypi-publish dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 904387fb2..5c80bc141 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -425,7 +425,7 @@ jobs: python setup.py sdist bdist_wheel - name: Publish to PyPI (Test) - uses: pypa/gh-action-pypi-publish@v1.8.1 + uses: pypa/gh-action-pypi-publish@v1.8.3 if: (github.event_name == 'release') with: user: __token__ @@ -433,7 +433,7 @@ jobs: repository_url: https://test.pypi.org/legacy/ - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@v1.8.1 + uses: pypa/gh-action-pypi-publish@v1.8.3 if: (github.event_name == 'release') with: user: __token__ From 8845f765db7e217e6810184cb7efe3b3415f741d Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 27 Mar 2023 06:25:11 +0200 Subject: [PATCH 339/360] pre-commit - bump requests --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ca3da8e90..4784055a9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: additional_dependencies: - types-cachetools==5.3.0.4 - types-filelock==3.2.7 - - types-requests==2.28.11.15 + - types-requests==2.28.11.16 - types-tabulate==0.9.0.1 - types-python-dateutil==2.8.19.10 - SQLAlchemy==2.0.7 From 4891174a71730db31aeef08e431d890ceb2afea1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 27 Mar 2023 06:44:36 +0200 Subject: [PATCH 340/360] list-data should sort pairs also in timerange mode --- freqtrade/commands/data_commands.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/freqtrade/commands/data_commands.py b/freqtrade/commands/data_commands.py index 1e74e1036..bcef1c252 100644 --- a/freqtrade/commands/data_commands.py +++ b/freqtrade/commands/data_commands.py @@ -204,11 +204,14 @@ def start_list_data(args: Dict[str, Any]) -> None: pair, timeframe, candle_type, *dhc.ohlcv_data_min_max(pair, timeframe, candle_type) ) for pair, timeframe, candle_type in paircombs] + print(tabulate([ (pair, timeframe, candle_type, start.strftime(DATETIME_PRINT_FORMAT), end.strftime(DATETIME_PRINT_FORMAT)) - for pair, timeframe, candle_type, start, end in paircombs1 + for pair, timeframe, candle_type, start, end in sorted( + paircombs1, + key=lambda x: (x[0], timeframe_to_minutes(x[1]), x[2])) ], headers=("Pair", "Timeframe", "Type", 'From', 'To'), tablefmt='psql', stralign='right')) From 1743ad7946ddc6e1c6a550e0716094ab246d1ff5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Mar 2023 05:14:04 +0000 Subject: [PATCH 341/360] Bump pre-commit from 3.2.0 to 3.2.1 Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 3.2.0 to 3.2.1. - [Release notes](https://github.com/pre-commit/pre-commit/releases) - [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md) - [Commits](https://github.com/pre-commit/pre-commit/compare/v3.2.0...v3.2.1) --- updated-dependencies: - dependency-name: pre-commit dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 02d384889..afb8d0bf4 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -9,7 +9,7 @@ coveralls==3.3.1 ruff==0.0.259 mypy==1.1.1 -pre-commit==3.2.0 +pre-commit==3.2.1 pytest==7.2.2 pytest-asyncio==0.21.0 pytest-cov==4.0.0 From bc0816aa66d4acb41d7b98c04529421a55e32faa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Mar 2023 05:15:59 +0000 Subject: [PATCH 342/360] Bump cryptography from 39.0.2 to 40.0.1 Bumps [cryptography](https://github.com/pyca/cryptography) from 39.0.2 to 40.0.1. - [Release notes](https://github.com/pyca/cryptography/releases) - [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pyca/cryptography/compare/39.0.2...40.0.1) --- updated-dependencies: - dependency-name: cryptography dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7e7463d9f..b32ed2808 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ pandas==1.5.3 pandas-ta==0.3.14b ccxt==3.0.36 -cryptography==39.0.2 +cryptography==40.0.1 aiohttp==3.8.4 SQLAlchemy==2.0.7 python-telegram-bot==13.15 From 90669e0ba9ecb1387d32d759a1372bb89d0fe901 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Mar 2023 07:49:56 +0000 Subject: [PATCH 343/360] Bump ccxt from 3.0.36 to 3.0.37 Bumps [ccxt](https://github.com/ccxt/ccxt) from 3.0.36 to 3.0.37. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/CHANGELOG.md) - [Commits](https://github.com/ccxt/ccxt/compare/3.0.36...3.0.37) --- 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 7d25fa5a1..809925488 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.24.2 pandas==1.5.3 pandas-ta==0.3.14b -ccxt==3.0.36 +ccxt==3.0.37 cryptography==40.0.1 aiohttp==3.8.4 SQLAlchemy==2.0.7 From 4f4dfa2a59a03acf3e877727fb0bfe6c7ae8af0e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Mar 2023 07:50:06 +0000 Subject: [PATCH 344/360] Bump pydantic from 1.10.6 to 1.10.7 Bumps [pydantic](https://github.com/pydantic/pydantic) from 1.10.6 to 1.10.7. - [Release notes](https://github.com/pydantic/pydantic/releases) - [Changelog](https://github.com/pydantic/pydantic/blob/v1.10.7/HISTORY.md) - [Commits](https://github.com/pydantic/pydantic/compare/v1.10.6...v1.10.7) --- updated-dependencies: - dependency-name: pydantic 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 7d25fa5a1..00b8dc390 100644 --- a/requirements.txt +++ b/requirements.txt @@ -35,7 +35,7 @@ sdnotify==0.3.2 # API Server fastapi==0.95.0 -pydantic==1.10.6 +pydantic==1.10.7 uvicorn==0.21.1 pyjwt==2.6.0 aiofiles==23.1.0 From e35c85000e0ee872960157c1a887468d44b4f60b Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 27 Mar 2023 20:19:23 +0200 Subject: [PATCH 345/360] Excude raspberry from catboost installs closes #8404 --- requirements-freqai.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-freqai.txt b/requirements-freqai.txt index bc0be85e5..e6eae667c 100644 --- a/requirements-freqai.txt +++ b/requirements-freqai.txt @@ -5,7 +5,7 @@ # Required for freqai scikit-learn==1.1.3 joblib==1.2.0 -catboost==1.1.1; platform_machine != 'aarch64' and python_version < '3.11' +catboost==1.1.1; platform_machine != 'aarch64' and 'arm' not in platform_machine and python_version < '3.11' lightgbm==3.3.5 xgboost==1.7.4 tensorboard==2.12.0 From 3928051baf6faa162dd865f6b4f2ce7281944ade Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 27 Mar 2023 20:35:19 +0200 Subject: [PATCH 346/360] Revert unneeded formatting changes --- freqtrade/data/history/featherdatahandler.py | 3 +- tests/data/test_datahandler.py | 54 +++++++------------- 2 files changed, 19 insertions(+), 38 deletions(-) diff --git a/freqtrade/data/history/featherdatahandler.py b/freqtrade/data/history/featherdatahandler.py index 87c1d0886..4e016080c 100644 --- a/freqtrade/data/history/featherdatahandler.py +++ b/freqtrade/data/history/featherdatahandler.py @@ -30,8 +30,7 @@ class FeatherDataHandler(IDataHandler): :param candle_type: Any of the enum CandleType (must match trading mode!) :return: None """ - filename = self._pair_data_filename( - self._datadir, pair, timeframe, candle_type) + filename = self._pair_data_filename(self._datadir, pair, timeframe, candle_type) self.create_dir_if_needed(filename) data.reset_index(drop=True).loc[:, self._columns].to_feather( diff --git a/tests/data/test_datahandler.py b/tests/data/test_datahandler.py index 2c5241367..f19b15455 100644 --- a/tests/data/test_datahandler.py +++ b/tests/data/test_datahandler.py @@ -20,31 +20,25 @@ from tests.conftest import log_has, log_has_re def test_datahandler_ohlcv_get_pairs(testdatadir): - pairs = JsonDataHandler.ohlcv_get_pairs( - testdatadir, '5m', candle_type=CandleType.SPOT) + pairs = JsonDataHandler.ohlcv_get_pairs(testdatadir, '5m', candle_type=CandleType.SPOT) # Convert to set to avoid failures due to sorting assert set(pairs) == {'UNITTEST/BTC', 'XLM/BTC', 'ETH/BTC', 'TRX/BTC', 'LTC/BTC', 'XMR/BTC', 'ZEC/BTC', 'ADA/BTC', 'ETC/BTC', 'NXT/BTC', 'DASH/BTC', 'XRP/ETH'} - pairs = JsonGzDataHandler.ohlcv_get_pairs( - testdatadir, '8m', candle_type=CandleType.SPOT) + pairs = JsonGzDataHandler.ohlcv_get_pairs(testdatadir, '8m', candle_type=CandleType.SPOT) assert set(pairs) == {'UNITTEST/BTC'} - pairs = HDF5DataHandler.ohlcv_get_pairs( - testdatadir, '5m', candle_type=CandleType.SPOT) + pairs = HDF5DataHandler.ohlcv_get_pairs(testdatadir, '5m', candle_type=CandleType.SPOT) assert set(pairs) == {'UNITTEST/BTC'} - pairs = JsonDataHandler.ohlcv_get_pairs( - testdatadir, '1h', candle_type=CandleType.MARK) + pairs = JsonDataHandler.ohlcv_get_pairs(testdatadir, '1h', candle_type=CandleType.MARK) assert set(pairs) == {'UNITTEST/USDT:USDT', 'XRP/USDT:USDT'} - pairs = JsonGzDataHandler.ohlcv_get_pairs( - testdatadir, '1h', candle_type=CandleType.FUTURES) + pairs = JsonGzDataHandler.ohlcv_get_pairs(testdatadir, '1h', candle_type=CandleType.FUTURES) assert set(pairs) == {'XRP/USDT:USDT'} - pairs = HDF5DataHandler.ohlcv_get_pairs( - testdatadir, '1h', candle_type=CandleType.MARK) + pairs = HDF5DataHandler.ohlcv_get_pairs(testdatadir, '1h', candle_type=CandleType.MARK) assert set(pairs) == {'UNITTEST/USDT:USDT'} @@ -85,8 +79,7 @@ def test_rebuild_pair_from_filename(input, expected): def test_datahandler_ohlcv_get_available_data(testdatadir): - paircombs = JsonDataHandler.ohlcv_get_available_data( - testdatadir, TradingMode.SPOT) + paircombs = JsonDataHandler.ohlcv_get_available_data(testdatadir, TradingMode.SPOT) # Convert to set to avoid failures due to sorting assert set(paircombs) == { ('UNITTEST/BTC', '5m', CandleType.SPOT), @@ -108,8 +101,7 @@ def test_datahandler_ohlcv_get_available_data(testdatadir): ('NOPAIR/XXX', '4m', CandleType.SPOT), } - paircombs = JsonDataHandler.ohlcv_get_available_data( - testdatadir, TradingMode.FUTURES) + paircombs = JsonDataHandler.ohlcv_get_available_data(testdatadir, TradingMode.FUTURES) # Convert to set to avoid failures due to sorting assert set(paircombs) == { ('UNITTEST/USDT:USDT', '1h', 'mark'), @@ -120,11 +112,9 @@ def test_datahandler_ohlcv_get_available_data(testdatadir): ('XRP/USDT:USDT', '8h', 'funding_rate'), } - paircombs = JsonGzDataHandler.ohlcv_get_available_data( - testdatadir, TradingMode.SPOT) + paircombs = JsonGzDataHandler.ohlcv_get_available_data(testdatadir, TradingMode.SPOT) assert set(paircombs) == {('UNITTEST/BTC', '8m', CandleType.SPOT)} - paircombs = HDF5DataHandler.ohlcv_get_available_data( - testdatadir, TradingMode.SPOT) + paircombs = HDF5DataHandler.ohlcv_get_available_data(testdatadir, TradingMode.SPOT) assert set(paircombs) == {('UNITTEST/BTC', '5m', CandleType.SPOT)} @@ -416,21 +406,18 @@ def test_hdf5datahandler_ohlcv_load_and_resave( assert not ohlcv[ohlcv['date'] < startdt].empty - timerange = TimeRange.parse_timerange( - f"{startdt.replace('-', '')}-{enddt.replace('-', '')}") + timerange = TimeRange.parse_timerange(f"{startdt.replace('-', '')}-{enddt.replace('-', '')}") # Call private function to ensure timerange is filtered in hdf5 ohlcv = dh._ohlcv_load(pair, timeframe, timerange, candle_type=candle_type) - ohlcv1 = dh1._ohlcv_load('UNITTEST/NEW', timeframe, - timerange, candle_type=candle_type) + ohlcv1 = dh1._ohlcv_load('UNITTEST/NEW', timeframe, timerange, candle_type=candle_type) assert len(ohlcv) == len(ohlcv1) assert ohlcv.equals(ohlcv1) assert ohlcv[ohlcv['date'] < startdt].empty assert ohlcv[ohlcv['date'] > enddt].empty # Try loading inexisting file - ohlcv = dh.ohlcv_load('UNITTEST/NONEXIST', timeframe, - candle_type=candle_type) + ohlcv = dh.ohlcv_load('UNITTEST/NONEXIST', timeframe, candle_type=candle_type) assert ohlcv.empty @@ -465,8 +452,7 @@ def test_generic_datahandler_ohlcv_load_and_resave( # Get data to test dh = get_datahandler(testdatadir, datahandler) - file = tmpdir2 / \ - f"UNITTEST_NEW-{timeframe}{candle_append}.{dh._get_file_extension()}" + file = tmpdir2 / f"UNITTEST_NEW-{timeframe}{candle_append}.{dh._get_file_extension()}" assert not file.is_file() dh1 = get_datahandler(tmpdir1, datahandler) @@ -475,14 +461,11 @@ def test_generic_datahandler_ohlcv_load_and_resave( assert not ohlcv[ohlcv['date'] < startdt].empty - timerange = TimeRange.parse_timerange( - f"{startdt.replace('-', '')}-{enddt.replace('-', '')}") + timerange = TimeRange.parse_timerange(f"{startdt.replace('-', '')}-{enddt.replace('-', '')}") - ohlcv = dhbase.ohlcv_load( - pair, timeframe, timerange=timerange, candle_type=candle_type) + ohlcv = dhbase.ohlcv_load(pair, timeframe, timerange=timerange, candle_type=candle_type) if datahandler == 'hdf5': - ohlcv1 = dh1._ohlcv_load( - 'UNITTEST/NEW', timeframe, timerange, candle_type=candle_type) + ohlcv1 = dh1._ohlcv_load('UNITTEST/NEW', timeframe, timerange, candle_type=candle_type) if candle_type == 'mark': ohlcv1['volume'] = 0.0 else: @@ -495,8 +478,7 @@ def test_generic_datahandler_ohlcv_load_and_resave( assert ohlcv[ohlcv['date'] > enddt].empty # Try loading inexisting file - ohlcv = dh.ohlcv_load('UNITTEST/NONEXIST', timeframe, - candle_type=candle_type) + ohlcv = dh.ohlcv_load('UNITTEST/NONEXIST', timeframe, candle_type=candle_type) assert ohlcv.empty From ed0e7ead312e5ba0a678bddb5669ef75949d8f99 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 27 Mar 2023 20:36:05 +0200 Subject: [PATCH 347/360] Fix wrong import --- freqtrade/data/history/featherdatahandler.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/freqtrade/data/history/featherdatahandler.py b/freqtrade/data/history/featherdatahandler.py index 4e016080c..bb387fc84 100644 --- a/freqtrade/data/history/featherdatahandler.py +++ b/freqtrade/data/history/featherdatahandler.py @@ -4,9 +4,8 @@ from typing import Optional from pandas import DataFrame, read_feather, to_datetime from freqtrade.configuration import TimeRange -from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, TradeList, DEFAULT_TRADES_COLUMNS +from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS, TradeList from freqtrade.enums import CandleType -from freqtrade.data.converter import trades_dict_to_list from .idatahandler import IDataHandler From cde432fef05c19218d0ce8883c732e36d7178eb9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 23 Mar 2023 18:15:54 +0100 Subject: [PATCH 348/360] Enable gate market orders closes #8368 --- freqtrade/exchange/gate.py | 8 -------- tests/exchange/test_gate.py | 33 --------------------------------- 2 files changed, 41 deletions(-) diff --git a/freqtrade/exchange/gate.py b/freqtrade/exchange/gate.py index bf6d5b59c..f0c1053a6 100644 --- a/freqtrade/exchange/gate.py +++ b/freqtrade/exchange/gate.py @@ -50,14 +50,6 @@ class Gate(Exchange): (TradingMode.FUTURES, MarginMode.ISOLATED) ] - def validate_ordertypes(self, order_types: Dict) -> None: - - if self.trading_mode != TradingMode.FUTURES: - if any(v == 'market' for k, v in order_types.items()): - raise OperationalException( - f'Exchange {self.name} does not support market orders.') - super().validate_stop_ordertypes(order_types) - def _get_params( self, side: BuySell, diff --git a/tests/exchange/test_gate.py b/tests/exchange/test_gate.py index db7591a40..3cb5a9a3e 100644 --- a/tests/exchange/test_gate.py +++ b/tests/exchange/test_gate.py @@ -4,42 +4,9 @@ from unittest.mock import MagicMock import pytest from freqtrade.enums import MarginMode, TradingMode -from freqtrade.exceptions import OperationalException -from freqtrade.exchange import Gate -from freqtrade.resolvers.exchange_resolver import ExchangeResolver from tests.conftest import EXMS, get_patched_exchange -def test_validate_order_types_gate(default_conf, mocker): - default_conf['exchange']['name'] = 'gate' - mocker.patch(f'{EXMS}._init_ccxt') - mocker.patch(f'{EXMS}._load_markets', return_value={}) - mocker.patch(f'{EXMS}.validate_pairs') - mocker.patch(f'{EXMS}.validate_timeframes') - mocker.patch(f'{EXMS}.validate_stakecurrency') - mocker.patch(f'{EXMS}.validate_pricing') - mocker.patch(f'{EXMS}.name', 'Gate') - exch = ExchangeResolver.load_exchange('gate', default_conf, True) - assert isinstance(exch, Gate) - - default_conf['order_types'] = { - 'entry': 'market', - 'exit': 'limit', - 'stoploss': 'market', - 'stoploss_on_exchange': False - } - - with pytest.raises(OperationalException, - match=r'Exchange .* does not support market orders.'): - ExchangeResolver.load_exchange('gate', default_conf, True) - - # market-orders supported on futures markets. - default_conf['trading_mode'] = 'futures' - default_conf['margin_mode'] = 'isolated' - ex = ExchangeResolver.load_exchange('gate', default_conf, True) - assert ex - - @pytest.mark.usefixtures("init_persistence") def test_fetch_stoploss_order_gate(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf, id='gate') From 19b78fbc22856160d8f2d436ce6af7be8dbb61fd Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 28 Mar 2023 06:55:55 +0200 Subject: [PATCH 349/360] Override ccxt's marketOrderRequiresPrice settings for gate --- freqtrade/exchange/exchange.py | 12 ++++++++++-- freqtrade/exchange/gate.py | 3 ++- tests/exchange/test_exchange.py | 20 ++++++++++++++++---- 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 92eb4b58a..bbe9585ae 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -80,6 +80,8 @@ class Exchange: "fee_cost_in_contracts": False, # Fee cost needs contract conversion "needs_trading_fees": False, # use fetch_trading_fees to cache fees "order_props_in_contracts": ['amount', 'cost', 'filled', 'remaining'], + # Override createMarketBuyOrderRequiresPrice where ccxt has it wrong + "marketOrderRequiresPrice": False, } _ft_has: Dict = {} _ft_has_futures: Dict = {} @@ -1040,6 +1042,13 @@ class Exchange: params.update({'reduceOnly': True}) return params + def _order_needs_price(self, ordertype: str) -> bool: + return ( + ordertype != 'market' + or self._api.options.get("createMarketBuyOrderRequiresPrice", False) + or self._ft_has.get('marketOrderRequiresPrice', False) + ) + def create_order( self, *, @@ -1062,8 +1071,7 @@ class Exchange: try: # Set the precision for amount and price(rate) as accepted by the exchange amount = self.amount_to_precision(pair, self._amount_to_contracts(pair, amount)) - needs_price = (ordertype != 'market' - or self._api.options.get("createMarketBuyOrderRequiresPrice", False)) + needs_price = self._order_needs_price(ordertype) rate_for_order = self.price_to_precision(pair, rate) if needs_price else None if not reduceOnly: diff --git a/freqtrade/exchange/gate.py b/freqtrade/exchange/gate.py index f0c1053a6..2ac135fc1 100644 --- a/freqtrade/exchange/gate.py +++ b/freqtrade/exchange/gate.py @@ -5,7 +5,6 @@ from typing import Any, Dict, List, Optional, Tuple from freqtrade.constants import BuySell from freqtrade.enums import MarginMode, PriceType, TradingMode -from freqtrade.exceptions import OperationalException from freqtrade.exchange import Exchange from freqtrade.misc import safe_value_fallback2 @@ -28,10 +27,12 @@ class Gate(Exchange): "order_time_in_force": ['GTC', 'IOC'], "stoploss_order_types": {"limit": "limit"}, "stoploss_on_exchange": True, + "marketOrderRequiresPrice": True, } _ft_has_futures: Dict = { "needs_trading_fees": True, + "marketOrderRequiresPrice": False, "tickers_have_bid_ask": False, "fee_cost_in_contracts": False, # Set explicitly to false for clarity "order_props_in_contracts": ['amount', 'filled', 'remaining'], diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 5350f4e3e..e08815e61 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1439,7 +1439,10 @@ def test_buy_prod(default_conf, mocker, exchange_name): assert api_mock.create_order.call_args[0][1] == order_type assert api_mock.create_order.call_args[0][2] == 'buy' assert api_mock.create_order.call_args[0][3] == 1 - assert api_mock.create_order.call_args[0][4] is None + if exchange._order_needs_price(order_type): + assert api_mock.create_order.call_args[0][4] == 200 + else: + assert api_mock.create_order.call_args[0][4] is None api_mock.create_order.reset_mock() order_type = 'limit' @@ -1544,7 +1547,10 @@ def test_buy_considers_time_in_force(default_conf, mocker, exchange_name): assert api_mock.create_order.call_args[0][1] == order_type assert api_mock.create_order.call_args[0][2] == 'buy' assert api_mock.create_order.call_args[0][3] == 1 - assert api_mock.create_order.call_args[0][4] is None + if exchange._order_needs_price(order_type): + assert api_mock.create_order.call_args[0][4] == 200 + else: + assert api_mock.create_order.call_args[0][4] is None # Market orders should not send timeInForce!! assert "timeInForce" not in api_mock.create_order.call_args[0][5] @@ -1588,7 +1594,10 @@ def test_sell_prod(default_conf, mocker, exchange_name): assert api_mock.create_order.call_args[0][1] == order_type assert api_mock.create_order.call_args[0][2] == 'sell' assert api_mock.create_order.call_args[0][3] == 1 - assert api_mock.create_order.call_args[0][4] is None + if exchange._order_needs_price(order_type): + assert api_mock.create_order.call_args[0][4] == 200 + else: + assert api_mock.create_order.call_args[0][4] is None api_mock.create_order.reset_mock() order_type = 'limit' @@ -1682,7 +1691,10 @@ def test_sell_considers_time_in_force(default_conf, mocker, exchange_name): assert api_mock.create_order.call_args[0][1] == order_type assert api_mock.create_order.call_args[0][2] == 'sell' assert api_mock.create_order.call_args[0][3] == 1 - assert api_mock.create_order.call_args[0][4] is None + if exchange._order_needs_price(order_type): + assert api_mock.create_order.call_args[0][4] == 200 + else: + assert api_mock.create_order.call_args[0][4] is None # Market orders should not send timeInForce!! assert "timeInForce" not in api_mock.create_order.call_args[0][5] From 2860e817bd24400acf1ec4d92ac2b115552a1896 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 28 Mar 2023 07:05:37 +0200 Subject: [PATCH 350/360] Update cached binance leverage Tiers --- .../exchange/binance_leverage_tiers.json | 3262 ++++++++++------- 1 file changed, 1953 insertions(+), 1309 deletions(-) diff --git a/freqtrade/exchange/binance_leverage_tiers.json b/freqtrade/exchange/binance_leverage_tiers.json index 07fdcb5a4..597db27ff 100644 --- a/freqtrade/exchange/binance_leverage_tiers.json +++ b/freqtrade/exchange/binance_leverage_tiers.json @@ -999,15 +999,15 @@ "currency": "USDT", "minNotional": 5000.0, "maxNotional": 10000.0, - "maintenanceMarginRate": 0.0065, + "maintenanceMarginRate": 0.006, "maxLeverage": 50.0, "info": { "bracket": "2", "initialLeverage": "50", "notionalCap": "10000", "notionalFloor": "5000", - "maintMarginRatio": "0.0065", - "cum": "7.5" + "maintMarginRatio": "0.006", + "cum": "5.0" } }, { @@ -1023,7 +1023,7 @@ "notionalCap": "50000", "notionalFloor": "10000", "maintMarginRatio": "0.01", - "cum": "42.5" + "cum": "45.0" } }, { @@ -1039,7 +1039,7 @@ "notionalCap": "250000", "notionalFloor": "50000", "maintMarginRatio": "0.02", - "cum": "542.5" + "cum": "545.0" } }, { @@ -1055,77 +1055,77 @@ "notionalCap": "1000000", "notionalFloor": "250000", "maintMarginRatio": "0.05", - "cum": "8042.5" + "cum": "8045.0" } }, { "tier": 6.0, "currency": "USDT", "minNotional": 1000000.0, - "maxNotional": 2000000.0, + "maxNotional": 5000000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "6", "initialLeverage": "5", - "notionalCap": "2000000", + "notionalCap": "5000000", "notionalFloor": "1000000", "maintMarginRatio": "0.1", - "cum": "58042.5" + "cum": "58045.0" } }, { "tier": 7.0, "currency": "USDT", - "minNotional": 2000000.0, - "maxNotional": 5000000.0, + "minNotional": 5000000.0, + "maxNotional": 10000000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 4.0, "info": { "bracket": "7", "initialLeverage": "4", - "notionalCap": "5000000", - "notionalFloor": "2000000", + "notionalCap": "10000000", + "notionalFloor": "5000000", "maintMarginRatio": "0.125", - "cum": "108042.5" + "cum": "183045.0" } }, { "tier": 8.0, "currency": "USDT", - "minNotional": 5000000.0, - "maxNotional": 10000000.0, + "minNotional": 10000000.0, + "maxNotional": 20000000.0, "maintenanceMarginRate": 0.15, "maxLeverage": 3.0, "info": { "bracket": "8", "initialLeverage": "3", - "notionalCap": "10000000", - "notionalFloor": "5000000", + "notionalCap": "20000000", + "notionalFloor": "10000000", "maintMarginRatio": "0.15", - "cum": "233042.5" + "cum": "433045.0" } }, { "tier": 9.0, "currency": "USDT", - "minNotional": 10000000.0, - "maxNotional": 20000000.0, + "minNotional": 20000000.0, + "maxNotional": 30000000.0, "maintenanceMarginRate": 0.25, "maxLeverage": 2.0, "info": { "bracket": "9", "initialLeverage": "2", - "notionalCap": "20000000", - "notionalFloor": "10000000", + "notionalCap": "30000000", + "notionalFloor": "20000000", "maintMarginRatio": "0.25", - "cum": "1233042.5" + "cum": "2433045.0" } }, { "tier": 10.0, "currency": "USDT", - "minNotional": 20000000.0, + "minNotional": 30000000.0, "maxNotional": 50000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, @@ -1133,9 +1133,9 @@ "bracket": "10", "initialLeverage": "1", "notionalCap": "50000000", - "notionalFloor": "20000000", + "notionalFloor": "30000000", "maintMarginRatio": "0.5", - "cum": "6233042.5" + "cum": "9933045.0" } } ], @@ -1274,13 +1274,13 @@ "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 200000.0, + "maxNotional": 600000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "3", "initialLeverage": "10", - "notionalCap": "200000", + "notionalCap": "600000", "notionalFloor": "25000", "maintMarginRatio": "0.05", "cum": "650.0" @@ -1289,65 +1289,65 @@ { "tier": 4.0, "currency": "USDT", - "minNotional": 200000.0, - "maxNotional": 500000.0, + "minNotional": 600000.0, + "maxNotional": 1600000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "500000", - "notionalFloor": "200000", + "notionalCap": "1600000", + "notionalFloor": "600000", "maintMarginRatio": "0.1", - "cum": "10650.0" + "cum": "30650.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 500000.0, - "maxNotional": 1000000.0, + "minNotional": 1600000.0, + "maxNotional": 2000000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 4.0, "info": { "bracket": "5", "initialLeverage": "4", - "notionalCap": "1000000", - "notionalFloor": "500000", + "notionalCap": "2000000", + "notionalFloor": "1600000", "maintMarginRatio": "0.125", - "cum": "23150.0" + "cum": "70650.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 3000000.0, + "minNotional": 2000000.0, + "maxNotional": 6000000.0, "maintenanceMarginRate": 0.25, "maxLeverage": 2.0, "info": { "bracket": "6", "initialLeverage": "2", - "notionalCap": "3000000", - "notionalFloor": "1000000", + "notionalCap": "6000000", + "notionalFloor": "2000000", "maintMarginRatio": "0.25", - "cum": "148150.0" + "cum": "320650.0" } }, { "tier": 7.0, "currency": "USDT", - "minNotional": 3000000.0, - "maxNotional": 5000000.0, + "minNotional": 6000000.0, + "maxNotional": 10000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { "bracket": "7", "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "3000000", + "notionalCap": "10000000", + "notionalFloor": "6000000", "maintMarginRatio": "0.5", - "cum": "898150.0" + "cum": "1820650.0" } } ], @@ -2304,10 +2304,10 @@ "minNotional": 0.0, "maxNotional": 5000.0, "maintenanceMarginRate": 0.02, - "maxLeverage": 20.0, + "maxLeverage": 25.0, "info": { "bracket": "1", - "initialLeverage": "20", + "initialLeverage": "25", "notionalCap": "5000", "notionalFloor": "0", "maintMarginRatio": "0.02", @@ -2320,10 +2320,10 @@ "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, + "maxLeverage": 20.0, "info": { "bracket": "2", - "initialLeverage": "10", + "initialLeverage": "20", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", @@ -2334,13 +2334,13 @@ "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, + "maxNotional": 600000.0, "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, + "maxLeverage": 10.0, "info": { "bracket": "3", - "initialLeverage": "8", - "notionalCap": "100000", + "initialLeverage": "10", + "notionalCap": "600000", "notionalFloor": "25000", "maintMarginRatio": "0.05", "cum": "650.0" @@ -2349,49 +2349,65 @@ { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, + "minNotional": 600000.0, + "maxNotional": 1600000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", + "notionalCap": "1600000", + "notionalFloor": "600000", "maintMarginRatio": "0.1", - "cum": "5650.0" + "cum": "30650.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, + "minNotional": 1600000.0, + "maxNotional": 2000000.0, "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maxLeverage": 4.0, "info": { "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", + "initialLeverage": "4", + "notionalCap": "2000000", + "notionalFloor": "1600000", "maintMarginRatio": "0.125", - "cum": "11900.0" + "cum": "70650.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 3000000.0, + "minNotional": 2000000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "6000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.25", + "cum": "320650.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 6000000.0, + "maxNotional": 10000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "1", - "notionalCap": "3000000", - "notionalFloor": "1000000", + "notionalCap": "10000000", + "notionalFloor": "6000000", "maintMarginRatio": "0.5", - "cum": "386900.0" + "cum": "1820650.0" } } ], @@ -2546,13 +2562,13 @@ "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 300000.0, + "maxNotional": 900000.0, "maintenanceMarginRate": 0.02, "maxLeverage": 20.0, "info": { "bracket": "3", "initialLeverage": "20", - "notionalCap": "300000", + "notionalCap": "900000", "notionalFloor": "25000", "maintMarginRatio": "0.02", "cum": "150.0" @@ -2561,39 +2577,39 @@ { "tier": 4.0, "currency": "USDT", - "minNotional": 300000.0, - "maxNotional": 1200000.0, + "minNotional": 900000.0, + "maxNotional": 1800000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "4", "initialLeverage": "10", - "notionalCap": "1200000", - "notionalFloor": "300000", + "notionalCap": "1800000", + "notionalFloor": "900000", "maintMarginRatio": "0.05", - "cum": "9150.0" + "cum": "27150.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 1200000.0, - "maxNotional": 3000000.0, + "minNotional": 1800000.0, + "maxNotional": 4800000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "5", "initialLeverage": "5", - "notionalCap": "3000000", - "notionalFloor": "1200000", + "notionalCap": "4800000", + "notionalFloor": "1800000", "maintMarginRatio": "0.1", - "cum": "69150.0" + "cum": "117150.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 3000000.0, + "minNotional": 4800000.0, "maxNotional": 6000000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 4.0, @@ -2601,9 +2617,9 @@ "bracket": "6", "initialLeverage": "4", "notionalCap": "6000000", - "notionalFloor": "3000000", + "notionalFloor": "4800000", "maintMarginRatio": "0.125", - "cum": "144150.0" + "cum": "237150.0" } }, { @@ -2619,7 +2635,7 @@ "notionalCap": "18000000", "notionalFloor": "6000000", "maintMarginRatio": "0.25", - "cum": "894150.0" + "cum": "987150.0" } }, { @@ -2635,7 +2651,7 @@ "notionalCap": "30000000", "notionalFloor": "18000000", "maintMarginRatio": "0.5", - "cum": "5394150.0" + "cum": "5487150.0" } } ], @@ -2737,6 +2753,136 @@ } } ], + "ARB/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.006, + "maxLeverage": 50.0, + "info": { + "bracket": "1", + "initialLeverage": "50", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.006", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 50000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 25.0, + "info": { + "bracket": "2", + "initialLeverage": "25", + "notionalCap": "50000", + "notionalFloor": "5000", + "maintMarginRatio": "0.01", + "cum": "20.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 50000.0, + "maxNotional": 400000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "3", + "initialLeverage": "20", + "notionalCap": "400000", + "notionalFloor": "50000", + "maintMarginRatio": "0.025", + "cum": "770.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 400000.0, + "maxNotional": 800000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "4", + "initialLeverage": "10", + "notionalCap": "800000", + "notionalFloor": "400000", + "maintMarginRatio": "0.05", + "cum": "10770.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 800000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "5", + "initialLeverage": "5", + "notionalCap": "2000000", + "notionalFloor": "800000", + "maintMarginRatio": "0.1", + "cum": "50770.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 2000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "6", + "initialLeverage": "4", + "notionalCap": "5000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.125", + "cum": "100770.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 5000000.0, + "maxNotional": 12000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "7", + "initialLeverage": "2", + "notionalCap": "12000000", + "notionalFloor": "5000000", + "maintMarginRatio": "0.25", + "cum": "725770.0" + } + }, + { + "tier": 8.0, + "currency": "USDT", + "minNotional": 12000000.0, + "maxNotional": 20000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "8", + "initialLeverage": "1", + "notionalCap": "20000000", + "notionalFloor": "12000000", + "maintMarginRatio": "0.5", + "cum": "3725770.0" + } + } + ], "ARPA/USDT:USDT": [ { "tier": 1.0, @@ -2760,10 +2906,10 @@ "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, + "maxLeverage": 15.0, "info": { "bracket": "2", - "initialLeverage": "10", + "initialLeverage": "15", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", @@ -2774,13 +2920,13 @@ "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, + "maxNotional": 600000.0, "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, + "maxLeverage": 10.0, "info": { "bracket": "3", - "initialLeverage": "8", - "notionalCap": "100000", + "initialLeverage": "10", + "notionalCap": "600000", "notionalFloor": "25000", "maintMarginRatio": "0.05", "cum": "650.0" @@ -2789,49 +2935,65 @@ { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, + "minNotional": 600000.0, + "maxNotional": 1600000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", + "notionalCap": "1600000", + "notionalFloor": "600000", "maintMarginRatio": "0.1", - "cum": "5650.0" + "cum": "30650.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, + "minNotional": 1600000.0, + "maxNotional": 2000000.0, "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maxLeverage": 4.0, "info": { "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", + "initialLeverage": "4", + "notionalCap": "2000000", + "notionalFloor": "1600000", "maintMarginRatio": "0.125", - "cum": "11900.0" + "cum": "70650.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 3000000.0, + "minNotional": 2000000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "6000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.25", + "cum": "320650.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 6000000.0, + "maxNotional": 10000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "1", - "notionalCap": "3000000", - "notionalFloor": "1000000", + "notionalCap": "10000000", + "notionalFloor": "6000000", "maintMarginRatio": "0.5", - "cum": "386900.0" + "cum": "1820650.0" } } ], @@ -3639,14 +3801,14 @@ "currency": "USDT", "minNotional": 0.0, "maxNotional": 5000.0, - "maintenanceMarginRate": 0.0065, - "maxLeverage": 25.0, + "maintenanceMarginRate": 0.006, + "maxLeverage": 50.0, "info": { "bracket": "1", - "initialLeverage": "25", + "initialLeverage": "50", "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.0065", + "maintMarginRatio": "0.006", "cum": "0.0" } }, @@ -3655,14 +3817,14 @@ "currency": "USDT", "minNotional": 5000.0, "maxNotional": 25000.0, - "maintenanceMarginRate": 0.0075, - "maxLeverage": 20.0, + "maintenanceMarginRate": 0.007, + "maxLeverage": 30.0, "info": { "bracket": "2", - "initialLeverage": "20", + "initialLeverage": "30", "notionalCap": "25000", "notionalFloor": "5000", - "maintMarginRatio": "0.0075", + "maintMarginRatio": "0.007", "cum": "5.0" } }, @@ -3670,70 +3832,70 @@ "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 50000.0, + "maxNotional": 400000.0, "maintenanceMarginRate": 0.01, - "maxLeverage": 18.0, + "maxLeverage": 25.0, "info": { "bracket": "3", - "initialLeverage": "18", - "notionalCap": "50000", + "initialLeverage": "25", + "notionalCap": "400000", "notionalFloor": "25000", "maintMarginRatio": "0.01", - "cum": "67.5" + "cum": "80.0" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 50000.0, - "maxNotional": 250000.0, + "minNotional": 400000.0, + "maxNotional": 600000.0, "maintenanceMarginRate": 0.02, - "maxLeverage": 15.0, + "maxLeverage": 20.0, "info": { "bracket": "4", - "initialLeverage": "15", - "notionalCap": "250000", - "notionalFloor": "50000", + "initialLeverage": "20", + "notionalCap": "600000", + "notionalFloor": "400000", "maintMarginRatio": "0.02", - "cum": "567.5" + "cum": "4080.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, + "minNotional": 600000.0, + "maxNotional": 1200000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "5", "initialLeverage": "10", - "notionalCap": "1000000", - "notionalFloor": "250000", + "notionalCap": "1200000", + "notionalFloor": "600000", "maintMarginRatio": "0.05", - "cum": "8067.5" + "cum": "22080.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 2000000.0, + "minNotional": 1200000.0, + "maxNotional": 3200000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "6", "initialLeverage": "5", - "notionalCap": "2000000", - "notionalFloor": "1000000", + "notionalCap": "3200000", + "notionalFloor": "1200000", "maintMarginRatio": "0.1", - "cum": "58067.5" + "cum": "82080.0" } }, { "tier": 7.0, "currency": "USDT", - "minNotional": 2000000.0, + "minNotional": 3200000.0, "maxNotional": 5000000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 4.0, @@ -3741,9 +3903,9 @@ "bracket": "7", "initialLeverage": "4", "notionalCap": "5000000", - "notionalFloor": "2000000", + "notionalFloor": "3200000", "maintMarginRatio": "0.125", - "cum": "108067.5" + "cum": "162080.0" } }, { @@ -3751,15 +3913,15 @@ "currency": "USDT", "minNotional": 5000000.0, "maxNotional": 10000000.0, - "maintenanceMarginRate": 0.1665, + "maintenanceMarginRate": 0.15, "maxLeverage": 3.0, "info": { "bracket": "8", "initialLeverage": "3", "notionalCap": "10000000", "notionalFloor": "5000000", - "maintMarginRatio": "0.1665", - "cum": "315567.5" + "maintMarginRatio": "0.15", + "cum": "287080.0" } }, { @@ -3775,7 +3937,7 @@ "notionalCap": "15000000", "notionalFloor": "10000000", "maintMarginRatio": "0.25", - "cum": "1150567.5" + "cum": "1287080.0" } }, { @@ -3791,7 +3953,7 @@ "notionalCap": "20000000", "notionalFloor": "15000000", "maintMarginRatio": "0.5", - "cum": "4900567.5" + "cum": "5037080.0" } } ], @@ -4763,15 +4925,15 @@ "currency": "USDT", "minNotional": 5000.0, "maxNotional": 10000.0, - "maintenanceMarginRate": 0.0065, + "maintenanceMarginRate": 0.006, "maxLeverage": 50.0, "info": { "bracket": "2", "initialLeverage": "50", "notionalCap": "10000", "notionalFloor": "5000", - "maintMarginRatio": "0.0065", - "cum": "7.5" + "maintMarginRatio": "0.006", + "cum": "5.0" } }, { @@ -4787,7 +4949,7 @@ "notionalCap": "50000", "notionalFloor": "10000", "maintMarginRatio": "0.01", - "cum": "42.5" + "cum": "45.0" } }, { @@ -4803,7 +4965,7 @@ "notionalCap": "250000", "notionalFloor": "50000", "maintMarginRatio": "0.02", - "cum": "542.5" + "cum": "545.0" } }, { @@ -4819,77 +4981,77 @@ "notionalCap": "1000000", "notionalFloor": "250000", "maintMarginRatio": "0.05", - "cum": "8042.5" + "cum": "8045.0" } }, { "tier": 6.0, "currency": "USDT", "minNotional": 1000000.0, - "maxNotional": 2000000.0, + "maxNotional": 5000000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "6", "initialLeverage": "5", - "notionalCap": "2000000", + "notionalCap": "5000000", "notionalFloor": "1000000", "maintMarginRatio": "0.1", - "cum": "58042.5" + "cum": "58045.0" } }, { "tier": 7.0, "currency": "USDT", - "minNotional": 2000000.0, - "maxNotional": 5000000.0, + "minNotional": 5000000.0, + "maxNotional": 10000000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 4.0, "info": { "bracket": "7", "initialLeverage": "4", - "notionalCap": "5000000", - "notionalFloor": "2000000", + "notionalCap": "10000000", + "notionalFloor": "5000000", "maintMarginRatio": "0.125", - "cum": "108042.5" + "cum": "183045.0" } }, { "tier": 8.0, "currency": "USDT", - "minNotional": 5000000.0, - "maxNotional": 10000000.0, + "minNotional": 10000000.0, + "maxNotional": 20000000.0, "maintenanceMarginRate": 0.15, "maxLeverage": 3.0, "info": { "bracket": "8", "initialLeverage": "3", - "notionalCap": "10000000", - "notionalFloor": "5000000", + "notionalCap": "20000000", + "notionalFloor": "10000000", "maintMarginRatio": "0.15", - "cum": "233042.5" + "cum": "433045.0" } }, { "tier": 9.0, "currency": "USDT", - "minNotional": 10000000.0, - "maxNotional": 20000000.0, + "minNotional": 20000000.0, + "maxNotional": 30000000.0, "maintenanceMarginRate": 0.25, "maxLeverage": 2.0, "info": { "bracket": "9", "initialLeverage": "2", - "notionalCap": "20000000", - "notionalFloor": "10000000", + "notionalCap": "30000000", + "notionalFloor": "20000000", "maintMarginRatio": "0.25", - "cum": "1233042.5" + "cum": "2433045.0" } }, { "tier": 10.0, "currency": "USDT", - "minNotional": 20000000.0, + "minNotional": 30000000.0, "maxNotional": 50000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, @@ -4897,9 +5059,9 @@ "bracket": "10", "initialLeverage": "1", "notionalCap": "50000000", - "notionalFloor": "20000000", + "notionalFloor": "30000000", "maintMarginRatio": "0.5", - "cum": "6233042.5" + "cum": "9933045.0" } } ], @@ -5439,6 +5601,120 @@ } } ], + "BTC/USDT:USDT-230630": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 375000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, + "info": { + "bracket": "1", + "initialLeverage": "25", + "notionalCap": "375000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 375000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "2", + "initialLeverage": "10", + "notionalCap": "2000000", + "notionalFloor": "375000", + "maintMarginRatio": "0.05", + "cum": "11250.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 2000000.0, + "maxNotional": 4000000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "3", + "initialLeverage": "5", + "notionalCap": "4000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.1", + "cum": "111250.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 4000000.0, + "maxNotional": 10000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "4", + "initialLeverage": "4", + "notionalCap": "10000000", + "notionalFloor": "4000000", + "maintMarginRatio": "0.125", + "cum": "211250.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 10000000.0, + "maxNotional": 20000000.0, + "maintenanceMarginRate": 0.15, + "maxLeverage": 3.0, + "info": { + "bracket": "5", + "initialLeverage": "3", + "notionalCap": "20000000", + "notionalFloor": "10000000", + "maintMarginRatio": "0.15", + "cum": "461250.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 20000000.0, + "maxNotional": 40000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "40000000", + "notionalFloor": "20000000", + "maintMarginRatio": "0.25", + "cum": "2461250.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 40000000.0, + "maxNotional": 400000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "400000000", + "notionalFloor": "40000000", + "maintMarginRatio": "0.5", + "cum": "1.246125E7" + } + } + ], "BTCDOM/USDT:USDT": [ { "tier": 1.0, @@ -5936,10 +6212,10 @@ "minNotional": 0.0, "maxNotional": 5000.0, "maintenanceMarginRate": 0.02, - "maxLeverage": 20.0, + "maxLeverage": 25.0, "info": { "bracket": "1", - "initialLeverage": "20", + "initialLeverage": "25", "notionalCap": "5000", "notionalFloor": "0", "maintMarginRatio": "0.02", @@ -5952,10 +6228,10 @@ "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, + "maxLeverage": 20.0, "info": { "bracket": "2", - "initialLeverage": "10", + "initialLeverage": "20", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", @@ -5966,13 +6242,13 @@ "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, + "maxNotional": 600000.0, "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, + "maxLeverage": 10.0, "info": { "bracket": "3", - "initialLeverage": "8", - "notionalCap": "100000", + "initialLeverage": "10", + "notionalCap": "600000", "notionalFloor": "25000", "maintMarginRatio": "0.05", "cum": "650.0" @@ -5981,49 +6257,65 @@ { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, + "minNotional": 600000.0, + "maxNotional": 1600000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", + "notionalCap": "1600000", + "notionalFloor": "600000", "maintMarginRatio": "0.1", - "cum": "5650.0" + "cum": "30650.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, + "minNotional": 1600000.0, + "maxNotional": 2000000.0, "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maxLeverage": 4.0, "info": { "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", + "initialLeverage": "4", + "notionalCap": "2000000", + "notionalFloor": "1600000", "maintMarginRatio": "0.125", - "cum": "11900.0" + "cum": "70650.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 3000000.0, + "minNotional": 2000000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "6000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.25", + "cum": "320650.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 6000000.0, + "maxNotional": 10000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "1", - "notionalCap": "3000000", - "notionalFloor": "1000000", + "notionalCap": "10000000", + "notionalFloor": "6000000", "maintMarginRatio": "0.5", - "cum": "386900.0" + "cum": "1820650.0" } } ], @@ -6980,10 +7272,10 @@ "minNotional": 0.0, "maxNotional": 5000.0, "maintenanceMarginRate": 0.02, - "maxLeverage": 20.0, + "maxLeverage": 25.0, "info": { "bracket": "1", - "initialLeverage": "20", + "initialLeverage": "25", "notionalCap": "5000", "notionalFloor": "0", "maintMarginRatio": "0.02", @@ -6996,10 +7288,10 @@ "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, + "maxLeverage": 20.0, "info": { "bracket": "2", - "initialLeverage": "10", + "initialLeverage": "20", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", @@ -7010,13 +7302,13 @@ "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, + "maxNotional": 300000.0, "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, + "maxLeverage": 10.0, "info": { "bracket": "3", - "initialLeverage": "8", - "notionalCap": "100000", + "initialLeverage": "10", + "notionalCap": "300000", "notionalFloor": "25000", "maintMarginRatio": "0.05", "cum": "650.0" @@ -7025,33 +7317,33 @@ { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, + "minNotional": 300000.0, + "maxNotional": 800000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", + "notionalCap": "800000", + "notionalFloor": "300000", "maintMarginRatio": "0.1", - "cum": "5650.0" + "cum": "15650.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, + "minNotional": 800000.0, "maxNotional": 1000000.0, "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maxLeverage": 4.0, "info": { "bracket": "5", - "initialLeverage": "2", + "initialLeverage": "4", "notionalCap": "1000000", - "notionalFloor": "250000", + "notionalFloor": "800000", "maintMarginRatio": "0.125", - "cum": "11900.0" + "cum": "35650.0" } }, { @@ -7059,15 +7351,31 @@ "currency": "USDT", "minNotional": 1000000.0, "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "160650.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, + "maxNotional": 5000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "1", - "notionalCap": "3000000", - "notionalFloor": "1000000", + "notionalCap": "5000000", + "notionalFloor": "3000000", "maintMarginRatio": "0.5", - "cum": "386900.0" + "cum": "910650.0" } } ], @@ -7160,10 +7468,10 @@ "minNotional": 0.0, "maxNotional": 5000.0, "maintenanceMarginRate": 0.02, - "maxLeverage": 20.0, + "maxLeverage": 10.0, "info": { "bracket": "1", - "initialLeverage": "20", + "initialLeverage": "10", "notionalCap": "5000", "notionalFloor": "0", "maintMarginRatio": "0.02", @@ -7176,10 +7484,10 @@ "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, + "maxLeverage": 8.0, "info": { "bracket": "2", - "initialLeverage": "10", + "initialLeverage": "8", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", @@ -7192,10 +7500,10 @@ "minNotional": 25000.0, "maxNotional": 100000.0, "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, + "maxLeverage": 6.0, "info": { "bracket": "3", - "initialLeverage": "8", + "initialLeverage": "6", "notionalCap": "100000", "notionalFloor": "25000", "maintMarginRatio": "0.05", @@ -7222,13 +7530,13 @@ "tier": 5.0, "currency": "BUSD", "minNotional": 250000.0, - "maxNotional": 1000000.0, + "maxNotional": 500000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 2.0, "info": { "bracket": "5", "initialLeverage": "2", - "notionalCap": "1000000", + "notionalCap": "500000", "notionalFloor": "250000", "maintMarginRatio": "0.125", "cum": "11900.0" @@ -7237,17 +7545,17 @@ { "tier": 6.0, "currency": "BUSD", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, + "minNotional": 500000.0, + "maxNotional": 1000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { "bracket": "6", "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", + "notionalCap": "1000000", + "notionalFloor": "500000", "maintMarginRatio": "0.5", - "cum": "386900.0" + "cum": "199400.0" } } ], @@ -7340,10 +7648,10 @@ "minNotional": 0.0, "maxNotional": 5000.0, "maintenanceMarginRate": 0.02, - "maxLeverage": 20.0, + "maxLeverage": 25.0, "info": { "bracket": "1", - "initialLeverage": "20", + "initialLeverage": "25", "notionalCap": "5000", "notionalFloor": "0", "maintMarginRatio": "0.02", @@ -7356,10 +7664,10 @@ "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, + "maxLeverage": 20.0, "info": { "bracket": "2", - "initialLeverage": "10", + "initialLeverage": "20", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", @@ -7370,13 +7678,13 @@ "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, + "maxNotional": 600000.0, "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, + "maxLeverage": 10.0, "info": { "bracket": "3", - "initialLeverage": "8", - "notionalCap": "100000", + "initialLeverage": "10", + "notionalCap": "600000", "notionalFloor": "25000", "maintMarginRatio": "0.05", "cum": "650.0" @@ -7385,49 +7693,65 @@ { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, + "minNotional": 600000.0, + "maxNotional": 1600000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", + "notionalCap": "1600000", + "notionalFloor": "600000", "maintMarginRatio": "0.1", - "cum": "5650.0" + "cum": "30650.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, + "minNotional": 1600000.0, + "maxNotional": 2000000.0, "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maxLeverage": 4.0, "info": { "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", + "initialLeverage": "4", + "notionalCap": "2000000", + "notionalFloor": "1600000", "maintMarginRatio": "0.125", - "cum": "11900.0" + "cum": "70650.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, + "minNotional": 2000000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "6000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.25", + "cum": "320650.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 6000000.0, + "maxNotional": 10000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", + "notionalCap": "10000000", + "notionalFloor": "6000000", "maintMarginRatio": "0.5", - "cum": "386900.0" + "cum": "1820650.0" } } ], @@ -7634,10 +7958,10 @@ "minNotional": 0.0, "maxNotional": 5000.0, "maintenanceMarginRate": 0.02, - "maxLeverage": 15.0, + "maxLeverage": 25.0, "info": { "bracket": "1", - "initialLeverage": "15", + "initialLeverage": "25", "notionalCap": "5000", "notionalFloor": "0", "maintMarginRatio": "0.02", @@ -7650,10 +7974,10 @@ "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, + "maxLeverage": 20.0, "info": { "bracket": "2", - "initialLeverage": "10", + "initialLeverage": "20", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", @@ -7664,13 +7988,13 @@ "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 200000.0, + "maxNotional": 300000.0, "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, + "maxLeverage": 10.0, "info": { "bracket": "3", - "initialLeverage": "8", - "notionalCap": "200000", + "initialLeverage": "10", + "notionalCap": "300000", "notionalFloor": "25000", "maintMarginRatio": "0.05", "cum": "650.0" @@ -7679,49 +8003,65 @@ { "tier": 4.0, "currency": "USDT", - "minNotional": 200000.0, - "maxNotional": 500000.0, + "minNotional": 300000.0, + "maxNotional": 800000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "500000", - "notionalFloor": "200000", + "notionalCap": "800000", + "notionalFloor": "300000", "maintMarginRatio": "0.1", - "cum": "10650.0" + "cum": "15650.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 500000.0, + "minNotional": 800000.0, "maxNotional": 1000000.0, "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maxLeverage": 4.0, "info": { "bracket": "5", - "initialLeverage": "2", + "initialLeverage": "4", "notionalCap": "1000000", - "notionalFloor": "500000", + "notionalFloor": "800000", "maintMarginRatio": "0.125", - "cum": "23150.0" + "cum": "35650.0" } }, { "tier": 6.0, "currency": "USDT", "minNotional": 1000000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "160650.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, "maxNotional": 5000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "1", "notionalCap": "5000000", - "notionalFloor": "1000000", + "notionalFloor": "3000000", "maintMarginRatio": "0.5", - "cum": "398150.0" + "cum": "910650.0" } } ], @@ -8025,14 +8365,14 @@ "currency": "USDT", "minNotional": 0.0, "maxNotional": 5000.0, - "maintenanceMarginRate": 0.0065, + "maintenanceMarginRate": 0.006, "maxLeverage": 50.0, "info": { "bracket": "1", "initialLeverage": "50", "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.0065", + "maintMarginRatio": "0.006", "cum": "0.0" } }, @@ -8041,14 +8381,14 @@ "currency": "USDT", "minNotional": 5000.0, "maxNotional": 25000.0, - "maintenanceMarginRate": 0.0075, + "maintenanceMarginRate": 0.007, "maxLeverage": 40.0, "info": { "bracket": "2", "initialLeverage": "40", "notionalCap": "25000", "notionalFloor": "5000", - "maintMarginRatio": "0.0075", + "maintMarginRatio": "0.007", "cum": "5.0" } }, @@ -8056,112 +8396,112 @@ "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 50000.0, + "maxNotional": 600000.0, "maintenanceMarginRate": 0.01, "maxLeverage": 25.0, "info": { "bracket": "3", "initialLeverage": "25", - "notionalCap": "50000", + "notionalCap": "600000", "notionalFloor": "25000", "maintMarginRatio": "0.01", - "cum": "67.5" + "cum": "80.0" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 50000.0, - "maxNotional": 250000.0, + "minNotional": 600000.0, + "maxNotional": 900000.0, "maintenanceMarginRate": 0.025, "maxLeverage": 20.0, "info": { "bracket": "4", "initialLeverage": "20", - "notionalCap": "250000", - "notionalFloor": "50000", + "notionalCap": "900000", + "notionalFloor": "600000", "maintMarginRatio": "0.025", - "cum": "817.5" + "cum": "9080.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, + "minNotional": 900000.0, + "maxNotional": 1800000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "5", "initialLeverage": "10", - "notionalCap": "1000000", - "notionalFloor": "250000", + "notionalCap": "1800000", + "notionalFloor": "900000", "maintMarginRatio": "0.05", - "cum": "7067.5" + "cum": "31580.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 2000000.0, + "minNotional": 1800000.0, + "maxNotional": 4800000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "6", "initialLeverage": "5", - "notionalCap": "2000000", - "notionalFloor": "1000000", + "notionalCap": "4800000", + "notionalFloor": "1800000", "maintMarginRatio": "0.1", - "cum": "57067.5" + "cum": "121580.0" } }, { "tier": 7.0, "currency": "USDT", - "minNotional": 2000000.0, - "maxNotional": 5000000.0, + "minNotional": 4800000.0, + "maxNotional": 6000000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 4.0, "info": { "bracket": "7", "initialLeverage": "4", - "notionalCap": "5000000", - "notionalFloor": "2000000", + "notionalCap": "6000000", + "notionalFloor": "4800000", "maintMarginRatio": "0.125", - "cum": "107067.5" + "cum": "241580.0" } }, { "tier": 8.0, "currency": "USDT", - "minNotional": 5000000.0, - "maxNotional": 10000000.0, + "minNotional": 6000000.0, + "maxNotional": 18000000.0, "maintenanceMarginRate": 0.25, "maxLeverage": 2.0, "info": { "bracket": "8", "initialLeverage": "2", - "notionalCap": "10000000", - "notionalFloor": "5000000", + "notionalCap": "18000000", + "notionalFloor": "6000000", "maintMarginRatio": "0.25", - "cum": "732067.5" + "cum": "991580.0" } }, { "tier": 9.0, "currency": "USDT", - "minNotional": 10000000.0, - "maxNotional": 20000000.0, + "minNotional": 18000000.0, + "maxNotional": 30000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { "bracket": "9", "initialLeverage": "1", - "notionalCap": "20000000", - "notionalFloor": "10000000", + "notionalCap": "30000000", + "notionalFloor": "18000000", "maintMarginRatio": "0.5", - "cum": "3232067.5" + "cum": "5491580.0" } } ], @@ -8706,13 +9046,13 @@ "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, + "maxNotional": 600000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "3", "initialLeverage": "10", - "notionalCap": "100000", + "notionalCap": "600000", "notionalFloor": "25000", "maintMarginRatio": "0.05", "cum": "700.0" @@ -8721,49 +9061,65 @@ { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, + "minNotional": 600000.0, + "maxNotional": 1600000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", + "notionalCap": "1600000", + "notionalFloor": "600000", "maintMarginRatio": "0.1", - "cum": "5700.0" + "cum": "30700.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, + "minNotional": 1600000.0, + "maxNotional": 2000000.0, "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maxLeverage": 4.0, "info": { "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", + "initialLeverage": "4", + "notionalCap": "2000000", + "notionalFloor": "1600000", "maintMarginRatio": "0.125", - "cum": "11950.0" + "cum": "70700.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 50000000.0, + "minNotional": 2000000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "6000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.25", + "cum": "320700.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 6000000.0, + "maxNotional": 10000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "1", - "notionalCap": "50000000", - "notionalFloor": "1000000", + "notionalCap": "10000000", + "notionalFloor": "6000000", "maintMarginRatio": "0.5", - "cum": "386950.0" + "cum": "1820700.0" } } ], @@ -9017,15 +9373,15 @@ "currency": "USDT", "minNotional": 5000.0, "maxNotional": 10000.0, - "maintenanceMarginRate": 0.0065, + "maintenanceMarginRate": 0.006, "maxLeverage": 50.0, "info": { "bracket": "2", "initialLeverage": "50", "notionalCap": "10000", "notionalFloor": "5000", - "maintMarginRatio": "0.0065", - "cum": "7.5" + "maintMarginRatio": "0.006", + "cum": "5.0" } }, { @@ -9041,7 +9397,7 @@ "notionalCap": "50000", "notionalFloor": "10000", "maintMarginRatio": "0.01", - "cum": "42.5" + "cum": "45.0" } }, { @@ -9057,7 +9413,7 @@ "notionalCap": "250000", "notionalFloor": "50000", "maintMarginRatio": "0.02", - "cum": "542.5" + "cum": "545.0" } }, { @@ -9073,77 +9429,77 @@ "notionalCap": "1000000", "notionalFloor": "250000", "maintMarginRatio": "0.05", - "cum": "8042.5" + "cum": "8045.0" } }, { "tier": 6.0, "currency": "USDT", "minNotional": 1000000.0, - "maxNotional": 2000000.0, + "maxNotional": 5000000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "6", "initialLeverage": "5", - "notionalCap": "2000000", + "notionalCap": "5000000", "notionalFloor": "1000000", "maintMarginRatio": "0.1", - "cum": "58042.5" + "cum": "58045.0" } }, { "tier": 7.0, "currency": "USDT", - "minNotional": 2000000.0, - "maxNotional": 5000000.0, + "minNotional": 5000000.0, + "maxNotional": 10000000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 4.0, "info": { "bracket": "7", "initialLeverage": "4", - "notionalCap": "5000000", - "notionalFloor": "2000000", + "notionalCap": "10000000", + "notionalFloor": "5000000", "maintMarginRatio": "0.125", - "cum": "108042.5" + "cum": "183045.0" } }, { "tier": 8.0, "currency": "USDT", - "minNotional": 5000000.0, - "maxNotional": 10000000.0, + "minNotional": 10000000.0, + "maxNotional": 20000000.0, "maintenanceMarginRate": 0.15, "maxLeverage": 3.0, "info": { "bracket": "8", "initialLeverage": "3", - "notionalCap": "10000000", - "notionalFloor": "5000000", + "notionalCap": "20000000", + "notionalFloor": "10000000", "maintMarginRatio": "0.15", - "cum": "233042.5" + "cum": "433045.0" } }, { "tier": 9.0, "currency": "USDT", - "minNotional": 10000000.0, - "maxNotional": 20000000.0, + "minNotional": 20000000.0, + "maxNotional": 30000000.0, "maintenanceMarginRate": 0.25, "maxLeverage": 2.0, "info": { "bracket": "9", "initialLeverage": "2", - "notionalCap": "20000000", - "notionalFloor": "10000000", + "notionalCap": "30000000", + "notionalFloor": "20000000", "maintMarginRatio": "0.25", - "cum": "1233042.5" + "cum": "2433045.0" } }, { "tier": 10.0, "currency": "USDT", - "minNotional": 20000000.0, + "minNotional": 30000000.0, "maxNotional": 50000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, @@ -9151,9 +9507,9 @@ "bracket": "10", "initialLeverage": "1", "notionalCap": "50000000", - "notionalFloor": "20000000", + "notionalFloor": "30000000", "maintMarginRatio": "0.5", - "cum": "6233042.5" + "cum": "9933045.0" } } ], @@ -9277,15 +9633,15 @@ "currency": "USDT", "minNotional": 5000.0, "maxNotional": 10000.0, - "maintenanceMarginRate": 0.0065, + "maintenanceMarginRate": 0.006, "maxLeverage": 50.0, "info": { "bracket": "2", "initialLeverage": "50", "notionalCap": "10000", "notionalFloor": "5000", - "maintMarginRatio": "0.0065", - "cum": "7.5" + "maintMarginRatio": "0.006", + "cum": "5.0" } }, { @@ -9301,7 +9657,7 @@ "notionalCap": "50000", "notionalFloor": "10000", "maintMarginRatio": "0.01", - "cum": "42.5" + "cum": "45.0" } }, { @@ -9317,7 +9673,7 @@ "notionalCap": "250000", "notionalFloor": "50000", "maintMarginRatio": "0.02", - "cum": "542.5" + "cum": "545.0" } }, { @@ -9333,77 +9689,77 @@ "notionalCap": "1000000", "notionalFloor": "250000", "maintMarginRatio": "0.05", - "cum": "8042.5" + "cum": "8045.0" } }, { "tier": 6.0, "currency": "USDT", "minNotional": 1000000.0, - "maxNotional": 2000000.0, + "maxNotional": 5000000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "6", "initialLeverage": "5", - "notionalCap": "2000000", + "notionalCap": "5000000", "notionalFloor": "1000000", "maintMarginRatio": "0.1", - "cum": "58042.5" + "cum": "58045.0" } }, { "tier": 7.0, "currency": "USDT", - "minNotional": 2000000.0, - "maxNotional": 5000000.0, + "minNotional": 5000000.0, + "maxNotional": 10000000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 4.0, "info": { "bracket": "7", "initialLeverage": "4", - "notionalCap": "5000000", - "notionalFloor": "2000000", + "notionalCap": "10000000", + "notionalFloor": "5000000", "maintMarginRatio": "0.125", - "cum": "108042.5" + "cum": "183045.0" } }, { "tier": 8.0, "currency": "USDT", - "minNotional": 5000000.0, - "maxNotional": 10000000.0, + "minNotional": 10000000.0, + "maxNotional": 20000000.0, "maintenanceMarginRate": 0.15, "maxLeverage": 3.0, "info": { "bracket": "8", "initialLeverage": "3", - "notionalCap": "10000000", - "notionalFloor": "5000000", + "notionalCap": "20000000", + "notionalFloor": "10000000", "maintMarginRatio": "0.15", - "cum": "233042.5" + "cum": "433045.0" } }, { "tier": 9.0, "currency": "USDT", - "minNotional": 10000000.0, - "maxNotional": 20000000.0, + "minNotional": 20000000.0, + "maxNotional": 30000000.0, "maintenanceMarginRate": 0.25, "maxLeverage": 2.0, "info": { "bracket": "9", "initialLeverage": "2", - "notionalCap": "20000000", - "notionalFloor": "10000000", + "notionalCap": "30000000", + "notionalFloor": "20000000", "maintMarginRatio": "0.25", - "cum": "1233042.5" + "cum": "2433045.0" } }, { "tier": 10.0, "currency": "USDT", - "minNotional": 20000000.0, + "minNotional": 30000000.0, "maxNotional": 50000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, @@ -9411,9 +9767,9 @@ "bracket": "10", "initialLeverage": "1", "notionalCap": "50000000", - "notionalFloor": "20000000", + "notionalFloor": "30000000", "maintMarginRatio": "0.5", - "cum": "6233042.5" + "cum": "9933045.0" } } ], @@ -9855,6 +10211,120 @@ } } ], + "ETH/USDT:USDT-230630": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 375000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, + "info": { + "bracket": "1", + "initialLeverage": "25", + "notionalCap": "375000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 375000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "2", + "initialLeverage": "10", + "notionalCap": "2000000", + "notionalFloor": "375000", + "maintMarginRatio": "0.05", + "cum": "11250.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 2000000.0, + "maxNotional": 4000000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "3", + "initialLeverage": "5", + "notionalCap": "4000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.1", + "cum": "111250.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 4000000.0, + "maxNotional": 10000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "4", + "initialLeverage": "4", + "notionalCap": "10000000", + "notionalFloor": "4000000", + "maintMarginRatio": "0.125", + "cum": "211250.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 10000000.0, + "maxNotional": 20000000.0, + "maintenanceMarginRate": 0.15, + "maxLeverage": 3.0, + "info": { + "bracket": "5", + "initialLeverage": "3", + "notionalCap": "20000000", + "notionalFloor": "10000000", + "maintMarginRatio": "0.15", + "cum": "461250.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 20000000.0, + "maxNotional": 40000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "40000000", + "notionalFloor": "20000000", + "maintMarginRatio": "0.25", + "cum": "2461250.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 40000000.0, + "maxNotional": 400000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "400000000", + "notionalFloor": "40000000", + "maintMarginRatio": "0.5", + "cum": "1.246125E7" + } + } + ], "FET/USDT:USDT": [ { "tier": 1.0, @@ -11036,10 +11506,10 @@ "minNotional": 0.0, "maxNotional": 5000.0, "maintenanceMarginRate": 0.02, - "maxLeverage": 20.0, + "maxLeverage": 10.0, "info": { "bracket": "1", - "initialLeverage": "20", + "initialLeverage": "10", "notionalCap": "5000", "notionalFloor": "0", "maintMarginRatio": "0.02", @@ -11052,10 +11522,10 @@ "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, + "maxLeverage": 8.0, "info": { "bracket": "2", - "initialLeverage": "10", + "initialLeverage": "8", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", @@ -11068,10 +11538,10 @@ "minNotional": 25000.0, "maxNotional": 100000.0, "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, + "maxLeverage": 6.0, "info": { "bracket": "3", - "initialLeverage": "8", + "initialLeverage": "6", "notionalCap": "100000", "notionalFloor": "25000", "maintMarginRatio": "0.05", @@ -11098,13 +11568,13 @@ "tier": 5.0, "currency": "BUSD", "minNotional": 250000.0, - "maxNotional": 1000000.0, + "maxNotional": 500000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 2.0, "info": { "bracket": "5", "initialLeverage": "2", - "notionalCap": "1000000", + "notionalCap": "500000", "notionalFloor": "250000", "maintMarginRatio": "0.125", "cum": "11900.0" @@ -11113,17 +11583,17 @@ { "tier": 6.0, "currency": "BUSD", - "minNotional": 1000000.0, - "maxNotional": 3000000.0, + "minNotional": 500000.0, + "maxNotional": 1000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { "bracket": "6", "initialLeverage": "1", - "notionalCap": "3000000", - "notionalFloor": "1000000", + "notionalCap": "1000000", + "notionalFloor": "500000", "maintMarginRatio": "0.5", - "cum": "386900.0" + "cum": "199400.0" } } ], @@ -11329,14 +11799,14 @@ "currency": "USDT", "minNotional": 0.0, "maxNotional": 5000.0, - "maintenanceMarginRate": 0.0065, + "maintenanceMarginRate": 0.006, "maxLeverage": 50.0, "info": { "bracket": "1", "initialLeverage": "50", "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.0065", + "maintMarginRatio": "0.006", "cum": "0.0" } }, @@ -11353,103 +11823,103 @@ "notionalCap": "50000", "notionalFloor": "5000", "maintMarginRatio": "0.01", - "cum": "17.5" + "cum": "20.0" } }, { "tier": 3.0, "currency": "USDT", "minNotional": 50000.0, - "maxNotional": 200000.0, + "maxNotional": 900000.0, "maintenanceMarginRate": 0.025, "maxLeverage": 20.0, "info": { "bracket": "3", "initialLeverage": "20", - "notionalCap": "200000", + "notionalCap": "900000", "notionalFloor": "50000", "maintMarginRatio": "0.025", - "cum": "767.5" + "cum": "770.0" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 200000.0, - "maxNotional": 400000.0, + "minNotional": 900000.0, + "maxNotional": 1800000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "4", "initialLeverage": "10", - "notionalCap": "400000", - "notionalFloor": "200000", + "notionalCap": "1800000", + "notionalFloor": "900000", "maintMarginRatio": "0.05", - "cum": "5767.5" + "cum": "23270.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 400000.0, - "maxNotional": 1000000.0, + "minNotional": 1800000.0, + "maxNotional": 4800000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "5", "initialLeverage": "5", - "notionalCap": "1000000", - "notionalFloor": "400000", + "notionalCap": "4800000", + "notionalFloor": "1800000", "maintMarginRatio": "0.1", - "cum": "25767.5" + "cum": "113270.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, + "minNotional": 4800000.0, + "maxNotional": 6000000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 4.0, "info": { "bracket": "6", "initialLeverage": "4", - "notionalCap": "5000000", - "notionalFloor": "1000000", + "notionalCap": "6000000", + "notionalFloor": "4800000", "maintMarginRatio": "0.125", - "cum": "50767.5" + "cum": "233270.0" } }, { "tier": 7.0, "currency": "USDT", - "minNotional": 5000000.0, - "maxNotional": 6000000.0, + "minNotional": 6000000.0, + "maxNotional": 18000000.0, "maintenanceMarginRate": 0.25, "maxLeverage": 2.0, "info": { "bracket": "7", "initialLeverage": "2", - "notionalCap": "6000000", - "notionalFloor": "5000000", + "notionalCap": "18000000", + "notionalFloor": "6000000", "maintMarginRatio": "0.25", - "cum": "675767.5" + "cum": "983270.0" } }, { "tier": 8.0, "currency": "USDT", - "minNotional": 6000000.0, - "maxNotional": 10000000.0, + "minNotional": 18000000.0, + "maxNotional": 30000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { "bracket": "8", "initialLeverage": "1", - "notionalCap": "10000000", - "notionalFloor": "6000000", + "notionalCap": "30000000", + "notionalFloor": "18000000", "maintMarginRatio": "0.5", - "cum": "2175767.5" + "cum": "5483270.0" } } ], @@ -11672,10 +12142,10 @@ "minNotional": 0.0, "maxNotional": 5000.0, "maintenanceMarginRate": 0.02, - "maxLeverage": 20.0, + "maxLeverage": 25.0, "info": { "bracket": "1", - "initialLeverage": "20", + "initialLeverage": "25", "notionalCap": "5000", "notionalFloor": "0", "maintMarginRatio": "0.02", @@ -11688,10 +12158,10 @@ "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 15.0, + "maxLeverage": 20.0, "info": { "bracket": "2", - "initialLeverage": "15", + "initialLeverage": "20", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", @@ -11702,13 +12172,13 @@ "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, + "maxNotional": 480000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "3", "initialLeverage": "10", - "notionalCap": "100000", + "notionalCap": "480000", "notionalFloor": "25000", "maintMarginRatio": "0.05", "cum": "650.0" @@ -11717,49 +12187,65 @@ { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, + "minNotional": 480000.0, + "maxNotional": 1280000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", + "notionalCap": "1280000", + "notionalFloor": "480000", "maintMarginRatio": "0.1", - "cum": "5650.0" + "cum": "24650.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, + "minNotional": 1280000.0, + "maxNotional": 1600000.0, "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maxLeverage": 4.0, "info": { "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", + "initialLeverage": "4", + "notionalCap": "1600000", + "notionalFloor": "1280000", "maintMarginRatio": "0.125", - "cum": "11900.0" + "cum": "56650.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, + "minNotional": 1600000.0, + "maxNotional": 4800000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "4800000", + "notionalFloor": "1600000", + "maintMarginRatio": "0.25", + "cum": "256650.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 4800000.0, + "maxNotional": 8000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", + "notionalCap": "8000000", + "notionalFloor": "4800000", "maintMarginRatio": "0.5", - "cum": "386900.0" + "cum": "1456650.0" } } ], @@ -12194,10 +12680,10 @@ "minNotional": 0.0, "maxNotional": 5000.0, "maintenanceMarginRate": 0.02, - "maxLeverage": 15.0, + "maxLeverage": 10.0, "info": { "bracket": "1", - "initialLeverage": "15", + "initialLeverage": "10", "notionalCap": "5000", "notionalFloor": "0", "maintMarginRatio": "0.02", @@ -12210,10 +12696,10 @@ "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, + "maxLeverage": 8.0, "info": { "bracket": "2", - "initialLeverage": "10", + "initialLeverage": "8", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", @@ -12224,13 +12710,13 @@ "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 120000.0, + "maxNotional": 300000.0, "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, + "maxLeverage": 6.0, "info": { "bracket": "3", - "initialLeverage": "8", - "notionalCap": "120000", + "initialLeverage": "6", + "notionalCap": "300000", "notionalFloor": "25000", "maintMarginRatio": "0.05", "cum": "650.0" @@ -12239,49 +12725,65 @@ { "tier": 4.0, "currency": "USDT", - "minNotional": 120000.0, - "maxNotional": 300000.0, + "minNotional": 300000.0, + "maxNotional": 800000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "300000", - "notionalFloor": "120000", + "notionalCap": "800000", + "notionalFloor": "300000", "maintMarginRatio": "0.1", - "cum": "6650.0" + "cum": "15650.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 300000.0, + "minNotional": 800000.0, "maxNotional": 1000000.0, "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maxLeverage": 4.0, "info": { "bracket": "5", - "initialLeverage": "2", + "initialLeverage": "4", "notionalCap": "1000000", - "notionalFloor": "300000", + "notionalFloor": "800000", "maintMarginRatio": "0.125", - "cum": "14150.0" + "cum": "35650.0" } }, { "tier": 6.0, "currency": "USDT", "minNotional": 1000000.0, - "maxNotional": 3000000.0, + "maxNotional": 1500000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "1500000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "160650.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 1500000.0, + "maxNotional": 2000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "1", - "notionalCap": "3000000", - "notionalFloor": "1000000", + "notionalCap": "2000000", + "notionalFloor": "1500000", "maintMarginRatio": "0.5", - "cum": "389150.0" + "cum": "535650.0" } } ], @@ -12481,104 +12983,6 @@ } } ], - "ICP/BUSD:BUSD": [ - { - "tier": 1.0, - "currency": "BUSD", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.02, - "maxLeverage": 20.0, - "info": { - "bracket": "1", - "initialLeverage": "20", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.02", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "BUSD", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, - "info": { - "bracket": "2", - "initialLeverage": "10", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "25.0" - } - }, - { - "tier": 3.0, - "currency": "BUSD", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, - "info": { - "bracket": "3", - "initialLeverage": "8", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "650.0" - } - }, - { - "tier": 4.0, - "currency": "BUSD", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5650.0" - } - }, - { - "tier": 5.0, - "currency": "BUSD", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11900.0" - } - }, - { - "tier": 6.0, - "currency": "BUSD", - "minNotional": 1000000.0, - "maxNotional": 3000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "3000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386900.0" - } - } - ], "ICP/USDT:USDT": [ { "tier": 1.0, @@ -12586,10 +12990,10 @@ "minNotional": 0.0, "maxNotional": 5000.0, "maintenanceMarginRate": 0.01, - "maxLeverage": 20.0, + "maxLeverage": 25.0, "info": { "bracket": "1", - "initialLeverage": "20", + "initialLeverage": "25", "notionalCap": "5000", "notionalFloor": "0", "maintMarginRatio": "0.01", @@ -12602,10 +13006,10 @@ "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, + "maxLeverage": 20.0, "info": { "bracket": "2", - "initialLeverage": "10", + "initialLeverage": "20", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", @@ -12616,13 +13020,13 @@ "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, + "maxNotional": 600000.0, "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, + "maxLeverage": 10.0, "info": { "bracket": "3", - "initialLeverage": "8", - "notionalCap": "100000", + "initialLeverage": "10", + "notionalCap": "600000", "notionalFloor": "25000", "maintMarginRatio": "0.05", "cum": "700.0" @@ -12631,49 +13035,65 @@ { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, + "minNotional": 600000.0, + "maxNotional": 1600000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", + "notionalCap": "1600000", + "notionalFloor": "600000", "maintMarginRatio": "0.1", - "cum": "5700.0" + "cum": "30700.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, + "minNotional": 1600000.0, + "maxNotional": 2000000.0, "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maxLeverage": 4.0, "info": { "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", + "initialLeverage": "4", + "notionalCap": "2000000", + "notionalFloor": "1600000", "maintMarginRatio": "0.125", - "cum": "11950.0" + "cum": "70700.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 30000000.0, + "minNotional": 2000000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "6000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.25", + "cum": "320700.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 6000000.0, + "maxNotional": 10000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "1", - "notionalCap": "30000000", - "notionalFloor": "1000000", + "notionalCap": "10000000", + "notionalFloor": "6000000", "maintMarginRatio": "0.5", - "cum": "386950.0" + "cum": "1820700.0" } } ], @@ -12775,105 +13195,7 @@ } } ], - "IMX/USDT:USDT": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 20.0, - "info": { - "bracket": "1", - "initialLeverage": "20", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.01", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, - "info": { - "bracket": "2", - "initialLeverage": "10", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "75.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, - "info": { - "bracket": "3", - "initialLeverage": "8", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "700.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5700.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11950.0" - } - }, - { - "tier": 6.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386950.0" - } - } - ], - "INJ/USDT:USDT": [ + "ID/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", @@ -12896,10 +13218,10 @@ "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, + "maxLeverage": 15.0, "info": { "bracket": "2", - "initialLeverage": "10", + "initialLeverage": "15", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", @@ -12910,13 +13232,13 @@ "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, + "maxNotional": 200000.0, "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, + "maxLeverage": 10.0, "info": { "bracket": "3", - "initialLeverage": "8", - "notionalCap": "100000", + "initialLeverage": "10", + "notionalCap": "200000", "notionalFloor": "25000", "maintMarginRatio": "0.05", "cum": "650.0" @@ -12925,33 +13247,33 @@ { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, + "minNotional": 200000.0, + "maxNotional": 500000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", + "notionalCap": "500000", + "notionalFloor": "200000", "maintMarginRatio": "0.1", - "cum": "5650.0" + "cum": "10650.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, + "minNotional": 500000.0, "maxNotional": 1000000.0, "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maxLeverage": 4.0, "info": { "bracket": "5", - "initialLeverage": "2", + "initialLeverage": "4", "notionalCap": "1000000", - "notionalFloor": "250000", + "notionalFloor": "500000", "maintMarginRatio": "0.125", - "cum": "11900.0" + "cum": "23150.0" } }, { @@ -12959,15 +13281,259 @@ "currency": "USDT", "minNotional": 1000000.0, "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "148150.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, + "maxNotional": 5000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "1", - "notionalCap": "3000000", - "notionalFloor": "1000000", + "notionalCap": "5000000", + "notionalFloor": "3000000", "maintMarginRatio": "0.5", - "cum": "386900.0" + "cum": "898150.0" + } + } + ], + "IMX/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 25.0, + "info": { + "bracket": "1", + "initialLeverage": "25", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.01", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "2", + "initialLeverage": "20", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "75.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 600000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "600000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "700.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 600000.0, + "maxNotional": 1600000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "1600000", + "notionalFloor": "600000", + "maintMarginRatio": "0.1", + "cum": "30700.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 1600000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "2000000", + "notionalFloor": "1600000", + "maintMarginRatio": "0.125", + "cum": "70700.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 2000000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "6000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.25", + "cum": "320700.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 6000000.0, + "maxNotional": 10000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "10000000", + "notionalFloor": "6000000", + "maintMarginRatio": "0.5", + "cum": "1820700.0" + } + } + ], + "INJ/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, + "info": { + "bracket": "1", + "initialLeverage": "25", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "2", + "initialLeverage": "20", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 600000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "600000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 600000.0, + "maxNotional": 1600000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "1600000", + "notionalFloor": "600000", + "maintMarginRatio": "0.1", + "cum": "30650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 1600000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "2000000", + "notionalFloor": "1600000", + "maintMarginRatio": "0.125", + "cum": "70650.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 2000000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "6000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.25", + "cum": "320650.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 6000000.0, + "maxNotional": 10000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "10000000", + "notionalFloor": "6000000", + "maintMarginRatio": "0.5", + "cum": "1820650.0" } } ], @@ -14136,10 +14702,10 @@ "minNotional": 0.0, "maxNotional": 5000.0, "maintenanceMarginRate": 0.02, - "maxLeverage": 20.0, + "maxLeverage": 25.0, "info": { "bracket": "1", - "initialLeverage": "20", + "initialLeverage": "25", "notionalCap": "5000", "notionalFloor": "0", "maintMarginRatio": "0.02", @@ -14152,10 +14718,10 @@ "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, + "maxLeverage": 20.0, "info": { "bracket": "2", - "initialLeverage": "10", + "initialLeverage": "20", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", @@ -14166,13 +14732,13 @@ "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, + "maxNotional": 900000.0, "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, + "maxLeverage": 10.0, "info": { "bracket": "3", - "initialLeverage": "8", - "notionalCap": "100000", + "initialLeverage": "10", + "notionalCap": "900000", "notionalFloor": "25000", "maintMarginRatio": "0.05", "cum": "650.0" @@ -14181,49 +14747,65 @@ { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, + "minNotional": 900000.0, + "maxNotional": 2400000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", + "notionalCap": "2400000", + "notionalFloor": "900000", "maintMarginRatio": "0.1", - "cum": "5650.0" + "cum": "45650.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, + "minNotional": 2400000.0, + "maxNotional": 3000000.0, "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maxLeverage": 4.0, "info": { "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", + "initialLeverage": "4", + "notionalCap": "3000000", + "notionalFloor": "2400000", "maintMarginRatio": "0.125", - "cum": "11900.0" + "cum": "105650.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 3000000.0, + "minNotional": 3000000.0, + "maxNotional": 9000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "9000000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.25", + "cum": "480650.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 9000000.0, + "maxNotional": 15000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "1", - "notionalCap": "3000000", - "notionalFloor": "1000000", + "notionalCap": "15000000", + "notionalFloor": "9000000", "maintMarginRatio": "0.5", - "cum": "386900.0" + "cum": "2730650.0" } } ], @@ -14363,15 +14945,15 @@ "currency": "USDT", "minNotional": 5000.0, "maxNotional": 10000.0, - "maintenanceMarginRate": 0.0065, + "maintenanceMarginRate": 0.006, "maxLeverage": 50.0, "info": { "bracket": "2", "initialLeverage": "50", "notionalCap": "10000", "notionalFloor": "5000", - "maintMarginRatio": "0.0065", - "cum": "7.5" + "maintMarginRatio": "0.006", + "cum": "5.0" } }, { @@ -14387,7 +14969,7 @@ "notionalCap": "50000", "notionalFloor": "10000", "maintMarginRatio": "0.01", - "cum": "42.5" + "cum": "45.0" } }, { @@ -14403,7 +14985,7 @@ "notionalCap": "250000", "notionalFloor": "50000", "maintMarginRatio": "0.02", - "cum": "542.5" + "cum": "545.0" } }, { @@ -14419,77 +15001,77 @@ "notionalCap": "1000000", "notionalFloor": "250000", "maintMarginRatio": "0.05", - "cum": "8042.5" + "cum": "8045.0" } }, { "tier": 6.0, "currency": "USDT", "minNotional": 1000000.0, - "maxNotional": 2000000.0, + "maxNotional": 5000000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "6", "initialLeverage": "5", - "notionalCap": "2000000", + "notionalCap": "5000000", "notionalFloor": "1000000", "maintMarginRatio": "0.1", - "cum": "58042.5" + "cum": "58045.0" } }, { "tier": 7.0, "currency": "USDT", - "minNotional": 2000000.0, - "maxNotional": 5000000.0, + "minNotional": 5000000.0, + "maxNotional": 10000000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 4.0, "info": { "bracket": "7", "initialLeverage": "4", - "notionalCap": "5000000", - "notionalFloor": "2000000", + "notionalCap": "10000000", + "notionalFloor": "5000000", "maintMarginRatio": "0.125", - "cum": "108042.5" + "cum": "183045.0" } }, { "tier": 8.0, "currency": "USDT", - "minNotional": 5000000.0, - "maxNotional": 10000000.0, + "minNotional": 10000000.0, + "maxNotional": 20000000.0, "maintenanceMarginRate": 0.15, "maxLeverage": 3.0, "info": { "bracket": "8", "initialLeverage": "3", - "notionalCap": "10000000", - "notionalFloor": "5000000", + "notionalCap": "20000000", + "notionalFloor": "10000000", "maintMarginRatio": "0.15", - "cum": "233042.5" + "cum": "433045.0" } }, { "tier": 9.0, "currency": "USDT", - "minNotional": 10000000.0, - "maxNotional": 20000000.0, + "minNotional": 20000000.0, + "maxNotional": 30000000.0, "maintenanceMarginRate": 0.25, "maxLeverage": 2.0, "info": { "bracket": "9", "initialLeverage": "2", - "notionalCap": "20000000", - "notionalFloor": "10000000", + "notionalCap": "30000000", + "notionalFloor": "20000000", "maintMarginRatio": "0.25", - "cum": "1233042.5" + "cum": "2433045.0" } }, { "tier": 10.0, "currency": "USDT", - "minNotional": 20000000.0, + "minNotional": 30000000.0, "maxNotional": 50000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, @@ -14497,9 +15079,9 @@ "bracket": "10", "initialLeverage": "1", "notionalCap": "50000000", - "notionalFloor": "20000000", + "notionalFloor": "30000000", "maintMarginRatio": "0.5", - "cum": "6233042.5" + "cum": "9933045.0" } } ], @@ -15031,15 +15613,15 @@ "currency": "USDT", "minNotional": 5000.0, "maxNotional": 10000.0, - "maintenanceMarginRate": 0.0065, + "maintenanceMarginRate": 0.006, "maxLeverage": 50.0, "info": { "bracket": "2", "initialLeverage": "50", "notionalCap": "10000", "notionalFloor": "5000", - "maintMarginRatio": "0.0065", - "cum": "7.5" + "maintMarginRatio": "0.006", + "cum": "5.0" } }, { @@ -15055,7 +15637,7 @@ "notionalCap": "50000", "notionalFloor": "10000", "maintMarginRatio": "0.01", - "cum": "42.5" + "cum": "45.0" } }, { @@ -15071,7 +15653,7 @@ "notionalCap": "250000", "notionalFloor": "50000", "maintMarginRatio": "0.02", - "cum": "542.5" + "cum": "545.0" } }, { @@ -15087,77 +15669,77 @@ "notionalCap": "1000000", "notionalFloor": "250000", "maintMarginRatio": "0.05", - "cum": "8042.5" + "cum": "8045.0" } }, { "tier": 6.0, "currency": "USDT", "minNotional": 1000000.0, - "maxNotional": 2000000.0, + "maxNotional": 5000000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "6", "initialLeverage": "5", - "notionalCap": "2000000", + "notionalCap": "5000000", "notionalFloor": "1000000", "maintMarginRatio": "0.1", - "cum": "58042.5" + "cum": "58045.0" } }, { "tier": 7.0, "currency": "USDT", - "minNotional": 2000000.0, - "maxNotional": 5000000.0, + "minNotional": 5000000.0, + "maxNotional": 10000000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 4.0, "info": { "bracket": "7", "initialLeverage": "4", - "notionalCap": "5000000", - "notionalFloor": "2000000", + "notionalCap": "10000000", + "notionalFloor": "5000000", "maintMarginRatio": "0.125", - "cum": "108042.5" + "cum": "183045.0" } }, { "tier": 8.0, "currency": "USDT", - "minNotional": 5000000.0, - "maxNotional": 10000000.0, + "minNotional": 10000000.0, + "maxNotional": 20000000.0, "maintenanceMarginRate": 0.15, "maxLeverage": 3.0, "info": { "bracket": "8", "initialLeverage": "3", - "notionalCap": "10000000", - "notionalFloor": "5000000", + "notionalCap": "20000000", + "notionalFloor": "10000000", "maintMarginRatio": "0.15", - "cum": "233042.5" + "cum": "433045.0" } }, { "tier": 9.0, "currency": "USDT", - "minNotional": 10000000.0, - "maxNotional": 20000000.0, + "minNotional": 20000000.0, + "maxNotional": 30000000.0, "maintenanceMarginRate": 0.25, "maxLeverage": 2.0, "info": { "bracket": "9", "initialLeverage": "2", - "notionalCap": "20000000", - "notionalFloor": "10000000", + "notionalCap": "30000000", + "notionalFloor": "20000000", "maintMarginRatio": "0.25", - "cum": "1233042.5" + "cum": "2433045.0" } }, { "tier": 10.0, "currency": "USDT", - "minNotional": 20000000.0, + "minNotional": 30000000.0, "maxNotional": 50000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, @@ -15165,107 +15747,9 @@ "bracket": "10", "initialLeverage": "1", "notionalCap": "50000000", - "notionalFloor": "20000000", + "notionalFloor": "30000000", "maintMarginRatio": "0.5", - "cum": "6233042.5" - } - } - ], - "LUNA2/BUSD:BUSD": [ - { - "tier": 1.0, - "currency": "BUSD", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.02, - "maxLeverage": 20.0, - "info": { - "bracket": "1", - "initialLeverage": "20", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.02", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "BUSD", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, - "info": { - "bracket": "2", - "initialLeverage": "10", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "25.0" - } - }, - { - "tier": 3.0, - "currency": "BUSD", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, - "info": { - "bracket": "3", - "initialLeverage": "8", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "650.0" - } - }, - { - "tier": 4.0, - "currency": "BUSD", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5650.0" - } - }, - { - "tier": 5.0, - "currency": "BUSD", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11900.0" - } - }, - { - "tier": 6.0, - "currency": "BUSD", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386900.0" + "cum": "9933045.0" } } ], @@ -15617,14 +16101,14 @@ "currency": "USDT", "minNotional": 0.0, "maxNotional": 5000.0, - "maintenanceMarginRate": 0.02, - "maxLeverage": 25.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 50.0, "info": { "bracket": "1", - "initialLeverage": "25", + "initialLeverage": "50", "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.02", + "maintMarginRatio": "0.01", "cum": "0.0" } }, @@ -15633,95 +16117,111 @@ "currency": "USDT", "minNotional": 5000.0, "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, "info": { "bracket": "2", - "initialLeverage": "20", + "initialLeverage": "25", "notionalCap": "25000", "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "25.0" + "maintMarginRatio": "0.02", + "cum": "50.0" } }, { "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 600000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, + "maxNotional": 900000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, "info": { "bracket": "3", - "initialLeverage": "10", - "notionalCap": "600000", + "initialLeverage": "20", + "notionalCap": "900000", "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "650.0" + "maintMarginRatio": "0.025", + "cum": "175.0" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 600000.0, - "maxNotional": 1600000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, + "minNotional": 900000.0, + "maxNotional": 1800000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, "info": { "bracket": "4", - "initialLeverage": "5", - "notionalCap": "1600000", - "notionalFloor": "600000", - "maintMarginRatio": "0.1", - "cum": "30650.0" + "initialLeverage": "10", + "notionalCap": "1800000", + "notionalFloor": "900000", + "maintMarginRatio": "0.05", + "cum": "22675.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 1600000.0, - "maxNotional": 3000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 4.0, + "minNotional": 1800000.0, + "maxNotional": 4800000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, "info": { "bracket": "5", - "initialLeverage": "4", - "notionalCap": "3000000", - "notionalFloor": "1600000", - "maintMarginRatio": "0.125", - "cum": "70650.0" + "initialLeverage": "5", + "notionalCap": "4800000", + "notionalFloor": "1800000", + "maintMarginRatio": "0.1", + "cum": "112675.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 3000000.0, + "minNotional": 4800000.0, "maxNotional": 6000000.0, - "maintenanceMarginRate": 0.25, - "maxLeverage": 2.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, "info": { "bracket": "6", - "initialLeverage": "2", + "initialLeverage": "4", "notionalCap": "6000000", - "notionalFloor": "3000000", - "maintMarginRatio": "0.25", - "cum": "445650.0" + "notionalFloor": "4800000", + "maintMarginRatio": "0.125", + "cum": "232675.0" } }, { "tier": 7.0, "currency": "USDT", "minNotional": 6000000.0, - "maxNotional": 10000000.0, + "maxNotional": 18000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "7", + "initialLeverage": "2", + "notionalCap": "18000000", + "notionalFloor": "6000000", + "maintMarginRatio": "0.25", + "cum": "982675.0" + } + }, + { + "tier": 8.0, + "currency": "USDT", + "minNotional": 18000000.0, + "maxNotional": 30000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "7", + "bracket": "8", "initialLeverage": "1", - "notionalCap": "10000000", - "notionalFloor": "6000000", + "notionalCap": "30000000", + "notionalFloor": "18000000", "maintMarginRatio": "0.5", - "cum": "1945650.0" + "cum": "5482675.0" } } ], @@ -15876,13 +16376,13 @@ "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 50000.0, + "maxNotional": 600000.0, "maintenanceMarginRate": 0.01, "maxLeverage": 25.0, "info": { "bracket": "3", "initialLeverage": "25", - "notionalCap": "50000", + "notionalCap": "600000", "notionalFloor": "25000", "maintMarginRatio": "0.01", "cum": "80.0" @@ -15891,97 +16391,97 @@ { "tier": 4.0, "currency": "USDT", - "minNotional": 50000.0, - "maxNotional": 400000.0, + "minNotional": 600000.0, + "maxNotional": 900000.0, "maintenanceMarginRate": 0.025, "maxLeverage": 20.0, "info": { "bracket": "4", "initialLeverage": "20", - "notionalCap": "400000", - "notionalFloor": "50000", + "notionalCap": "900000", + "notionalFloor": "600000", "maintMarginRatio": "0.025", - "cum": "830.0" + "cum": "9080.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 400000.0, - "maxNotional": 800000.0, + "minNotional": 900000.0, + "maxNotional": 1800000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "5", "initialLeverage": "10", - "notionalCap": "800000", - "notionalFloor": "400000", + "notionalCap": "1800000", + "notionalFloor": "900000", "maintMarginRatio": "0.05", - "cum": "10830.0" + "cum": "31580.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 800000.0, - "maxNotional": 2000000.0, + "minNotional": 1800000.0, + "maxNotional": 4800000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "6", "initialLeverage": "5", - "notionalCap": "2000000", - "notionalFloor": "800000", + "notionalCap": "4800000", + "notionalFloor": "1800000", "maintMarginRatio": "0.1", - "cum": "50830.0" + "cum": "121580.0" } }, { "tier": 7.0, "currency": "USDT", - "minNotional": 2000000.0, - "maxNotional": 5000000.0, + "minNotional": 4800000.0, + "maxNotional": 6000000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 4.0, "info": { "bracket": "7", "initialLeverage": "4", - "notionalCap": "5000000", - "notionalFloor": "2000000", + "notionalCap": "6000000", + "notionalFloor": "4800000", "maintMarginRatio": "0.125", - "cum": "100830.0" + "cum": "241580.0" } }, { "tier": 8.0, "currency": "USDT", - "minNotional": 5000000.0, - "maxNotional": 12000000.0, + "minNotional": 6000000.0, + "maxNotional": 18000000.0, "maintenanceMarginRate": 0.25, "maxLeverage": 2.0, "info": { "bracket": "8", "initialLeverage": "2", - "notionalCap": "12000000", - "notionalFloor": "5000000", + "notionalCap": "18000000", + "notionalFloor": "6000000", "maintMarginRatio": "0.25", - "cum": "725830.0" + "cum": "991580.0" } }, { "tier": 9.0, "currency": "USDT", - "minNotional": 12000000.0, - "maxNotional": 20000000.0, + "minNotional": 18000000.0, + "maxNotional": 30000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { "bracket": "9", "initialLeverage": "1", - "notionalCap": "20000000", - "notionalFloor": "12000000", + "notionalCap": "30000000", + "notionalFloor": "18000000", "maintMarginRatio": "0.5", - "cum": "3725830.0" + "cum": "5491580.0" } } ], @@ -16105,14 +16605,14 @@ "currency": "USDT", "minNotional": 0.0, "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 25.0, + "maintenanceMarginRate": 0.006, + "maxLeverage": 50.0, "info": { "bracket": "1", - "initialLeverage": "25", + "initialLeverage": "50", "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.01", + "maintMarginRatio": "0.006", "cum": "0.0" } }, @@ -16121,79 +16621,111 @@ "currency": "USDT", "minNotional": 5000.0, "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 25.0, "info": { "bracket": "2", - "initialLeverage": "20", + "initialLeverage": "25", "notionalCap": "25000", "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "75.0" + "maintMarginRatio": "0.01", + "cum": "20.0" } }, { "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, + "maxNotional": 450000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, "info": { "bracket": "3", - "initialLeverage": "10", - "notionalCap": "100000", + "initialLeverage": "20", + "notionalCap": "450000", "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "700.0" + "maintMarginRatio": "0.025", + "cum": "395.0" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, + "minNotional": 450000.0, + "maxNotional": 900000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, "info": { "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5700.0" + "initialLeverage": "10", + "notionalCap": "900000", + "notionalFloor": "450000", + "maintMarginRatio": "0.05", + "cum": "11645.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "minNotional": 900000.0, + "maxNotional": 2400000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, "info": { "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11950.0" + "initialLeverage": "5", + "notionalCap": "2400000", + "notionalFloor": "900000", + "maintMarginRatio": "0.1", + "cum": "56645.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, + "minNotional": 2400000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "6", + "initialLeverage": "4", + "notionalCap": "3000000", + "notionalFloor": "2400000", + "maintMarginRatio": "0.125", + "cum": "116645.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, + "maxNotional": 9000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "7", + "initialLeverage": "2", + "notionalCap": "9000000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.25", + "cum": "491645.0" + } + }, + { + "tier": 8.0, + "currency": "USDT", + "minNotional": 9000000.0, + "maxNotional": 15000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "8", "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", + "notionalCap": "15000000", + "notionalFloor": "9000000", "maintMarginRatio": "0.5", - "cum": "386950.0" + "cum": "2741645.0" } } ], @@ -16204,10 +16736,10 @@ "minNotional": 0.0, "maxNotional": 5000.0, "maintenanceMarginRate": 0.02, - "maxLeverage": 20.0, + "maxLeverage": 25.0, "info": { "bracket": "1", - "initialLeverage": "20", + "initialLeverage": "25", "notionalCap": "5000", "notionalFloor": "0", "maintMarginRatio": "0.02", @@ -16220,10 +16752,10 @@ "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, + "maxLeverage": 20.0, "info": { "bracket": "2", - "initialLeverage": "10", + "initialLeverage": "20", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", @@ -16234,13 +16766,13 @@ "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, + "maxNotional": 600000.0, "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, + "maxLeverage": 10.0, "info": { "bracket": "3", - "initialLeverage": "8", - "notionalCap": "100000", + "initialLeverage": "10", + "notionalCap": "600000", "notionalFloor": "25000", "maintMarginRatio": "0.05", "cum": "650.0" @@ -16249,49 +16781,65 @@ { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, + "minNotional": 600000.0, + "maxNotional": 1600000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", + "notionalCap": "1600000", + "notionalFloor": "600000", "maintMarginRatio": "0.1", - "cum": "5650.0" + "cum": "30650.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, + "minNotional": 1600000.0, + "maxNotional": 2000000.0, "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maxLeverage": 4.0, "info": { "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", + "initialLeverage": "4", + "notionalCap": "2000000", + "notionalFloor": "1600000", "maintMarginRatio": "0.125", - "cum": "11900.0" + "cum": "70650.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 3000000.0, + "minNotional": 2000000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "6000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.25", + "cum": "320650.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 6000000.0, + "maxNotional": 10000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "1", - "notionalCap": "3000000", - "notionalFloor": "1000000", + "notionalCap": "10000000", + "notionalFloor": "6000000", "maintMarginRatio": "0.5", - "cum": "386900.0" + "cum": "1820650.0" } } ], @@ -16968,13 +17516,13 @@ "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, + "maxNotional": 900000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 8.0, "info": { "bracket": "3", "initialLeverage": "8", - "notionalCap": "100000", + "notionalCap": "900000", "notionalFloor": "25000", "maintMarginRatio": "0.05", "cum": "650.0" @@ -16983,49 +17531,65 @@ { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, + "minNotional": 900000.0, + "maxNotional": 2400000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", + "notionalCap": "2400000", + "notionalFloor": "900000", "maintMarginRatio": "0.1", - "cum": "5650.0" + "cum": "45650.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, + "minNotional": 2400000.0, + "maxNotional": 3000000.0, "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maxLeverage": 4.0, "info": { "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", + "initialLeverage": "4", + "notionalCap": "3000000", + "notionalFloor": "2400000", "maintMarginRatio": "0.125", - "cum": "11900.0" + "cum": "105650.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 3000000.0, + "minNotional": 3000000.0, + "maxNotional": 9000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "9000000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.25", + "cum": "480650.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 9000000.0, + "maxNotional": 15000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "1", - "notionalCap": "3000000", - "notionalFloor": "1000000", + "notionalCap": "15000000", + "notionalFloor": "9000000", "maintMarginRatio": "0.5", - "cum": "386900.0" + "cum": "2730650.0" } } ], @@ -17164,13 +17728,13 @@ "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, + "maxNotional": 600000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "3", "initialLeverage": "10", - "notionalCap": "100000", + "notionalCap": "600000", "notionalFloor": "25000", "maintMarginRatio": "0.05", "cum": "700.0" @@ -17179,49 +17743,65 @@ { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, + "minNotional": 600000.0, + "maxNotional": 1600000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", + "notionalCap": "1600000", + "notionalFloor": "600000", "maintMarginRatio": "0.1", - "cum": "5700.0" + "cum": "30700.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, + "minNotional": 1600000.0, + "maxNotional": 2000000.0, "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maxLeverage": 4.0, "info": { "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", + "initialLeverage": "4", + "notionalCap": "2000000", + "notionalFloor": "1600000", "maintMarginRatio": "0.125", - "cum": "11950.0" + "cum": "70700.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, + "minNotional": 2000000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "6000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.25", + "cum": "320700.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 6000000.0, + "maxNotional": 10000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", + "notionalCap": "10000000", + "notionalFloor": "6000000", "maintMarginRatio": "0.5", - "cum": "386950.0" + "cum": "1820700.0" } } ], @@ -18208,13 +18788,13 @@ "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 200000.0, + "maxNotional": 600000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "3", "initialLeverage": "10", - "notionalCap": "200000", + "notionalCap": "600000", "notionalFloor": "25000", "maintMarginRatio": "0.05", "cum": "650.0" @@ -18223,65 +18803,65 @@ { "tier": 4.0, "currency": "USDT", - "minNotional": 200000.0, - "maxNotional": 500000.0, + "minNotional": 600000.0, + "maxNotional": 1600000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "500000", - "notionalFloor": "200000", + "notionalCap": "1600000", + "notionalFloor": "600000", "maintMarginRatio": "0.1", - "cum": "10650.0" + "cum": "30650.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 500000.0, - "maxNotional": 1000000.0, + "minNotional": 1600000.0, + "maxNotional": 2000000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 4.0, "info": { "bracket": "5", "initialLeverage": "4", - "notionalCap": "1000000", - "notionalFloor": "500000", + "notionalCap": "2000000", + "notionalFloor": "1600000", "maintMarginRatio": "0.125", - "cum": "23150.0" + "cum": "70650.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 3000000.0, + "minNotional": 2000000.0, + "maxNotional": 6000000.0, "maintenanceMarginRate": 0.25, "maxLeverage": 2.0, "info": { "bracket": "6", "initialLeverage": "2", - "notionalCap": "3000000", - "notionalFloor": "1000000", + "notionalCap": "6000000", + "notionalFloor": "2000000", "maintMarginRatio": "0.25", - "cum": "148150.0" + "cum": "320650.0" } }, { "tier": 7.0, "currency": "USDT", - "minNotional": 3000000.0, - "maxNotional": 5000000.0, + "minNotional": 6000000.0, + "maxNotional": 10000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { "bracket": "7", "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "3000000", + "notionalCap": "10000000", + "notionalFloor": "6000000", "maintMarginRatio": "0.5", - "cum": "898150.0" + "cum": "1820650.0" } } ], @@ -18290,80 +18870,112 @@ "tier": 1.0, "currency": "USDT", "minNotional": 0.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.03, - "maxLeverage": 20.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, "info": { "bracket": "1", - "initialLeverage": "20", - "notionalCap": "25000", + "initialLeverage": "25", + "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.03", + "maintMarginRatio": "0.02", "cum": "0.0" } }, { "tier": 2.0, "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, "info": { "bracket": "2", - "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "500.0" + "initialLeverage": "20", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" } }, { "tier": 3.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, + "minNotional": 25000.0, + "maxNotional": 600000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, "info": { "bracket": "3", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5500.0" + "initialLeverage": "10", + "notionalCap": "600000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "minNotional": 600000.0, + "maxNotional": 1600000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, "info": { "bracket": "4", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11750.0" + "initialLeverage": "5", + "notionalCap": "1600000", + "notionalFloor": "600000", + "maintMarginRatio": "0.1", + "cum": "30650.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 3000000.0, + "minNotional": 1600000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "2000000", + "notionalFloor": "1600000", + "maintMarginRatio": "0.125", + "cum": "70650.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 2000000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "6000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.25", + "cum": "320650.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 6000000.0, + "maxNotional": 10000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "5", + "bracket": "7", "initialLeverage": "1", - "notionalCap": "3000000", - "notionalFloor": "1000000", + "notionalCap": "10000000", + "notionalFloor": "6000000", "maintMarginRatio": "0.5", - "cum": "386750.0" + "cum": "1820650.0" } } ], @@ -19678,10 +20290,10 @@ "minNotional": 50000.0, "maxNotional": 150000.0, "maintenanceMarginRate": 0.02, - "maxLeverage": 20.0, + "maxLeverage": 25.0, "info": { "bracket": "2", - "initialLeverage": "20", + "initialLeverage": "25", "notionalCap": "150000", "notionalFloor": "50000", "maintMarginRatio": "0.02", @@ -19692,13 +20304,13 @@ "tier": 3.0, "currency": "USDT", "minNotional": 150000.0, - "maxNotional": 250000.0, + "maxNotional": 900000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 15.0, + "maxLeverage": 20.0, "info": { "bracket": "3", - "initialLeverage": "15", - "notionalCap": "250000", + "initialLeverage": "20", + "notionalCap": "900000", "notionalFloor": "150000", "maintMarginRatio": "0.025", "cum": "1250.0" @@ -19707,81 +20319,81 @@ { "tier": 4.0, "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, + "minNotional": 900000.0, + "maxNotional": 1800000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "4", "initialLeverage": "10", - "notionalCap": "1000000", - "notionalFloor": "250000", + "notionalCap": "1800000", + "notionalFloor": "900000", "maintMarginRatio": "0.05", - "cum": "7500.0" + "cum": "23750.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 2000000.0, + "minNotional": 1800000.0, + "maxNotional": 4800000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "5", "initialLeverage": "5", - "notionalCap": "2000000", - "notionalFloor": "1000000", + "notionalCap": "4800000", + "notionalFloor": "1800000", "maintMarginRatio": "0.1", - "cum": "57500.0" + "cum": "113750.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 2000000.0, - "maxNotional": 5000000.0, + "minNotional": 4800000.0, + "maxNotional": 6000000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 4.0, "info": { "bracket": "6", "initialLeverage": "4", - "notionalCap": "5000000", - "notionalFloor": "2000000", + "notionalCap": "6000000", + "notionalFloor": "4800000", "maintMarginRatio": "0.125", - "cum": "107500.0" + "cum": "233750.0" } }, { "tier": 7.0, "currency": "USDT", - "minNotional": 5000000.0, - "maxNotional": 10000000.0, + "minNotional": 6000000.0, + "maxNotional": 18000000.0, "maintenanceMarginRate": 0.25, "maxLeverage": 2.0, "info": { "bracket": "7", "initialLeverage": "2", - "notionalCap": "10000000", - "notionalFloor": "5000000", + "notionalCap": "18000000", + "notionalFloor": "6000000", "maintMarginRatio": "0.25", - "cum": "732500.0" + "cum": "983750.0" } }, { "tier": 8.0, "currency": "USDT", - "minNotional": 10000000.0, - "maxNotional": 20000000.0, + "minNotional": 18000000.0, + "maxNotional": 30000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { "bracket": "8", "initialLeverage": "1", - "notionalCap": "20000000", - "notionalFloor": "10000000", + "notionalCap": "30000000", + "notionalFloor": "18000000", "maintMarginRatio": "0.5", - "cum": "3232500.0" + "cum": "5483750.0" } } ], @@ -20200,10 +20812,10 @@ "minNotional": 0.0, "maxNotional": 5000.0, "maintenanceMarginRate": 0.02, - "maxLeverage": 20.0, + "maxLeverage": 10.0, "info": { "bracket": "1", - "initialLeverage": "20", + "initialLeverage": "10", "notionalCap": "5000", "notionalFloor": "0", "maintMarginRatio": "0.02", @@ -20216,10 +20828,10 @@ "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, + "maxLeverage": 8.0, "info": { "bracket": "2", - "initialLeverage": "10", + "initialLeverage": "8", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", @@ -20232,10 +20844,10 @@ "minNotional": 25000.0, "maxNotional": 100000.0, "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, + "maxLeverage": 6.0, "info": { "bracket": "3", - "initialLeverage": "8", + "initialLeverage": "6", "notionalCap": "100000", "notionalFloor": "25000", "maintMarginRatio": "0.05", @@ -20278,13 +20890,13 @@ "tier": 6.0, "currency": "USDT", "minNotional": 1000000.0, - "maxNotional": 3000000.0, + "maxNotional": 1500000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { "bracket": "6", "initialLeverage": "1", - "notionalCap": "3000000", + "notionalCap": "1500000", "notionalFloor": "1000000", "maintMarginRatio": "0.5", "cum": "386900.0" @@ -20526,10 +21138,10 @@ "minNotional": 0.0, "maxNotional": 5000.0, "maintenanceMarginRate": 0.01, - "maxLeverage": 20.0, + "maxLeverage": 25.0, "info": { "bracket": "1", - "initialLeverage": "20", + "initialLeverage": "25", "notionalCap": "5000", "notionalFloor": "0", "maintMarginRatio": "0.01", @@ -20542,10 +21154,10 @@ "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, + "maxLeverage": 20.0, "info": { "bracket": "2", - "initialLeverage": "10", + "initialLeverage": "20", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", @@ -20556,13 +21168,13 @@ "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, + "maxNotional": 600000.0, "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, + "maxLeverage": 10.0, "info": { "bracket": "3", - "initialLeverage": "8", - "notionalCap": "100000", + "initialLeverage": "10", + "notionalCap": "600000", "notionalFloor": "25000", "maintMarginRatio": "0.05", "cum": "700.0" @@ -20571,49 +21183,65 @@ { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, + "minNotional": 600000.0, + "maxNotional": 1600000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", + "notionalCap": "1600000", + "notionalFloor": "600000", "maintMarginRatio": "0.1", - "cum": "5700.0" + "cum": "30700.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, + "minNotional": 1600000.0, + "maxNotional": 2000000.0, "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maxLeverage": 4.0, "info": { "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", + "initialLeverage": "4", + "notionalCap": "2000000", + "notionalFloor": "1600000", "maintMarginRatio": "0.125", - "cum": "11950.0" + "cum": "70700.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, + "minNotional": 2000000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "6000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.25", + "cum": "320700.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 6000000.0, + "maxNotional": 10000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", + "notionalCap": "10000000", + "notionalFloor": "6000000", "maintMarginRatio": "0.5", - "cum": "386950.0" + "cum": "1820700.0" } } ], @@ -21146,10 +21774,10 @@ "minNotional": 0.0, "maxNotional": 5000.0, "maintenanceMarginRate": 0.02, - "maxLeverage": 20.0, + "maxLeverage": 25.0, "info": { "bracket": "1", - "initialLeverage": "20", + "initialLeverage": "25", "notionalCap": "5000", "notionalFloor": "0", "maintMarginRatio": "0.02", @@ -21162,10 +21790,10 @@ "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, + "maxLeverage": 20.0, "info": { "bracket": "2", - "initialLeverage": "10", + "initialLeverage": "20", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", @@ -21176,13 +21804,13 @@ "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, + "maxNotional": 480000.0, "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, + "maxLeverage": 10.0, "info": { "bracket": "3", - "initialLeverage": "8", - "notionalCap": "100000", + "initialLeverage": "10", + "notionalCap": "480000", "notionalFloor": "25000", "maintMarginRatio": "0.05", "cum": "650.0" @@ -21191,49 +21819,65 @@ { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, + "minNotional": 480000.0, + "maxNotional": 1280000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", + "notionalCap": "1280000", + "notionalFloor": "480000", "maintMarginRatio": "0.1", - "cum": "5650.0" + "cum": "24650.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, + "minNotional": 1280000.0, + "maxNotional": 1600000.0, "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maxLeverage": 4.0, "info": { "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", + "initialLeverage": "4", + "notionalCap": "1600000", + "notionalFloor": "1280000", "maintMarginRatio": "0.125", - "cum": "11900.0" + "cum": "56650.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 3000000.0, + "minNotional": 1600000.0, + "maxNotional": 4800000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "4800000", + "notionalFloor": "1600000", + "maintMarginRatio": "0.25", + "cum": "256650.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 4800000.0, + "maxNotional": 8000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "1", - "notionalCap": "3000000", - "notionalFloor": "1000000", + "notionalCap": "8000000", + "notionalFloor": "4800000", "maintMarginRatio": "0.5", - "cum": "386900.0" + "cum": "1456650.0" } } ], @@ -21942,13 +22586,13 @@ "tier": 3.0, "currency": "USDT", "minNotional": 50000.0, - "maxNotional": 400000.0, + "maxNotional": 900000.0, "maintenanceMarginRate": 0.025, "maxLeverage": 20.0, "info": { "bracket": "3", "initialLeverage": "20", - "notionalCap": "400000", + "notionalCap": "900000", "notionalFloor": "50000", "maintMarginRatio": "0.025", "cum": "770.0" @@ -21957,81 +22601,81 @@ { "tier": 4.0, "currency": "USDT", - "minNotional": 400000.0, - "maxNotional": 800000.0, + "minNotional": 900000.0, + "maxNotional": 1800000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "4", "initialLeverage": "10", - "notionalCap": "800000", - "notionalFloor": "400000", + "notionalCap": "1800000", + "notionalFloor": "900000", "maintMarginRatio": "0.05", - "cum": "10770.0" + "cum": "23270.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 800000.0, - "maxNotional": 2000000.0, + "minNotional": 1800000.0, + "maxNotional": 4800000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "5", "initialLeverage": "5", - "notionalCap": "2000000", - "notionalFloor": "800000", + "notionalCap": "4800000", + "notionalFloor": "1800000", "maintMarginRatio": "0.1", - "cum": "50770.0" + "cum": "113270.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 2000000.0, - "maxNotional": 5000000.0, + "minNotional": 4800000.0, + "maxNotional": 6000000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 4.0, "info": { "bracket": "6", "initialLeverage": "4", - "notionalCap": "5000000", - "notionalFloor": "2000000", + "notionalCap": "6000000", + "notionalFloor": "4800000", "maintMarginRatio": "0.125", - "cum": "100770.0" + "cum": "233270.0" } }, { "tier": 7.0, "currency": "USDT", - "minNotional": 5000000.0, - "maxNotional": 12000000.0, + "minNotional": 6000000.0, + "maxNotional": 18000000.0, "maintenanceMarginRate": 0.25, "maxLeverage": 2.0, "info": { "bracket": "7", "initialLeverage": "2", - "notionalCap": "12000000", - "notionalFloor": "5000000", + "notionalCap": "18000000", + "notionalFloor": "6000000", "maintMarginRatio": "0.25", - "cum": "725770.0" + "cum": "983270.0" } }, { "tier": 8.0, "currency": "USDT", - "minNotional": 12000000.0, - "maxNotional": 20000000.0, + "minNotional": 18000000.0, + "maxNotional": 30000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { "bracket": "8", "initialLeverage": "1", - "notionalCap": "20000000", - "notionalFloor": "12000000", + "notionalCap": "30000000", + "notionalFloor": "18000000", "maintMarginRatio": "0.5", - "cum": "3725770.0" + "cum": "5483270.0" } } ], @@ -22364,13 +23008,13 @@ "tier": 5.0, "currency": "BUSD", "minNotional": 250000.0, - "maxNotional": 1000000.0, + "maxNotional": 500000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 2.0, "info": { "bracket": "5", "initialLeverage": "2", - "notionalCap": "1000000", + "notionalCap": "500000", "notionalFloor": "250000", "maintMarginRatio": "0.125", "cum": "11900.0" @@ -22379,17 +23023,17 @@ { "tier": 6.0, "currency": "BUSD", - "minNotional": 1000000.0, - "maxNotional": 3000000.0, + "minNotional": 500000.0, + "maxNotional": 1000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { "bracket": "6", "initialLeverage": "1", - "notionalCap": "3000000", - "notionalFloor": "1000000", + "notionalCap": "1000000", + "notionalFloor": "500000", "maintMarginRatio": "0.5", - "cum": "386900.0" + "cum": "199400.0" } } ], @@ -23067,15 +23711,15 @@ "currency": "USDT", "minNotional": 5000.0, "maxNotional": 10000.0, - "maintenanceMarginRate": 0.0065, + "maintenanceMarginRate": 0.006, "maxLeverage": 50.0, "info": { "bracket": "2", "initialLeverage": "50", "notionalCap": "10000", "notionalFloor": "5000", - "maintMarginRatio": "0.0065", - "cum": "7.5" + "maintMarginRatio": "0.006", + "cum": "5.0" } }, { @@ -23091,7 +23735,7 @@ "notionalCap": "50000", "notionalFloor": "10000", "maintMarginRatio": "0.01", - "cum": "42.5" + "cum": "45.0" } }, { @@ -23107,7 +23751,7 @@ "notionalCap": "250000", "notionalFloor": "50000", "maintMarginRatio": "0.02", - "cum": "542.5" + "cum": "545.0" } }, { @@ -23123,77 +23767,77 @@ "notionalCap": "1000000", "notionalFloor": "250000", "maintMarginRatio": "0.05", - "cum": "8042.5" + "cum": "8045.0" } }, { "tier": 6.0, "currency": "USDT", "minNotional": 1000000.0, - "maxNotional": 2000000.0, + "maxNotional": 5000000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "6", "initialLeverage": "5", - "notionalCap": "2000000", + "notionalCap": "5000000", "notionalFloor": "1000000", "maintMarginRatio": "0.1", - "cum": "58042.5" + "cum": "58045.0" } }, { "tier": 7.0, "currency": "USDT", - "minNotional": 2000000.0, - "maxNotional": 5000000.0, + "minNotional": 5000000.0, + "maxNotional": 10000000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 4.0, "info": { "bracket": "7", "initialLeverage": "4", - "notionalCap": "5000000", - "notionalFloor": "2000000", + "notionalCap": "10000000", + "notionalFloor": "5000000", "maintMarginRatio": "0.125", - "cum": "108042.5" + "cum": "183045.0" } }, { "tier": 8.0, "currency": "USDT", - "minNotional": 5000000.0, - "maxNotional": 10000000.0, + "minNotional": 10000000.0, + "maxNotional": 20000000.0, "maintenanceMarginRate": 0.15, "maxLeverage": 3.0, "info": { "bracket": "8", "initialLeverage": "3", - "notionalCap": "10000000", - "notionalFloor": "5000000", + "notionalCap": "20000000", + "notionalFloor": "10000000", "maintMarginRatio": "0.15", - "cum": "233042.5" + "cum": "433045.0" } }, { "tier": 9.0, "currency": "USDT", - "minNotional": 10000000.0, - "maxNotional": 20000000.0, + "minNotional": 20000000.0, + "maxNotional": 30000000.0, "maintenanceMarginRate": 0.25, "maxLeverage": 2.0, "info": { "bracket": "9", "initialLeverage": "2", - "notionalCap": "20000000", - "notionalFloor": "10000000", + "notionalCap": "30000000", + "notionalFloor": "20000000", "maintMarginRatio": "0.25", - "cum": "1233042.5" + "cum": "2433045.0" } }, { "tier": 10.0, "currency": "USDT", - "minNotional": 20000000.0, + "minNotional": 30000000.0, "maxNotional": 50000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, @@ -23201,9 +23845,9 @@ "bracket": "10", "initialLeverage": "1", "notionalCap": "50000000", - "notionalFloor": "20000000", + "notionalFloor": "30000000", "maintMarginRatio": "0.5", - "cum": "6233042.5" + "cum": "9933045.0" } } ], From 736c396d98422f533c0a920e4f225327b43313b1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 28 Mar 2023 16:09:46 +0200 Subject: [PATCH 351/360] Use correct amount for stoploss test --- tests/test_freqtradebot.py | 60 +++++++++++++++++++++++++++++--------- tests/test_integration.py | 2 +- 2 files changed, 47 insertions(+), 15 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index bf3cc6ab8..adb581252 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1395,7 +1395,7 @@ def test_handle_stoploss_on_exchange_trailing( # When trailing stoploss is set enter_order = limit_order[entry_side(is_short)] exit_order = limit_order[exit_side(is_short)] - stoploss = MagicMock(return_value={'id': 13434334}) + stoploss = MagicMock(return_value={'id': 13434334, 'status': 'open'}) patch_RPCManager(mocker) mocker.patch.multiple( EXMS, @@ -1442,6 +1442,16 @@ def test_handle_stoploss_on_exchange_trailing( trade.open_order_id = None trade.stoploss_order_id = '100' trade.stoploss_last_update = arrow.utcnow().shift(minutes=-20).datetime + trade.orders.append( + Order( + ft_order_side='stoploss', + ft_pair=trade.pair, + ft_is_open=True, + ft_amount=trade.amount, + ft_price=trade.stop_loss, + order_id='100', + ) + ) stoploss_order_hanging = MagicMock(return_value={ 'id': '100', @@ -1471,7 +1481,7 @@ def test_handle_stoploss_on_exchange_trailing( ) cancel_order_mock = MagicMock() - stoploss_order_mock = MagicMock(return_value={'id': 'so1'}) + stoploss_order_mock = MagicMock(return_value={'id': 'so1', 'status': 'open'}) mocker.patch(f'{EXMS}.cancel_stoploss_order', cancel_order_mock) mocker.patch(f'{EXMS}.create_stoploss', stoploss_order_mock) @@ -1520,7 +1530,7 @@ def test_handle_stoploss_on_exchange_trailing_error( enter_order = limit_order[entry_side(is_short)] exit_order = limit_order[exit_side(is_short)] # When trailing stoploss is set - stoploss = MagicMock(return_value={'id': 13434334}) + stoploss = MagicMock(return_value={'id': '13434334', 'status': 'open'}) patch_exchange(mocker) mocker.patch.multiple( @@ -1629,7 +1639,7 @@ def test_handle_stoploss_on_exchange_custom_stop( enter_order = limit_order[entry_side(is_short)] exit_order = limit_order[exit_side(is_short)] # When trailing stoploss is set - stoploss = MagicMock(return_value={'id': 13434334}) + stoploss = MagicMock(return_value={'id': 13434334, 'status': 'open'}) patch_RPCManager(mocker) mocker.patch.multiple( EXMS, @@ -1676,6 +1686,16 @@ def test_handle_stoploss_on_exchange_custom_stop( trade.open_order_id = None trade.stoploss_order_id = '100' trade.stoploss_last_update = arrow.utcnow().shift(minutes=-601).datetime + trade.orders.append( + Order( + ft_order_side='stoploss', + ft_pair=trade.pair, + ft_is_open=True, + ft_amount=trade.amount, + ft_price=trade.stop_loss, + order_id='100', + ) + ) stoploss_order_hanging = MagicMock(return_value={ 'id': '100', @@ -1704,7 +1724,7 @@ def test_handle_stoploss_on_exchange_custom_stop( ) cancel_order_mock = MagicMock() - stoploss_order_mock = MagicMock(return_value={'id': 'so1'}) + stoploss_order_mock = MagicMock(return_value={'id': 'so1', 'status': 'open'}) mocker.patch(f'{EXMS}.cancel_stoploss_order', cancel_order_mock) mocker.patch(f'{EXMS}.create_stoploss', stoploss_order_mock) trade.stoploss_order_id = '100' @@ -1753,7 +1773,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, limit_orde exit_order = limit_order['sell'] # When trailing stoploss is set - stoploss = MagicMock(return_value={'id': 13434334}) + stoploss = MagicMock(return_value={'id': '13434334', 'status': 'open'}) patch_RPCManager(mocker) patch_exchange(mocker) patch_edge(mocker) @@ -1802,11 +1822,21 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, limit_orde trade = Trade.session.scalars(select(Trade)).first() trade.is_open = True trade.open_order_id = None - trade.stoploss_order_id = 100 - trade.stoploss_last_update = arrow.utcnow() + trade.stoploss_order_id = '100' + trade.stoploss_last_update = arrow.utcnow().datetime + trade.orders.append( + Order( + ft_order_side='stoploss', + ft_pair=trade.pair, + ft_is_open=True, + ft_amount=trade.amount, + ft_price=trade.stop_loss, + order_id='100', + ) + ) stoploss_order_hanging = MagicMock(return_value={ - 'id': 100, + 'id': '100', 'status': 'open', 'type': 'stop_loss_limit', 'price': 3, @@ -1853,7 +1883,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, limit_orde # stoploss should be set to 1% as trailing is on assert trade.stop_loss == 4.4 * 0.99 - cancel_order_mock.assert_called_once_with(100, 'NEO/BTC') + cancel_order_mock.assert_called_once_with('100', 'NEO/BTC') stoploss_order_mock.assert_called_once_with( amount=pytest.approx(11.41438356), pair='NEO/BTC', @@ -3606,10 +3636,12 @@ def test_execute_trade_exit_with_stoploss_on_exchange( patch_exchange(mocker) stoploss = MagicMock(return_value={ 'id': 123, + 'status': 'open', 'info': { 'foo': 'bar' } }) + mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_order_fee') cancel_order = MagicMock(return_value=True) mocker.patch.multiple( @@ -3707,12 +3739,12 @@ def test_may_execute_trade_exit_after_stoploss_on_exchange_hit( "lastTradeTimestamp": None, "symbol": "BTC/USDT", "type": "stop_loss_limit", - "side": "sell", + "side": "buy" if is_short else "sell", "price": 1.08801, - "amount": 90.99181074, - "cost": 99.0000000032274, + "amount": trade.amount, + "cost": 1.08801 * trade.amount, "average": 1.08801, - "filled": 90.99181074, + "filled": trade.amount, "remaining": 0.0, "status": "closed", "fee": None, diff --git a/tests/test_integration.py b/tests/test_integration.py index 5cbedd818..af8473514 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -35,7 +35,7 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee, "type": "stop_loss_limit", "side": "sell", "price": 1.08801, - "amount": 90.99181074, + "amount": 91.07468123, "cost": 0.0, "average": 0.0, "filled": 0.0, From f0b5f95fd651cb642b68fb6b0614de7fc60e7470 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 28 Mar 2023 18:10:26 +0200 Subject: [PATCH 352/360] Remove missleading comment --- tests/test_integration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_integration.py b/tests/test_integration.py index af8473514..66aa7b4ee 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -93,7 +93,7 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee, wallets_mock.reset_mock() trades = Trade.session.scalars(select(Trade)).all() - # Make sure stoploss-order is open and trade is bought (since we mock update_trade_state) + # Make sure stoploss-order is open and trade is bought for trade in trades: stoploss_order_closed['id'] = '3' oobj = Order.parse_from_ccxt_object(stoploss_order_closed, trade.pair, 'stoploss') From 411e21f430a51d3f36236bd71f81889356a0a701 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 28 Mar 2023 18:13:26 +0200 Subject: [PATCH 353/360] Improve stop test --- tests/test_freqtradebot.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index adb581252..2033a5518 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1149,10 +1149,12 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_ canceled_stoploss_order = MagicMock(return_value={'status': 'canceled'}) mocker.patch(f'{EXMS}.fetch_stoploss_order', canceled_stoploss_order) stoploss.reset_mock() + amount_before = trade.amount assert freqtrade.handle_stoploss_on_exchange(trade) is False assert stoploss.call_count == 1 assert trade.stoploss_order_id == "13434334" + assert trade.amount == amount_before # Fourth case: when stoploss is set and it is hit # should unset stoploss_order_id and return true From 513df4515b85519df66a2f990389885cb9ae2df4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 28 Mar 2023 19:19:55 +0200 Subject: [PATCH 354/360] Improve stoploss tests --- tests/test_freqtradebot.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 2033a5518..7b89bfb9b 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1060,9 +1060,19 @@ def test_execute_entry_min_leverage(mocker, default_conf_usdt, fee, limit_order, @pytest.mark.parametrize("is_short", [False, True]) -def test_add_stoploss_on_exchange(mocker, default_conf_usdt, limit_order, is_short) -> None: +def test_add_stoploss_on_exchange(mocker, default_conf_usdt, limit_order, is_short, fee) -> None: patch_RPCManager(mocker) patch_exchange(mocker) + mocker.patch.multiple( + EXMS, + fetch_ticker=MagicMock(return_value={ + 'bid': 1.9, + 'ask': 2.2, + 'last': 1.9 + }), + create_order=MagicMock(return_value=limit_order[entry_side(is_short)]), + get_fee=fee, + ) order = limit_order[entry_side(is_short)] mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_trade', MagicMock(return_value=True)) mocker.patch(f'{EXMS}.fetch_order', return_value=order) @@ -1074,8 +1084,10 @@ def test_add_stoploss_on_exchange(mocker, default_conf_usdt, limit_order, is_sho freqtrade = FreqtradeBot(default_conf_usdt) freqtrade.strategy.order_types['stoploss_on_exchange'] = True - # TODO: should not be magicmock - trade = MagicMock() + patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) + + freqtrade.enter_positions() + trade = Trade.session.scalars(select(Trade)).first() trade.is_short = is_short trade.open_order_id = None trade.stoploss_order_id = None @@ -1227,7 +1239,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_ 'amount': enter_order['amount'], 'info': {'stopPrice': 22}, }]) - trade.stoploss_order_id = 100 + trade.stoploss_order_id = "100" trade.is_open = True trade.stoploss_last_update = arrow.utcnow().shift(hours=-1).datetime trade.stop_loss = 24 @@ -1275,7 +1287,7 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf_usdt, fee, caplog, freqtrade.enter_positions() trade = Trade.session.scalars(select(Trade)).first() - trade.is_short = is_short + assert trade.is_short == is_short trade.is_open = True trade.open_order_id = None trade.stoploss_order_id = 100 From 6282b42741543f5df494a43ad06c412953e16328 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 28 Mar 2023 19:38:43 +0200 Subject: [PATCH 355/360] Remove further Magicmock trade --- tests/test_freqtradebot.py | 35 ++++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 7b89bfb9b..bbe23fa9e 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1103,7 +1103,8 @@ def test_add_stoploss_on_exchange(mocker, default_conf_usdt, limit_order, is_sho @pytest.mark.parametrize("is_short", [False, True]) def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_short, limit_order) -> None: - stoploss = MagicMock(return_value={'id': 13434334}) + stop_order_dict = {'id': "13434334"} + stoploss = MagicMock(return_value=stop_order_dict) enter_order = limit_order[entry_side(is_short)] exit_order = limit_order[exit_side(is_short)] patch_RPCManager(mocker) @@ -1128,8 +1129,9 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_ # First case: when stoploss is not yet set but the order is open # should get the stoploss order id immediately # and should return false as no trade actually happened - # TODO: should not be magicmock - trade = MagicMock() + + freqtrade.enter_positions() + trade = Trade.session.scalars(select(Trade)).first() trade.is_short = is_short trade.is_open = True trade.open_order_id = None @@ -1141,31 +1143,34 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_ # Second case: when stoploss is set but it is not yet hit # should do nothing and return false + stop_order_dict.update({'id': "102"}) trade.is_open = True trade.open_order_id = None - trade.stoploss_order_id = "100" + trade.stoploss_order_id = "102" hanging_stoploss_order = MagicMock(return_value={'status': 'open'}) mocker.patch(f'{EXMS}.fetch_stoploss_order', hanging_stoploss_order) assert freqtrade.handle_stoploss_on_exchange(trade) is False - assert trade.stoploss_order_id == "100" + assert trade.stoploss_order_id == "102" # Third case: when stoploss was set but it was canceled for some reason # should set a stoploss immediately and return False caplog.clear() trade.is_open = True trade.open_order_id = None - trade.stoploss_order_id = "100" + trade.stoploss_order_id = "103" canceled_stoploss_order = MagicMock(return_value={'status': 'canceled'}) mocker.patch(f'{EXMS}.fetch_stoploss_order', canceled_stoploss_order) stoploss.reset_mock() amount_before = trade.amount + stop_order_dict.update({'id': "103_1"}) + assert freqtrade.handle_stoploss_on_exchange(trade) is False assert stoploss.call_count == 1 - assert trade.stoploss_order_id == "13434334" + assert trade.stoploss_order_id == "103_1" assert trade.amount == amount_before # Fourth case: when stoploss is set and it is hit @@ -1173,14 +1178,16 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_ # as a trade actually happened caplog.clear() freqtrade.enter_positions() + stop_order_dict.update({'id': "104"}) + trade = Trade.session.scalars(select(Trade)).first() trade.is_short = is_short trade.is_open = True trade.open_order_id = None - trade.stoploss_order_id = "100" + trade.stoploss_order_id = "104" trade.orders.append(Order( ft_order_side='stoploss', - order_id='100', + order_id='104', ft_pair=trade.pair, ft_is_open=True, ft_amount=trade.amount, @@ -1189,7 +1196,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_ assert trade stoploss_order_hit = MagicMock(return_value={ - 'id': "100", + 'id': "104", 'status': 'closed', 'type': 'stop_loss_limit', 'price': 3, @@ -1211,7 +1218,8 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_ # Fifth case: fetch_order returns InvalidOrder # It should try to add stoploss order - trade.stoploss_order_id = 100 + stop_order_dict.update({'id': "105"}) + trade.stoploss_order_id = "105" stoploss.reset_mock() mocker.patch(f'{EXMS}.fetch_stoploss_order', side_effect=InvalidOrderException()) mocker.patch(f'{EXMS}.create_stoploss', stoploss) @@ -1231,7 +1239,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_ # Seventh case: emergency exit triggered # Trailing stop should not act anymore stoploss_order_cancelled = MagicMock(side_effect=[{ - 'id': "100", + 'id': "107", 'status': 'canceled', 'type': 'stop_loss_limit', 'price': 3, @@ -1239,13 +1247,14 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_ 'amount': enter_order['amount'], 'info': {'stopPrice': 22}, }]) - trade.stoploss_order_id = "100" + trade.stoploss_order_id = "107" trade.is_open = True trade.stoploss_last_update = arrow.utcnow().shift(hours=-1).datetime trade.stop_loss = 24 freqtrade.config['trailing_stop'] = True stoploss = MagicMock(side_effect=InvalidOrderException()) + Trade.commit() mocker.patch(f'{EXMS}.cancel_stoploss_order_with_result', side_effect=InvalidOrderException()) mocker.patch(f'{EXMS}.fetch_stoploss_order', stoploss_order_cancelled) From eb96490c999f3617bfc6949f1fa440f692f765bb Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 28 Mar 2023 20:27:31 +0200 Subject: [PATCH 356/360] Improve some more stoploss tests --- tests/test_freqtradebot.py | 42 +++++++++++++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index bbe23fa9e..01aa730cb 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1147,6 +1147,17 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_ trade.is_open = True trade.open_order_id = None trade.stoploss_order_id = "102" + trade.orders.append( + Order( + ft_order_side='stoploss', + ft_pair=trade.pair, + ft_is_open=True, + ft_amount=trade.amount, + ft_price=trade.stop_loss, + order_id='102', + status='open', + ) + ) hanging_stoploss_order = MagicMock(return_value={'status': 'open'}) mocker.patch(f'{EXMS}.fetch_stoploss_order', hanging_stoploss_order) @@ -1159,9 +1170,9 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_ caplog.clear() trade.is_open = True trade.open_order_id = None - trade.stoploss_order_id = "103" + trade.stoploss_order_id = "102" - canceled_stoploss_order = MagicMock(return_value={'status': 'canceled'}) + canceled_stoploss_order = MagicMock(return_value={'id': '103_1', 'status': 'canceled'}) mocker.patch(f'{EXMS}.fetch_stoploss_order', canceled_stoploss_order) stoploss.reset_mock() amount_before = trade.amount @@ -1245,12 +1256,26 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_ 'price': 3, 'average': 2, 'amount': enter_order['amount'], + 'filled': 0, + 'remaining': enter_order['amount'], 'info': {'stopPrice': 22}, }]) trade.stoploss_order_id = "107" trade.is_open = True trade.stoploss_last_update = arrow.utcnow().shift(hours=-1).datetime trade.stop_loss = 24 + trade.exit_reason = None + trade.orders.append( + Order( + ft_order_side='stoploss', + ft_pair=trade.pair, + ft_is_open=True, + ft_amount=trade.amount, + ft_price=trade.stop_loss, + order_id='107', + status='open', + ) + ) freqtrade.config['trailing_stop'] = True stoploss = MagicMock(side_effect=InvalidOrderException()) @@ -1299,7 +1324,18 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf_usdt, fee, caplog, assert trade.is_short == is_short trade.is_open = True trade.open_order_id = None - trade.stoploss_order_id = 100 + trade.stoploss_order_id = "100" + trade.orders.append( + Order( + ft_order_side='stoploss', + ft_pair=trade.pair, + ft_is_open=True, + ft_amount=trade.amount, + ft_price=trade.stop_loss, + order_id='100', + status='open', + ) + ) assert trade assert freqtrade.handle_stoploss_on_exchange(trade) is False From a64252492804cb6c254a4d4f09f03a7e1ed02d48 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 29 Mar 2023 06:47:34 +0200 Subject: [PATCH 357/360] Improve integration test correctness --- tests/test_integration.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tests/test_integration.py b/tests/test_integration.py index 66aa7b4ee..9fb9fd8b3 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -49,8 +49,9 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee, stoploss_order_closed['filled'] = stoploss_order_closed['amount'] # Sell first trade based on stoploss, keep 2nd and 3rd trade open + stop_orders = [stoploss_order_closed, stoploss_order_open, stoploss_order_open] stoploss_order_mock = MagicMock( - side_effect=[stoploss_order_closed, stoploss_order_open, stoploss_order_open]) + side_effect=stop_orders) # Sell 3rd trade (not called for the first trade) should_sell_mock = MagicMock(side_effect=[ [], @@ -94,12 +95,13 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee, trades = Trade.session.scalars(select(Trade)).all() # Make sure stoploss-order is open and trade is bought - for trade in trades: - stoploss_order_closed['id'] = '3' - oobj = Order.parse_from_ccxt_object(stoploss_order_closed, trade.pair, 'stoploss') + for idx, trade in enumerate(trades): + stop_order = stop_orders[idx] + stop_order['id'] = f"stop{idx}" + oobj = Order.parse_from_ccxt_object(stop_order, trade.pair, 'stoploss') trade.orders.append(oobj) - trade.stoploss_order_id = '3' + trade.stoploss_order_id = f"stop{idx}" trade.open_order_id = None n = freqtrade.exit_positions(trades) From 8a49d620687d3c367cec1efbec6d4e5ab41c8137 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 29 Mar 2023 06:49:11 +0200 Subject: [PATCH 358/360] Don't update liquidation price for closed trades --- freqtrade/freqtradebot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 42db121e9..9746ac3d8 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1811,7 +1811,7 @@ class FreqtradeBot(LoggingMixin): # TODO: should shorting/leverage be supported by Edge, # then this will need to be fixed. trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss, initial=True) - if order.get('side') == trade.entry_side or trade.amount > 0: + if order.get('side') == trade.entry_side or (trade.amount > 0 and trade.is_open): # Must also run for partial exits # TODO: Margin will need to use interest_rate as well. # interest_rate = self.exchange.get_interest_rate() From fa7c29fe9f03483e02ac752617062b7b1c0b3963 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 29 Mar 2023 20:43:23 +0200 Subject: [PATCH 359/360] Update producer docs to reflect proper datatype closes #8419 --- docs/producer-consumer.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/producer-consumer.md b/docs/producer-consumer.md index 88e34d0d6..c52279f26 100644 --- a/docs/producer-consumer.md +++ b/docs/producer-consumer.md @@ -42,14 +42,14 @@ Enable subscribing to an instance by adding the `external_message_consumer` sect | `producers` | **Required.** List of producers
**Datatype:** Array. | `producers.name` | **Required.** Name of this producer. This name must be used in calls to `get_producer_pairs()` and `get_producer_df()` if more than one producer is used.
**Datatype:** string | `producers.host` | **Required.** The hostname or IP address from your producer.
**Datatype:** string -| `producers.port` | **Required.** The port matching the above host.
**Datatype:** string +| `producers.port` | **Required.** The port matching the above host.
*Defaults to `8080`.*
**Datatype:** Integer | `producers.secure` | **Optional.** Use ssl in websockets connection. Default False.
**Datatype:** string | `producers.ws_token` | **Required.** `ws_token` as configured on the producer.
**Datatype:** string | | **Optional settings** | `wait_timeout` | Timeout until we ping again if no message is received.
*Defaults to `300`.*
**Datatype:** Integer - in seconds. -| `wait_timeout` | Ping timeout
*Defaults to `10`.*
**Datatype:** Integer - in seconds. +| `ping_timeout` | Ping timeout
*Defaults to `10`.*
**Datatype:** Integer - in seconds. | `sleep_time` | Sleep time before retrying to connect.
*Defaults to `10`.*
**Datatype:** Integer - in seconds. -| `remove_entry_exit_signals` | Remove signal columns from the dataframe (set them to 0) on dataframe receipt.
*Defaults to `10`.*
**Datatype:** Integer - in seconds. +| `remove_entry_exit_signals` | Remove signal columns from the dataframe (set them to 0) on dataframe receipt.
*Defaults to `False`.*
**Datatype:** Boolean. | `message_size_limit` | Size limit per message
*Defaults to `8`.*
**Datatype:** Integer - Megabytes. Instead of (or as well as) calculating indicators in `populate_indicators()` the follower instance listens on the connection to a producer instance's messages (or multiple producer instances in advanced configurations) and requests the producer's most recently analyzed dataframes for each pair in the active whitelist. From 2ea77b22e0386e3f2d9a78025895b5949795f267 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 30 Mar 2023 06:56:49 +0200 Subject: [PATCH 360/360] Bump version to 2023.3 --- freqtrade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index f25bb2e52..f56328674 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ Freqtrade bot """ -__version__ = '2023.2' +__version__ = '2023.3' if 'dev' in __version__: from pathlib import Path