Merge pull request #6648 from freqtrade/exit_type_rename

Exit type rename
This commit is contained in:
Matthias 2022-04-05 06:44:56 +02:00 committed by GitHub
commit d3e6fa19d5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 348 additions and 296 deletions

View File

@ -139,21 +139,21 @@
"status": "on", "status": "on",
"warning": "on", "warning": "on",
"startup": "on", "startup": "on",
"buy": "on", "entry": "on",
"buy_fill": "on", "entry_fill": "on",
"sell": { "exit": {
"roi": "off", "roi": "off",
"emergency_sell": "off", "emergency_exit": "off",
"force_sell": "off", "force_exit": "off",
"sell_signal": "off", "exit_signal": "off",
"trailing_stop_loss": "off", "trailing_stop_loss": "off",
"stop_loss": "off", "stop_loss": "off",
"stoploss_on_exchange": "off", "stoploss_on_exchange": "off",
"custom_sell": "off" "custom_exit": "off"
}, },
"sell_fill": "on", "exit_fill": "on",
"buy_cancel": "on", "entry_cancel": "on",
"sell_cancel": "on", "exit_cancel": "on",
"protection_trigger": "off", "protection_trigger": "off",
"protection_trigger_global": "on" "protection_trigger_global": "on"
}, },

View File

@ -279,8 +279,8 @@ A backtesting result will look like that:
|:-------------------|--------:|------:|-------:|--------:| |:-------------------|--------:|------:|-------:|--------:|
| trailing_stop_loss | 205 | 150 | 0 | 55 | | trailing_stop_loss | 205 | 150 | 0 | 55 |
| stop_loss | 166 | 0 | 0 | 166 | | stop_loss | 166 | 0 | 0 | 166 |
| sell_signal | 56 | 36 | 0 | 20 | | exit_signal | 56 | 36 | 0 | 20 |
| force_sell | 2 | 0 | 0 | 2 | | force_exit | 2 | 0 | 0 | 2 |
====================================================== LEFT OPEN TRADES REPORT ====================================================== ====================================================== LEFT OPEN TRADES REPORT ======================================================
| Pair | Buys | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Win Draw Loss Win% | | Pair | Buys | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Win Draw Loss Win% |
|:---------|-------:|---------------:|---------------:|-----------------:|---------------:|:---------------|--------------------:| |:---------|-------:|---------------:|---------------:|-----------------:|---------------:|:---------------|--------------------:|
@ -362,7 +362,7 @@ Hence, keep in mind that your performance is an integral mix of all different el
### Exit reasons table ### Exit reasons table
The 2nd table contains a recap of exit reasons. The 2nd table contains a recap of exit reasons.
This table can tell you which area needs some additional work (e.g. all or many of the `sell_signal` trades are losses, so you should work on improving the sell signal, or consider disabling it). This table can tell you which area needs some additional work (e.g. all or many of the `exit_signal` trades are losses, so you should work on improving the sell signal, or consider disabling it).
### Left open trades table ### Left open trades table

View File

@ -150,10 +150,12 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| `telegram.balance_dust_level` | Dust-level (in stake currency) - currencies with a balance below this will not be shown by `/balance`. <br> **Datatype:** float | `telegram.balance_dust_level` | Dust-level (in stake currency) - currencies with a balance below this will not be shown by `/balance`. <br> **Datatype:** float
| `webhook.enabled` | Enable usage of Webhook notifications <br> **Datatype:** Boolean | `webhook.enabled` | Enable usage of Webhook notifications <br> **Datatype:** Boolean
| `webhook.url` | URL for the webhook. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String | `webhook.url` | URL for the webhook. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String
| `webhook.webhookbuy` | Payload to send on buy. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String | `webhook.webhookentry` | Payload to send on entry. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String
| `webhook.webhookbuycancel` | Payload to send on buy order cancel. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String | `webhook.webhookentrycancel` | Payload to send on entry order cancel. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String
| `webhook.webhooksell` | Payload to send on sell. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String | `webhook.webhookentryfill` | Payload to send on entry order filled. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String
| `webhook.webhooksellcancel` | Payload to send on sell order cancel. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String | `webhook.webhookexit` | Payload to send on exit. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String
| `webhook.webhookexitcancel` | Payload to send on exit order cancel. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String
| `webhook.webhookexitfill` | Payload to send on exit order filled. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String
| `webhook.webhookstatus` | Payload to send on status calls. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String | `webhook.webhookstatus` | Payload to send on status calls. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String
| `api_server.enabled` | Enable usage of API Server. See the [API Server documentation](rest-api.md) for more details. <br> **Datatype:** Boolean | `api_server.enabled` | Enable usage of API Server. See the [API Server documentation](rest-api.md) for more details. <br> **Datatype:** Boolean
| `api_server.listen_ip_address` | Bind IP address. See the [API Server documentation](rest-api.md) for more details. <br> **Datatype:** IPv4 | `api_server.listen_ip_address` | Bind IP address. See the [API Server documentation](rest-api.md) for more details. <br> **Datatype:** IPv4

View File

@ -57,7 +57,19 @@ While we may drop support for the current interface sometime in the future, we w
Please follow the [Strategy migration](strategy_migration.md) guide to migrate your strategy to the new format to start using the new functionalities. Please follow the [Strategy migration](strategy_migration.md) guide to migrate your strategy to the new format to start using the new functionalities.
### webhooks - `buy_tag` has been renamed to `enter_tag` ### webhooks - changes with 2022.4
#### `buy_tag` has been renamed to `enter_tag`
This should apply only to your strategy and potentially to webhooks. This should apply only to your strategy and potentially to webhooks.
We will keep a compatibility layer for 1-2 versions (so both `buy_tag` and `enter_tag` will still work), but support for this in webhooks will disappear after that. We will keep a compatibility layer for 1-2 versions (so both `buy_tag` and `enter_tag` will still work), but support for this in webhooks will disappear after that.
#### Naming changes
Webhook terminology changed from "sell" to "exit", and from "buy" to "entry".
*`webhookbuy` -> `webhookentry`
* `webhookbuyfill` -> `webhookentryfill`
*`webhookbuycancel` -> `webhookentrycancel`
* `webhooksell` -> `webhookexit`
*`webhooksellfill` -> `webhookexitfill`
* `webhooksellcancel` -> `webhookexitcancel`

View File

@ -78,7 +78,7 @@ SET is_open=0,
close_rate=0.19638016, close_rate=0.19638016,
close_profit=0.0496, 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)))),
exit_reason='force_sell' exit_reason='force_exit'
WHERE id=31; WHERE id=31;
``` ```

View File

@ -393,7 +393,7 @@ class AwesomeStrategy(IStrategy):
!!! Warning "Backtesting" !!! Warning "Backtesting"
Custom prices are supported in backtesting (starting with 2021.12), and orders will fill if the price falls within the candle's low/high range. Custom prices are supported in backtesting (starting with 2021.12), and orders will fill if the price falls within the candle's low/high range.
Orders that don't fill immediately are subject to regular timeout handling, which happens once per (detail) candle. Orders that don't fill immediately are subject to regular timeout handling, which happens once per (detail) candle.
`custom_exit_price()` is only called for sells of type Sell_signal and Custom exit. All other exit-types will use regular backtesting prices. `custom_exit_price()` is only called for sells of type exit_signal and Custom exit. All other exit-types will use regular backtesting prices.
## Custom order timeout rules ## Custom order timeout rules
@ -564,13 +564,13 @@ class AwesomeStrategy(IStrategy):
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled). :param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
:param exit_reason: Exit reason. :param exit_reason: Exit reason.
Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss', Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss',
'sell_signal', 'force_sell', 'emergency_sell'] 'exit_signal', 'force_exit', 'emergency_exit']
:param current_time: datetime object, containing the current datetime :param current_time: datetime object, containing the current datetime
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return bool: When True is returned, then the sell-order is placed on the exchange. :return bool: When True is returned, then the exit-order is placed on the exchange.
False aborts the process False aborts the process
""" """
if exit_reason == 'force_sell' and trade.calc_profit_ratio(rate) < 0: if exit_reason == 'force_exit' and trade.calc_profit_ratio(rate) < 0:
# Reject force-sells with negative profit # Reject force-sells with negative profit
# This is just a sample, please adjust to your needs # This is just a sample, please adjust to your needs
# (this does not necessarily make sense, assuming you know when you're force-selling) # (this does not necessarily make sense, assuming you know when you're force-selling)

View File

@ -41,6 +41,28 @@ You can use the quick summary as checklist. Please refer to the detailed section
* `order_time_in_force` buy -> entry, sell -> exit. * `order_time_in_force` buy -> entry, sell -> exit.
* `order_types` buy -> entry, sell -> exit. * `order_types` buy -> entry, sell -> exit.
* `unfilledtimeout` buy -> entry, sell -> exit. * `unfilledtimeout` buy -> entry, sell -> exit.
* Terminology changes
* Sell reasons changed to reflect the new naming of "exit" instead of sells. Be careful in your strategy if you're using `exit_reason` checks and eventually update your strategy.
* `sell_signal` -> `exit_signal`
* `custom_sell` -> `custom_exit`
* `force_sell` -> `force_exit`
* `emergency_sell` -> `emergency_exit`
* Webhook terminology changed from "sell" to "exit", and from "buy" to entry
* `webhookbuy` -> `webhookentry`
* `webhookbuyfill` -> `webhookentryfill`
* `webhookbuycancel` -> `webhookentrycancel`
* `webhooksell` -> `webhookexit`
* `webhooksellfill` -> `webhookexitfill`
* `webhooksellcancel` -> `webhookexitcancel`
* Telegram notification settings
* `buy` -> `entry`
* `buy_fill` -> `entry_fill`
* `buy_cancel` -> `entry_cancel`
* `sell` -> `exit`
* `sell_fill` -> `exit_fill`
* `sell_cancel` -> `exit_cancel`
## Extensive explanation ## Extensive explanation

View File

@ -81,21 +81,21 @@ Example configuration showing the different settings:
"status": "silent", "status": "silent",
"warning": "on", "warning": "on",
"startup": "off", "startup": "off",
"buy": "silent", "entry": "silent",
"sell": { "exit": {
"roi": "silent", "roi": "silent",
"emergency_sell": "on", "emergency_exit": "on",
"force_sell": "on", "force_exit": "on",
"sell_signal": "silent", "exit_signal": "silent",
"trailing_stop_loss": "on", "trailing_stop_loss": "on",
"stop_loss": "on", "stop_loss": "on",
"stoploss_on_exchange": "on", "stoploss_on_exchange": "on",
"custom_sell": "silent" "custom_exit": "silent"
}, },
"buy_cancel": "silent", "entry_cancel": "silent",
"sell_cancel": "on", "exit_cancel": "on",
"buy_fill": "off", "entry_fill": "off",
"sell_fill": "off", "exit_fill": "off",
"protection_trigger": "off", "protection_trigger": "off",
"protection_trigger_global": "on" "protection_trigger_global": "on"
}, },
@ -104,8 +104,8 @@ Example configuration showing the different settings:
}, },
``` ```
`buy` notifications are sent when the order is placed, while `buy_fill` notifications are sent when the order is filled on the exchange. `entry` notifications are sent when the order is placed, while `entry_fill` notifications are sent when the order is filled on the exchange.
`sell` notifications are sent when the order is placed, while `sell_fill` notifications are sent when the order is filled on the exchange. `exit` notifications are sent when the order is placed, while `exit_fill` notifications are sent when the order is filled on the exchange.
`*_fill` notifications are off by default and must be explicitly enabled. `*_fill` notifications are off by default and must be explicitly enabled.
`protection_trigger` notifications are sent when a protection triggers and `protection_trigger_global` notifications trigger when global protections are triggered. `protection_trigger` notifications are sent when a protection triggers and `protection_trigger_global` notifications trigger when global protections are triggered.

View File

