diff --git a/.dependabot/config.yml b/.dependabot/config.yml deleted file mode 100644 index 66b91e99f..000000000 --- a/.dependabot/config.yml +++ /dev/null @@ -1,17 +0,0 @@ -version: 1 - -update_configs: - - package_manager: "python" - directory: "/" - update_schedule: "weekly" - allowed_updates: - - match: - update_type: "all" - target_branch: "develop" - - - package_manager: "docker" - directory: "/" - update_schedule: "daily" - allowed_updates: - - match: - update_type: "all" diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..44ff606b4 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,13 @@ +version: 2 +updates: +- package-ecosystem: docker + directory: "/" + schedule: + interval: daily + open-pull-requests-limit: 10 +- package-ecosystem: pip + directory: "/" + schedule: + interval: weekly + open-pull-requests-limit: 10 + target-branch: develop diff --git a/Dockerfile b/Dockerfile index f27167cc5..e1220e3b8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.8.4-slim-buster +FROM python:3.8.5-slim-buster RUN apt-get update \ && apt-get -y install curl build-essential libssl-dev sqlite3 \ diff --git a/config_full.json.example b/config_full.json.example index e1be01690..d5bfd3fe1 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -66,7 +66,7 @@ }, {"method": "AgeFilter", "min_days_listed": 10}, {"method": "PrecisionFilter"}, - {"method": "PriceFilter", "low_price_ratio": 0.01}, + {"method": "PriceFilter", "low_price_ratio": 0.01, "min_price": 0.00000010}, {"method": "SpreadFilter", "max_spread_ratio": 0.005} ], "exchange": { diff --git a/docs/configuration.md b/docs/configuration.md index 09a1e76fe..a200d6411 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -662,16 +662,25 @@ Filters low-value coins which would not allow setting stoplosses. #### PriceFilter -The `PriceFilter` allows filtering of pairs by price. +The `PriceFilter` allows filtering of pairs by price. Currently the following price filters are supported: +* `min_price` +* `max_price` +* `low_price_ratio` -Currently, only `low_price_ratio` setting is implemented, where a raise of 1 price unit (pip) is below the `low_price_ratio` ratio. +The `min_price` setting removes pairs where the price is below the specified price. This is useful if you wish to avoid trading very low-priced pairs. +This option is disabled by default, and will only apply if set to <> 0. + +The `max_price` setting removes pairs where the price is above the specified price. This is useful if you wish to trade only low-priced pairs. +This option is disabled by default, and will only apply if set to <> 0. + +The `low_price_ratio` setting removes pairs where a raise of 1 price unit (pip) is above the `low_price_ratio` ratio. This option is disabled by default, and will only apply if set to <> 0. Calculation example: Min price precision is 8 decimals. If price is 0.00000011 - one step would be 0.00000012 - which is almost 10% higher than the previous value. -These pairs are dangerous since it may be impossible to place the desired stoploss - and often result in high losses. Here is what the PriceFilters takes over. +These pairs are dangerous since it may be impossible to place the desired stoploss - and often result in high losses. #### ShuffleFilter diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 3a236ee87..4068e364b 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,2 +1,2 @@ -mkdocs-material==5.4.0 +mkdocs-material==5.5.3 mdx_truly_sane_lists==1.2 diff --git a/docs/rest-api.md b/docs/rest-api.md index a8d902b53..68754f79a 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -46,7 +46,7 @@ secrets.token_hex() ### Configuration with docker -If you run your bot using docker, you'll need to have the bot listen to incomming connections. The security is then handled by docker. +If you run your bot using docker, you'll need to have the bot listen to incoming connections. The security is then handled by docker. ``` json "api_server": { @@ -106,26 +106,29 @@ python3 scripts/rest_client.py --config rest_config.json [optional par ## Available commands -| Command | Default | Description | -|----------|---------|-------------| -| `start` | | Starts the trader -| `stop` | | Stops the trader -| `stopbuy` | | Stops the trader from opening new trades. Gracefully closes open trades according to their rules. -| `reload_config` | | Reloads the configuration file -| `show_config` | | Shows part of the current configuration with relevant settings to operation -| `status` | | Lists all open trades -| `count` | | Displays number of trades used and available -| `profit` | | Display a summary of your profit/loss from close trades and some stats about your performance -| `forcesell ` | | Instantly sells the given trade (Ignoring `minimum_roi`). -| `forcesell all` | | Instantly sells all open trades (Ignoring `minimum_roi`). -| `forcebuy [rate]` | | Instantly buys the given pair. Rate is optional. (`forcebuy_enable` must be set to True) -| `performance` | | Show performance of each finished trade grouped by pair -| `balance` | | Show account balance per currency -| `daily ` | 7 | Shows profit or loss per day, over the last n days -| `whitelist` | | Show the current whitelist -| `blacklist [pair]` | | Show the current blacklist, or adds a pair to the blacklist. -| `edge` | | Show validated pairs by Edge if it is enabled. -| `version` | | Show version +| Command | Description | +|----------|-------------| +| `ping` | Simple command testing the API Readiness - requires no authentication. +| `start` | Starts the trader +| `stop` | Stops the trader +| `stopbuy` | Stops the trader from opening new trades. Gracefully closes open trades according to their rules. +| `reload_config` | Reloads the configuration file +| `trades` | List last trades. +| `delete_trade ` | Remove trade from the database. Tries to close open orders. Requires manual handling of this trade on the exchange. +| `show_config` | Shows part of the current configuration with relevant settings to operation +| `status` | Lists all open trades +| `count` | Displays number of trades used and available +| `profit` | Display a summary of your profit/loss from close trades and some stats about your performance +| `forcesell ` | Instantly sells the given trade (Ignoring `minimum_roi`). +| `forcesell all` | Instantly sells all open trades (Ignoring `minimum_roi`). +| `forcebuy [rate]` | Instantly buys the given pair. Rate is optional. (`forcebuy_enable` must be set to True) +| `performance` | Show performance of each finished trade grouped by pair +| `balance` | Show account balance per currency +| `daily ` | Shows profit or loss per day, over the last n days (n defaults to 7) +| `whitelist` | Show the current whitelist +| `blacklist [pair]` | Show the current blacklist, or adds a pair to the blacklist. +| `edge` | Show validated pairs by Edge if it is enabled. +| `version` | Show version Possible commands can be listed from the rest-client script using the `help` command. diff --git a/docs/sql_cheatsheet.md b/docs/sql_cheatsheet.md index f4cb473ff..748b16928 100644 --- a/docs/sql_cheatsheet.md +++ b/docs/sql_cheatsheet.md @@ -123,7 +123,7 @@ SET is_open=0, close_date='2020-06-20 03:08:45.103418', close_rate=0.19638016, close_profit=0.0496, - close_profit_abs = (amount * 0.19638016 * (1 - fee_close) - (amount * open_rate * (1 - fee_open))) + close_profit_abs = (amount * 0.19638016 * (1 - fee_close) - (amount * open_rate * (1 - fee_open))), sell_reason='force_sell' WHERE id=31; ``` diff --git a/docs/stoploss.md b/docs/stoploss.md index ed00c1e33..bf7270dff 100644 --- a/docs/stoploss.md +++ b/docs/stoploss.md @@ -84,7 +84,7 @@ This option can be used with or without `trailing_stop_positive`, but uses `trai ``` python trailing_stop_positive_offset = 0.011 - trailing_only_offset_is_reached = true + trailing_only_offset_is_reached = True ``` Simplified example: diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 50fec79dc..98c71b4b2 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -392,9 +392,9 @@ Imagine you've developed a strategy that trades the `5m` timeframe using signals The strategy might look something like this: -*Scan through the top 10 pairs by volume using the `VolumePairList` every 5 minutes and use a 14 day ATR to buy and sell.* +*Scan through the top 10 pairs by volume using the `VolumePairList` every 5 minutes and use a 14 day RSI to buy and sell.* -Due to the limited available data, it's very difficult to resample our `5m` candles into daily candles for use in a 14 day ATR. Most exchanges limit us to just 500 candles which effectively gives us around 1.74 daily candles. We need 14 days at least! +Due to the limited available data, it's very difficult to resample our `5m` candles into daily candles for use in a 14 day RSI. Most exchanges limit us to just 500 candles which effectively gives us around 1.74 daily candles. We need 14 days at least! Since we can't resample our data we will have to use an informative pair; and since our whitelist will be dynamic we don't know which pair(s) to use. @@ -410,18 +410,49 @@ class SampleStrategy(IStrategy): def informative_pairs(self): - # get access to all pairs available in whitelist. + # get access to all pairs available in whitelist. pairs = self.dp.current_whitelist() # Assign tf to each pair so they can be downloaded and cached for strategy. informative_pairs = [(pair, '1d') for pair in pairs] return informative_pairs - def populate_indicators(self, dataframe, metadata): + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + + inf_tf = '1d' # Get the informative pair informative = self.dp.get_pair_dataframe(pair=metadata['pair'], timeframe='1d') - # Get the 14 day ATR. - atr = ta.ATR(informative, timeperiod=14) + # Get the 14 day rsi + informative['rsi'] = ta.RSI(informative, timeperiod=14) + + # Rename columns to be unique + informative.columns = [f"{col}_{inf_tf}" for col in informative.columns] + # Assuming inf_tf = '1d' - then the columns will now be: + # date_1d, open_1d, high_1d, low_1d, close_1d, rsi_1d + + # Combine the 2 dataframes + # all indicators on the informative sample MUST be calculated before this point + dataframe = pd.merge(dataframe, informative, left_on='date', right_on=f'date_{inf_tf}', how='left') + # FFill to have the 1d value available in every row throughout the day. + # Without this, comparisons would only work once per day. + dataframe = dataframe.ffill() + # Calculate rsi of the original dataframe (5m timeframe) + dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) + # Do other stuff + # ... + + return dataframe + + def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + + dataframe.loc[ + ( + (qtpylib.crossed_above(dataframe['rsi'], 30)) & # Signal: RSI crosses above 30 + (dataframe['rsi_1d'] < 30) & # Ensure daily RSI is < 30 + (dataframe['volume'] > 0) # Ensure this candle had volume (important for backtesting) + ), + 'buy'] = 1 + ``` #### *get_pair_dataframe(pair, timeframe)* @@ -460,7 +491,7 @@ if self.dp: !!! Warning "Warning in hyperopt" This option cannot currently be used during hyperopt. - + #### *orderbook(pair, maximum)* ``` python @@ -493,6 +524,7 @@ if self.dp: data returned from the exchange and add appropriate error handling / defaults. *** + ### Additional data (Wallets) The strategy provides access to the `Wallets` object. This contains the current balances on the exchange. @@ -516,6 +548,7 @@ if self.wallets: - `get_total(asset)` - total available balance - sum of the 2 above *** + ### Additional data (Trades) A history of Trades can be retrieved in the strategy by querying the database. diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index f423a9376..9776b26ba 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -9,7 +9,7 @@ Telegram user id. Start a chat with the [Telegram BotFather](https://telegram.me/BotFather) -Send the message `/newbot`. +Send the message `/newbot`. *BotFather response:* @@ -47,28 +47,30 @@ Per default, the Telegram bot shows predefined commands. Some commands are only available by sending them to the bot. The table below list the official commands. You can ask at any moment for help with `/help`. -| Command | Default | Description | -|----------|---------|-------------| -| `/start` | | Starts the trader -| `/stop` | | Stops the trader -| `/stopbuy` | | Stops the trader from opening new trades. Gracefully closes open trades according to their rules. -| `/reload_config` | | Reloads the configuration file -| `/show_config` | | Shows part of the current configuration with relevant settings to operation -| `/status` | | Lists all open trades -| `/status table` | | List all open trades in a table format. Pending buy orders are marked with an asterisk (*) Pending sell orders are marked with a double asterisk (**) -| `/count` | | Displays number of trades used and available -| `/profit` | | Display a summary of your profit/loss from close trades and some stats about your performance -| `/forcesell ` | | Instantly sells the given trade (Ignoring `minimum_roi`). -| `/forcesell all` | | Instantly sells all open trades (Ignoring `minimum_roi`). -| `/forcebuy [rate]` | | Instantly buys the given pair. Rate is optional. (`forcebuy_enable` must be set to True) -| `/performance` | | Show performance of each finished trade grouped by pair -| `/balance` | | Show account balance per currency -| `/daily ` | 7 | Shows profit or loss per day, over the last n days -| `/whitelist` | | Show the current whitelist -| `/blacklist [pair]` | | Show the current blacklist, or adds a pair to the blacklist. -| `/edge` | | Show validated pairs by Edge if it is enabled. -| `/help` | | Show help message -| `/version` | | Show version +| Command | Description | +|----------|-------------| +| `/start` | Starts the trader +| `/stop` | Stops the trader +| `/stopbuy` | Stops the trader from opening new trades. Gracefully closes open trades according to their rules. +| `/reload_config` | Reloads the configuration file +| `/show_config` | Shows part of the current configuration with relevant settings to operation +| `/status` | Lists all open trades +| `/status table` | List all open trades in a table format. Pending buy orders are marked with an asterisk (*) Pending sell orders are marked with a double asterisk (**) +| `/trades [limit]` | List all recently closed trades in a table format. +| `/delete ` | Delete a specific trade from the Database. Tries to close open orders. Requires manual handling of this trade on the exchange. +| `/count` | Displays number of trades used and available +| `/profit` | Display a summary of your profit/loss from close trades and some stats about your performance +| `/forcesell ` | Instantly sells the given trade (Ignoring `minimum_roi`). +| `/forcesell all` | Instantly sells all open trades (Ignoring `minimum_roi`). +| `/forcebuy [rate]` | Instantly buys the given pair. Rate is optional. (`forcebuy_enable` must be set to True) +| `/performance` | Show performance of each finished trade grouped by pair +| `/balance` | Show account balance per currency +| `/daily ` | Shows profit or loss per day, over the last n days (n defaults to 7) +| `/whitelist` | Show the current whitelist +| `/blacklist [pair]` | Show the current blacklist, or adds a pair to the blacklist. +| `/edge` | Show validated pairs by Edge if it is enabled. +| `/help` | Show help message +| `/version` | Show version ## Telegram commands in action @@ -113,6 +115,7 @@ For each open trade, the bot will send you the following message. ### /status table Return the status of all open trades in a table format. + ``` ID Pair Since Profit ---- -------- ------- -------- @@ -123,6 +126,7 @@ Return the status of all open trades in a table format. ### /count Return the number of trades used and available. + ``` current max --------- ----- @@ -208,7 +212,7 @@ Shows the current whitelist Shows the current blacklist. If Pair is set, then this pair will be added to the pairlist. -Also supports multiple pairs, seperated by a space. +Also supports multiple pairs, separated by a space. Use `/reload_config` to reset the blacklist. > Using blacklist `StaticPairList` with 2 pairs @@ -216,7 +220,7 @@ Use `/reload_config` to reset the blacklist. ### /edge -Shows pairs validated by Edge along with their corresponding winrate, expectancy and stoploss values. +Shows pairs validated by Edge along with their corresponding win-rate, expectancy and stoploss values. > **Edge only validated following pairs:** ``` diff --git a/docs/utils.md b/docs/utils.md index 793c84a93..8c7e381ff 100644 --- a/docs/utils.md +++ b/docs/utils.md @@ -432,9 +432,9 @@ usage: freqtrade hyperopt-list [-h] [-v] [--logfile FILE] [-V] [-c PATH] [--max-trades INT] [--min-avg-time FLOAT] [--max-avg-time FLOAT] [--min-avg-profit FLOAT] [--max-avg-profit FLOAT] - [--min-total-profit FLOAT] - [--max-total-profit FLOAT] [--no-color] - [--print-json] [--no-details] + [--min-total-profit FLOAT] [--max-total-profit FLOAT] + [--min-objective FLOAT] [--max-objective FLOAT] + [--no-color] [--print-json] [--no-details] [--export-csv FILE] optional arguments: @@ -453,6 +453,10 @@ optional arguments: Select epochs on above total profit. --max-total-profit FLOAT Select epochs on below total profit. + --min-objective FLOAT + Select epochs on above objective (- is added by default). + --max-objective FLOAT + Select epochs on below objective (- is added by default). --no-color Disable colorization of hyperopt results. May be useful if you are redirecting output to a file. --print-json Print best result detailization in JSON format. diff --git a/docs/webhook-config.md b/docs/webhook-config.md index 70a41dd46..db6d4d1ef 100644 --- a/docs/webhook-config.md +++ b/docs/webhook-config.md @@ -47,6 +47,7 @@ Different payloads can be configured for different events. Not all fields are ne The fields in `webhook.webhookbuy` are filled when the bot executes a buy. Parameters are filled using string.format. Possible parameters are: +* `trade_id` * `exchange` * `pair` * `limit` @@ -63,6 +64,7 @@ Possible parameters are: The fields in `webhook.webhookbuycancel` are filled when the bot cancels a buy order. Parameters are filled using string.format. Possible parameters are: +* `trade_id` * `exchange` * `pair` * `limit` @@ -79,6 +81,7 @@ Possible parameters are: The fields in `webhook.webhooksell` are filled when the bot sells a trade. Parameters are filled using string.format. Possible parameters are: +* `trade_id` * `exchange` * `pair` * `gain` @@ -100,6 +103,7 @@ Possible parameters are: The fields in `webhook.webhooksellcancel` are filled when the bot cancels a sell order. Parameters are filled using string.format. Possible parameters are: +* `trade_id` * `exchange` * `pair` * `gain` diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index e6f6f8167..4a87def88 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -73,6 +73,7 @@ ARGS_HYPEROPT_LIST = ["hyperopt_list_best", "hyperopt_list_profitable", "hyperopt_list_min_avg_time", "hyperopt_list_max_avg_time", "hyperopt_list_min_avg_profit", "hyperopt_list_max_avg_profit", "hyperopt_list_min_total_profit", "hyperopt_list_max_total_profit", + "hyperopt_list_min_objective", "hyperopt_list_max_objective", "print_colorized", "print_json", "hyperopt_list_no_details", "export_csv"] diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index 3ed2f81d1..8eb5c3ce8 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -455,37 +455,49 @@ AVAILABLE_CLI_OPTIONS = { ), "hyperopt_list_min_avg_time": Arg( '--min-avg-time', - help='Select epochs on above average time.', + help='Select epochs above average time.', type=float, metavar='FLOAT', ), "hyperopt_list_max_avg_time": Arg( '--max-avg-time', - help='Select epochs on under average time.', + help='Select epochs below average time.', type=float, metavar='FLOAT', ), "hyperopt_list_min_avg_profit": Arg( '--min-avg-profit', - help='Select epochs on above average profit.', + help='Select epochs above average profit.', type=float, metavar='FLOAT', ), "hyperopt_list_max_avg_profit": Arg( '--max-avg-profit', - help='Select epochs on below average profit.', + help='Select epochs below average profit.', type=float, metavar='FLOAT', ), "hyperopt_list_min_total_profit": Arg( '--min-total-profit', - help='Select epochs on above total profit.', + help='Select epochs above total profit.', type=float, metavar='FLOAT', ), "hyperopt_list_max_total_profit": Arg( '--max-total-profit', - help='Select epochs on below total profit.', + help='Select epochs below total profit.', + type=float, + metavar='FLOAT', + ), + "hyperopt_list_min_objective": Arg( + '--min-objective', + help='Select epochs above objective.', + type=float, + metavar='FLOAT', + ), + "hyperopt_list_max_objective": Arg( + '--max-objective', + help='Select epochs below objective.', type=float, metavar='FLOAT', ), diff --git a/freqtrade/commands/hyperopt_commands.py b/freqtrade/commands/hyperopt_commands.py index 517f47d16..4fae51e28 100755 --- a/freqtrade/commands/hyperopt_commands.py +++ b/freqtrade/commands/hyperopt_commands.py @@ -35,7 +35,9 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None: 'filter_min_avg_profit': config.get('hyperopt_list_min_avg_profit', None), 'filter_max_avg_profit': config.get('hyperopt_list_max_avg_profit', None), 'filter_min_total_profit': config.get('hyperopt_list_min_total_profit', None), - 'filter_max_total_profit': config.get('hyperopt_list_max_total_profit', None) + 'filter_max_total_profit': config.get('hyperopt_list_max_total_profit', None), + 'filter_min_objective': config.get('hyperopt_list_min_objective', None), + 'filter_max_objective': config.get('hyperopt_list_max_objective', None), } results_file = (config['user_data_dir'] / @@ -45,7 +47,7 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None: epochs = Hyperopt.load_previous_results(results_file) total_epochs = len(epochs) - epochs = _hyperopt_filter_epochs(epochs, filteroptions) + epochs = hyperopt_filter_epochs(epochs, filteroptions) if print_colorized: colorama_init(autoreset=True) @@ -92,14 +94,16 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None: 'filter_min_avg_profit': config.get('hyperopt_list_min_avg_profit', None), 'filter_max_avg_profit': config.get('hyperopt_list_max_avg_profit', None), 'filter_min_total_profit': config.get('hyperopt_list_min_total_profit', None), - 'filter_max_total_profit': config.get('hyperopt_list_max_total_profit', None) + 'filter_max_total_profit': config.get('hyperopt_list_max_total_profit', None), + 'filter_min_objective': config.get('hyperopt_list_min_objective', None), + 'filter_max_objective': config.get('hyperopt_list_max_objective', None) } # Previous evaluations epochs = Hyperopt.load_previous_results(results_file) total_epochs = len(epochs) - epochs = _hyperopt_filter_epochs(epochs, filteroptions) + epochs = hyperopt_filter_epochs(epochs, filteroptions) filtered_epochs = len(epochs) if n > filtered_epochs: @@ -119,7 +123,7 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None: header_str="Epoch details") -def _hyperopt_filter_epochs(epochs: List, filteroptions: dict) -> List: +def hyperopt_filter_epochs(epochs: List, filteroptions: dict) -> List: """ Filter our items from the list of hyperopt results """ @@ -127,6 +131,24 @@ def _hyperopt_filter_epochs(epochs: List, filteroptions: dict) -> List: epochs = [x for x in epochs if x['is_best']] if filteroptions['only_profitable']: epochs = [x for x in epochs if x['results_metrics']['profit'] > 0] + + epochs = _hyperopt_filter_epochs_trade_count(epochs, filteroptions) + + epochs = _hyperopt_filter_epochs_duration(epochs, filteroptions) + + epochs = _hyperopt_filter_epochs_profit(epochs, filteroptions) + + epochs = _hyperopt_filter_epochs_objective(epochs, filteroptions) + + logger.info(f"{len(epochs)} " + + ("best " if filteroptions['only_best'] else "") + + ("profitable " if filteroptions['only_profitable'] else "") + + "epochs found.") + return epochs + + +def _hyperopt_filter_epochs_trade_count(epochs: List, filteroptions: dict) -> List: + if filteroptions['filter_min_trades'] > 0: epochs = [ x for x in epochs @@ -137,6 +159,11 @@ def _hyperopt_filter_epochs(epochs: List, filteroptions: dict) -> List: x for x in epochs if x['results_metrics']['trade_count'] < filteroptions['filter_max_trades'] ] + return epochs + + +def _hyperopt_filter_epochs_duration(epochs: List, filteroptions: dict) -> List: + if filteroptions['filter_min_avg_time'] is not None: epochs = [x for x in epochs if x['results_metrics']['trade_count'] > 0] epochs = [ @@ -149,6 +176,12 @@ def _hyperopt_filter_epochs(epochs: List, filteroptions: dict) -> List: x for x in epochs if x['results_metrics']['duration'] < filteroptions['filter_max_avg_time'] ] + + return epochs + + +def _hyperopt_filter_epochs_profit(epochs: List, filteroptions: dict) -> List: + if filteroptions['filter_min_avg_profit'] is not None: epochs = [x for x in epochs if x['results_metrics']['trade_count'] > 0] epochs = [ @@ -173,10 +206,18 @@ def _hyperopt_filter_epochs(epochs: List, filteroptions: dict) -> List: x for x in epochs if x['results_metrics']['profit'] < filteroptions['filter_max_total_profit'] ] + return epochs - logger.info(f"{len(epochs)} " + - ("best " if filteroptions['only_best'] else "") + - ("profitable " if filteroptions['only_profitable'] else "") + - "epochs found.") + +def _hyperopt_filter_epochs_objective(epochs: List, filteroptions: dict) -> List: + + if filteroptions['filter_min_objective'] is not None: + epochs = [x for x in epochs if x['results_metrics']['trade_count'] > 0] + + epochs = [x for x in epochs if x['loss'] < filteroptions['filter_min_objective']] + if filteroptions['filter_max_objective'] is not None: + epochs = [x for x in epochs if x['results_metrics']['trade_count'] > 0] + + epochs = [x for x in epochs if x['loss'] > filteroptions['filter_max_objective']] return epochs diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 139e42084..08a600176 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -334,6 +334,12 @@ class Configuration: self._args_to_config(config, argname='hyperopt_list_max_total_profit', logstring='Parameter --max-total-profit detected: {}') + self._args_to_config(config, argname='hyperopt_list_min_objective', + logstring='Parameter --min-objective detected: {}') + + self._args_to_config(config, argname='hyperopt_list_max_objective', + logstring='Parameter --max-objective detected: {}') + self._args_to_config(config, argname='hyperopt_list_no_details', logstring='Parameter --no-details detected: {}') diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 8a5332475..1dadc6e16 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -156,7 +156,9 @@ CONF_SCHEMA = { 'emergencysell': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES}, 'stoploss': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES}, 'stoploss_on_exchange': {'type': 'boolean'}, - 'stoploss_on_exchange_interval': {'type': 'number'} + 'stoploss_on_exchange_interval': {'type': 'number'}, + 'stoploss_on_exchange_limit_ratio': {'type': 'number', 'minimum': 0.0, + 'maximum': 1.0} }, 'required': ['buy', 'sell', 'stoploss', 'stoploss_on_exchange'] }, diff --git a/freqtrade/edge/edge_positioning.py b/freqtrade/edge/edge_positioning.py index 41252ee51..1993eded3 100644 --- a/freqtrade/edge/edge_positioning.py +++ b/freqtrade/edge/edge_positioning.py @@ -281,8 +281,8 @@ class Edge: # # Removing Pumps if self.edge_config.get('remove_pumps', False): - results = results.groupby(['pair', 'stoploss']).apply( - lambda x: x[x['profit_abs'] < 2 * x['profit_abs'].std() + x['profit_abs'].mean()]) + results = results[results['profit_abs'] < 2 * results['profit_abs'].std() + + results['profit_abs'].mean()] ########################################################################## # Removing trades having a duration more than X minutes (set in config) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 08e84ee34..f2fe1d6ad 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -81,7 +81,7 @@ class Binance(Exchange): return order except ccxt.InsufficientFunds as e: raise ExchangeError( - f'Insufficient funds to create {ordertype} sell order on market {pair}.' + f'Insufficient funds to create {ordertype} sell order on market {pair}. ' f'Tried to sell amount {amount} at rate {rate}. ' f'Message: {e}') from e except ccxt.InvalidOrder as e: diff --git a/freqtrade/exchange/common.py b/freqtrade/exchange/common.py index 0610e8447..3bba9be72 100644 --- a/freqtrade/exchange/common.py +++ b/freqtrade/exchange/common.py @@ -107,12 +107,12 @@ def retrier_async(f): except TemporaryError as ex: logger.warning('%s() returned exception: "%s"', f.__name__, ex) if count > 0: + logger.warning('retrying %s() still for %s times', f.__name__, count) count -= 1 kwargs.update({'count': count}) - logger.warning('retrying %s() still for %s times', f.__name__, count) if isinstance(ex, DDosProtection): backoff_delay = calculate_backoff(count + 1, API_RETRY_COUNT) - logger.debug(f"Applying DDosProtection backoff delay: {backoff_delay}") + logger.info(f"Applying DDosProtection backoff delay: {backoff_delay}") await asyncio.sleep(backoff_delay) return await wrapper(*args, **kwargs) else: @@ -131,13 +131,13 @@ def retrier(_func=None, retries=API_RETRY_COUNT): except (TemporaryError, RetryableOrderError) as ex: logger.warning('%s() returned exception: "%s"', f.__name__, ex) if count > 0: + logger.warning('retrying %s() still for %s times', f.__name__, count) count -= 1 kwargs.update({'count': count}) - logger.warning('retrying %s() still for %s times', f.__name__, count) if isinstance(ex, DDosProtection) or isinstance(ex, RetryableOrderError): # increasing backoff backoff_delay = calculate_backoff(count + 1, retries) - logger.debug(f"Applying DDosProtection backoff delay: {backoff_delay}") + logger.info(f"Applying DDosProtection backoff delay: {backoff_delay}") time.sleep(backoff_delay) return wrapper(*args, **kwargs) else: diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 9858eb518..df47e7b10 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -187,6 +187,11 @@ class Exchange: def timeframes(self) -> List[str]: return list((self._api.timeframes or {}).keys()) + @property + def ohlcv_candle_limit(self) -> int: + """exchange ohlcv candle limit""" + return int(self._ohlcv_candle_limit) + @property def markets(self) -> Dict: """exchange ccxt markets""" @@ -253,8 +258,8 @@ class Exchange: api.urls['api'] = api.urls['test'] logger.info("Enabled Sandbox API on %s", name) else: - logger.warning(name, "No Sandbox URL in CCXT, exiting. " - "Please check your config.json") + logger.warning( + f"No Sandbox URL in CCXT for {name}, exiting. Please check your config.json") raise OperationalException(f'Exchange {name} does not provide a sandbox api') def _load_async_markets(self, reload: bool = False) -> None: @@ -521,13 +526,13 @@ class Exchange: except ccxt.InsufficientFunds as e: raise ExchangeError( - f'Insufficient funds to create {ordertype} {side} order on market {pair}.' + f'Insufficient funds to create {ordertype} {side} order on market {pair}. ' f'Tried to {side} amount {amount} at rate {rate}.' f'Message: {e}') from e except ccxt.InvalidOrder as e: raise ExchangeError( - f'Could not create {ordertype} {side} order on market {pair}.' - f'Tried to {side} amount {amount} at rate {rate}.' + f'Could not create {ordertype} {side} order on market {pair}. ' + f'Tried to {side} amount {amount} at rate {rate}. ' f'Message: {e}') from e except ccxt.DDoSProtection as e: raise DDosProtection(e) from e @@ -995,7 +1000,7 @@ class Exchange: if self.is_cancel_order_result_suitable(corder): return corder except InvalidOrderException: - logger.warning(f"Could not cancel order {order_id}.") + logger.warning(f"Could not cancel order {order_id} for {pair}.") try: order = self.fetch_order(order_id, pair) except InvalidOrderException: @@ -1004,7 +1009,7 @@ class Exchange: return order - @retrier + @retrier(retries=5) def fetch_order(self, order_id: str, pair: str) -> Dict: if self._config['dry_run']: try: @@ -1018,10 +1023,10 @@ class Exchange: return self._api.fetch_order(order_id, pair) except ccxt.OrderNotFound as e: raise RetryableOrderError( - f'Order not found (id: {order_id}). Message: {e}') from e + f'Order not found (pair: {pair} id: {order_id}). Message: {e}') from e except ccxt.InvalidOrder as e: raise InvalidOrderException( - f'Tried to get an invalid order (id: {order_id}). Message: {e}') from e + f'Tried to get an invalid order (pair: {pair} id: {order_id}). Message: {e}') from e except ccxt.DDoSProtection as e: raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index b75f77ca4..01e8267ad 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -78,7 +78,7 @@ class Ftx(Exchange): except ccxt.BaseError as e: raise OperationalException(e) from e - @retrier + @retrier(retries=5) def fetch_stoploss_order(self, order_id: str, pair: str) -> Dict: if self._config['dry_run']: try: diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 2ca4ba167..7b9d0f09b 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -89,7 +89,7 @@ class Kraken(Exchange): return order except ccxt.InsufficientFunds as e: raise ExchangeError( - f'Insufficient funds to create {ordertype} sell order on market {pair}.' + f'Insufficient funds to create {ordertype} sell order on market {pair}. ' f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. ' f'Message: {e}') from e except ccxt.InvalidOrder as e: diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 8c5b5b460..b4ef2b086 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -600,6 +600,7 @@ class FreqtradeBot: Sends rpc notification when a buy occured. """ msg = { + 'trade_id': trade.id, 'type': RPCMessageType.BUY_NOTIFICATION, 'exchange': self.exchange.name.capitalize(), 'pair': trade.pair, @@ -623,6 +624,7 @@ class FreqtradeBot: current_rate = self.get_buy_rate(trade.pair, False) msg = { + 'trade_id': trade.id, 'type': RPCMessageType.BUY_CANCEL_NOTIFICATION, 'exchange': self.exchange.name.capitalize(), 'pair': trade.pair, @@ -660,7 +662,7 @@ class FreqtradeBot: trades_closed += 1 except DependencyException as exception: - logger.warning('Unable to sell trade: %s', exception) + logger.warning('Unable to sell trade %s: %s', trade.pair, exception) # Updating wallets if any trade occured if trades_closed: @@ -827,10 +829,8 @@ class FreqtradeBot: return False # If buy order is fulfilled but there is no stoploss, we add a stoploss on exchange - if (not stoploss_order): - + if not stoploss_order: stoploss = self.edge.stoploss(pair=trade.pair) if self.edge else self.strategy.stoploss - stop_price = trade.open_rate * (1 + stoploss) if self.create_stoploss_order(trade=trade, stop_price=stop_price, rate=stop_price): @@ -978,6 +978,12 @@ class FreqtradeBot: reason = constants.CANCEL_REASON['TIMEOUT'] corder = self.exchange.cancel_order_with_result(trade.open_order_id, trade.pair, trade.amount) + # Avoid race condition where the order could not be cancelled coz its already filled. + # Simply bailing here is the only safe way - as this order will then be + # handled in the next iteration. + if corder.get('status') not in ('canceled', 'closed'): + logger.warning(f"Order {trade.open_order_id} for {trade.pair} not cancelled.") + return False else: # Order was cancelled already, so we can reuse the existing dict corder = order @@ -1153,6 +1159,7 @@ class FreqtradeBot: msg = { 'type': RPCMessageType.SELL_NOTIFICATION, + 'trade_id': trade.id, 'exchange': trade.exchange.capitalize(), 'pair': trade.pair, 'gain': gain, @@ -1195,6 +1202,7 @@ class FreqtradeBot: msg = { 'type': RPCMessageType.SELL_CANCEL_NOTIFICATION, + 'trade_id': trade.id, 'exchange': trade.exchange.capitalize(), 'pair': trade.pair, 'gain': gain, diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index e5014dd5a..214c92e0e 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -101,7 +101,7 @@ class Backtesting: if len(self.pairlists.whitelist) == 0: raise OperationalException("No pair in whitelist.") - if config.get('fee'): + if config.get('fee', None) is not None: self.fee = config['fee'] else: self.fee = self.exchange.get_fee(symbol=self.pairlists.whitelist[0]) diff --git a/freqtrade/pairlist/AgeFilter.py b/freqtrade/pairlist/AgeFilter.py index b489a59bc..7b6b126c3 100644 --- a/freqtrade/pairlist/AgeFilter.py +++ b/freqtrade/pairlist/AgeFilter.py @@ -5,6 +5,7 @@ import logging import arrow from typing import Any, Dict +from freqtrade.exceptions import OperationalException from freqtrade.misc import plural from freqtrade.pairlist.IPairList import IPairList @@ -23,6 +24,13 @@ class AgeFilter(IPairList): super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) self._min_days_listed = pairlistconfig.get('min_days_listed', 10) + + if self._min_days_listed < 1: + raise OperationalException("AgeFilter requires min_days_listed must be >= 1") + if self._min_days_listed > exchange.ohlcv_candle_limit: + raise OperationalException("AgeFilter requires min_days_listed must not exceed " + "exchange max request size " + f"({exchange.ohlcv_candle_limit})") self._enabled = self._min_days_listed >= 1 @property diff --git a/freqtrade/pairlist/PriceFilter.py b/freqtrade/pairlist/PriceFilter.py index 29dd88a76..b3b2f43dc 100644 --- a/freqtrade/pairlist/PriceFilter.py +++ b/freqtrade/pairlist/PriceFilter.py @@ -18,7 +18,11 @@ class PriceFilter(IPairList): super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) self._low_price_ratio = pairlistconfig.get('low_price_ratio', 0) - self._enabled = self._low_price_ratio != 0 + self._min_price = pairlistconfig.get('min_price', 0) + self._max_price = pairlistconfig.get('max_price', 0) + self._enabled = ((self._low_price_ratio != 0) or + (self._min_price != 0) or + (self._max_price != 0)) @property def needstickers(self) -> bool: @@ -33,7 +37,18 @@ class PriceFilter(IPairList): """ Short whitelist method description - used for startup-messages """ - return f"{self.name} - Filtering pairs priced below {self._low_price_ratio * 100}%." + active_price_filters = [] + if self._low_price_ratio != 0: + active_price_filters.append(f"below {self._low_price_ratio * 100}%") + if self._min_price != 0: + active_price_filters.append(f"below {self._min_price:.8f}") + if self._max_price != 0: + active_price_filters.append(f"above {self._max_price:.8f}") + + if len(active_price_filters): + return f"{self.name} - Filtering pairs priced {' or '.join(active_price_filters)}." + + return f"{self.name} - No price filters configured." def _validate_pair(self, ticker) -> bool: """ @@ -41,15 +56,33 @@ class PriceFilter(IPairList): :param ticker: ticker dict as returned from ccxt.load_markets() :return: True if the pair can stay, false if it should be removed """ - if ticker['last'] is None: + if ticker['last'] is None or ticker['last'] == 0: self.log_on_refresh(logger.info, f"Removed {ticker['symbol']} from whitelist, because " "ticker['last'] is empty (Usually no trade in the last 24h).") return False - compare = self._exchange.price_get_one_pip(ticker['symbol'], ticker['last']) - changeperc = compare / ticker['last'] - if changeperc > self._low_price_ratio: - self.log_on_refresh(logger.info, f"Removed {ticker['symbol']} from whitelist, " - f"because 1 unit is {changeperc * 100:.3f}%") - return False + + # Perform low_price_ratio check. + if self._low_price_ratio != 0: + compare = self._exchange.price_get_one_pip(ticker['symbol'], ticker['last']) + changeperc = compare / ticker['last'] + if changeperc > self._low_price_ratio: + self.log_on_refresh(logger.info, f"Removed {ticker['symbol']} from whitelist, " + f"because 1 unit is {changeperc * 100:.3f}%") + return False + + # Perform min_price check. + if self._min_price != 0: + if ticker['last'] < self._min_price: + self.log_on_refresh(logger.info, f"Removed {ticker['symbol']} from whitelist, " + f"because last price < {self._min_price:.8f}") + return False + + # Perform max_price check. + if self._max_price != 0: + if ticker['last'] > self._max_price: + self.log_on_refresh(logger.info, f"Removed {ticker['symbol']} from whitelist, " + f"because last price > {self._max_price:.8f}") + return False + return True diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index e8b0b4938..a933c6a76 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -10,11 +10,13 @@ from freqtrade.data.btanalysis import (calculate_max_drawdown, create_cum_profit, extract_trades_of_period, load_trades) from freqtrade.data.converter import trim_dataframe +from freqtrade.data.dataprovider import DataProvider from freqtrade.data.history import load_data from freqtrade.exceptions import OperationalException from freqtrade.exchange import timeframe_to_prev_date from freqtrade.misc import pair_to_filename -from freqtrade.resolvers import StrategyResolver +from freqtrade.resolvers import ExchangeResolver, StrategyResolver +from freqtrade.strategy import IStrategy logger = logging.getLogger(__name__) @@ -467,6 +469,8 @@ def load_and_plot_trades(config: Dict[str, Any]): """ strategy = StrategyResolver.load_strategy(config) + exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config) + IStrategy.dp = DataProvider(config, exchange) plot_elements = init_plotscript(config) trades = plot_elements['trades'] pair_counter = 0 diff --git a/freqtrade/resolvers/hyperopt_resolver.py b/freqtrade/resolvers/hyperopt_resolver.py index 633363134..abbfee6ed 100644 --- a/freqtrade/resolvers/hyperopt_resolver.py +++ b/freqtrade/resolvers/hyperopt_resolver.py @@ -42,14 +42,14 @@ class HyperOptResolver(IResolver): extra_dir=config.get('hyperopt_path')) if not hasattr(hyperopt, 'populate_indicators'): - logger.warning("Hyperopt class does not provide populate_indicators() method. " - "Using populate_indicators from the strategy.") + logger.info("Hyperopt class does not provide populate_indicators() method. " + "Using populate_indicators from the strategy.") if not hasattr(hyperopt, 'populate_buy_trend'): - logger.warning("Hyperopt class does not provide populate_buy_trend() method. " - "Using populate_buy_trend from the strategy.") + logger.info("Hyperopt class does not provide populate_buy_trend() method. " + "Using populate_buy_trend from the strategy.") if not hasattr(hyperopt, 'populate_sell_trend'): - logger.warning("Hyperopt class does not provide populate_sell_trend() method. " - "Using populate_sell_trend from the strategy.") + logger.info("Hyperopt class does not provide populate_sell_trend() method. " + "Using populate_sell_trend from the strategy.") return hyperopt diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 351842e10..06926ac35 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -56,7 +56,7 @@ def require_login(func: Callable[[Any, Any], Any]): # Type should really be Callable[[ApiServer], Any], but that will create a circular dependency -def rpc_catch_errors(func: Callable[[Any], Any]): +def rpc_catch_errors(func: Callable[..., Any]): def func_wrapper(obj, *args, **kwargs): @@ -200,6 +200,8 @@ class ApiServer(RPC): view_func=self._ping, methods=['GET']) self.app.add_url_rule(f'{BASE_URI}/trades', 'trades', view_func=self._trades, methods=['GET']) + self.app.add_url_rule(f'{BASE_URI}/trades/', 'trades_delete', + view_func=self._trades_delete, methods=['DELETE']) # Combined actions and infos self.app.add_url_rule(f'{BASE_URI}/blacklist', 'blacklist', view_func=self._blacklist, methods=['GET', 'POST']) @@ -424,6 +426,19 @@ class ApiServer(RPC): results = self._rpc_trade_history(limit) return self.rest_dump(results) + @require_login + @rpc_catch_errors + def _trades_delete(self, tradeid): + """ + Handler for DELETE /trades/ endpoint. + Removes the trade from the database (tries to cancel open orders first!) + get: + param: + tradeid: Numeric trade-id assigned to the trade. + """ + result = self._rpc_delete(tradeid) + return self.rest_dump(result) + @require_login @rpc_catch_errors def _whitelist(self): diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index c73fcbf54..8a1ff7e96 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -6,14 +6,14 @@ from abc import abstractmethod from datetime import date, datetime, timedelta from enum import Enum from math import isnan -from typing import Any, Dict, List, Optional, Tuple +from typing import Any, Dict, List, Optional, Tuple, Union import arrow from numpy import NAN, mean -from freqtrade.exceptions import ExchangeError, PricingError - -from freqtrade.exchange import timeframe_to_msecs, timeframe_to_minutes +from freqtrade.exceptions import (ExchangeError, InvalidOrderException, + PricingError) +from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs from freqtrade.misc import shorten_date from freqtrade.persistence import Trade from freqtrade.rpc.fiat_convert import CryptoToFiatConverter @@ -252,9 +252,10 @@ class RPC: def _rpc_trade_history(self, limit: int) -> Dict: """ Returns the X last trades """ if limit > 0: - trades = Trade.get_trades().order_by(Trade.id.desc()).limit(limit) + trades = Trade.get_trades([Trade.is_open.is_(False)]).order_by( + Trade.id.desc()).limit(limit) else: - trades = Trade.get_trades().order_by(Trade.id.desc()).all() + trades = Trade.get_trades([Trade.is_open.is_(False)]).order_by(Trade.id.desc()).all() output = [trade.to_json() for trade in trades] @@ -523,7 +524,7 @@ class RPC: # check if valid pair # check if pair already has an open pair - trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair.is_(pair)]).first() + trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first() if trade: raise RPCException(f'position for {pair} already open - id: {trade.id}') @@ -532,11 +533,51 @@ class RPC: # execute buy if self._freqtrade.execute_buy(pair, stakeamount, price): - trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair.is_(pair)]).first() + trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first() return trade else: return None + def _rpc_delete(self, trade_id: str) -> Dict[str, Union[str, int]]: + """ + Handler for delete . + Delete the given trade and close eventually existing open orders. + """ + with self._freqtrade._sell_lock: + c_count = 0 + trade = Trade.get_trades(trade_filter=[Trade.id == trade_id]).first() + if not trade: + logger.warning('delete trade: Invalid argument received') + raise RPCException('invalid argument') + + # Try cancelling regular order if that exists + if trade.open_order_id: + try: + self._freqtrade.exchange.cancel_order(trade.open_order_id, trade.pair) + c_count += 1 + except (ExchangeError, InvalidOrderException): + pass + + # cancel stoploss on exchange ... + if (self._freqtrade.strategy.order_types.get('stoploss_on_exchange') + and trade.stoploss_order_id): + try: + self._freqtrade.exchange.cancel_stoploss_order(trade.stoploss_order_id, + trade.pair) + c_count += 1 + except (ExchangeError, InvalidOrderException): + pass + + Trade.session.delete(trade) + Trade.session.flush() + self._freqtrade.wallets.update() + return { + 'result': 'success', + 'trade_id': trade_id, + 'result_msg': f'Deleted trade {trade_id}. Closed {c_count} open orders.', + 'cancel_order_count': c_count, + } + def _rpc_performance(self) -> List[Dict[str, Any]]: """ Handler for performance. diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 13cc1afaf..f1d3cde21 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -5,6 +5,7 @@ This module manage Telegram communication """ import json import logging +import arrow from typing import Any, Callable, Dict from tabulate import tabulate @@ -92,6 +93,8 @@ class Telegram(RPC): CommandHandler('stop', self._stop), CommandHandler('forcesell', self._forcesell), CommandHandler('forcebuy', self._forcebuy), + CommandHandler('trades', self._trades), + CommandHandler('delete', self._delete_trade), CommandHandler('performance', self._performance), CommandHandler('daily', self._daily), CommandHandler('count', self._count), @@ -496,6 +499,62 @@ class Telegram(RPC): except RPCException as e: self._send_msg(str(e)) + @authorized_only + def _trades(self, update: Update, context: CallbackContext) -> None: + """ + Handler for /trades + Returns last n recent trades. + :param bot: telegram bot + :param update: message update + :return: None + """ + stake_cur = self._config['stake_currency'] + try: + nrecent = int(context.args[0]) + except (TypeError, ValueError, IndexError): + nrecent = 10 + try: + trades = self._rpc_trade_history( + nrecent + ) + trades_tab = tabulate( + [[arrow.get(trade['open_date']).humanize(), + trade['pair'], + f"{(100 * trade['close_profit']):.2f}% ({trade['close_profit_abs']})"] + for trade in trades['trades']], + headers=[ + 'Open Date', + 'Pair', + f'Profit ({stake_cur})', + ], + tablefmt='simple') + message = (f"{min(trades['trades_count'], nrecent)} recent trades:\n" + + (f"
{trades_tab}
" if trades['trades_count'] > 0 else '')) + self._send_msg(message, parse_mode=ParseMode.HTML) + except RPCException as e: + self._send_msg(str(e)) + + @authorized_only + def _delete_trade(self, update: Update, context: CallbackContext) -> None: + """ + Handler for /delete . + Delete the given trade + :param bot: telegram bot + :param update: message update + :return: None + """ + + trade_id = context.args[0] if len(context.args) > 0 else None + try: + msg = self._rpc_delete(trade_id) + self._send_msg(( + '`{result_msg}`\n' + 'Please make sure to take care of this asset on the exchange manually.' + ).format(**msg)) + + except RPCException as e: + self._send_msg(str(e)) + @authorized_only def _performance(self, update: Update, context: CallbackContext) -> None: """ @@ -609,10 +668,12 @@ class Telegram(RPC): " *table :* `will display trades in a table`\n" " `pending buy orders are marked with an asterisk (*)`\n" " `pending sell orders are marked with a double asterisk (**)`\n" + "*/trades [limit]:* `Lists last closed trades (limited to 10 by default)`\n" "*/profit:* `Lists cumulative profit from all finished trades`\n" "*/forcesell |all:* `Instantly sells the given trade or all trades, " "regardless of profit`\n" f"{forcebuy_text if self._config.get('forcebuy_enable', False) else ''}" + "*/delete :* `Instantly delete the given trade in the database`\n" "*/performance:* `Show performance of each finished trade grouped by pair`\n" "*/daily :* `Shows profit or loss per day, over the last n days`\n" "*/count:* `Show number of trades running compared to allowed number of trades`" diff --git a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 index c7ce41bb7..5ca6e6971 100644 --- a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 +++ b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 @@ -34,7 +34,7 @@ def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: f """ return True -def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float, +def confirm_trade_exit(self, pair: str, trade: 'Trade', order_type: str, amount: float, rate: float, time_in_force: str, sell_reason: str, **kwargs) -> bool: """ Called right before placing a regular sell order. diff --git a/requirements-common.txt b/requirements-common.txt index 1cfc44ab4..b7e71eada 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -1,12 +1,12 @@ # requirements without requirements installable via conda # mainly used for Raspberry pi installs -ccxt==1.30.93 +ccxt==1.32.88 SQLAlchemy==1.3.18 python-telegram-bot==12.8 -arrow==0.15.7 +arrow==0.15.8 cachetools==4.1.1 requests==2.24.0 -urllib3==1.25.9 +urllib3==1.25.10 wrapt==1.12.1 jsonschema==3.2.0 TA-Lib==0.4.18 diff --git a/requirements-dev.txt b/requirements-dev.txt index 9f9be638d..c02a439d3 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -8,7 +8,7 @@ flake8==3.8.3 flake8-type-annotations==0.1.0 flake8-tidy-imports==4.1.0 mypy==0.782 -pytest==5.4.3 +pytest==6.0.1 pytest-asyncio==0.14.0 pytest-cov==2.10.0 pytest-mock==3.2.0 diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index 4773d9877..ce08f08e0 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -2,7 +2,7 @@ -r requirements.txt # Required for hyperopt -scipy==1.5.1 +scipy==1.5.2 scikit-learn==0.23.1 scikit-optimize==0.7.4 filelock==3.0.12 diff --git a/requirements-plot.txt b/requirements-plot.txt index ec5af3dbf..51d14d636 100644 --- a/requirements-plot.txt +++ b/requirements-plot.txt @@ -1,5 +1,5 @@ # Include all requirements to run the bot. -r requirements.txt -plotly==4.8.2 +plotly==4.9.0 diff --git a/requirements.txt b/requirements.txt index 1e61d165f..d65f90325 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ # Load common requirements -r requirements-common.txt -numpy==1.19.0 -pandas==1.0.5 +numpy==1.19.1 +pandas==1.1.0 diff --git a/scripts/rest_client.py b/scripts/rest_client.py index 1f96bcb69..51ea596f6 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -62,6 +62,9 @@ class FtRestClient(): def _get(self, apipath, params: dict = None): return self._call("GET", apipath, params=params) + def _delete(self, apipath, params: dict = None): + return self._call("DELETE", apipath, params=params) + def _post(self, apipath, params: dict = None, data: dict = None): return self._call("POST", apipath, params=params, data=data) @@ -164,6 +167,15 @@ class FtRestClient(): """ return self._get("trades", params={"limit": limit} if limit else 0) + def delete_trade(self, trade_id): + """Delete trade from the database. + Tries to close open orders. Requires manual handling of this asset on the exchange. + + :param trade_id: Deletes the trade with this ID from the database. + :return: json object + """ + return self._delete("trades/{}".format(trade_id)) + def whitelist(self): """Show the current whitelist. diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index ffced956d..69d80d2cd 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -736,7 +736,7 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results): args = [ "hyperopt-list", - "--no-details" + "--no-details", ] pargs = get_args(args) pargs['config'] = None @@ -749,7 +749,7 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results): args = [ "hyperopt-list", "--best", - "--no-details" + "--no-details", ] pargs = get_args(args) pargs['config'] = None @@ -763,7 +763,7 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results): args = [ "hyperopt-list", "--profitable", - "--no-details" + "--no-details", ] pargs = get_args(args) pargs['config'] = None @@ -776,7 +776,7 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results): " 11/12", " 12/12"]) args = [ "hyperopt-list", - "--profitable" + "--profitable", ] pargs = get_args(args) pargs['config'] = None @@ -792,7 +792,7 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results): "hyperopt-list", "--no-details", "--no-color", - "--min-trades", "20" + "--min-trades", "20", ] pargs = get_args(args) pargs['config'] = None @@ -806,7 +806,7 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results): "hyperopt-list", "--profitable", "--no-details", - "--max-trades", "20" + "--max-trades", "20", ] pargs = get_args(args) pargs['config'] = None @@ -821,7 +821,7 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results): "hyperopt-list", "--profitable", "--no-details", - "--min-avg-profit", "0.11" + "--min-avg-profit", "0.11", ] pargs = get_args(args) pargs['config'] = None @@ -835,7 +835,7 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results): args = [ "hyperopt-list", "--no-details", - "--max-avg-profit", "0.10" + "--max-avg-profit", "0.10", ] pargs = get_args(args) pargs['config'] = None @@ -849,7 +849,7 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results): args = [ "hyperopt-list", "--no-details", - "--min-total-profit", "0.4" + "--min-total-profit", "0.4", ] pargs = get_args(args) pargs['config'] = None @@ -863,7 +863,35 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results): args = [ "hyperopt-list", "--no-details", - "--max-total-profit", "0.4" + "--max-total-profit", "0.4", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 1/12", " 2/12", " 3/12", " 5/12", " 6/12", " 7/12", " 8/12", + " 9/12", " 11/12"]) + assert all(x not in captured.out + for x in [" 4/12", " 10/12", " 12/12"]) + args = [ + "hyperopt-list", + "--no-details", + "--min-objective", "0.1", + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 10/12"]) + assert all(x not in captured.out + for x in [" 1/12", " 2/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", + " 9/12", " 11/12", " 12/12"]) + args = [ + "hyperopt-list", + "--no-details", + "--max-objective", "0.1", ] pargs = get_args(args) pargs['config'] = None @@ -878,7 +906,7 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results): "hyperopt-list", "--profitable", "--no-details", - "--min-avg-time", "2000" + "--min-avg-time", "2000", ] pargs = get_args(args) pargs['config'] = None @@ -892,7 +920,7 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results): args = [ "hyperopt-list", "--no-details", - "--max-avg-time", "1500" + "--max-avg-time", "1500", ] pargs = get_args(args) pargs['config'] = None @@ -906,7 +934,7 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results): args = [ "hyperopt-list", "--no-details", - "--export-csv", "test_file.csv" + "--export-csv", "test_file.csv", ] pargs = get_args(args) pargs['config'] = None @@ -1089,7 +1117,7 @@ def test_show_trades(mocker, fee, capsys, caplog): pargs = get_args(args) pargs['config'] = None start_show_trades(pargs) - assert log_has("Printing 3 Trades: ", caplog) + assert log_has("Printing 4 Trades: ", caplog) captured = capsys.readouterr() assert "Trade(id=1" in captured.out assert "Trade(id=2" in captured.out diff --git a/tests/conftest.py b/tests/conftest.py index 8501a98b9..1ac8256a8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -201,6 +201,20 @@ def create_mock_trades(fee): ) Trade.session.add(trade) + trade = Trade( + pair='XRP/BTC', + stake_amount=0.001, + amount=123.0, + fee_open=fee.return_value, + fee_close=fee.return_value, + open_rate=0.05, + close_rate=0.06, + close_profit=0.01, + exchange='bittrex', + is_open=False, + ) + Trade.session.add(trade) + # Simulate prod entry trade = Trade( pair='ETC/BTC', @@ -664,7 +678,8 @@ def shitcoinmarkets(markets): Fixture with shitcoin markets - used to test filters in pairlists """ shitmarkets = deepcopy(markets) - shitmarkets.update({'HOT/BTC': { + shitmarkets.update({ + 'HOT/BTC': { 'id': 'HOTBTC', 'symbol': 'HOT/BTC', 'base': 'HOT', @@ -769,7 +784,32 @@ def shitcoinmarkets(markets): "spot": True, "future": False, "active": True - }, + }, + 'ADADOUBLE/USDT': { + "percentage": True, + "tierBased": False, + "taker": 0.001, + "maker": 0.001, + "precision": { + "base": 8, + "quote": 8, + "amount": 2, + "price": 4 + }, + "limits": { + }, + "id": "ADADOUBLEUSDT", + "symbol": "ADADOUBLE/USDT", + "base": "ADADOUBLE", + "quote": "USDT", + "baseId": "ADADOUBLE", + "quoteId": "USDT", + "info": {}, + "type": "spot", + "spot": True, + "future": False, + "active": True + }, }) return shitmarkets @@ -1391,6 +1431,28 @@ def tickers(): "quoteVolume": 0.0, "info": {} }, + "ADADOUBLE/USDT": { + "symbol": "ADADOUBLE/USDT", + "timestamp": 1580469388244, + "datetime": "2020-01-31T11:16:28.244Z", + "high": None, + "low": None, + "bid": 0.7305, + "bidVolume": None, + "ask": 0.7342, + "askVolume": None, + "vwap": None, + "open": None, + "close": None, + "last": 0, + "previousClose": None, + "change": None, + "percentage": 2.628, + "average": None, + "baseVolume": 0.0, + "quoteVolume": 0.0, + "info": {} + }, }) diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index b65db7fd8..718c02f05 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -43,7 +43,7 @@ def test_load_trades_from_db(default_conf, fee, mocker): trades = load_trades_from_db(db_url=default_conf['db_url']) assert init_mock.call_count == 1 - assert len(trades) == 3 + assert len(trades) == 4 assert isinstance(trades, DataFrame) assert "pair" in trades.columns assert "open_time" in trades.columns diff --git a/tests/edge/test_edge.py b/tests/edge/test_edge.py index cf9cb6fe1..7373778ad 100644 --- a/tests/edge/test_edge.py +++ b/tests/edge/test_edge.py @@ -409,3 +409,98 @@ def test_process_expectancy(mocker, edge_conf, fee, risk_reward_ratio, expectanc final = edge._process_expectancy(trades_df) assert len(final) == 0 assert isinstance(final, dict) + + +def test_process_expectancy_remove_pumps(mocker, edge_conf, fee,): + edge_conf['edge']['min_trade_number'] = 2 + edge_conf['edge']['remove_pumps'] = True + freqtrade = get_patched_freqtradebot(mocker, edge_conf) + + freqtrade.exchange.get_fee = fee + edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy) + + trades = [ + {'pair': 'TEST/BTC', + 'stoploss': -0.9, + 'profit_percent': '', + 'profit_abs': '', + 'open_time': np.datetime64('2018-10-03T00:05:00.000000000'), + 'close_time': np.datetime64('2018-10-03T00:10:00.000000000'), + 'open_index': 1, + 'close_index': 1, + 'trade_duration': '', + 'open_rate': 17, + 'close_rate': 15, + 'exit_type': 'sell_signal'}, + + {'pair': 'TEST/BTC', + 'stoploss': -0.9, + 'profit_percent': '', + 'profit_abs': '', + 'open_time': np.datetime64('2018-10-03T00:20:00.000000000'), + 'close_time': np.datetime64('2018-10-03T00:25:00.000000000'), + 'open_index': 4, + 'close_index': 4, + 'trade_duration': '', + 'open_rate': 20, + 'close_rate': 10, + 'exit_type': 'sell_signal'}, + {'pair': 'TEST/BTC', + 'stoploss': -0.9, + 'profit_percent': '', + 'profit_abs': '', + 'open_time': np.datetime64('2018-10-03T00:20:00.000000000'), + 'close_time': np.datetime64('2018-10-03T00:25:00.000000000'), + 'open_index': 4, + 'close_index': 4, + 'trade_duration': '', + 'open_rate': 20, + 'close_rate': 10, + 'exit_type': 'sell_signal'}, + {'pair': 'TEST/BTC', + 'stoploss': -0.9, + 'profit_percent': '', + 'profit_abs': '', + 'open_time': np.datetime64('2018-10-03T00:20:00.000000000'), + 'close_time': np.datetime64('2018-10-03T00:25:00.000000000'), + 'open_index': 4, + 'close_index': 4, + 'trade_duration': '', + 'open_rate': 20, + 'close_rate': 10, + 'exit_type': 'sell_signal'}, + {'pair': 'TEST/BTC', + 'stoploss': -0.9, + 'profit_percent': '', + 'profit_abs': '', + 'open_time': np.datetime64('2018-10-03T00:20:00.000000000'), + 'close_time': np.datetime64('2018-10-03T00:25:00.000000000'), + 'open_index': 4, + 'close_index': 4, + 'trade_duration': '', + 'open_rate': 20, + 'close_rate': 10, + 'exit_type': 'sell_signal'}, + + {'pair': 'TEST/BTC', + 'stoploss': -0.9, + 'profit_percent': '', + 'profit_abs': '', + 'open_time': np.datetime64('2018-10-03T00:30:00.000000000'), + 'close_time': np.datetime64('2018-10-03T00:40:00.000000000'), + 'open_index': 6, + 'close_index': 7, + 'trade_duration': '', + 'open_rate': 26, + 'close_rate': 134, + 'exit_type': 'sell_signal'} + ] + + trades_df = DataFrame(trades) + trades_df = edge._fill_calculable_fields(trades_df) + final = edge._process_expectancy(trades_df) + + assert 'TEST/BTC' in final + assert final['TEST/BTC'].stoploss == -0.9 + assert final['TEST/BTC'].nb_trades == len(trades_df) - 1 + assert round(final['TEST/BTC'].winrate, 10) == 0.0 diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 251f257f7..350c2d3cb 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -714,13 +714,13 @@ def test_validate_order_types(default_conf, mocker): mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') mocker.patch('freqtrade.exchange.Exchange.name', 'Bittrex') + default_conf['order_types'] = { 'buy': 'limit', 'sell': 'limit', 'stoploss': 'market', 'stoploss_on_exchange': False } - Exchange(default_conf) type(api_mock).has = PropertyMock(return_value={'createMarketOrder': False}) @@ -730,9 +730,8 @@ def test_validate_order_types(default_conf, mocker): 'buy': 'limit', 'sell': 'limit', 'stoploss': 'market', - 'stoploss_on_exchange': 'false' + 'stoploss_on_exchange': False } - with pytest.raises(OperationalException, match=r'Exchange .* does not support market orders.'): Exchange(default_conf) @@ -743,7 +742,6 @@ def test_validate_order_types(default_conf, mocker): 'stoploss': 'limit', 'stoploss_on_exchange': True } - with pytest.raises(OperationalException, match=r'On exchange stoploss is not supported for .*'): Exchange(default_conf) @@ -1820,7 +1818,7 @@ def test_cancel_order_with_result_error(default_conf, mocker, exchange_name, cap res = exchange.cancel_order_with_result('1234', 'ETH/BTC', 1541) assert isinstance(res, dict) - assert log_has("Could not cancel order 1234.", caplog) + assert log_has("Could not cancel order 1234 for ETH/BTC.", caplog) assert log_has("Could not fetch cancelled order 1234.", caplog) assert res['amount'] == 1541 @@ -1898,10 +1896,10 @@ def test_fetch_order(default_conf, mocker, exchange_name): assert tm.call_args_list[1][0][0] == 2 assert tm.call_args_list[2][0][0] == 5 assert tm.call_args_list[3][0][0] == 10 - assert api_mock.fetch_order.call_count == API_RETRY_COUNT + 1 + assert api_mock.fetch_order.call_count == 6 ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, - 'fetch_order', 'fetch_order', + 'fetch_order', 'fetch_order', retries=6, order_id='_', pair='TKN/BTC') @@ -1934,6 +1932,7 @@ def test_fetch_stoploss_order(default_conf, mocker, exchange_name): ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, 'fetch_stoploss_order', 'fetch_order', + retries=6, order_id='_', pair='TKN/BTC') @@ -2317,6 +2316,18 @@ def test_calculate_fee_rate(mocker, default_conf, order, expected) -> None: (3, 3, 1), (0, 1, 2), (1, 1, 1), + (0, 4, 17), + (1, 4, 10), + (2, 4, 5), + (3, 4, 2), + (4, 4, 1), + (0, 5, 26), + (1, 5, 17), + (2, 5, 10), + (3, 5, 5), + (4, 5, 2), + (5, 5, 1), + ]) def test_calculate_backoff(retrycount, max_retries, expected): assert calculate_backoff(retrycount, max_retries) == expected diff --git a/tests/exchange/test_ftx.py b/tests/exchange/test_ftx.py index eb7d83be3..bed92d276 100644 --- a/tests/exchange/test_ftx.py +++ b/tests/exchange/test_ftx.py @@ -154,4 +154,5 @@ def test_fetch_stoploss_order(default_conf, mocker): ccxt_exceptionhandlers(mocker, default_conf, api_mock, 'ftx', 'fetch_stoploss_order', 'fetch_orders', + retries=6, order_id='_', pair='TKN/BTC') diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 67da38648..caa40fe84 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -308,6 +308,11 @@ def test_data_with_fee(default_conf, mocker, testdatadir) -> None: assert backtesting.fee == 0.1234 assert fee_mock.call_count == 0 + default_conf['fee'] = 0.0 + backtesting = Backtesting(default_conf) + assert backtesting.fee == 0.0 + assert fee_mock.call_count == 0 + def test_data_to_dataframe_bt(default_conf, mocker, testdatadir) -> None: patch_exchange(mocker) diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index a2644fe8c..efe4a784b 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -235,7 +235,7 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "bidVolume"}], "BTC", ['HOT/BTC', 'FUEL/BTC', 'XRP/BTC', 'LTC/BTC', 'TKN/BTC']), ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}], - "USDT", ['ETH/USDT', 'NANO/USDT', 'ADAHALF/USDT']), + "USDT", ['ETH/USDT', 'NANO/USDT', 'ADAHALF/USDT', 'ADADOUBLE/USDT']), # No pair for ETH, VolumePairList ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}], "ETH", []), @@ -275,11 +275,16 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, {"method": "PriceFilter", "low_price_ratio": 0.03}], "USDT", ['ETH/USDT', 'NANO/USDT']), - # Hot is removed by precision_filter, Fuel by low_price_filter. + # Hot is removed by precision_filter, Fuel by low_price_ratio, Ripple by min_price. ([{"method": "VolumePairList", "number_assets": 6, "sort_key": "quoteVolume"}, {"method": "PrecisionFilter"}, - {"method": "PriceFilter", "low_price_ratio": 0.02}], - "BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'XRP/BTC']), + {"method": "PriceFilter", "low_price_ratio": 0.02, "min_price": 0.01}], + "BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC']), + # Hot is removed by precision_filter, Fuel by low_price_ratio, Ethereum by max_price. + ([{"method": "VolumePairList", "number_assets": 6, "sort_key": "quoteVolume"}, + {"method": "PrecisionFilter"}, + {"method": "PriceFilter", "low_price_ratio": 0.02, "max_price": 0.05}], + "BTC", ['TKN/BTC', 'LTC/BTC', 'XRP/BTC']), # HOT and XRP are removed because below 1250 quoteVolume ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume", "min_value": 1250}], @@ -298,11 +303,11 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): # ShuffleFilter ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, {"method": "ShuffleFilter", "seed": 77}], - "USDT", ['ETH/USDT', 'ADAHALF/USDT', 'NANO/USDT']), + "USDT", ['ADADOUBLE/USDT', 'ETH/USDT', 'NANO/USDT', 'ADAHALF/USDT']), # ShuffleFilter, other seed ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, {"method": "ShuffleFilter", "seed": 42}], - "USDT", ['NANO/USDT', 'ETH/USDT', 'ADAHALF/USDT']), + "USDT", ['ADAHALF/USDT', 'NANO/USDT', 'ADADOUBLE/USDT', 'ETH/USDT']), # ShuffleFilter, no seed ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, {"method": "ShuffleFilter"}], @@ -319,7 +324,7 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): "BTC", 'filter_at_the_beginning'), # OperationalException expected # PriceFilter after StaticPairList ([{"method": "StaticPairList"}, - {"method": "PriceFilter", "low_price_ratio": 0.02}], + {"method": "PriceFilter", "low_price_ratio": 0.02, "min_price": 0.000001, "max_price": 0.1}], "BTC", ['ETH/BTC', 'TKN/BTC']), # PriceFilter only ([{"method": "PriceFilter", "low_price_ratio": 0.02}], @@ -342,6 +347,9 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "bidVolume"}, {"method": "StaticPairList"}], "BTC", 'static_in_the_middle'), + ([{"method": "VolumePairList", "number_assets": 20, "sort_key": "quoteVolume"}, + {"method": "PriceFilter", "low_price_ratio": 0.02}], + "USDT", ['ETH/USDT', 'NANO/USDT']), ]) def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, tickers, ohlcv_history_list, pairlists, base_currency, @@ -396,6 +404,10 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t r'would be <= stop limit.*', caplog) if pairlist['method'] == 'PriceFilter' and whitelist_result: assert (log_has_re(r'^Removed .* from whitelist, because 1 unit is .*%$', caplog) or + log_has_re(r'^Removed .* from whitelist, ' + r'because last price < .*%$', caplog) or + log_has_re(r'^Removed .* from whitelist, ' + r'because last price > .*%$', caplog) or log_has_re(r"^Removed .* from whitelist, because ticker\['last'\] " r"is empty.*", caplog)) if pairlist['method'] == 'VolumePairList': @@ -524,6 +536,37 @@ def test_volumepairlist_caching(mocker, markets, whitelist_conf, tickers): assert freqtrade.pairlists._pairlist_handlers[0]._last_refresh == lrf +def test_agefilter_min_days_listed_too_small(mocker, default_conf, markets, tickers, caplog): + default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}, + {'method': 'AgeFilter', 'min_days_listed': -1}] + + mocker.patch.multiple('freqtrade.exchange.Exchange', + markets=PropertyMock(return_value=markets), + exchange_has=MagicMock(return_value=True), + get_tickers=tickers + ) + + with pytest.raises(OperationalException, + match=r'AgeFilter requires min_days_listed must be >= 1'): + get_patched_freqtradebot(mocker, default_conf) + + +def test_agefilter_min_days_listed_too_large(mocker, default_conf, markets, tickers, caplog): + default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}, + {'method': 'AgeFilter', 'min_days_listed': 99999}] + + mocker.patch.multiple('freqtrade.exchange.Exchange', + markets=PropertyMock(return_value=markets), + exchange_has=MagicMock(return_value=True), + get_tickers=tickers + ) + + with pytest.raises(OperationalException, + match=r'AgeFilter requires min_days_listed must not exceed ' + r'exchange max request size \([0-9]+\)'): + get_patched_freqtradebot(mocker, default_conf) + + def test_agefilter_caching(mocker, markets, whitelist_conf_3, tickers, ohlcv_history_list): mocker.patch.multiple('freqtrade.exchange.Exchange', @@ -547,6 +590,36 @@ def test_agefilter_caching(mocker, markets, whitelist_conf_3, tickers, ohlcv_his assert freqtrade.exchange.get_historic_ohlcv.call_count == previous_call_count +@pytest.mark.parametrize("pairlistconfig,expected", [ + ({"method": "PriceFilter", "low_price_ratio": 0.001, "min_price": 0.00000010, + "max_price": 1.0}, "[{'PriceFilter': 'PriceFilter - Filtering pairs priced below " + "0.1% or below 0.00000010 or above 1.00000000.'}]" + ), + ({"method": "PriceFilter", "low_price_ratio": 0.001, "min_price": 0.00000010}, + "[{'PriceFilter': 'PriceFilter - Filtering pairs priced below 0.1% or below 0.00000010.'}]" + ), + ({"method": "PriceFilter", "low_price_ratio": 0.001, "max_price": 1.00010000}, + "[{'PriceFilter': 'PriceFilter - Filtering pairs priced below 0.1% or above 1.00010000.'}]" + ), + ({"method": "PriceFilter", "min_price": 0.00002000}, + "[{'PriceFilter': 'PriceFilter - Filtering pairs priced below 0.00002000.'}]" + ), + ({"method": "PriceFilter"}, + "[{'PriceFilter': 'PriceFilter - No price filters configured.'}]" + ), +]) +def test_pricefilter_desc(mocker, whitelist_conf, markets, pairlistconfig, expected): + mocker.patch.multiple('freqtrade.exchange.Exchange', + markets=PropertyMock(return_value=markets), + exchange_has=MagicMock(return_value=True) + ) + whitelist_conf['pairlists'] = [pairlistconfig] + + freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) + short_desc = str(freqtrade.pairlists.short_desc()) + assert short_desc == expected + + def test_pairlistmanager_no_pairlist(mocker, markets, whitelist_conf, caplog): mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 2d5370e1e..9bbd34672 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -8,7 +8,7 @@ import pytest from numpy import isnan from freqtrade.edge import PairInfo -from freqtrade.exceptions import ExchangeError, TemporaryError +from freqtrade.exceptions import ExchangeError, InvalidOrderException, TemporaryError from freqtrade.persistence import Trade from freqtrade.rpc import RPC, RPCException from freqtrade.rpc.fiat_convert import CryptoToFiatConverter @@ -286,12 +286,66 @@ def test_rpc_trade_history(mocker, default_conf, markets, fee): assert isinstance(trades['trades'][1], dict) trades = rpc._rpc_trade_history(0) - assert len(trades['trades']) == 3 - assert trades['trades_count'] == 3 - # The first trade is for ETH ... sorting is descending - assert trades['trades'][-1]['pair'] == 'ETH/BTC' - assert trades['trades'][0]['pair'] == 'ETC/BTC' - assert trades['trades'][1]['pair'] == 'ETC/BTC' + assert len(trades['trades']) == 2 + assert trades['trades_count'] == 2 + # The first closed trade is for ETC ... sorting is descending + assert trades['trades'][-1]['pair'] == 'ETC/BTC' + assert trades['trades'][0]['pair'] == 'XRP/BTC' + + +def test_rpc_delete_trade(mocker, default_conf, fee, markets, caplog): + mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) + stoploss_mock = MagicMock() + cancel_mock = MagicMock() + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + markets=PropertyMock(return_value=markets), + cancel_order=cancel_mock, + cancel_stoploss_order=stoploss_mock, + ) + + freqtradebot = get_patched_freqtradebot(mocker, default_conf) + freqtradebot.strategy.order_types['stoploss_on_exchange'] = True + create_mock_trades(fee) + rpc = RPC(freqtradebot) + with pytest.raises(RPCException, match='invalid argument'): + rpc._rpc_delete('200') + + create_mock_trades(fee) + trades = Trade.query.all() + trades[1].stoploss_order_id = '1234' + trades[2].stoploss_order_id = '1234' + assert len(trades) > 2 + + res = rpc._rpc_delete('1') + assert isinstance(res, dict) + assert res['result'] == 'success' + assert res['trade_id'] == '1' + assert res['cancel_order_count'] == 1 + assert cancel_mock.call_count == 1 + assert stoploss_mock.call_count == 0 + cancel_mock.reset_mock() + stoploss_mock.reset_mock() + + res = rpc._rpc_delete('2') + assert isinstance(res, dict) + assert cancel_mock.call_count == 1 + 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) + + 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) + + res = rpc._rpc_delete('4') + assert cancel_mock.call_count == 1 + assert stoploss_mock.call_count == 0 def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee, diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index c7259bdc6..408f7e537 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -50,6 +50,12 @@ def client_get(client, url): 'Origin': 'http://example.com'}) +def client_delete(client, url): + # Add fake Origin to ensure CORS kicks in + return client.delete(url, headers={'Authorization': _basic_auth_str(_TEST_USER, _TEST_PASS), + 'Origin': 'http://example.com'}) + + def assert_response(response, expected_code=200, needs_cors=True): assert response.status_code == expected_code assert response.content_type == "application/json" @@ -352,7 +358,7 @@ def test_api_daily(botclient, mocker, ticker, fee, markets): assert rc.json['data'][0]['date'] == str(datetime.utcnow().date()) -def test_api_trades(botclient, mocker, ticker, fee, markets): +def test_api_trades(botclient, mocker, fee, markets): ftbot, client = botclient patch_get_signal(ftbot, (True, False)) mocker.patch.multiple( @@ -368,12 +374,53 @@ def test_api_trades(botclient, mocker, ticker, fee, markets): rc = client_get(client, f"{BASE_URI}/trades") assert_response(rc) - assert len(rc.json['trades']) == 3 - assert rc.json['trades_count'] == 3 - rc = client_get(client, f"{BASE_URI}/trades?limit=2") - assert_response(rc) assert len(rc.json['trades']) == 2 assert rc.json['trades_count'] == 2 + rc = client_get(client, f"{BASE_URI}/trades?limit=1") + assert_response(rc) + assert len(rc.json['trades']) == 1 + assert rc.json['trades_count'] == 1 + + +def test_api_delete_trade(botclient, mocker, fee, markets): + ftbot, client = botclient + patch_get_signal(ftbot, (True, False)) + stoploss_mock = MagicMock() + cancel_mock = MagicMock() + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + markets=PropertyMock(return_value=markets), + cancel_order=cancel_mock, + cancel_stoploss_order=stoploss_mock, + ) + rc = client_delete(client, f"{BASE_URI}/trades/1") + # Error - trade won't exist yet. + assert_response(rc, 502) + + create_mock_trades(fee) + ftbot.strategy.order_types['stoploss_on_exchange'] = True + trades = Trade.query.all() + trades[1].stoploss_order_id = '1234' + assert len(trades) > 2 + + 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 cancel_mock.call_count == 1 + + cancel_mock.reset_mock() + rc = client_delete(client, f"{BASE_URI}/trades/1") + # Trade is gone now. + assert_response(rc, 502) + assert cancel_mock.call_count == 0 + + assert len(trades) - 1 == len(Trade.query.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 stoploss_mock.call_count == 1 def test_api_edge_disabled(botclient, mocker, ticker, fee, markets): diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 08d4dc7ec..bfa774856 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -21,8 +21,9 @@ from freqtrade.rpc import RPCMessageType from freqtrade.rpc.telegram import Telegram, authorized_only from freqtrade.state import State from freqtrade.strategy.interface import SellType -from tests.conftest import (get_patched_freqtradebot, log_has, patch_exchange, - patch_get_signal, patch_whitelist) +from tests.conftest import (create_mock_trades, get_patched_freqtradebot, + log_has, patch_exchange, patch_get_signal, + patch_whitelist) class DummyCls(Telegram): @@ -60,7 +61,7 @@ def test__init__(default_conf, mocker) -> None: assert telegram._config == default_conf -def test_init(default_conf, mocker, caplog) -> None: +def test_telegram_init(default_conf, mocker, caplog) -> None: start_polling = MagicMock() mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock(return_value=start_polling)) @@ -72,10 +73,10 @@ def test_init(default_conf, mocker, caplog) -> None: assert start_polling.start_polling.call_count == 1 message_str = ("rpc.telegram is listening for following commands: [['status'], ['profit'], " - "['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], " - "['performance'], ['daily'], ['count'], ['reload_config', 'reload_conf'], " - "['show_config', 'show_conf'], ['stopbuy'], ['whitelist'], ['blacklist'], " - "['edge'], ['help'], ['version']]") + "['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], ['trades'], " + "['delete'], ['performance'], ['daily'], ['count'], ['reload_config', " + "'reload_conf'], ['show_config', 'show_conf'], ['stopbuy'], " + "['whitelist'], ['blacklist'], ['edge'], ['help'], ['version']]") assert log_has(message_str, caplog) @@ -725,6 +726,7 @@ def test_telegram_forcesell_handle(default_conf, update, ticker, fee, last_msg = rpc_mock.call_args_list[-1][0][0] assert { 'type': RPCMessageType.SELL_NOTIFICATION, + 'trade_id': 1, 'exchange': 'Bittrex', 'pair': 'ETH/BTC', 'gain': 'profit', @@ -784,6 +786,7 @@ def test_telegram_forcesell_down_handle(default_conf, update, ticker, fee, last_msg = rpc_mock.call_args_list[-1][0][0] assert { 'type': RPCMessageType.SELL_NOTIFICATION, + 'trade_id': 1, 'exchange': 'Bittrex', 'pair': 'ETH/BTC', 'gain': 'loss', @@ -832,6 +835,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None msg = rpc_mock.call_args_list[0][0][0] assert { 'type': RPCMessageType.SELL_NOTIFICATION, + 'trade_id': 1, 'exchange': 'Bittrex', 'pair': 'ETH/BTC', 'gain': 'loss', @@ -1143,6 +1147,63 @@ def test_edge_enabled(edge_conf, update, mocker) -> None: assert 'Pair Winrate Expectancy Stoploss' in msg_mock.call_args_list[0][0][0] +def test_telegram_trades(mocker, update, default_conf, fee): + msg_mock = MagicMock() + mocker.patch.multiple( + 'freqtrade.rpc.telegram.Telegram', + _init=MagicMock(), + _send_msg=msg_mock + ) + + freqtradebot = get_patched_freqtradebot(mocker, default_conf) + telegram = Telegram(freqtradebot) + context = MagicMock() + context.args = [] + + telegram._trades(update=update, context=context) + assert "0 recent trades:" in msg_mock.call_args_list[0][0][0] + assert "
" not in msg_mock.call_args_list[0][0][0]
+
+    msg_mock.reset_mock()
+    create_mock_trades(fee)
+
+    context = MagicMock()
+    context.args = [5]
+    telegram._trades(update=update, context=context)
+    msg_mock.call_count == 1
+    assert "2 recent trades:" in msg_mock.call_args_list[0][0][0]
+    assert "Profit (" in msg_mock.call_args_list[0][0][0]
+    assert "Open Date" in msg_mock.call_args_list[0][0][0]
+    assert "
" in msg_mock.call_args_list[0][0][0]
+
+
+def test_telegram_delete_trade(mocker, update, default_conf, fee):
+    msg_mock = MagicMock()
+    mocker.patch.multiple(
+        'freqtrade.rpc.telegram.Telegram',
+        _init=MagicMock(),
+        _send_msg=msg_mock
+    )
+
+    freqtradebot = get_patched_freqtradebot(mocker, default_conf)
+    telegram = Telegram(freqtradebot)
+    context = MagicMock()
+    context.args = []
+
+    telegram._delete_trade(update=update, context=context)
+    assert "invalid argument" in msg_mock.call_args_list[0][0][0]
+
+    msg_mock.reset_mock()
+    create_mock_trades(fee)
+
+    context = MagicMock()
+    context.args = [1]
+    telegram._delete_trade(update=update, context=context)
+    msg_mock.call_count == 1
+    assert "Deleted trade 1." in msg_mock.call_args_list[0][0][0]
+    assert "Please make sure to take care of this asset" in msg_mock.call_args_list[0][0][0]
+
+
 def test_help_handle(default_conf, update, mocker) -> None:
     msg_mock = MagicMock()
     mocker.patch.multiple(
diff --git a/tests/test_configuration.py b/tests/test_configuration.py
index cccc87670..ca5d6eadc 100644
--- a/tests/test_configuration.py
+++ b/tests/test_configuration.py
@@ -871,6 +871,14 @@ def test_load_config_default_exchange_name(all_conf) -> None:
         validate_config_schema(all_conf)
 
 
+def test_load_config_stoploss_exchange_limit_ratio(all_conf) -> None:
+    all_conf['order_types']['stoploss_on_exchange_limit_ratio'] = 1.15
+
+    with pytest.raises(ValidationError,
+                       match=r"1.15 is greater than the maximum"):
+        validate_config_schema(all_conf)
+
+
 @pytest.mark.parametrize("keys", [("exchange", "sandbox", False),
                                   ("exchange", "key", ""),
                                   ("exchange", "secret", ""),
diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py
index 685b269d7..b8b4a0f7a 100644
--- a/tests/test_freqtradebot.py
+++ b/tests/test_freqtradebot.py
@@ -1660,6 +1660,7 @@ def test_exit_positions_exception(mocker, default_conf, limit_buy_order, caplog)
     trade = MagicMock()
     trade.open_order_id = None
     trade.open_fee = 0.001
+    trade.pair = 'ETH/BTC'
     trades = [trade]
 
     # Test raise of DependencyException exception
@@ -1669,7 +1670,7 @@ def test_exit_positions_exception(mocker, default_conf, limit_buy_order, caplog)
     )
     n = freqtrade.exit_positions(trades)
     assert n == 0
-    assert log_has('Unable to sell trade: ', caplog)
+    assert log_has('Unable to sell trade ETH/BTC: ', caplog)
 
 
 def test_update_trade_state(mocker, default_conf, limit_buy_order, caplog) -> None:
@@ -1726,6 +1727,7 @@ def test_update_trade_state_withorderdict(default_conf, trades_for_order, limit_
         amount=amount,
         exchange='binance',
         open_rate=0.245441,
+        open_date=arrow.utcnow().datetime,
         fee_open=fee.return_value,
         fee_close=fee.return_value,
         open_order_id="123456",
@@ -1816,6 +1818,7 @@ def test_update_trade_state_sell(default_conf, trades_for_order, limit_sell_orde
         open_rate=0.245441,
         fee_open=0.0025,
         fee_close=0.0025,
+        open_date=arrow.utcnow().datetime,
         open_order_id="123456",
         is_open=True,
     )
@@ -2023,11 +2026,16 @@ def test_check_handle_timedout_buy_usercustom(default_conf, ticker, limit_buy_or
 
     rpc_mock = patch_RPCManager(mocker)
     cancel_order_mock = MagicMock(return_value=limit_buy_order_old)
+    cancel_buy_order = deepcopy(limit_buy_order_old)
+    cancel_buy_order['status'] = 'canceled'
+    cancel_order_wr_mock = MagicMock(return_value=cancel_buy_order)
+
     patch_exchange(mocker)
     mocker.patch.multiple(
         'freqtrade.exchange.Exchange',
         fetch_ticker=ticker,
         fetch_order=MagicMock(return_value=limit_buy_order_old),
+        cancel_order_with_result=cancel_order_wr_mock,
         cancel_order=cancel_order_mock,
         get_fee=fee
     )
@@ -2060,7 +2068,7 @@ def test_check_handle_timedout_buy_usercustom(default_conf, ticker, limit_buy_or
     freqtrade.strategy.check_buy_timeout = MagicMock(return_value=True)
     # Trade should be closed since the function returns true
     freqtrade.check_handle_timedout()
-    assert cancel_order_mock.call_count == 1
+    assert cancel_order_wr_mock.call_count == 1
     assert rpc_mock.call_count == 1
     trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all()
     nb_trades = len(trades)
@@ -2071,7 +2079,9 @@ def test_check_handle_timedout_buy_usercustom(default_conf, ticker, limit_buy_or
 def test_check_handle_timedout_buy(default_conf, ticker, limit_buy_order_old, open_trade,
                                    fee, mocker) -> None:
     rpc_mock = patch_RPCManager(mocker)
-    cancel_order_mock = MagicMock(return_value=limit_buy_order_old)
+    limit_buy_cancel = deepcopy(limit_buy_order_old)
+    limit_buy_cancel['status'] = 'canceled'
+    cancel_order_mock = MagicMock(return_value=limit_buy_cancel)
     patch_exchange(mocker)
     mocker.patch.multiple(
         'freqtrade.exchange.Exchange',
@@ -2259,7 +2269,10 @@ def test_check_handle_cancelled_sell(default_conf, ticker, limit_sell_order_old,
 def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old_partial,
                                        open_trade, mocker) -> None:
     rpc_mock = patch_RPCManager(mocker)
-    cancel_order_mock = MagicMock(return_value=limit_buy_order_old_partial)
+    limit_buy_canceled = deepcopy(limit_buy_order_old_partial)
+    limit_buy_canceled['status'] = 'canceled'
+
+    cancel_order_mock = MagicMock(return_value=limit_buy_canceled)
     patch_exchange(mocker)
     mocker.patch.multiple(
         'freqtrade.exchange.Exchange',
@@ -2392,7 +2405,11 @@ def test_check_handle_timedout_exception(default_conf, ticker, open_trade, mocke
 def test_handle_cancel_buy(mocker, caplog, default_conf, limit_buy_order) -> None:
     patch_RPCManager(mocker)
     patch_exchange(mocker)
-    cancel_order_mock = MagicMock(return_value=limit_buy_order)
+    cancel_buy_order = deepcopy(limit_buy_order)
+    cancel_buy_order['status'] = 'canceled'
+    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)
 
     freqtrade = FreqtradeBot(default_conf)
@@ -2412,9 +2429,12 @@ def test_handle_cancel_buy(mocker, caplog, default_conf, limit_buy_order) -> Non
     assert not freqtrade.handle_cancel_buy(trade, limit_buy_order, reason)
     assert cancel_order_mock.call_count == 1
 
-    limit_buy_order['filled'] = 2
-    mocker.patch('freqtrade.exchange.Exchange.cancel_order', side_effect=InvalidOrderException)
+    # 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)
     assert not freqtrade.handle_cancel_buy(trade, limit_buy_order, reason)
+    assert log_has_re(r"Order .* for .* not cancelled.", caplog)
 
 
 @pytest.mark.parametrize("limit_buy_order_canceled_empty", ['binance', 'ftx', 'kraken', 'bittrex'],
@@ -2572,6 +2592,7 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> N
     assert rpc_mock.call_count == 1
     last_msg = rpc_mock.call_args_list[-1][0][0]
     assert {
+        'trade_id': 1,
         'type': RPCMessageType.SELL_NOTIFICATION,
         'exchange': 'Bittrex',
         'pair': 'ETH/BTC',
@@ -2622,6 +2643,7 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, mocker)
     last_msg = rpc_mock.call_args_list[-1][0][0]
     assert {
         'type': RPCMessageType.SELL_NOTIFICATION,
+        'trade_id': 1,
         'exchange': 'Bittrex',
         'pair': 'ETH/BTC',
         'gain': 'loss',
@@ -2678,6 +2700,7 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe
 
     assert {
         'type': RPCMessageType.SELL_NOTIFICATION,
+        'trade_id': 1,
         'exchange': 'Bittrex',
         'pair': 'ETH/BTC',
         'gain': 'loss',
@@ -2883,6 +2906,7 @@ def test_execute_sell_market_order(default_conf, ticker, fee,
     last_msg = rpc_mock.call_args_list[-1][0][0]
     assert {
         'type': RPCMessageType.SELL_NOTIFICATION,
+        'trade_id': 1,
         'exchange': 'Bittrex',
         'pair': 'ETH/BTC',
         'gain': 'profit',
@@ -4090,7 +4114,7 @@ def test_cancel_all_open_orders(mocker, default_conf, fee, limit_buy_order, limi
     freqtrade = get_patched_freqtradebot(mocker, default_conf)
     create_mock_trades(fee)
     trades = Trade.query.all()
-    assert len(trades) == 3
+    assert len(trades) == 4
     freqtrade.cancel_all_open_orders()
     assert buy_mock.call_count == 1
     assert sell_mock.call_count == 1
diff --git a/tests/test_persistence.py b/tests/test_persistence.py
index c39b2015e..65c83e05b 100644
--- a/tests/test_persistence.py
+++ b/tests/test_persistence.py
@@ -995,7 +995,7 @@ def test_get_overall_performance(fee):
     create_mock_trades(fee)
     res = Trade.get_overall_performance()
 
-    assert len(res) == 1
+    assert len(res) == 2
     assert 'pair' in res[0]
     assert 'profit' in res[0]
     assert 'count' in res[0]
@@ -1010,5 +1010,5 @@ def test_get_best_pair(fee):
     create_mock_trades(fee)
     res = Trade.get_best_pair()
     assert len(res) == 2
-    assert res[0] == 'ETC/BTC'
-    assert res[1] == 0.005
+    assert res[0] == 'XRP/BTC'
+    assert res[1] == 0.01
diff --git a/tests/test_plotting.py b/tests/test_plotting.py
index 05805eb24..8f4512c4b 100644
--- a/tests/test_plotting.py
+++ b/tests/test_plotting.py
@@ -21,7 +21,7 @@ from freqtrade.plot.plotting import (add_indicators, add_profit,
                                      load_and_plot_trades, plot_profit,
                                      plot_trades, store_plot_file)
 from freqtrade.resolvers import StrategyResolver
-from tests.conftest import get_args, log_has, log_has_re
+from tests.conftest import get_args, log_has, log_has_re, patch_exchange
 
 
 def fig_generating_mock(fig, *args, **kwargs):
@@ -316,6 +316,8 @@ def test_start_plot_dataframe(mocker):
 
 
 def test_load_and_plot_trades(default_conf, mocker, caplog, testdatadir):
+    patch_exchange(mocker)
+
     default_conf['trade_source'] = 'file'
     default_conf["datadir"] = testdatadir
     default_conf['exportfilename'] = testdatadir / "backtest-result_test.json"