@ -10,33 +10,33 @@ Sample configuration (tested using IFTTT).
"webhook": { "webhook": {
"enabled": true, "enabled": true,
"url": "https://maker.ifttt.com/trigger/<YOUREVENT>/with/key/<YOURKEY>/", "url": "https://maker.ifttt.com/trigger/<YOUREVENT>/with/key/<YOURKEY>/",
"webhookbuy": { "webhookentry": {
"value1": "Buying {pair}", "value1": "Buying {pair}",
"value2": "limit {limit:8f}", "value2": "limit {limit:8f}",
"value3": "{stake_amount:8f} {stake_currency}" "value3": "{stake_amount:8f} {stake_currency}"
}, },
"webhookbuycancel": { "webhookentrycancel": {
"value1": "Cancelling Open Buy Order for {pair}", "value1": "Cancelling Open Buy Order for {pair}",
"value2": "limit {limit:8f}", "value2": "limit {limit:8f}",
"value3": "{stake_amount:8f} {stake_currency}" "value3": "{stake_amount:8f} {stake_currency}"
}, },
"webhookbuyfill": { "webhookentryfill": {
"value1": "Buy Order for {pair} filled", "value1": "Buy Order for {pair} filled",
"value2": "at {open_rate:8f}", "value2": "at {open_rate:8f}",
"value3": "" "value3": ""
}, },
"webhooksell": { "webhookexit": {
"value1": "Selling {pair}", "value1": "Exiting {pair}",
"value2": "limit {limit:8f}", "value2": "limit {limit:8f}",
"value3": "profit: {profit_amount:8f} {stake_currency} ({profit_ratio})" "value3": "profit: {profit_amount:8f} {stake_currency} ({profit_ratio})"
}, },
"webhooksellcancel": { "webhookexitcancel": {
"value1": "Cancelling Open Sell Order for {pair}", "value1": "Cancelling Open Exit Order for {pair}",
"value2": "limit {limit:8f}", "value2": "limit {limit:8f}",
"value3": "profit: {profit_amount:8f} {stake_currency} ({profit_ratio})" "value3": "profit: {profit_amount:8f} {stake_currency} ({profit_ratio})"
}, },
"webhooksellfill": { "webhookexitfill": {
"value1": "Sell Order for {pair} filled", "value1": "Exit Order for {pair} filled",
"value2": "at {close_rate:8f}.", "value2": "at {close_rate:8f}.",
"value3": "" "value3": ""
}, },
@ -96,9 +96,9 @@ Optional parameters are available to enable automatic retries for webhook messag
Different payloads can be configured for different events. Not all fields are necessary, but you should configure at least one of the dicts, otherwise the webhook will never be called. Different payloads can be configured for different events. Not all fields are necessary, but you should configure at least one of the dicts, otherwise the webhook will never be called.
### Webhookbuy ### Webhookentry
The fields in `webhook.webhookbuy` are filled when the bot executes a long/short. Parameters are filled using string.format. The fields in `webhook.webhookentry` are filled when the bot executes a long/short. Parameters are filled using string.format.
Possible parameters are: Possible parameters are:
* `trade_id` * `trade_id`
@ -118,9 +118,9 @@ Possible parameters are:
* `current_rate` * `current_rate`
* `enter_tag` * `enter_tag`
### Webhookbuycancel ### Webhookentrycancel
The fields in `webhook.webhookbuycancel` are filled when the bot cancels a long/short order. Parameters are filled using string.format. The fields in `webhook.webhookentrycancel` are filled when the bot cancels a long/short order. Parameters are filled using string.format.
Possible parameters are: Possible parameters are:
* `trade_id` * `trade_id`
@ -139,9 +139,9 @@ Possible parameters are:
* `current_rate` * `current_rate`
* `enter_tag` * `enter_tag`
### Webhookbuyfill ### Webhookentryfill
The fields in `webhook.webhookbuyfill` are filled when the bot filled a long/short order. Parameters are filled using string.format. The fields in `webhook.webhookentryfill` are filled when the bot filled a long/short order. Parameters are filled using string.format.
Possible parameters are: Possible parameters are:
* `trade_id` * `trade_id`
@ -160,8 +160,9 @@ Possible parameters are:
* `current_rate` * `current_rate`
* `enter_tag` * `enter_tag`
### Webhooksell ### Webhookexit
The fields in `webhook.webhooksell` are filled when the bot sells a trade. Parameters are filled using string.format.
The fields in `webhook.webhookexit` are filled when the bot exits a trade. Parameters are filled using string.format.
Possible parameters are: Possible parameters are:
* `trade_id` * `trade_id`
@ -183,9 +184,9 @@ Possible parameters are:
* `open_date` * `open_date`
* `close_date` * `close_date`
### Webhooksellfill ### Webhookexitfill
The fields in `webhook.webhooksellfill` are filled when the bot fills a sell order (closes a Trae). Parameters are filled using string.format. The fields in `webhook.webhookexitfill` are filled when the bot fills a exit order (closes a Trade). Parameters are filled using string.format.
Possible parameters are: Possible parameters are:
* `trade_id` * `trade_id`
@ -208,9 +209,9 @@ Possible parameters are:
* `open_date` * `open_date`
* `close_date` * `close_date`
### Webhooksellcancel ### Webhookexitcancel
The fields in `webhook.webhooksellcancel` are filled when the bot cancels a sell order. Parameters are filled using string.format. The fields in `webhook.webhookexitcancel` are filled when the bot cancels a exit order. Parameters are filled using string.format.
Possible parameters are: Possible parameters are:
* `trade_id` * `trade_id`

View File

@ -82,6 +82,31 @@ def process_temporary_deprecated_settings(config: Dict[str, Any]) -> None:
None, 'ignore_roi_if_buy_signal') None, 'ignore_roi_if_buy_signal')
process_deprecated_setting(config, 'ask_strategy', 'ignore_buying_expired_candle_after', process_deprecated_setting(config, 'ask_strategy', 'ignore_buying_expired_candle_after',
None, 'ignore_buying_expired_candle_after') None, 'ignore_buying_expired_candle_after')
# New settings
if config.get('telegram'):
process_deprecated_setting(config['telegram'], 'notification_settings', 'sell',
'notification_settings', 'exit')
process_deprecated_setting(config['telegram'], 'notification_settings', 'sell_fill',
'notification_settings', 'exit_fill')
process_deprecated_setting(config['telegram'], 'notification_settings', 'sell_cancel',
'notification_settings', 'exit_cancel')
process_deprecated_setting(config['telegram'], 'notification_settings', 'buy',
'notification_settings', 'entry')
process_deprecated_setting(config['telegram'], 'notification_settings', 'buy_fill',
'notification_settings', 'entry_fill')
process_deprecated_setting(config['telegram'], 'notification_settings', 'buy_cancel',
'notification_settings', 'entry_cancel')
if config.get('webhook'):
process_deprecated_setting(config, 'webhook', 'webhookbuy', 'webhook', 'webhookentry')
process_deprecated_setting(config, 'webhook', 'webhookbuycancel',
'webhook', 'webhookentrycancel')
process_deprecated_setting(config, 'webhook', 'webhookbuyfill',
'webhook', 'webhookentryfill')
process_deprecated_setting(config, 'webhook', 'webhooksell', 'webhook', 'webhookexit')
process_deprecated_setting(config, 'webhook', 'webhooksellcancel',
'webhook', 'webhookexitcancel')
process_deprecated_setting(config, 'webhook', 'webhooksellfill',
'webhook', 'webhookexitfill')
# Legacy way - having them in experimental ... # Legacy way - having them in experimental ...
process_removed_setting(config, 'experimental', 'use_sell_signal', process_removed_setting(config, 'experimental', 'use_sell_signal',

View File

@ -285,21 +285,21 @@ CONF_SCHEMA = {
'status': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS}, 'status': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS},
'warning': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS}, 'warning': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS},
'startup': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS}, 'startup': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS},
'buy': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS}, 'entry': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS},
'buy_cancel': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS}, 'entry_cancel': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS},
'buy_fill': {'type': 'string', 'entry_fill': {'type': 'string',
'enum': TELEGRAM_SETTING_OPTIONS, 'enum': TELEGRAM_SETTING_OPTIONS,
'default': 'off' 'default': 'off'
}, },
'sell': { 'exit': {
'type': ['string', 'object'], 'type': ['string', 'object'],
'additionalProperties': { 'additionalProperties': {
'type': 'string', 'type': 'string',
'enum': TELEGRAM_SETTING_OPTIONS 'enum': TELEGRAM_SETTING_OPTIONS
} }
}, },
'sell_cancel': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS}, 'exit_cancel': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS},
'sell_fill': { 'exit_fill': {
'type': 'string', 'type': 'string',
'enum': TELEGRAM_SETTING_OPTIONS, 'enum': TELEGRAM_SETTING_OPTIONS,
'default': 'off' 'default': 'off'
@ -327,12 +327,12 @@ CONF_SCHEMA = {
'format': {'type': 'string', 'enum': WEBHOOK_FORMAT_OPTIONS, 'default': 'form'}, 'format': {'type': 'string', 'enum': WEBHOOK_FORMAT_OPTIONS, 'default': 'form'},
'retries': {'type': 'integer', 'minimum': 0}, 'retries': {'type': 'integer', 'minimum': 0},
'retry_delay': {'type': 'number', 'minimum': 0}, 'retry_delay': {'type': 'number', 'minimum': 0},
'webhookbuy': {'type': 'object'}, 'webhookentry': {'type': 'object'},
'webhookbuycancel': {'type': 'object'}, 'webhookentrycancel': {'type': 'object'},
'webhookbuyfill': {'type': 'object'}, 'webhookentryfill': {'type': 'object'},
'webhooksell': {'type': 'object'}, 'webhookexit': {'type': 'object'},
'webhooksellcancel': {'type': 'object'}, 'webhookexitcancel': {'type': 'object'},
'webhooksellfill': {'type': 'object'}, 'webhookexitfill': {'type': 'object'},
'webhookstatus': {'type': 'object'}, 'webhookstatus': {'type': 'object'},
}, },
}, },
@ -478,7 +478,7 @@ CANCEL_REASON = {
"FULLY_CANCELLED": "fully cancelled", "FULLY_CANCELLED": "fully cancelled",
"ALL_CANCELLED": "cancelled (all unfilled and partially filled open orders cancelled)", "ALL_CANCELLED": "cancelled (all unfilled and partially filled open orders cancelled)",
"CANCELLED_ON_EXCHANGE": "cancelled on exchange", "CANCELLED_ON_EXCHANGE": "cancelled on exchange",
"FORCE_SELL": "forcesold", "FORCE_EXIT": "forcesold",
} }
# List of pairs with their timeframes # List of pairs with their timeframes

View File

@ -470,7 +470,7 @@ class Edge:
if len(ohlc_columns) - 1 < exit_index: if len(ohlc_columns) - 1 < exit_index:
break break
exit_type = ExitType.SELL_SIGNAL exit_type = ExitType.EXIT_SIGNAL
exit_price = ohlc_columns[exit_index, 0] exit_price = ohlc_columns[exit_index, 0]
trade = {'pair': pair, trade = {'pair': pair,

View File

@ -9,10 +9,10 @@ class ExitType(Enum):
STOP_LOSS = "stop_loss" STOP_LOSS = "stop_loss"
STOPLOSS_ON_EXCHANGE = "stoploss_on_exchange" STOPLOSS_ON_EXCHANGE = "stoploss_on_exchange"
TRAILING_STOP_LOSS = "trailing_stop_loss" TRAILING_STOP_LOSS = "trailing_stop_loss"
SELL_SIGNAL = "sell_signal" EXIT_SIGNAL = "exit_signal"
FORCE_SELL = "force_sell" FORCE_EXIT = "force_exit"
EMERGENCY_SELL = "emergency_sell" EMERGENCY_EXIT = "emergency_exit"
CUSTOM_SELL = "custom_sell" CUSTOM_EXIT = "custom_exit"
NONE = "" NONE = ""
def __str__(self): def __str__(self):

View File

@ -6,19 +6,13 @@ class RPCMessageType(Enum):
WARNING = 'warning' WARNING = 'warning'
STARTUP = 'startup' STARTUP = 'startup'
BUY = 'buy' ENTRY = 'entry'
BUY_FILL = 'buy_fill' ENTRY_FILL = 'entry_fill'
BUY_CANCEL = 'buy_cancel' ENTRY_CANCEL = 'entry_cancel'
SHORT = 'short' EXIT = 'exit'
SHORT_FILL = 'short_fill' EXIT_FILL = 'exit_fill'
SHORT_CANCEL = 'short_cancel' EXIT_CANCEL = 'exit_cancel'
# TODO: The below messagetypes should be renamed to "exit"!
# Careful - has an impact on webhooks, therefore needs proper communication
SELL = 'sell'
SELL_FILL = 'sell_fill'
SELL_CANCEL = 'sell_cancel'
PROTECTION_TRIGGER = 'protection_trigger' PROTECTION_TRIGGER = 'protection_trigger'
PROTECTION_TRIGGER_GLOBAL = 'protection_trigger_global' PROTECTION_TRIGGER_GLOBAL = 'protection_trigger_global'

View File

@ -819,10 +819,7 @@ class FreqtradeBot(LoggingMixin):
""" """
Sends rpc notification when a entry order occurred. Sends rpc notification when a entry order occurred.
""" """
if fill: msg_type = RPCMessageType.ENTRY_FILL if fill else RPCMessageType.ENTRY
msg_type = RPCMessageType.SHORT_FILL if trade.is_short else RPCMessageType.BUY_FILL
else:
msg_type = RPCMessageType.SHORT if trade.is_short else RPCMessageType.BUY
open_rate = safe_value_fallback(order, 'average', 'price') open_rate = safe_value_fallback(order, 'average', 'price')
if open_rate is None: if open_rate is None:
open_rate = trade.open_rate open_rate = trade.open_rate
@ -861,10 +858,10 @@ class FreqtradeBot(LoggingMixin):
""" """
current_rate = self.exchange.get_rate( current_rate = self.exchange.get_rate(
trade.pair, side='entry', is_short=trade.is_short, refresh=False) trade.pair, side='entry', is_short=trade.is_short, refresh=False)
msg_type = RPCMessageType.SHORT_CANCEL if trade.is_short else RPCMessageType.BUY_CANCEL
msg = { msg = {
'trade_id': trade.id, 'trade_id': trade.id,
'type': msg_type, 'type': RPCMessageType.ENTRY_CANCEL,
'buy_tag': trade.enter_tag, 'buy_tag': trade.enter_tag,
'enter_tag': trade.enter_tag, 'enter_tag': trade.enter_tag,
'exchange': self.exchange.name.capitalize(), 'exchange': self.exchange.name.capitalize(),
@ -981,7 +978,7 @@ class FreqtradeBot(LoggingMixin):
logger.error(f'Unable to place a stoploss order on exchange. {e}') logger.error(f'Unable to place a stoploss order on exchange. {e}')
logger.warning('Exiting the trade forcefully') logger.warning('Exiting the trade forcefully')
self.execute_trade_exit(trade, trade.stop_loss, exit_check=ExitCheckTuple( self.execute_trade_exit(trade, trade.stop_loss, exit_check=ExitCheckTuple(
exit_type=ExitType.EMERGENCY_SELL)) exit_type=ExitType.EMERGENCY_EXIT))
except ExchangeError: except ExchangeError:
trade.stoploss_order_id = None trade.stoploss_order_id = None
@ -1162,7 +1159,7 @@ class FreqtradeBot(LoggingMixin):
try: try:
self.execute_trade_exit( self.execute_trade_exit(
trade, order.get('price'), trade, order.get('price'),
exit_check=ExitCheckTuple(exit_type=ExitType.EMERGENCY_SELL)) exit_check=ExitCheckTuple(exit_type=ExitType.EMERGENCY_EXIT))
except DependencyException as exception: except DependencyException as exception:
logger.warning( logger.warning(
f'Unable to emergency sell trade {trade.pair}: {exception}') f'Unable to emergency sell trade {trade.pair}: {exception}')
@ -1380,7 +1377,7 @@ class FreqtradeBot(LoggingMixin):
trade = self.cancel_stoploss_on_exchange(trade) trade = self.cancel_stoploss_on_exchange(trade)
order_type = ordertype or self.strategy.order_types[exit_type] order_type = ordertype or self.strategy.order_types[exit_type]
if exit_check.exit_type == ExitType.EMERGENCY_SELL: if exit_check.exit_type == ExitType.EMERGENCY_EXIT:
# Emergency sells (default to market!) # Emergency sells (default to market!)
order_type = self.strategy.order_types.get("emergencyexit", "market") order_type = self.strategy.order_types.get("emergencyexit", "market")
@ -1446,8 +1443,8 @@ class FreqtradeBot(LoggingMixin):
gain = "profit" if profit_ratio > 0 else "loss" gain = "profit" if profit_ratio > 0 else "loss"
msg = { msg = {
'type': (RPCMessageType.SELL_FILL if fill 'type': (RPCMessageType.EXIT_FILL if fill
else RPCMessageType.SELL), else RPCMessageType.EXIT),
'trade_id': trade.id, 'trade_id': trade.id,
'exchange': trade.exchange.capitalize(), 'exchange': trade.exchange.capitalize(),
'pair': trade.pair, 'pair': trade.pair,
@ -1497,7 +1494,7 @@ class FreqtradeBot(LoggingMixin):
gain = "profit" if profit_ratio > 0 else "loss" gain = "profit" if profit_ratio > 0 else "loss"
msg = { msg = {
'type': RPCMessageType.SELL_CANCEL, 'type': RPCMessageType.EXIT_CANCEL,
'trade_id': trade.id, 'trade_id': trade.id,
'exchange': trade.exchange.capitalize(), 'exchange': trade.exchange.capitalize(),
'pair': trade.pair, 'pair': trade.pair,

View File

@ -529,7 +529,7 @@ class Backtesting:
# call the custom exit price,with default value as previous closerate # call the custom exit price,with default value as previous closerate
current_profit = trade.calc_profit_ratio(closerate) current_profit = trade.calc_profit_ratio(closerate)
order_type = self.strategy.order_types['exit'] order_type = self.strategy.order_types['exit']
if sell.exit_type in (ExitType.SELL_SIGNAL, ExitType.CUSTOM_SELL): if sell.exit_type in (ExitType.EXIT_SIGNAL, ExitType.CUSTOM_EXIT):
# Custom exit pricing only for sell-signals # Custom exit pricing only for sell-signals
if order_type == 'limit': if order_type == 'limit':
closerate = strategy_safe_wrapper(self.strategy.custom_exit_price, closerate = strategy_safe_wrapper(self.strategy.custom_exit_price,
@ -812,7 +812,7 @@ class Backtesting:
sell_row = data[pair][-1] sell_row = data[pair][-1]
trade.close_date = sell_row[DATE_IDX].to_pydatetime() trade.close_date = sell_row[DATE_IDX].to_pydatetime()
trade.exit_reason = ExitType.FORCE_SELL.value trade.exit_reason = ExitType.FORCE_EXIT.value
trade.close(sell_row[OPEN_IDX], show_msg=False) trade.close(sell_row[OPEN_IDX], show_msg=False)
LocalTrade.close_bt_trade(trade) LocalTrade.close_bt_trade(trade)
# Deepcopy object to have wallets update correctly # Deepcopy object to have wallets update correctly

View File

@ -153,7 +153,13 @@ def migrate_trades_and_orders_table(
{initial_stop_loss} initial_stop_loss, {initial_stop_loss} initial_stop_loss,
{initial_stop_loss_pct} initial_stop_loss_pct, {initial_stop_loss_pct} initial_stop_loss_pct,
{stoploss_order_id} stoploss_order_id, {stoploss_last_update} stoploss_last_update, {stoploss_order_id} stoploss_order_id, {stoploss_last_update} stoploss_last_update,
{max_rate} max_rate, {min_rate} min_rate, {exit_reason} exit_reason, {max_rate} max_rate, {min_rate} min_rate,
case when {exit_reason} == 'sell_signal' then 'exit_signal'
when {exit_reason} == 'custom_sell' then 'custom_exit'
when {exit_reason} == 'force_sell' then 'force_exit'
when {exit_reason} == 'emergency_sell' then 'emergency_exit'
else {exit_reason}
end exit_reason,
{exit_order_status} exit_order_status, {exit_order_status} exit_order_status,
{strategy} strategy, {enter_tag} enter_tag, {timeframe} timeframe, {strategy} strategy, {enter_tag} enter_tag, {timeframe} timeframe,
{open_trade_value} open_trade_value, {close_profit_abs} close_profit_abs, {open_trade_value} open_trade_value, {close_profit_abs} close_profit_abs,

View File

@ -697,17 +697,17 @@ class RPC:
if order['side'] == trade.enter_side: if order['side'] == trade.enter_side:
fully_canceled = self._freqtrade.handle_cancel_enter( fully_canceled = self._freqtrade.handle_cancel_enter(
trade, order, CANCEL_REASON['FORCE_SELL']) trade, order, CANCEL_REASON['FORCE_EXIT'])
if order['side'] == trade.exit_side: if order['side'] == trade.exit_side:
# Cancel order - so it is placed anew with a fresh price. # Cancel order - so it is placed anew with a fresh price.
self._freqtrade.handle_cancel_exit(trade, order, CANCEL_REASON['FORCE_SELL']) self._freqtrade.handle_cancel_exit(trade, order, CANCEL_REASON['FORCE_EXIT'])
if not fully_canceled: if not fully_canceled:
# Get current rate and execute sell # Get current rate and execute sell
current_rate = self._freqtrade.exchange.get_rate( current_rate = self._freqtrade.exchange.get_rate(
trade.pair, side='exit', is_short=trade.is_short, refresh=True) trade.pair, side='exit', is_short=trade.is_short, refresh=True)
exit_check = ExitCheckTuple(exit_type=ExitType.FORCE_SELL) exit_check = ExitCheckTuple(exit_type=ExitType.FORCE_EXIT)
order_type = ordertype or self._freqtrade.strategy.order_types.get( order_type = ordertype or self._freqtrade.strategy.order_types.get(
"forceexit", self._freqtrade.strategy.order_types["exit"]) "forceexit", self._freqtrade.strategy.order_types["exit"])

View File

@ -224,17 +224,16 @@ class Telegram(RPCHandler):
# This can take up to `timeout` from the call to `start_polling`. # This can take up to `timeout` from the call to `start_polling`.
self._updater.stop() self._updater.stop()
def _format_buy_msg(self, msg: Dict[str, Any]) -> str: def _format_entry_msg(self, msg: Dict[str, Any]) -> str:
if self._rpc._fiat_converter: if self._rpc._fiat_converter:
msg['stake_amount_fiat'] = self._rpc._fiat_converter.convert_amount( msg['stake_amount_fiat'] = self._rpc._fiat_converter.convert_amount(
msg['stake_amount'], msg['stake_currency'], msg['fiat_currency']) msg['stake_amount'], msg['stake_currency'], msg['fiat_currency'])
else: else:
msg['stake_amount_fiat'] = 0 msg['stake_amount_fiat'] = 0
is_fill = msg['type'] in [RPCMessageType.BUY_FILL, RPCMessageType.SHORT_FILL] is_fill = msg['type'] in [RPCMessageType.ENTRY_FILL]
emoji = '\N{CHECK MARK}' if is_fill else '\N{LARGE BLUE CIRCLE}' emoji = '\N{CHECK MARK}' if is_fill else '\N{LARGE BLUE CIRCLE}'
enter_side = ({'enter': 'Long', 'entered': 'Longed'} if msg['type'] enter_side = ({'enter': 'Long', 'entered': 'Longed'} if msg['direction'] == 'Long'
in [RPCMessageType.BUY_FILL, RPCMessageType.BUY]
else {'enter': 'Short', 'entered': 'Shorted'}) else {'enter': 'Short', 'entered': 'Shorted'})
message = ( message = (
f"{emoji} *{msg['exchange']}:*" f"{emoji} *{msg['exchange']}:*"
@ -246,9 +245,9 @@ class Telegram(RPCHandler):
if msg.get('leverage') and msg.get('leverage', 1.0) != 1.0: if msg.get('leverage') and msg.get('leverage', 1.0) != 1.0:
message += f"*Leverage:* `{msg['leverage']}`\n" message += f"*Leverage:* `{msg['leverage']}`\n"
if msg['type'] in [RPCMessageType.BUY_FILL, RPCMessageType.SHORT_FILL]: if msg['type'] in [RPCMessageType.ENTRY_FILL]:
message += f"*Open Rate:* `{msg['open_rate']:.8f}`\n" message += f"*Open Rate:* `{msg['open_rate']:.8f}`\n"
elif msg['type'] in [RPCMessageType.BUY, RPCMessageType.SHORT]: elif msg['type'] in [RPCMessageType.ENTRY]:
message += f"*Open Rate:* `{msg['limit']:.8f}`\n"\ message += f"*Open Rate:* `{msg['limit']:.8f}`\n"\
f"*Current Rate:* `{msg['current_rate']:.8f}`\n" f"*Current Rate:* `{msg['current_rate']:.8f}`\n"
@ -260,7 +259,7 @@ class Telegram(RPCHandler):
message += ")`" message += ")`"
return message return message
def _format_sell_msg(self, msg: Dict[str, Any]) -> str: def _format_exit_msg(self, msg: Dict[str, Any]) -> str:
msg['amount'] = round(msg['amount'], 8) msg['amount'] = round(msg['amount'], 8)
msg['profit_percent'] = round(msg['profit_ratio'] * 100, 2) msg['profit_percent'] = round(msg['profit_ratio'] * 100, 2)
msg['duration'] = msg['close_date'].replace( msg['duration'] = msg['close_date'].replace(
@ -284,7 +283,7 @@ class Telegram(RPCHandler):
f" / {msg['profit_fiat']:.3f} {msg['fiat_currency']})") f" / {msg['profit_fiat']:.3f} {msg['fiat_currency']})")
else: else:
msg['profit_extra'] = '' msg['profit_extra'] = ''
is_fill = msg['type'] == RPCMessageType.SELL_FILL is_fill = msg['type'] == RPCMessageType.EXIT_FILL
message = ( message = (
f"{msg['emoji']} *{msg['exchange']}:* " f"{msg['emoji']} *{msg['exchange']}:* "
f"{'Exited' if is_fill else 'Exiting'} {msg['pair']} (#{msg['trade_id']})\n" f"{'Exited' if is_fill else 'Exiting'} {msg['pair']} (#{msg['trade_id']})\n"
@ -298,27 +297,24 @@ class Telegram(RPCHandler):
f"*Amount:* `{msg['amount']:.8f}`\n" f"*Amount:* `{msg['amount']:.8f}`\n"
f"*Open Rate:* `{msg['open_rate']:.8f}`\n" f"*Open Rate:* `{msg['open_rate']:.8f}`\n"
) )
if msg['type'] == RPCMessageType.SELL: if msg['type'] == RPCMessageType.EXIT:
message += (f"*Current Rate:* `{msg['current_rate']:.8f}`\n" message += (f"*Current Rate:* `{msg['current_rate']:.8f}`\n"
f"*Close Rate:* `{msg['limit']:.8f}`") f"*Close Rate:* `{msg['limit']:.8f}`")
elif msg['type'] == RPCMessageType.SELL_FILL: elif msg['type'] == RPCMessageType.EXIT_FILL:
message += f"*Close Rate:* `{msg['close_rate']:.8f}`" message += f"*Close Rate:* `{msg['close_rate']:.8f}`"
return message return message
def compose_message(self, msg: Dict[str, Any], msg_type: RPCMessageType) -> str: def compose_message(self, msg: Dict[str, Any], msg_type: RPCMessageType) -> str:
if msg_type in [RPCMessageType.BUY, RPCMessageType.BUY_FILL, RPCMessageType.SHORT, if msg_type in [RPCMessageType.ENTRY, RPCMessageType.ENTRY_FILL]:
RPCMessageType.SHORT_FILL]: message = self._format_entry_msg(msg)
message = self._format_buy_msg(msg)
elif msg_type in [RPCMessageType.SELL, RPCMessageType.SELL_FILL]: elif msg_type in [RPCMessageType.EXIT, RPCMessageType.EXIT_FILL]:
message = self._format_sell_msg(msg) message = self._format_exit_msg(msg)
elif msg_type in (RPCMessageType.BUY_CANCEL, RPCMessageType.SHORT_CANCEL, elif msg_type in (RPCMessageType.ENTRY_CANCEL, RPCMessageType.EXIT_CANCEL):
RPCMessageType.SELL_CANCEL): msg['message_side'] = 'enter' if msg_type in [RPCMessageType.ENTRY_CANCEL] else 'exit'
msg['message_side'] = 'enter' if msg_type in [RPCMessageType.BUY_CANCEL,
RPCMessageType.SHORT_CANCEL] else 'exit'
message = ("\N{WARNING SIGN} *{exchange}:* " message = ("\N{WARNING SIGN} *{exchange}:* "
"Cancelling {message_side} Order for {pair} (#{trade_id}). " "Cancelling {message_side} Order for {pair} (#{trade_id}). "
"Reason: {reason}.".format(**msg)) "Reason: {reason}.".format(**msg))
@ -355,7 +351,7 @@ class Telegram(RPCHandler):
msg_type = msg['type'] msg_type = msg['type']
noti = '' noti = ''
if msg_type == RPCMessageType.SELL: if msg_type == RPCMessageType.EXIT:
sell_noti = self._config['telegram'] \ sell_noti = self._config['telegram'] \
.get('notification_settings', {}).get(str(msg_type), {}) .get('notification_settings', {}).get(str(msg_type), {})
# For backward compatibility sell still can be string # For backward compatibility sell still can be string
@ -768,9 +764,9 @@ class Telegram(RPCHandler):
'stop_loss': 'Stoploss', 'stop_loss': 'Stoploss',
'trailing_stop_loss': 'Trail. Stop', 'trailing_stop_loss': 'Trail. Stop',
'stoploss_on_exchange': 'Stoploss', 'stoploss_on_exchange': 'Stoploss',
'sell_signal': 'Sell Signal', 'exit_signal': 'Exit Signal',
'force_sell': 'Forcesell', 'force_exit': 'Force Exit',
'emergency_sell': 'Emergency Sell', 'emergency_exit': 'Emergency Exit',
} }
exit_reasons_tabulate = [ exit_reasons_tabulate = [
[ [

View File

@ -43,23 +43,23 @@ class Webhook(RPCHandler):
def send_msg(self, msg: Dict[str, Any]) -> None: def send_msg(self, msg: Dict[str, Any]) -> None:
""" Send a message to telegram channel """ """ Send a message to telegram channel """
try: try:
whconfig = self._config['webhook']
if msg['type'] in [RPCMessageType.BUY, RPCMessageType.SHORT]: if msg['type'] in [RPCMessageType.ENTRY]:
valuedict = self._config['webhook'].get('webhookbuy', None) valuedict = whconfig.get('webhookentry', None)
elif msg['type'] in [RPCMessageType.BUY_CANCEL, RPCMessageType.SHORT_CANCEL]: elif msg['type'] in [RPCMessageType.ENTRY_CANCEL]:
valuedict = self._config['webhook'].get('webhookbuycancel', None) valuedict = whconfig.get('webhookentrycancel', None)
elif msg['type'] in [RPCMessageType.BUY_FILL, RPCMessageType.SHORT_FILL]: elif msg['type'] in [RPCMessageType.ENTRY_FILL]:
valuedict = self._config['webhook'].get('webhookbuyfill', None) valuedict = whconfig.get('webhookentryfill', None)
elif msg['type'] == RPCMessageType.SELL: elif msg['type'] == RPCMessageType.EXIT:
valuedict = self._config['webhook'].get('webhooksell', None) valuedict = whconfig.get('webhookexit', None)
elif msg['type'] == RPCMessageType.SELL_FILL: elif msg['type'] == RPCMessageType.EXIT_FILL:
valuedict = self._config['webhook'].get('webhooksellfill', None) valuedict = whconfig.get('webhookexitfill', None)
elif msg['type'] == RPCMessageType.SELL_CANCEL: elif msg['type'] == RPCMessageType.EXIT_CANCEL:
valuedict = self._config['webhook'].get('webhooksellcancel', None) valuedict = whconfig.get('webhookexitcancel', None)
elif msg['type'] in (RPCMessageType.STATUS, elif msg['type'] in (RPCMessageType.STATUS,
RPCMessageType.STARTUP, RPCMessageType.STARTUP,
RPCMessageType.WARNING): RPCMessageType.WARNING):
valuedict = self._config['webhook'].get('webhookstatus', None) valuedict = whconfig.get('webhookstatus', None)
else: else:
raise NotImplementedError('Unknown message type: {}'.format(msg['type'])) raise NotImplementedError('Unknown message type: {}'.format(msg['type']))
if not valuedict: if not valuedict:

View File

@ -308,10 +308,10 @@ class IStrategy(ABC, HyperStrategyMixin):
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled). :param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
:param exit_reason: Exit reason. :param exit_reason: Exit reason.
Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss', Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss',
'sell_signal', 'force_sell', 'emergency_sell'] 'exit_signal', 'force_exit', 'emergency_exit']
:param current_time: datetime object, containing the current datetime :param current_time: datetime object, containing the current datetime
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return bool: When True, then the sell-order/exit_short-order is placed on the exchange. :return bool: When True, then the exit-order is placed on the exchange.
False aborts the process False aborts the process
""" """
return True return True
@ -888,14 +888,14 @@ class IStrategy(ABC, HyperStrategyMixin):
pass pass
elif self.use_sell_signal and not enter: elif self.use_sell_signal and not enter:
if exit_: if exit_:
exit_signal = ExitType.SELL_SIGNAL exit_signal = ExitType.EXIT_SIGNAL
else: else:
trade_type = "exit_short" if trade.is_short else "sell" trade_type = "exit_short" if trade.is_short else "sell"
custom_reason = strategy_safe_wrapper(self.custom_exit, default_retval=False)( custom_reason = strategy_safe_wrapper(self.custom_exit, default_retval=False)(
pair=trade.pair, trade=trade, current_time=current_time, pair=trade.pair, trade=trade, current_time=current_time,
current_rate=current_rate, current_profit=current_profit) current_rate=current_rate, current_profit=current_profit)
if custom_reason: if custom_reason:
exit_signal = ExitType.CUSTOM_SELL exit_signal = ExitType.CUSTOM_EXIT
if isinstance(custom_reason, str): if isinstance(custom_reason, str):
if len(custom_reason) > CUSTOM_EXIT_MAX_LENGTH: if len(custom_reason) > CUSTOM_EXIT_MAX_LENGTH:
logger.warning(f'Custom {trade_type} reason returned from ' logger.warning(f'Custom {trade_type} reason returned from '
@ -904,7 +904,7 @@ class IStrategy(ABC, HyperStrategyMixin):
custom_reason = custom_reason[:CUSTOM_EXIT_MAX_LENGTH] custom_reason = custom_reason[:CUSTOM_EXIT_MAX_LENGTH]
else: else:
custom_reason = None custom_reason = None
if exit_signal in (ExitType.CUSTOM_SELL, ExitType.SELL_SIGNAL): if exit_signal in (ExitType.CUSTOM_EXIT, ExitType.EXIT_SIGNAL):
logger.debug(f"{trade.pair} - Sell signal received. " logger.debug(f"{trade.pair} - Sell signal received. "
f"exit_type=ExitType.{exit_signal.name}" + f"exit_type=ExitType.{exit_signal.name}" +
(f", custom_reason={custom_reason}" if custom_reason else "")) (f", custom_reason={custom_reason}" if custom_reason else ""))

View File

@ -162,10 +162,10 @@ def confirm_trade_exit(self, pair: str, trade: 'Trade', order_type: str, amount:
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled). :param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
:param exit_reason: Exit reason. :param exit_reason: Exit reason.
Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss', Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss',
'sell_signal', 'force_sell', 'emergency_sell'] 'exit_signal', 'force_exit', 'emergency_exit']
:param current_time: datetime object, containing the current datetime :param current_time: datetime object, containing the current datetime
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return bool: When True is returned, then the sell-order is placed on the exchange. :return bool: When True is returned, then the exit-order is placed on the exchange.
False aborts the process False aborts the process
""" """
return True return True
@ -206,7 +206,7 @@ def check_exit_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -
:param trade: trade object. :param trade: trade object.
:param order: Order dictionary as returned from CCXT. :param order: Order dictionary as returned from CCXT.
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return bool: When True is returned, then the sell-order is cancelled. :return bool: When True is returned, then the exit-order is cancelled.
""" """
return False return False

View File

@ -95,8 +95,8 @@ tc1 = BTContainer(data=[
[6, 5000, 5025, 4975, 4987, 6172, 0, 0], # should sell [6, 5000, 5025, 4975, 4987, 6172, 0, 0], # should sell
], ],
stop_loss=-0.99, roi={"0": float('inf')}, profit_perc=0.00, stop_loss=-0.99, roi={"0": float('inf')}, profit_perc=0.00,
trades=[BTrade(exit_reason=ExitType.SELL_SIGNAL, open_tick=1, close_tick=2), trades=[BTrade(exit_reason=ExitType.EXIT_SIGNAL, open_tick=1, close_tick=2),
BTrade(exit_reason=ExitType.SELL_SIGNAL, open_tick=4, close_tick=6)] BTrade(exit_reason=ExitType.EXIT_SIGNAL, open_tick=4, close_tick=6)]
) )
# 3) Entered, sl 1%, candle drops 8% => Trade closed, 1% loss # 3) Entered, sl 1%, candle drops 8% => Trade closed, 1% loss
@ -391,7 +391,7 @@ def test_process_expectancy(mocker, edge_conf, fee, risk_reward_ratio, expectanc
'trade_duration': '', 'trade_duration': '',
'open_rate': 17, 'open_rate': 17,
'close_rate': 17, 'close_rate': 17,
'exit_type': 'sell_signal'}, 'exit_type': 'exit_signal'},
{'pair': 'TEST/BTC', {'pair': 'TEST/BTC',
'stoploss': -0.9, 'stoploss': -0.9,
@ -402,7 +402,7 @@ def test_process_expectancy(mocker, edge_conf, fee, risk_reward_ratio, expectanc
'trade_duration': '', 'trade_duration': '',
'open_rate': 20, 'open_rate': 20,
'close_rate': 20, 'close_rate': 20,
'exit_type': 'sell_signal'}, 'exit_type': 'exit_signal'},
{'pair': 'TEST/BTC', {'pair': 'TEST/BTC',
'stoploss': -0.9, 'stoploss': -0.9,
@ -413,7 +413,7 @@ def test_process_expectancy(mocker, edge_conf, fee, risk_reward_ratio, expectanc
'trade_duration': '', 'trade_duration': '',
'open_rate': 26, 'open_rate': 26,
'close_rate': 34, 'close_rate': 34,
'exit_type': 'sell_signal'} 'exit_type': 'exit_signal'}
] ]
trades_df = DataFrame(trades) trades_df = DataFrame(trades)

View File

@ -23,7 +23,7 @@ tc0 = BTContainer(data=[
[4, 5010, 5011, 4977, 4995, 6172, 0, 0], [4, 5010, 5011, 4977, 4995, 6172, 0, 0],
[5, 4995, 4995, 4950, 4950, 6172, 0, 0]], [5, 4995, 4995, 4950, 4950, 6172, 0, 0]],
stop_loss=-0.01, roi={"0": 1}, profit_perc=0.002, use_exit_signal=True, stop_loss=-0.01, roi={"0": 1}, profit_perc=0.002, use_exit_signal=True,
trades=[BTrade(exit_reason=ExitType.SELL_SIGNAL, open_tick=1, close_tick=4)] trades=[BTrade(exit_reason=ExitType.EXIT_SIGNAL, open_tick=1, close_tick=4)]
) )
# Test 1: Stop-Loss Triggered 1% loss # Test 1: Stop-Loss Triggered 1% loss
@ -424,7 +424,7 @@ tc26 = BTContainer(data=[
[4, 5010, 5010, 4855, 4995, 6172, 0, 0], # Triggers stoploss + sellsignal acted on [4, 5010, 5010, 4855, 4995, 6172, 0, 0], # Triggers stoploss + sellsignal acted on
[5, 4995, 4995, 4950, 4950, 6172, 0, 0]], [5, 4995, 4995, 4950, 4950, 6172, 0, 0]],
stop_loss=-0.01, roi={"0": 1}, profit_perc=0.002, use_exit_signal=True, stop_loss=-0.01, roi={"0": 1}, profit_perc=0.002, use_exit_signal=True,
trades=[BTrade(exit_reason=ExitType.SELL_SIGNAL, open_tick=1, close_tick=4)] trades=[BTrade(exit_reason=ExitType.EXIT_SIGNAL, open_tick=1, close_tick=4)]
) )
# Test 27: (copy of test26 with leverage) # Test 27: (copy of test26 with leverage)
@ -441,7 +441,7 @@ tc27 = BTContainer(data=[
[5, 4995, 4995, 4950, 4950, 6172, 0, 0]], [5, 4995, 4995, 4950, 4950, 6172, 0, 0]],
stop_loss=-0.05, roi={"0": 1}, profit_perc=0.002 * 5.0, use_exit_signal=True, stop_loss=-0.05, roi={"0": 1}, profit_perc=0.002 * 5.0, use_exit_signal=True,
leverage=5.0, leverage=5.0,
trades=[BTrade(exit_reason=ExitType.SELL_SIGNAL, open_tick=1, close_tick=4)] trades=[BTrade(exit_reason=ExitType.EXIT_SIGNAL, open_tick=1, close_tick=4)]
) )
# Test 28: (copy of test26 with leverage and as short) # Test 28: (copy of test26 with leverage and as short)
@ -458,7 +458,7 @@ tc28 = BTContainer(data=[
[5, 4995, 4995, 4950, 4950, 6172, 0, 0, 0, 0]], [5, 4995, 4995, 4950, 4950, 6172, 0, 0, 0, 0]],
stop_loss=-0.05, roi={"0": 1}, profit_perc=0.002 * 5.0, use_exit_signal=True, stop_loss=-0.05, roi={"0": 1}, profit_perc=0.002 * 5.0, use_exit_signal=True,
leverage=5.0, leverage=5.0,
trades=[BTrade(exit_reason=ExitType.SELL_SIGNAL, open_tick=1, close_tick=4, is_short=True)] trades=[BTrade(exit_reason=ExitType.EXIT_SIGNAL, open_tick=1, close_tick=4, is_short=True)]
) )
# Test 29: Sell with signal sell in candle 3 (ROI at signal candle) # Test 29: Sell with signal sell in candle 3 (ROI at signal candle)
# Stoploss at 10% (irrelevant), ROI at 5% (will trigger) # Stoploss at 10% (irrelevant), ROI at 5% (will trigger)
@ -486,7 +486,7 @@ tc30 = BTContainer(data=[
[4, 5010, 5251, 4855, 4995, 6172, 0, 0], # Triggers ROI, sell-signal acted on [4, 5010, 5251, 4855, 4995, 6172, 0, 0], # Triggers ROI, sell-signal acted on
[5, 4995, 4995, 4950, 4950, 6172, 0, 0]], [5, 4995, 4995, 4950, 4950, 6172, 0, 0]],
stop_loss=-0.10, roi={"0": 0.05}, profit_perc=0.002, use_exit_signal=True, stop_loss=-0.10, roi={"0": 0.05}, profit_perc=0.002, use_exit_signal=True,
trades=[BTrade(exit_reason=ExitType.SELL_SIGNAL, open_tick=1, close_tick=4)] trades=[BTrade(exit_reason=ExitType.EXIT_SIGNAL, open_tick=1, close_tick=4)]
) )
# Test 31: trailing_stop should raise so candle 3 causes a stoploss # Test 31: trailing_stop should raise so candle 3 causes a stoploss
@ -708,7 +708,7 @@ tc44 = BTContainer(data=[
stop_loss=-0.10, roi={"0": 0.10}, profit_perc=-0.01, stop_loss=-0.10, roi={"0": 0.10}, profit_perc=-0.01,
use_exit_signal=True, use_exit_signal=True,
custom_exit_price=4552, custom_exit_price=4552,
trades=[BTrade(exit_reason=ExitType.SELL_SIGNAL, open_tick=1, close_tick=3)] trades=[BTrade(exit_reason=ExitType.EXIT_SIGNAL, open_tick=1, close_tick=3)]
) )
# Test 45: Custom exit price above all candles # Test 45: Custom exit price above all candles
@ -723,7 +723,7 @@ tc45 = BTContainer(data=[
stop_loss=-0.10, roi={"0": 0.10}, profit_perc=0.0, stop_loss=-0.10, roi={"0": 0.10}, profit_perc=0.0,
use_exit_signal=True, use_exit_signal=True,
custom_exit_price=6052, custom_exit_price=6052,
trades=[BTrade(exit_reason=ExitType.FORCE_SELL, open_tick=1, close_tick=4)] trades=[BTrade(exit_reason=ExitType.FORCE_EXIT, open_tick=1, close_tick=4)]
) )
# Test 46: (Short of tc45) Custom short exit price above below candles # Test 46: (Short of tc45) Custom short exit price above below candles
@ -738,7 +738,7 @@ tc46 = BTContainer(data=[
stop_loss=-0.10, roi={"0": 0.10}, profit_perc=0.0, stop_loss=-0.10, roi={"0": 0.10}, profit_perc=0.0,
use_exit_signal=True, use_exit_signal=True,
custom_exit_price=4700, custom_exit_price=4700,
trades=[BTrade(exit_reason=ExitType.FORCE_SELL, open_tick=1, close_tick=4, is_short=True)] trades=[BTrade(exit_reason=ExitType.FORCE_EXIT, open_tick=1, close_tick=4, is_short=True)]
) )
# Test 47: Colliding long and short signal # Test 47: Colliding long and short signal

View File

@ -358,7 +358,7 @@ def test_hyperopt_format_results(hyperopt):
"is_short": [False, False, False, False], "is_short": [False, False, False, False],
"stake_amount": [0.01, 0.01, 0.01, 0.01], "stake_amount": [0.01, 0.01, 0.01, 0.01],
"exit_reason": [ExitType.ROI, ExitType.STOP_LOSS, "exit_reason": [ExitType.ROI, ExitType.STOP_LOSS,
ExitType.ROI, ExitType.FORCE_SELL] ExitType.ROI, ExitType.FORCE_EXIT]
}), }),
'config': hyperopt.config, 'config': hyperopt.config,
'locks': [], 'locks': [],
@ -429,7 +429,7 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None:
"is_short": [False, False, False, False], "is_short": [False, False, False, False],
"stake_amount": [0.01, 0.01, 0.01, 0.01], "stake_amount": [0.01, 0.01, 0.01, 0.01],
"exit_reason": [ExitType.ROI, ExitType.STOP_LOSS, "exit_reason": [ExitType.ROI, ExitType.STOP_LOSS,
ExitType.ROI, ExitType.FORCE_SELL] ExitType.ROI, ExitType.FORCE_EXIT]
}), }),
'config': hyperopt_conf, 'config': hyperopt_conf,
'locks': [], 'locks': [],

View File

@ -77,7 +77,7 @@ def test_generate_backtest_stats(default_conf, testdatadir, tmpdir):
"is_short": [False, False, False, False], "is_short": [False, False, False, False],
"stake_amount": [0.01, 0.01, 0.01, 0.01], "stake_amount": [0.01, 0.01, 0.01, 0.01],
"exit_reason": [ExitType.ROI, ExitType.STOP_LOSS, "exit_reason": [ExitType.ROI, ExitType.STOP_LOSS,
ExitType.ROI, ExitType.FORCE_SELL] ExitType.ROI, ExitType.FORCE_EXIT]
}), }),
'config': default_conf, 'config': default_conf,
'locks': [], 'locks': [],
@ -129,7 +129,7 @@ def test_generate_backtest_stats(default_conf, testdatadir, tmpdir):
"is_short": [False, False, False, False], "is_short": [False, False, False, False],
"stake_amount": [0.01, 0.01, 0.01, 0.01], "stake_amount": [0.01, 0.01, 0.01, 0.01],
"exit_reason": [ExitType.ROI, ExitType.ROI, "exit_reason": [ExitType.ROI, ExitType.ROI,
ExitType.STOP_LOSS, ExitType.FORCE_SELL] ExitType.STOP_LOSS, ExitType.FORCE_EXIT]
}), }),
'config': default_conf, 'config': default_conf,
'locks': [], 'locks': [],

View File

@ -11,7 +11,7 @@ from tests.conftest import get_patched_freqtradebot, log_has_re
def generate_mock_trade(pair: str, fee: float, is_open: bool, def generate_mock_trade(pair: str, fee: float, is_open: bool,
sell_reason: str = ExitType.SELL_SIGNAL, sell_reason: str = ExitType.EXIT_SIGNAL,
min_ago_open: int = None, min_ago_close: int = None, min_ago_open: int = None, min_ago_close: int = None,
profit_rate: float = 0.9 profit_rate: float = 0.9
): ):

View File

@ -1040,7 +1040,7 @@ def test_telegram_forcesell_handle(default_conf, update, ticker, fee,
assert msg_mock.call_count == 4 assert msg_mock.call_count == 4
last_msg = msg_mock.call_args_list[-2][0][0] last_msg = msg_mock.call_args_list[-2][0][0]
assert { assert {
'type': RPCMessageType.SELL, 'type': RPCMessageType.EXIT,
'trade_id': 1, 'trade_id': 1,
'exchange': 'Binance', 'exchange': 'Binance',
'pair': 'ETH/BTC', 'pair': 'ETH/BTC',
@ -1059,8 +1059,8 @@ def test_telegram_forcesell_handle(default_conf, update, ticker, fee,
'fiat_currency': 'USD', 'fiat_currency': 'USD',
'buy_tag': ANY, 'buy_tag': ANY,
'enter_tag': ANY, 'enter_tag': ANY,
'sell_reason': ExitType.FORCE_SELL.value, 'sell_reason': ExitType.FORCE_EXIT.value,
'exit_reason': ExitType.FORCE_SELL.value, 'exit_reason': ExitType.FORCE_EXIT.value,
'open_date': ANY, 'open_date': ANY,
'close_date': ANY, 'close_date': ANY,
'close_rate': ANY, 'close_rate': ANY,
@ -1109,7 +1109,7 @@ def test_telegram_forcesell_down_handle(default_conf, update, ticker, fee,
last_msg = msg_mock.call_args_list[-2][0][0] last_msg = msg_mock.call_args_list[-2][0][0]
assert { assert {
'type': RPCMessageType.SELL, 'type': RPCMessageType.EXIT,
'trade_id': 1, 'trade_id': 1,
'exchange': 'Binance', 'exchange': 'Binance',
'pair': 'ETH/BTC', 'pair': 'ETH/BTC',
@ -1128,8 +1128,8 @@ def test_telegram_forcesell_down_handle(default_conf, update, ticker, fee,
'fiat_currency': 'USD', 'fiat_currency': 'USD',
'buy_tag': ANY, 'buy_tag': ANY,
'enter_tag': ANY, 'enter_tag': ANY,
'sell_reason': ExitType.FORCE_SELL.value, 'sell_reason': ExitType.FORCE_EXIT.value,
'exit_reason': ExitType.FORCE_SELL.value, 'exit_reason': ExitType.FORCE_EXIT.value,
'open_date': ANY, 'open_date': ANY,
'close_date': ANY, 'close_date': ANY,
'close_rate': ANY, 'close_rate': ANY,
@ -1168,7 +1168,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None
assert msg_mock.call_count == 8 assert msg_mock.call_count == 8
msg = msg_mock.call_args_list[0][0][0] msg = msg_mock.call_args_list[0][0][0]
assert { assert {
'type': RPCMessageType.SELL, 'type': RPCMessageType.EXIT,
'trade_id': 1, 'trade_id': 1,
'exchange': 'Binance', 'exchange': 'Binance',
'pair': 'ETH/BTC', 'pair': 'ETH/BTC',
@ -1187,8 +1187,8 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None
'fiat_currency': 'USD', 'fiat_currency': 'USD',
'buy_tag': ANY, 'buy_tag': ANY,
'enter_tag': ANY, 'enter_tag': ANY,
'sell_reason': ExitType.FORCE_SELL.value, 'sell_reason': ExitType.FORCE_EXIT.value,
'exit_reason': ExitType.FORCE_SELL.value, 'exit_reason': ExitType.FORCE_EXIT.value,
'open_date': ANY, 'open_date': ANY,
'close_date': ANY, 'close_date': ANY,
'close_rate': ANY, 'close_rate': ANY,
@ -1771,10 +1771,10 @@ def test_show_config_handle(default_conf, update, mocker) -> None:
@pytest.mark.parametrize('message_type,enter,enter_signal,leverage', [ @pytest.mark.parametrize('message_type,enter,enter_signal,leverage', [
(RPCMessageType.BUY, 'Long', 'long_signal_01', None), (RPCMessageType.ENTRY, 'Long', 'long_signal_01', None),
(RPCMessageType.BUY, 'Long', 'long_signal_01', 1.0), (RPCMessageType.ENTRY, 'Long', 'long_signal_01', 1.0),
(RPCMessageType.BUY, 'Long', 'long_signal_01', 5.0), (RPCMessageType.ENTRY, 'Long', 'long_signal_01', 5.0),
(RPCMessageType.SHORT, 'Short', 'short_signal_01', 2.0)]) (RPCMessageType.ENTRY, 'Short', 'short_signal_01', 2.0)])
def test_send_msg_buy_notification(default_conf, mocker, caplog, message_type, def test_send_msg_buy_notification(default_conf, mocker, caplog, message_type,
enter, enter_signal, leverage) -> None: enter, enter_signal, leverage) -> None:
@ -1787,6 +1787,7 @@ def test_send_msg_buy_notification(default_conf, mocker, caplog, message_type,
'leverage': leverage, 'leverage': leverage,
'limit': 1.099e-05, 'limit': 1.099e-05,
'order_type': 'limit', 'order_type': 'limit',
'direction': enter,
'stake_amount': 0.01465333, 'stake_amount': 0.01465333,
'stake_amount_fiat': 0.0, 'stake_amount_fiat': 0.0,
'stake_currency': 'BTC', 'stake_currency': 'BTC',
@ -1827,8 +1828,8 @@ def test_send_msg_buy_notification(default_conf, mocker, caplog, message_type,
@pytest.mark.parametrize('message_type,enter_signal', [ @pytest.mark.parametrize('message_type,enter_signal', [
(RPCMessageType.BUY_CANCEL, 'long_signal_01'), (RPCMessageType.ENTRY_CANCEL, 'long_signal_01'),
(RPCMessageType.SHORT_CANCEL, 'short_signal_01')]) (RPCMessageType.ENTRY_CANCEL, 'short_signal_01')])
def test_send_msg_buy_cancel_notification(default_conf, mocker, message_type, enter_signal) -> None: def test_send_msg_buy_cancel_notification(default_conf, mocker, message_type, enter_signal) -> None:
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
@ -1875,14 +1876,14 @@ def test_send_msg_protection_notification(default_conf, mocker, time_machine) ->
@pytest.mark.parametrize('message_type,entered,enter_signal,leverage', [ @pytest.mark.parametrize('message_type,entered,enter_signal,leverage', [
(RPCMessageType.BUY_FILL, 'Longed', 'long_signal_01', 1.0), (RPCMessageType.ENTRY_FILL, 'Long', 'long_signal_01', 1.0),
(RPCMessageType.BUY_FILL, 'Longed', 'long_signal_02', 2.0), (RPCMessageType.ENTRY_FILL, 'Long', 'long_signal_02', 2.0),
(RPCMessageType.SHORT_FILL, 'Shorted', 'short_signal_01', 2.0), (RPCMessageType.ENTRY_FILL, 'Short', 'short_signal_01', 2.0),
]) ])
def test_send_msg_buy_fill_notification(default_conf, mocker, message_type, entered, def test_send_msg_entry_fill_notification(default_conf, mocker, message_type, entered,
enter_signal, leverage) -> None: enter_signal, leverage) -> None:
default_conf['telegram']['notification_settings']['buy_fill'] = 'on' default_conf['telegram']['notification_settings']['entry_fill'] = 'on'
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
telegram.send_msg({ telegram.send_msg({
@ -1893,6 +1894,7 @@ def test_send_msg_buy_fill_notification(default_conf, mocker, message_type, ente
'pair': 'ETH/BTC', 'pair': 'ETH/BTC',
'leverage': leverage, 'leverage': leverage,
'stake_amount': 0.01465333, 'stake_amount': 0.01465333,
'direction': entered,
# 'stake_amount_fiat': 0.0, # 'stake_amount_fiat': 0.0,
'stake_currency': 'BTC', 'stake_currency': 'BTC',
'fiat_currency': 'USD', 'fiat_currency': 'USD',
@ -1902,7 +1904,7 @@ def test_send_msg_buy_fill_notification(default_conf, mocker, message_type, ente
}) })
leverage_text = f'*Leverage:* `{leverage}`\n' if leverage != 1.0 else '' leverage_text = f'*Leverage:* `{leverage}`\n' if leverage != 1.0 else ''
assert msg_mock.call_args[0][0] == ( assert msg_mock.call_args[0][0] == (
f'\N{CHECK MARK} *Binance:* {entered} ETH/BTC (#1)\n' f'\N{CHECK MARK} *Binance:* {entered}ed ETH/BTC (#1)\n'
f'*Enter Tag:* `{enter_signal}`\n' f'*Enter Tag:* `{enter_signal}`\n'
'*Amount:* `1333.33333333`\n' '*Amount:* `1333.33333333`\n'
f"{leverage_text}" f"{leverage_text}"
@ -1918,7 +1920,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
old_convamount = telegram._rpc._fiat_converter.convert_amount old_convamount = telegram._rpc._fiat_converter.convert_amount
telegram._rpc._fiat_converter.convert_amount = lambda a, b, c: -24.812 telegram._rpc._fiat_converter.convert_amount = lambda a, b, c: -24.812
telegram.send_msg({ telegram.send_msg({
'type': RPCMessageType.SELL, 'type': RPCMessageType.EXIT,
'trade_id': 1, 'trade_id': 1,
'exchange': 'Binance', 'exchange': 'Binance',
'pair': 'KEY/ETH', 'pair': 'KEY/ETH',
@ -1954,7 +1956,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
msg_mock.reset_mock() msg_mock.reset_mock()
telegram.send_msg({ telegram.send_msg({
'type': RPCMessageType.SELL, 'type': RPCMessageType.EXIT,
'trade_id': 1, 'trade_id': 1,
'exchange': 'Binance', 'exchange': 'Binance',
'pair': 'KEY/ETH', 'pair': 'KEY/ETH',
@ -1996,7 +1998,7 @@ def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None:
old_convamount = telegram._rpc._fiat_converter.convert_amount old_convamount = telegram._rpc._fiat_converter.convert_amount
telegram._rpc._fiat_converter.convert_amount = lambda a, b, c: -24.812 telegram._rpc._fiat_converter.convert_amount = lambda a, b, c: -24.812
telegram.send_msg({ telegram.send_msg({
'type': RPCMessageType.SELL_CANCEL, 'type': RPCMessageType.EXIT_CANCEL,
'trade_id': 1, 'trade_id': 1,
'exchange': 'Binance', 'exchange': 'Binance',
'pair': 'KEY/ETH', 'pair': 'KEY/ETH',
@ -2008,7 +2010,7 @@ def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None:
msg_mock.reset_mock() msg_mock.reset_mock()
telegram.send_msg({ telegram.send_msg({
'type': RPCMessageType.SELL_CANCEL, 'type': RPCMessageType.EXIT_CANCEL,
'trade_id': 1, 'trade_id': 1,
'exchange': 'Binance', 'exchange': 'Binance',
'pair': 'KEY/ETH', 'pair': 'KEY/ETH',
@ -2028,11 +2030,11 @@ def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None:
def test_send_msg_sell_fill_notification(default_conf, mocker, direction, def test_send_msg_sell_fill_notification(default_conf, mocker, direction,
enter_signal, leverage) -> None: enter_signal, leverage) -> None:
default_conf['telegram']['notification_settings']['sell_fill'] = 'on' default_conf['telegram']['notification_settings']['exit_fill'] = 'on'
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
telegram.send_msg({ telegram.send_msg({
'type': RPCMessageType.SELL_FILL, 'type': RPCMessageType.EXIT_FILL,
'trade_id': 1, 'trade_id': 1,
'exchange': 'Binance', 'exchange': 'Binance',
'pair': 'KEY/ETH', 'pair': 'KEY/ETH',
@ -2105,9 +2107,9 @@ def test_send_msg_unknown_type(default_conf, mocker) -> None:
@pytest.mark.parametrize('message_type,enter,enter_signal,leverage', [ @pytest.mark.parametrize('message_type,enter,enter_signal,leverage', [
(RPCMessageType.BUY, 'Long', 'long_signal_01', None), (RPCMessageType.ENTRY, 'Long', 'long_signal_01', None),
(RPCMessageType.BUY, 'Long', 'long_signal_01', 2.0), (RPCMessageType.ENTRY, 'Long', 'long_signal_01', 2.0),
(RPCMessageType.SHORT, 'Short', 'short_signal_01', 2.0)]) (RPCMessageType.ENTRY, 'Short', 'short_signal_01', 2.0)])
def test_send_msg_buy_notification_no_fiat( def test_send_msg_buy_notification_no_fiat(
default_conf, mocker, message_type, enter, enter_signal, leverage) -> None: default_conf, mocker, message_type, enter, enter_signal, leverage) -> None:
del default_conf['fiat_display_currency'] del default_conf['fiat_display_currency']
@ -2122,6 +2124,7 @@ def test_send_msg_buy_notification_no_fiat(
'leverage': leverage, 'leverage': leverage,
'limit': 1.099e-05, 'limit': 1.099e-05,
'order_type': 'limit', 'order_type': 'limit',
'direction': enter,
'stake_amount': 0.01465333, 'stake_amount': 0.01465333,
'stake_amount_fiat': 0.0, 'stake_amount_fiat': 0.0,
'stake_currency': 'BTC', 'stake_currency': 'BTC',
@ -2155,7 +2158,7 @@ def test_send_msg_sell_notification_no_fiat(
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
telegram.send_msg({ telegram.send_msg({
'type': RPCMessageType.SELL, 'type': RPCMessageType.EXIT,
'trade_id': 1, 'trade_id': 1,
'exchange': 'Binance', 'exchange': 'Binance',
'pair': 'KEY/ETH', 'pair': 'KEY/ETH',

View File

@ -15,38 +15,38 @@ def get_webhook_dict() -> dict:
return { return {
"enabled": True, "enabled": True,
"url": "https://maker.ifttt.com/trigger/freqtrade_test/with/key/c764udvJ5jfSlswVRukZZ2/", "url": "https://maker.ifttt.com/trigger/freqtrade_test/with/key/c764udvJ5jfSlswVRukZZ2/",
"webhookbuy": { "webhookentry": {
"value1": "Buying {pair}", "value1": "Buying {pair}",
"value2": "limit {limit:8f}", "value2": "limit {limit:8f}",
"value3": "{stake_amount:8f} {stake_currency}", "value3": "{stake_amount:8f} {stake_currency}",
"value4": "leverage {leverage:.1f}", "value4": "leverage {leverage:.1f}",
"value5": "direction {direction}" "value5": "direction {direction}"
}, },
"webhookbuycancel": { "webhookentrycancel": {
"value1": "Cancelling Open Buy Order for {pair}", "value1": "Cancelling Open Buy Order for {pair}",
"value2": "limit {limit:8f}", "value2": "limit {limit:8f}",
"value3": "{stake_amount:8f} {stake_currency}", "value3": "{stake_amount:8f} {stake_currency}",
"value4": "leverage {leverage:.1f}", "value4": "leverage {leverage:.1f}",
"value5": "direction {direction}" "value5": "direction {direction}"
}, },
"webhookbuyfill": { "webhookentryfill": {
"value1": "Buy Order for {pair} filled", "value1": "Buy Order for {pair} filled",
"value2": "at {open_rate:8f}", "value2": "at {open_rate:8f}",
"value3": "{stake_amount:8f} {stake_currency}", "value3": "{stake_amount:8f} {stake_currency}",
"value4": "leverage {leverage:.1f}", "value4": "leverage {leverage:.1f}",
"value5": "direction {direction}" "value5": "direction {direction}"
}, },
"webhooksell": { "webhookexit": {
"value1": "Selling {pair}", "value1": "Selling {pair}",
"value2": "limit {limit:8f}", "value2": "limit {limit:8f}",
"value3": "profit: {profit_amount:8f} {stake_currency} ({profit_ratio})" "value3": "profit: {profit_amount:8f} {stake_currency} ({profit_ratio})"
}, },
"webhooksellcancel": { "webhookexitcancel": {
"value1": "Cancelling Open Sell Order for {pair}", "value1": "Cancelling Open Sell Order for {pair}",
"value2": "limit {limit:8f}", "value2": "limit {limit:8f}",
"value3": "profit: {profit_amount:8f} {stake_currency} ({profit_ratio})" "value3": "profit: {profit_amount:8f} {stake_currency} ({profit_ratio})"
}, },
"webhooksellfill": { "webhookexitfill": {
"value1": "Sell Order for {pair} filled", "value1": "Sell Order for {pair} filled",
"value2": "at {close_rate:8f}", "value2": "at {close_rate:8f}",
"value3": "" "value3": ""
@ -74,7 +74,7 @@ def test_send_msg_webhook(default_conf, mocker):
msg_mock = MagicMock() msg_mock = MagicMock()
mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock) mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock)
msg = { msg = {
'type': RPCMessageType.BUY, 'type': RPCMessageType.ENTRY,
'exchange': 'Binance', 'exchange': 'Binance',
'pair': 'ETH/BTC', 'pair': 'ETH/BTC',
'leverage': 1.0, 'leverage': 1.0,
@ -88,20 +88,20 @@ def test_send_msg_webhook(default_conf, mocker):
webhook.send_msg(msg=msg) webhook.send_msg(msg=msg)
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert (msg_mock.call_args[0][0]["value1"] == assert (msg_mock.call_args[0][0]["value1"] ==
default_conf["webhook"]["webhookbuy"]["value1"].format(**msg)) default_conf["webhook"]["webhookentry"]["value1"].format(**msg))
assert (msg_mock.call_args[0][0]["value2"] == assert (msg_mock.call_args[0][0]["value2"] ==
default_conf["webhook"]["webhookbuy"]["value2"].format(**msg)) default_conf["webhook"]["webhookentry"]["value2"].format(**msg))
assert (msg_mock.call_args[0][0]["value3"] == assert (msg_mock.call_args[0][0]["value3"] ==
default_conf["webhook"]["webhookbuy"]["value3"].format(**msg)) default_conf["webhook"]["webhookentry"]["value3"].format(**msg))
assert (msg_mock.call_args[0][0]["value4"] == assert (msg_mock.call_args[0][0]["value4"] ==
default_conf["webhook"]["webhookbuy"]["value4"].format(**msg)) default_conf["webhook"]["webhookentry"]["value4"].format(**msg))
assert (msg_mock.call_args[0][0]["value5"] == assert (msg_mock.call_args[0][0]["value5"] ==
default_conf["webhook"]["webhookbuy"]["value5"].format(**msg)) default_conf["webhook"]["webhookentry"]["value5"].format(**msg))
# Test short # Test short
msg_mock.reset_mock() msg_mock.reset_mock()
msg = { msg = {
'type': RPCMessageType.SHORT, 'type': RPCMessageType.ENTRY,
'exchange': 'Binance', 'exchange': 'Binance',
'pair': 'ETH/BTC', 'pair': 'ETH/BTC',
'leverage': 2.0, 'leverage': 2.0,
@ -115,20 +115,20 @@ def test_send_msg_webhook(default_conf, mocker):
webhook.send_msg(msg=msg) webhook.send_msg(msg=msg)
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert (msg_mock.call_args[0][0]["value1"] == assert (msg_mock.call_args[0][0]["value1"] ==
default_conf["webhook"]["webhookbuy"]["value1"].format(**msg)) default_conf["webhook"]["webhookentry"]["value1"].format(**msg))
assert (msg_mock.call_args[0][0]["value2"] == assert (msg_mock.call_args[0][0]["value2"] ==
default_conf["webhook"]["webhookbuy"]["value2"].format(**msg)) default_conf["webhook"]["webhookentry"]["value2"].format(**msg))
assert (msg_mock.call_args[0][0]["value3"] == assert (msg_mock.call_args[0][0]["value3"] ==
default_conf["webhook"]["webhookbuy"]["value3"].format(**msg)) default_conf["webhook"]["webhookentry"]["value3"].format(**msg))
assert (msg_mock.call_args[0][0]["value4"] == assert (msg_mock.call_args[0][0]["value4"] ==
default_conf["webhook"]["webhookbuy"]["value4"].format(**msg)) default_conf["webhook"]["webhookentry"]["value4"].format(**msg))
assert (msg_mock.call_args[0][0]["value5"] == assert (msg_mock.call_args[0][0]["value5"] ==
default_conf["webhook"]["webhookbuy"]["value5"].format(**msg)) default_conf["webhook"]["webhookentry"]["value5"].format(**msg))
# Test buy cancel # Test buy cancel
msg_mock.reset_mock() msg_mock.reset_mock()
msg = { msg = {
'type': RPCMessageType.BUY_CANCEL, 'type': RPCMessageType.ENTRY_CANCEL,
'exchange': 'Binance', 'exchange': 'Binance',
'pair': 'ETH/BTC', 'pair': 'ETH/BTC',
'leverage': 1.0, 'leverage': 1.0,
@ -142,16 +142,16 @@ def test_send_msg_webhook(default_conf, mocker):
webhook.send_msg(msg=msg) webhook.send_msg(msg=msg)
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert (msg_mock.call_args[0][0]["value1"] == assert (msg_mock.call_args[0][0]["value1"] ==
default_conf["webhook"]["webhookbuycancel"]["value1"].format(**msg)) default_conf["webhook"]["webhookentrycancel"]["value1"].format(**msg))
assert (msg_mock.call_args[0][0]["value2"] == assert (msg_mock.call_args[0][0]["value2"] ==
default_conf["webhook"]["webhookbuycancel"]["value2"].format(**msg)) default_conf["webhook"]["webhookentrycancel"]["value2"].format(**msg))
assert (msg_mock.call_args[0][0]["value3"] == assert (msg_mock.call_args[0][0]["value3"] ==
default_conf["webhook"]["webhookbuycancel"]["value3"].format(**msg)) default_conf["webhook"]["webhookentrycancel"]["value3"].format(**msg))
# Test short cancel # Test short cancel
msg_mock.reset_mock() msg_mock.reset_mock()
msg = { msg = {
'type': RPCMessageType.SHORT_CANCEL, 'type': RPCMessageType.ENTRY_CANCEL,
'exchange': 'Binance', 'exchange': 'Binance',
'pair': 'ETH/BTC', 'pair': 'ETH/BTC',
'leverage': 2.0, 'leverage': 2.0,
@ -165,20 +165,20 @@ def test_send_msg_webhook(default_conf, mocker):
webhook.send_msg(msg=msg) webhook.send_msg(msg=msg)
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert (msg_mock.call_args[0][0]["value1"] == assert (msg_mock.call_args[0][0]["value1"] ==
default_conf["webhook"]["webhookbuycancel"]["value1"].format(**msg)) default_conf["webhook"]["webhookentrycancel"]["value1"].format(**msg))
assert (msg_mock.call_args[0][0]["value2"] == assert (msg_mock.call_args[0][0]["value2"] ==
default_conf["webhook"]["webhookbuycancel"]["value2"].format(**msg)) default_conf["webhook"]["webhookentrycancel"]["value2"].format(**msg))
assert (msg_mock.call_args[0][0]["value3"] == assert (msg_mock.call_args[0][0]["value3"] ==
default_conf["webhook"]["webhookbuycancel"]["value3"].format(**msg)) default_conf["webhook"]["webhookentrycancel"]["value3"].format(**msg))
assert (msg_mock.call_args[0][0]["value4"] == assert (msg_mock.call_args[0][0]["value4"] ==
default_conf["webhook"]["webhookbuycancel"]["value4"].format(**msg)) default_conf["webhook"]["webhookentrycancel"]["value4"].format(**msg))
assert (msg_mock.call_args[0][0]["value5"] == assert (msg_mock.call_args[0][0]["value5"] ==
default_conf["webhook"]["webhookbuycancel"]["value5"].format(**msg)) default_conf["webhook"]["webhookentrycancel"]["value5"].format(**msg))
# Test buy fill # Test buy fill
msg_mock.reset_mock() msg_mock.reset_mock()
msg = { msg = {
'type': RPCMessageType.BUY_FILL, 'type': RPCMessageType.ENTRY_FILL,
'exchange': 'Binance', 'exchange': 'Binance',
'pair': 'ETH/BTC', 'pair': 'ETH/BTC',
'leverage': 1.0, 'leverage': 1.0,
@ -192,20 +192,20 @@ def test_send_msg_webhook(default_conf, mocker):
webhook.send_msg(msg=msg) webhook.send_msg(msg=msg)
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert (msg_mock.call_args[0][0]["value1"] == assert (msg_mock.call_args[0][0]["value1"] ==
default_conf["webhook"]["webhookbuyfill"]["value1"].format(**msg)) default_conf["webhook"]["webhookentryfill"]["value1"].format(**msg))
assert (msg_mock.call_args[0][0]["value2"] == assert (msg_mock.call_args[0][0]["value2"] ==
default_conf["webhook"]["webhookbuyfill"]["value2"].format(**msg)) default_conf["webhook"]["webhookentryfill"]["value2"].format(**msg))
assert (msg_mock.call_args[0][0]["value3"] == assert (msg_mock.call_args[0][0]["value3"] ==
default_conf["webhook"]["webhookbuyfill"]["value3"].format(**msg)) default_conf["webhook"]["webhookentryfill"]["value3"].format(**msg))
assert (msg_mock.call_args[0][0]["value4"] == assert (msg_mock.call_args[0][0]["value4"] ==
default_conf["webhook"]["webhookbuycancel"]["value4"].format(**msg)) default_conf["webhook"]["webhookentrycancel"]["value4"].format(**msg))
assert (msg_mock.call_args[0][0]["value5"] == assert (msg_mock.call_args[0][0]["value5"] ==
default_conf["webhook"]["webhookbuycancel"]["value5"].format(**msg)) default_conf["webhook"]["webhookentrycancel"]["value5"].format(**msg))
# Test short fill # Test short fill
msg_mock.reset_mock() msg_mock.reset_mock()
msg = { msg = {
'type': RPCMessageType.SHORT_FILL, 'type': RPCMessageType.ENTRY_FILL,
'exchange': 'Binance', 'exchange': 'Binance',
'pair': 'ETH/BTC', 'pair': 'ETH/BTC',
'leverage': 2.0, 'leverage': 2.0,
@ -219,20 +219,20 @@ def test_send_msg_webhook(default_conf, mocker):
webhook.send_msg(msg=msg) webhook.send_msg(msg=msg)
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert (msg_mock.call_args[0][0]["value1"] == assert (msg_mock.call_args[0][0]["value1"] ==
default_conf["webhook"]["webhookbuyfill"]["value1"].format(**msg)) default_conf["webhook"]["webhookentryfill"]["value1"].format(**msg))
assert (msg_mock.call_args[0][0]["value2"] == assert (msg_mock.call_args[0][0]["value2"] ==
default_conf["webhook"]["webhookbuyfill"]["value2"].format(**msg)) default_conf["webhook"]["webhookentryfill"]["value2"].format(**msg))
assert (msg_mock.call_args[0][0]["value3"] == assert (msg_mock.call_args[0][0]["value3"] ==
default_conf["webhook"]["webhookbuyfill"]["value3"].format(**msg)) default_conf["webhook"]["webhookentryfill"]["value3"].format(**msg))
assert (msg_mock.call_args[0][0]["value4"] == assert (msg_mock.call_args[0][0]["value4"] ==
default_conf["webhook"]["webhookbuycancel"]["value4"].format(**msg)) default_conf["webhook"]["webhookentrycancel"]["value4"].format(**msg))
assert (msg_mock.call_args[0][0]["value5"] == assert (msg_mock.call_args[0][0]["value5"] ==
default_conf["webhook"]["webhookbuycancel"]["value5"].format(**msg)) default_conf["webhook"]["webhookentrycancel"]["value5"].format(**msg))
# Test sell # Test sell
msg_mock.reset_mock() msg_mock.reset_mock()
msg = { msg = {
'type': RPCMessageType.SELL, 'type': RPCMessageType.EXIT,
'exchange': 'Binance', 'exchange': 'Binance',
'pair': 'ETH/BTC', 'pair': 'ETH/BTC',
'gain': "profit", 'gain': "profit",
@ -249,15 +249,15 @@ def test_send_msg_webhook(default_conf, mocker):
webhook.send_msg(msg=msg) webhook.send_msg(msg=msg)
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert (msg_mock.call_args[0][0]["value1"] == assert (msg_mock.call_args[0][0]["value1"] ==
default_conf["webhook"]["webhooksell"]["value1"].format(**msg)) default_conf["webhook"]["webhookexit"]["value1"].format(**msg))
assert (msg_mock.call_args[0][0]["value2"] == assert (msg_mock.call_args[0][0]["value2"] ==
default_conf["webhook"]["webhooksell"]["value2"].format(**msg)) default_conf["webhook"]["webhookexit"]["value2"].format(**msg))
assert (msg_mock.call_args[0][0]["value3"] == assert (msg_mock.call_args[0][0]["value3"] ==
default_conf["webhook"]["webhooksell"]["value3"].format(**msg)) default_conf["webhook"]["webhookexit"]["value3"].format(**msg))
# Test sell cancel # Test sell cancel
msg_mock.reset_mock() msg_mock.reset_mock()
msg = { msg = {
'type': RPCMessageType.SELL_CANCEL, 'type': RPCMessageType.EXIT_CANCEL,
'exchange': 'Binance', 'exchange': 'Binance',
'pair': 'ETH/BTC', 'pair': 'ETH/BTC',
'gain': "profit", 'gain': "profit",
@ -274,15 +274,15 @@ def test_send_msg_webhook(default_conf, mocker):
webhook.send_msg(msg=msg) webhook.send_msg(msg=msg)
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert (msg_mock.call_args[0][0]["value1"] == assert (msg_mock.call_args[0][0]["value1"] ==
default_conf["webhook"]["webhooksellcancel"]["value1"].format(**msg)) default_conf["webhook"]["webhookexitcancel"]["value1"].format(**msg))
assert (msg_mock.call_args[0][0]["value2"] == assert (msg_mock.call_args[0][0]["value2"] ==
default_conf["webhook"]["webhooksellcancel"]["value2"].format(**msg)) default_conf["webhook"]["webhookexitcancel"]["value2"].format(**msg))
assert (msg_mock.call_args[0][0]["value3"] == assert (msg_mock.call_args[0][0]["value3"] ==
default_conf["webhook"]["webhooksellcancel"]["value3"].format(**msg)) default_conf["webhook"]["webhookexitcancel"]["value3"].format(**msg))
# Test Sell fill # Test Sell fill
msg_mock.reset_mock() msg_mock.reset_mock()
msg = { msg = {
'type': RPCMessageType.SELL_FILL, 'type': RPCMessageType.EXIT_FILL,
'exchange': 'Binance', 'exchange': 'Binance',
'pair': 'ETH/BTC', 'pair': 'ETH/BTC',
'gain': "profit", 'gain': "profit",
@ -299,11 +299,11 @@ def test_send_msg_webhook(default_conf, mocker):
webhook.send_msg(msg=msg) webhook.send_msg(msg=msg)
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert (msg_mock.call_args[0][0]["value1"] == assert (msg_mock.call_args[0][0]["value1"] ==
default_conf["webhook"]["webhooksellfill"]["value1"].format(**msg)) default_conf["webhook"]["webhookexitfill"]["value1"].format(**msg))
assert (msg_mock.call_args[0][0]["value2"] == assert (msg_mock.call_args[0][0]["value2"] ==
default_conf["webhook"]["webhooksellfill"]["value2"].format(**msg)) default_conf["webhook"]["webhookexitfill"]["value2"].format(**msg))
assert (msg_mock.call_args[0][0]["value3"] == assert (msg_mock.call_args[0][0]["value3"] ==
default_conf["webhook"]["webhooksellfill"]["value3"].format(**msg)) default_conf["webhook"]["webhookexitfill"]["value3"].format(**msg))
for msgtype in [RPCMessageType.STATUS, for msgtype in [RPCMessageType.STATUS,
RPCMessageType.WARNING, RPCMessageType.WARNING,
@ -327,20 +327,20 @@ def test_send_msg_webhook(default_conf, mocker):
def test_exception_send_msg(default_conf, mocker, caplog): def test_exception_send_msg(default_conf, mocker, caplog):
default_conf["webhook"] = get_webhook_dict() default_conf["webhook"] = get_webhook_dict()
del default_conf["webhook"]["webhookbuy"] del default_conf["webhook"]["webhookentry"]
webhook = Webhook(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf) webhook = Webhook(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf)
webhook.send_msg({'type': RPCMessageType.BUY}) webhook.send_msg({'type': RPCMessageType.ENTRY})
assert log_has(f"Message type '{RPCMessageType.BUY}' not configured for webhooks", assert log_has(f"Message type '{RPCMessageType.ENTRY}' not configured for webhooks",
caplog) caplog)
default_conf["webhook"] = get_webhook_dict() default_conf["webhook"] = get_webhook_dict()
default_conf["webhook"]["webhookbuy"]["value1"] = "{DEADBEEF:8f}" default_conf["webhook"]["webhookentry"]["value1"] = "{DEADBEEF:8f}"
msg_mock = MagicMock() msg_mock = MagicMock()
mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock) mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock)
webhook = Webhook(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf) webhook = Webhook(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf)
msg = { msg = {
'type': RPCMessageType.BUY, 'type': RPCMessageType.ENTRY,
'exchange': 'Binance', 'exchange': 'Binance',
'pair': 'ETH/BTC', 'pair': 'ETH/BTC',
'limit': 0.005, 'limit': 0.005,

View File

@ -503,15 +503,15 @@ def test_custom_exit(default_conf, fee, caplog) -> None:
enter=False, exit_=False, enter=False, exit_=False,
low=None, high=None) low=None, high=None)
assert res.exit_flag is True assert res.exit_flag is True
assert res.exit_type == ExitType.CUSTOM_SELL assert res.exit_type == ExitType.CUSTOM_EXIT
assert res.exit_reason == 'custom_sell' assert res.exit_reason == 'custom_exit'
strategy.custom_exit = MagicMock(return_value='hello world') strategy.custom_exit = MagicMock(return_value='hello world')
res = strategy.should_exit(trade, 1, now, res = strategy.should_exit(trade, 1, now,
enter=False, exit_=False, enter=False, exit_=False,
low=None, high=None) low=None, high=None)
assert res.exit_type == ExitType.CUSTOM_SELL assert res.exit_type == ExitType.CUSTOM_EXIT
assert res.exit_flag is True assert res.exit_flag is True
assert res.exit_reason == 'hello world' assert res.exit_reason == 'hello world'
@ -520,7 +520,7 @@ def test_custom_exit(default_conf, fee, caplog) -> None:
res = strategy.should_exit(trade, 1, now, res = strategy.should_exit(trade, 1, now,
enter=False, exit_=False, enter=False, exit_=False,
low=None, high=None) low=None, high=None)
assert res.exit_type == ExitType.CUSTOM_SELL assert res.exit_type == ExitType.CUSTOM_EXIT
assert res.exit_flag is True assert res.exit_flag is True
assert res.exit_reason == 'h' * 64 assert res.exit_reason == 'h' * 64
assert log_has_re('Custom sell reason returned from custom_exit is too long.*', caplog) assert log_has_re('Custom sell reason returned from custom_exit is too long.*', caplog)

View File

@ -1210,7 +1210,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_
assert freqtrade.handle_stoploss_on_exchange(trade) is False assert freqtrade.handle_stoploss_on_exchange(trade) is False
assert trade.stoploss_order_id is None assert trade.stoploss_order_id is None
assert trade.is_open is False assert trade.is_open is False
assert trade.exit_reason == str(ExitType.EMERGENCY_SELL) assert trade.exit_reason == str(ExitType.EMERGENCY_EXIT)
@pytest.mark.parametrize("is_short", [False, True]) @pytest.mark.parametrize("is_short", [False, True])
@ -1293,7 +1293,7 @@ def test_create_stoploss_order_invalid_order(
caplog.clear() caplog.clear()
freqtrade.create_stoploss_order(trade, 200) freqtrade.create_stoploss_order(trade, 200)
assert trade.stoploss_order_id is None assert trade.stoploss_order_id is None
assert trade.exit_reason == ExitType.EMERGENCY_SELL.value assert trade.exit_reason == ExitType.EMERGENCY_EXIT.value
assert log_has("Unable to place a stoploss order on exchange. ", caplog) assert log_has("Unable to place a stoploss order on exchange. ", caplog)
assert log_has("Exiting the trade forcefully", caplog) assert log_has("Exiting the trade forcefully", caplog)
@ -1305,7 +1305,7 @@ def test_create_stoploss_order_invalid_order(
# Rpc is sending first buy, then sell # Rpc is sending first buy, then sell
assert rpc_mock.call_count == 2 assert rpc_mock.call_count == 2
assert rpc_mock.call_args_list[1][0][0]['sell_reason'] == ExitType.EMERGENCY_SELL.value assert rpc_mock.call_args_list[1][0][0]['sell_reason'] == ExitType.EMERGENCY_EXIT.value
assert rpc_mock.call_args_list[1][0][0]['order_type'] == 'market' assert rpc_mock.call_args_list[1][0][0]['order_type'] == 'market'
@ -2310,7 +2310,7 @@ def test_handle_trade_use_sell_signal(
else: else:
patch_get_signal(freqtrade, enter_long=False, exit_long=True) patch_get_signal(freqtrade, enter_long=False, exit_long=True)
assert freqtrade.handle_trade(trade) assert freqtrade.handle_trade(trade)
assert log_has("ETH/USDT - Sell signal received. exit_type=ExitType.SELL_SIGNAL", assert log_has("ETH/USDT - Sell signal received. exit_type=ExitType.EXIT_SIGNAL",
caplog) caplog)
@ -3091,7 +3091,7 @@ def test_execute_trade_exit_up(default_conf_usdt, ticker_usdt, fee, ticker_usdt_
last_msg = rpc_mock.call_args_list[-1][0][0] last_msg = rpc_mock.call_args_list[-1][0][0]
assert { assert {
'trade_id': 1, 'trade_id': 1,
'type': RPCMessageType.SELL, 'type': RPCMessageType.EXIT,
'exchange': 'Binance', 'exchange': 'Binance',
'pair': 'ETH/USDT', 'pair': 'ETH/USDT',
'gain': 'profit', 'gain': 'profit',
@ -3150,7 +3150,7 @@ def test_execute_trade_exit_down(default_conf_usdt, ticker_usdt, fee, ticker_usd
assert rpc_mock.call_count == 2 assert rpc_mock.call_count == 2
last_msg = rpc_mock.call_args_list[-1][0][0] last_msg = rpc_mock.call_args_list[-1][0][0]
assert { assert {
'type': RPCMessageType.SELL, 'type': RPCMessageType.EXIT,
'trade_id': 1, 'trade_id': 1,
'exchange': 'Binance', 'exchange': 'Binance',
'pair': 'ETH/USDT', 'pair': 'ETH/USDT',
@ -3221,7 +3221,7 @@ def test_execute_trade_exit_custom_exit_price(
freqtrade.execute_trade_exit( freqtrade.execute_trade_exit(
trade=trade, trade=trade,
limit=ticker_usdt_sell_up()['ask' if is_short else 'bid'], limit=ticker_usdt_sell_up()['ask' if is_short else 'bid'],
exit_check=ExitCheckTuple(exit_type=ExitType.SELL_SIGNAL) exit_check=ExitCheckTuple(exit_type=ExitType.EXIT_SIGNAL)
) )
# Sell price must be different to default bid price # Sell price must be different to default bid price
@ -3232,7 +3232,7 @@ def test_execute_trade_exit_custom_exit_price(
last_msg = rpc_mock.call_args_list[-1][0][0] last_msg = rpc_mock.call_args_list[-1][0][0]
assert { assert {
'trade_id': 1, 'trade_id': 1,
'type': RPCMessageType.SELL, 'type': RPCMessageType.EXIT,
'exchange': 'Binance', 'exchange': 'Binance',
'pair': 'ETH/USDT', 'pair': 'ETH/USDT',
'direction': 'Short' if trade.is_short else 'Long', 'direction': 'Short' if trade.is_short else 'Long',
@ -3249,8 +3249,8 @@ def test_execute_trade_exit_custom_exit_price(
'profit_ratio': profit_ratio, 'profit_ratio': profit_ratio,
'stake_currency': 'USDT', 'stake_currency': 'USDT',
'fiat_currency': 'USD', 'fiat_currency': 'USD',
'sell_reason': ExitType.SELL_SIGNAL.value, 'sell_reason': ExitType.EXIT_SIGNAL.value,
'exit_reason': ExitType.SELL_SIGNAL.value, 'exit_reason': ExitType.EXIT_SIGNAL.value,
'open_date': ANY, 'open_date': ANY,
'close_date': ANY, 'close_date': ANY,
'close_rate': ANY, 'close_rate': ANY,
@ -3299,7 +3299,7 @@ def test_execute_trade_exit_down_stoploss_on_exchange_dry_run(
last_msg = rpc_mock.call_args_list[-1][0][0] last_msg = rpc_mock.call_args_list[-1][0][0]
assert { assert {
'type': RPCMessageType.SELL, 'type': RPCMessageType.EXIT,
'trade_id': 1, 'trade_id': 1,
'exchange': 'Binance', 'exchange': 'Binance',
'pair': 'ETH/USDT', 'pair': 'ETH/USDT',
@ -3487,15 +3487,9 @@ def test_may_execute_trade_exit_after_stoploss_on_exchange_hit(
assert trade.is_open is False assert trade.is_open is False
assert trade.exit_reason == ExitType.STOPLOSS_ON_EXCHANGE.value assert trade.exit_reason == ExitType.STOPLOSS_ON_EXCHANGE.value
assert rpc_mock.call_count == 3 assert rpc_mock.call_count == 3
if is_short: assert rpc_mock.call_args_list[0][0][0]['type'] == RPCMessageType.ENTRY
assert rpc_mock.call_args_list[0][0][0]['type'] == RPCMessageType.SHORT assert rpc_mock.call_args_list[1][0][0]['type'] == RPCMessageType.ENTRY_FILL
assert rpc_mock.call_args_list[1][0][0]['type'] == RPCMessageType.SHORT_FILL assert rpc_mock.call_args_list[2][0][0]['type'] == RPCMessageType.EXIT
assert rpc_mock.call_args_list[2][0][0]['type'] == RPCMessageType.SELL
else:
assert rpc_mock.call_args_list[0][0][0]['type'] == RPCMessageType.BUY
assert rpc_mock.call_args_list[1][0][0]['type'] == RPCMessageType.BUY_FILL
assert rpc_mock.call_args_list[2][0][0]['type'] == RPCMessageType.SELL
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -3563,7 +3557,7 @@ def test_execute_trade_exit_market_order(
assert rpc_mock.call_count == 3 assert rpc_mock.call_count == 3
last_msg = rpc_mock.call_args_list[-2][0][0] last_msg = rpc_mock.call_args_list[-2][0][0]
assert { assert {
'type': RPCMessageType.SELL, 'type': RPCMessageType.EXIT,
'trade_id': 1, 'trade_id': 1,
'exchange': 'Binance', 'exchange': 'Binance',
'pair': 'ETH/USDT', 'pair': 'ETH/USDT',
@ -3630,18 +3624,18 @@ def test_execute_trade_exit_insufficient_funds_error(default_conf_usdt, ticker_u
@pytest.mark.parametrize('profit_only,bid,ask,handle_first,handle_second,exit_type,is_short', [ @pytest.mark.parametrize('profit_only,bid,ask,handle_first,handle_second,exit_type,is_short', [
# Enable profit # Enable profit
(True, 2.18, 2.2, False, True, ExitType.SELL_SIGNAL.value, False), (True, 2.18, 2.2, False, True, ExitType.EXIT_SIGNAL.value, False),
(True, 2.18, 2.2, False, True, ExitType.SELL_SIGNAL.value, True), (True, 2.18, 2.2, False, True, ExitType.EXIT_SIGNAL.value, True),
# # Disable profit # # Disable profit
(False, 3.19, 3.2, True, False, ExitType.SELL_SIGNAL.value, False), (False, 3.19, 3.2, True, False, ExitType.EXIT_SIGNAL.value, False),
(False, 3.19, 3.2, True, False, ExitType.SELL_SIGNAL.value, True), (False, 3.19, 3.2, True, False, ExitType.EXIT_SIGNAL.value, True),
# # Enable loss # # Enable loss
# # * Shouldn't this be ExitType.STOP_LOSS.value # # * Shouldn't this be ExitType.STOP_LOSS.value
(True, 0.21, 0.22, False, False, None, False), (True, 0.21, 0.22, False, False, None, False),
(True, 2.41, 2.42, False, False, None, True), (True, 2.41, 2.42, False, False, None, True),
# Disable loss # Disable loss
(False, 0.10, 0.22, True, False, ExitType.SELL_SIGNAL.value, False), (False, 0.10, 0.22, True, False, ExitType.EXIT_SIGNAL.value, False),
(False, 0.10, 0.22, True, False, ExitType.SELL_SIGNAL.value, True), (False, 0.10, 0.22, True, False, ExitType.EXIT_SIGNAL.value, True),
]) ])
def test_sell_profit_only( def test_sell_profit_only(
default_conf_usdt, limit_order, limit_order_open, is_short, default_conf_usdt, limit_order, limit_order_open, is_short,
@ -3669,7 +3663,7 @@ def test_sell_profit_only(
}) })
freqtrade = FreqtradeBot(default_conf_usdt) freqtrade = FreqtradeBot(default_conf_usdt)
patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
if exit_type == ExitType.SELL_SIGNAL.value: if exit_type == ExitType.EXIT_SIGNAL.value:
freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) freqtrade.strategy.min_roi_reached = MagicMock(return_value=False)
else: else:
freqtrade.strategy.stop_loss_reached = MagicMock(return_value=ExitCheckTuple( freqtrade.strategy.stop_loss_reached = MagicMock(return_value=ExitCheckTuple(

View File

@ -53,7 +53,7 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee,
# Sell 3rd trade (not called for the first trade) # Sell 3rd trade (not called for the first trade)
should_sell_mock = MagicMock(side_effect=[ should_sell_mock = MagicMock(side_effect=[
ExitCheckTuple(exit_type=ExitType.NONE), ExitCheckTuple(exit_type=ExitType.NONE),
ExitCheckTuple(exit_type=ExitType.SELL_SIGNAL)] ExitCheckTuple(exit_type=ExitType.EXIT_SIGNAL)]
) )
cancel_order_mock = MagicMock() cancel_order_mock = MagicMock()
mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss) mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss)
@ -123,7 +123,7 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee,
assert trade.is_open assert trade.is_open
trade = trades[2] trade = trades[2]
assert trade.exit_reason == ExitType.SELL_SIGNAL.value assert trade.exit_reason == ExitType.EXIT_SIGNAL.value
assert not trade.is_open assert not trade.is_open
@ -161,7 +161,7 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, mocker, balance_rati
) )
should_sell_mock = MagicMock(side_effect=[ should_sell_mock = MagicMock(side_effect=[
ExitCheckTuple(exit_type=ExitType.NONE), ExitCheckTuple(exit_type=ExitType.NONE),
ExitCheckTuple(exit_type=ExitType.SELL_SIGNAL), ExitCheckTuple(exit_type=ExitType.EXIT_SIGNAL),
ExitCheckTuple(exit_type=ExitType.NONE), ExitCheckTuple(exit_type=ExitType.NONE),
ExitCheckTuple(exit_type=ExitType.NONE), ExitCheckTuple(exit_type=ExitType.NONE),
ExitCheckTuple(exit_type=ExitType.NONE)] ExitCheckTuple(exit_type=ExitType.NONE)]

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long