Merge branch 'freqtrade:feat/short' into feat/short

This commit is contained in:
Adriance 2022-03-27 01:30:45 +08:00 committed by GitHub
commit ad8eac960e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
52 changed files with 726 additions and 503 deletions

View File

@ -5,7 +5,6 @@ on:
branches: branches:
- stable - stable
- develop - develop
- feat/short
- ci/* - ci/*
tags: tags:
release: release:

View File

@ -8,8 +8,8 @@
"dry_run": true, "dry_run": true,
"cancel_open_orders_on_exit": false, "cancel_open_orders_on_exit": false,
"unfilledtimeout": { "unfilledtimeout": {
"buy": 10, "entry": 10,
"sell": 10, "exit": 10,
"exit_timeout_count": 0, "exit_timeout_count": 0,
"unit": "minutes" "unit": "minutes"
}, },

View File

@ -8,8 +8,8 @@
"dry_run": true, "dry_run": true,
"cancel_open_orders_on_exit": false, "cancel_open_orders_on_exit": false,
"unfilledtimeout": { "unfilledtimeout": {
"buy": 10, "entry": 10,
"sell": 10, "exit": 10,
"exit_timeout_count": 0, "exit_timeout_count": 0,
"unit": "minutes" "unit": "minutes"
}, },

View File

@ -8,8 +8,8 @@
"dry_run": true, "dry_run": true,
"cancel_open_orders_on_exit": false, "cancel_open_orders_on_exit": false,
"unfilledtimeout": { "unfilledtimeout": {
"buy": 10, "entry": 10,
"sell": 10, "exit": 10,
"exit_timeout_count": 0, "exit_timeout_count": 0,
"unit": "minutes" "unit": "minutes"
}, },

View File

@ -30,8 +30,8 @@
}, },
"stoploss": -0.10, "stoploss": -0.10,
"unfilledtimeout": { "unfilledtimeout": {
"buy": 10, "entry": 10,
"sell": 10, "exit": 10,
"exit_timeout_count": 0, "exit_timeout_count": 0,
"unit": "minutes" "unit": "minutes"
}, },

View File

@ -8,8 +8,8 @@
"dry_run": true, "dry_run": true,
"cancel_open_orders_on_exit": false, "cancel_open_orders_on_exit": false,
"unfilledtimeout": { "unfilledtimeout": {
"buy": 10, "entry": 10,
"sell": 10, "exit": 10,
"exit_timeout_count": 0, "exit_timeout_count": 0,
"unit": "minutes" "unit": "minutes"
}, },

View File

@ -32,8 +32,8 @@ By default, loop runs every few seconds (`internals.process_throttle_secs`) and
* Call `populate_entry_trend()` * Call `populate_entry_trend()`
* Call `populate_exit_trend()` * Call `populate_exit_trend()`
* Check timeouts for open orders. * Check timeouts for open orders.
* Calls `check_buy_timeout()` strategy callback for open entry orders. * Calls `check_entry_timeout()` strategy callback for open entry orders.
* Calls `check_sell_timeout()` strategy callback for open exit orders. * Calls `check_exit_timeout()` strategy callback for open exit orders.
* Verifies existing positions and eventually places exit orders. * Verifies existing positions and eventually places exit orders.
* Considers stoploss, ROI and exit-signal, `custom_exit()` and `custom_stoploss()`. * Considers stoploss, ROI and exit-signal, `custom_exit()` and `custom_stoploss()`.
* Determine exit-price based on `ask_strategy` configuration setting or by using the `custom_exit_price()` callback. * Determine exit-price based on `ask_strategy` configuration setting or by using the `custom_exit_price()` callback.
@ -64,7 +64,7 @@ This loop will be repeated again and again until the bot is stopped.
* Check position adjustments for open trades if enabled and call `adjust_trade_position()` to determine if an additional order is requested. * Check position adjustments for open trades if enabled and call `adjust_trade_position()` to determine if an additional order is requested.
* Call `custom_stoploss()` and `custom_exit()` to find custom exit points. * Call `custom_stoploss()` and `custom_exit()` to find custom exit points.
* For exits based on exit-signal and custom-exit: Call `custom_exit_price()` to determine exit price (Prices are moved to be within the closing candle). * For exits based on exit-signal and custom-exit: Call `custom_exit_price()` to determine exit price (Prices are moved to be within the closing candle).
* Check for Order timeouts, either via the `unfilledtimeout` configuration, or via `check_buy_timeout()` / `check_sell_timeout()` strategy callbacks. * Check for Order timeouts, either via the `unfilledtimeout` configuration, or via `check_entry_timeout()` / `check_exit_timeout()` strategy callbacks.
* Generate backtest report output * Generate backtest report output
!!! Note !!! Note

View File

@ -102,8 +102,8 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| `trading_mode` | Specifies if you want to trade regularly, trade with leverage, or trade contracts whose prices are derived from matching cryptocurrency prices. [leverage documentation](leverage.md). <br>*Defaults to `"spot"`.* <br> **Datatype:** String | `trading_mode` | Specifies if you want to trade regularly, trade with leverage, or trade contracts whose prices are derived from matching cryptocurrency prices. [leverage documentation](leverage.md). <br>*Defaults to `"spot"`.* <br> **Datatype:** String
| `margin_mode` | When trading with leverage, this determines if the collateral owned by the trader will be shared or isolated to each trading pair [leverage documentation](leverage.md). <br> **Datatype:** String | `margin_mode` | When trading with leverage, this determines if the collateral owned by the trader will be shared or isolated to each trading pair [leverage documentation](leverage.md). <br> **Datatype:** String
| `liquidation_buffer` | A ratio specifying how large of a safety net to place between the liquidation price and the stoploss to prevent a position from reaching the liquidation price [leverage documentation](leverage.md). <br>*Defaults to `0.05`.* <br> **Datatype:** Float | `liquidation_buffer` | A ratio specifying how large of a safety net to place between the liquidation price and the stoploss to prevent a position from reaching the liquidation price [leverage documentation](leverage.md). <br>*Defaults to `0.05`.* <br> **Datatype:** Float
| `unfilledtimeout.buy` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Integer | `unfilledtimeout.entry` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled entry order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Integer
| `unfilledtimeout.sell` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Integer | `unfilledtimeout.exit` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled exit order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Integer
| `unfilledtimeout.unit` | Unit to use in unfilledtimeout setting. Note: If you set unfilledtimeout.unit to "seconds", "internals.process_throttle_secs" must be inferior or equal to timeout [Strategy Override](#parameters-in-the-strategy). <br> *Defaults to `minutes`.* <br> **Datatype:** String | `unfilledtimeout.unit` | Unit to use in unfilledtimeout setting. Note: If you set unfilledtimeout.unit to "seconds", "internals.process_throttle_secs" must be inferior or equal to timeout [Strategy Override](#parameters-in-the-strategy). <br> *Defaults to `minutes`.* <br> **Datatype:** String
| `unfilledtimeout.exit_timeout_count` | How many times can exit orders time out. Once this number of timeouts is reached, an emergency sell is triggered. 0 to disable and allow unlimited order cancels. [Strategy Override](#parameters-in-the-strategy).<br>*Defaults to `0`.* <br> **Datatype:** Integer | `unfilledtimeout.exit_timeout_count` | How many times can exit orders time out. Once this number of timeouts is reached, an emergency sell is triggered. 0 to disable and allow unlimited order cancels. [Strategy Override](#parameters-in-the-strategy).<br>*Defaults to `0`.* <br> **Datatype:** Integer
| `bid_strategy.price_side` | Select the side of the spread the bot should look at to get the buy rate. [More information below](#buy-price-side).<br> *Defaults to `bid`.* <br> **Datatype:** String (either `ask` or `bid`). | `bid_strategy.price_side` | Select the side of the spread the bot should look at to get the buy rate. [More information below](#buy-price-side).<br> *Defaults to `bid`.* <br> **Datatype:** String (either `ask` or `bid`).

View File

@ -51,9 +51,9 @@ When buying with the orderbook enabled (`bid_strategy.use_order_book=True`), Fre
#### Buy price without Orderbook enabled #### Buy price without Orderbook enabled
The following section uses `side` as the configured `bid_strategy.price_side`. The following section uses `side` as the configured `bid_strategy.price_side` (defaults to `"bid"`).
When not using orderbook (`bid_strategy.use_order_book=False`), Freqtrade uses the best `side` price from the ticker if it's below the `last` traded price from the ticker. Otherwise (when the `side` price is above the `last` price), it calculates a rate between `side` and `last` price. When not using orderbook (`bid_strategy.use_order_book=False`), Freqtrade uses the best `side` price from the ticker if it's below the `last` traded price from the ticker. Otherwise (when the `side` price is above the `last` price), it calculates a rate between `side` and `last` price based on `bid_strategy.ask_last_balance`..
The `bid_strategy.ask_last_balance` configuration parameter controls this. A value of `0.0` will use `side` price, while `1.0` will use the `last` price and values between those interpolate between ask and last price. The `bid_strategy.ask_last_balance` configuration parameter controls this. A value of `0.0` will use `side` price, while `1.0` will use the `last` price and values between those interpolate between ask and last price.
@ -88,9 +88,9 @@ When selling with the orderbook enabled (`ask_strategy.use_order_book=True`), Fr
#### Sell price without Orderbook enabled #### Sell price without Orderbook enabled
When not using orderbook (`ask_strategy.use_order_book=False`), the price at the `ask_strategy.price_side` side (defaults to `"ask"`) from the ticker will be used as the sell price. The following section uses `side` as the configured `ask_strategy.price_side` (defaults to `"ask"`).
When not using orderbook (`ask_strategy.use_order_book=False`), Freqtrade uses the best `side` price from the ticker if it's below the `last` traded price from the ticker. Otherwise (when the `side` price is above the `last` price), it calculates a rate between `side` and `last` price. When not using orderbook (`ask_strategy.use_order_book=False`), Freqtrade uses the best `side` price from the ticker if it's above the `last` traded price from the ticker. Otherwise (when the `side` price is below the `last` price), it calculates a rate between `side` and `last` price based on `ask_strategy.bid_last_balance`.
The `ask_strategy.bid_last_balance` configuration parameter controls this. A value of `0.0` will use `side` price, while `1.0` will use the last price and values between those interpolate between `side` and last price. The `ask_strategy.bid_last_balance` configuration parameter controls this. A value of `0.0` will use `side` price, while `1.0` will use the last price and values between those interpolate between `side` and last price.

View File

@ -2,3 +2,4 @@ mkdocs==1.2.3
mkdocs-material==8.2.5 mkdocs-material==8.2.5
mdx_truly_sane_lists==1.2 mdx_truly_sane_lists==1.2
pymdown-extensions==9.3 pymdown-extensions==9.3
jinja2==3.0.3

View File

@ -49,7 +49,7 @@ from freqtrade.exchange import timeframe_to_prev_date
class AwesomeStrategy(IStrategy): class AwesomeStrategy(IStrategy):
def confirm_trade_exit(self, pair: str, trade: 'Trade', order_type: str, amount: float, def confirm_trade_exit(self, pair: str, trade: 'Trade', order_type: str, amount: float,
rate: float, time_in_force: str, sell_reason: str, rate: float, time_in_force: str, exit_reason: str,
current_time: 'datetime', **kwargs) -> bool: current_time: 'datetime', **kwargs) -> bool:
# Obtain pair dataframe. # Obtain pair dataframe.
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
@ -125,7 +125,7 @@ def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame
The provided exit-tag is then used as sell-reason - and shown as such in backtest results. The provided exit-tag is then used as sell-reason - and shown as such in backtest results.
!!! Note !!! Note
`sell_reason` is limited to 100 characters, remaining data will be truncated. `exit_reason` is limited to 100 characters, remaining data will be truncated.
## Strategy version ## Strategy version

View File

@ -12,7 +12,7 @@ Currently available callbacks:
* [`custom_exit()`](#custom-exit-signal) * [`custom_exit()`](#custom-exit-signal)
* [`custom_stoploss()`](#custom-stoploss) * [`custom_stoploss()`](#custom-stoploss)
* [`custom_entry_price()` and `custom_exit_price()`](#custom-order-price-rules) * [`custom_entry_price()` and `custom_exit_price()`](#custom-order-price-rules)
* [`check_buy_timeout()` and `check_sell_timeout()`](#custom-order-timeout-rules) * [`check_entry_timeout()` and `check_exit_timeout()`](#custom-order-timeout-rules)
* [`confirm_trade_entry()`](#trade-entry-buy-order-confirmation) * [`confirm_trade_entry()`](#trade-entry-buy-order-confirmation)
* [`confirm_trade_exit()`](#trade-exit-sell-order-confirmation) * [`confirm_trade_exit()`](#trade-exit-sell-order-confirmation)
* [`adjust_trade_position()`](#adjust-trade-position) * [`adjust_trade_position()`](#adjust-trade-position)
@ -408,7 +408,7 @@ However, freqtrade also offers a custom callback for both order types, which all
### Custom order timeout example ### Custom order timeout example
Called for every open order until that order is either filled or cancelled. Called for every open order until that order is either filled or cancelled.
`check_buy_timeout()` is called for trade entries, while `check_sell_timeout()` is called for trade exit orders. `check_entry_timeout()` is called for trade entries, while `check_exit_timeout()` is called for trade exit orders.
A simple example, which applies different unfilled-timeouts depending on the price of the asset can be seen below. A simple example, which applies different unfilled-timeouts depending on the price of the asset can be seen below.
It applies a tight timeout for higher priced assets, while allowing more time to fill on cheap coins. It applies a tight timeout for higher priced assets, while allowing more time to fill on cheap coins.
@ -425,12 +425,12 @@ class AwesomeStrategy(IStrategy):
# Set unfilledtimeout to 25 hours, since the maximum timeout from below is 24 hours. # Set unfilledtimeout to 25 hours, since the maximum timeout from below is 24 hours.
unfilledtimeout = { unfilledtimeout = {
'buy': 60 * 25, 'entry': 60 * 25,
'sell': 60 * 25 'exit': 60 * 25
} }
def check_buy_timeout(self, pair: str, trade: 'Trade', order: dict, def check_entry_timeout(self, pair: str, trade: 'Trade', order: dict,
current_time: datetime, **kwargs) -> bool: current_time: datetime, **kwargs) -> bool:
if trade.open_rate > 100 and trade.open_date_utc < current_time - timedelta(minutes=5): if trade.open_rate > 100 and trade.open_date_utc < current_time - timedelta(minutes=5):
return True return True
elif trade.open_rate > 10 and trade.open_date_utc < current_time - timedelta(minutes=3): elif trade.open_rate > 10 and trade.open_date_utc < current_time - timedelta(minutes=3):
@ -440,7 +440,7 @@ class AwesomeStrategy(IStrategy):
return False return False
def check_sell_timeout(self, pair: str, trade: Trade, order: dict, def check_exit_timeout(self, pair: str, trade: Trade, order: dict,
current_time: datetime, **kwargs) -> bool: current_time: datetime, **kwargs) -> bool:
if trade.open_rate > 100 and trade.open_date_utc < current_time - timedelta(minutes=5): if trade.open_rate > 100 and trade.open_date_utc < current_time - timedelta(minutes=5):
return True return True
@ -466,12 +466,12 @@ class AwesomeStrategy(IStrategy):
# Set unfilledtimeout to 25 hours, since the maximum timeout from below is 24 hours. # Set unfilledtimeout to 25 hours, since the maximum timeout from below is 24 hours.
unfilledtimeout = { unfilledtimeout = {
'buy': 60 * 25, 'entry': 60 * 25,
'sell': 60 * 25 'exit': 60 * 25
} }
def check_buy_timeout(self, pair: str, trade: Trade, order: dict, def check_entry_timeout(self, pair: str, trade: Trade, order: dict,
current_time: datetime, **kwargs) -> bool: current_time: datetime, **kwargs) -> bool:
ob = self.dp.orderbook(pair, 1) ob = self.dp.orderbook(pair, 1)
current_price = ob['bids'][0][0] current_price = ob['bids'][0][0]
# Cancel buy order if price is more than 2% above the order. # Cancel buy order if price is more than 2% above the order.
@ -480,7 +480,7 @@ class AwesomeStrategy(IStrategy):
return False return False
def check_sell_timeout(self, pair: str, trade: Trade, order: dict, def check_exit_timeout(self, pair: str, trade: Trade, order: dict,
current_time: datetime, **kwargs) -> bool: current_time: datetime, **kwargs) -> bool:
ob = self.dp.orderbook(pair, 1) ob = self.dp.orderbook(pair, 1)
current_price = ob['asks'][0][0] current_price = ob['asks'][0][0]
@ -546,7 +546,7 @@ class AwesomeStrategy(IStrategy):
# ... populate_* methods # ... populate_* methods
def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float, def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float,
rate: float, time_in_force: str, sell_reason: str, rate: float, time_in_force: str, exit_reason: str,
current_time: datetime, **kwargs) -> bool: current_time: datetime, **kwargs) -> bool:
""" """
Called right before placing a regular sell order. Called right before placing a regular sell order.
@ -562,7 +562,7 @@ class AwesomeStrategy(IStrategy):
:param amount: Amount in quote currency. :param amount: Amount in quote currency.
:param rate: Rate that's going to be used when using limit orders :param rate: Rate that's going to be used when using limit orders
: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 sell_reason: Sell 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'] 'sell_signal', 'force_sell', 'emergency_sell']
:param current_time: datetime object, containing the current datetime :param current_time: datetime object, containing the current datetime
@ -570,7 +570,7 @@ class AwesomeStrategy(IStrategy):
:return bool: When True is returned, then the sell-order is placed on the exchange. :return bool: When True is returned, then the sell-order is placed on the exchange.
False aborts the process False aborts the process
""" """
if sell_reason == 'force_sell' and trade.calc_profit_ratio(rate) < 0: if exit_reason == 'force_sell' 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

@ -908,7 +908,7 @@ In some situations it may be confusing to deal with stops relative to current ra
??? Example "Returning a stoploss using absolute price from the custom stoploss function" ??? Example "Returning a stoploss using absolute price from the custom stoploss function"
If we want to trail a stop price at 2xATR below current proce we can call `stoploss_from_absolute(current_rate - (candle['atr'] * 2), current_rate, is_short=trade.is_short)`. If we want to trail a stop price at 2xATR below current price we can call `stoploss_from_absolute(current_rate - (candle['atr'] * 2), current_rate, is_short=trade.is_short)`.
``` python ``` python
@ -1092,7 +1092,7 @@ When conflicting signals collide (e.g. both `'enter_long'` and `'exit_long'` are
The following rules apply, and entry signals will be ignored if more than one of the 3 signals is set: The following rules apply, and entry signals will be ignored if more than one of the 3 signals is set:
- `enter_long` -> `exit_long`, `exit_short` - `enter_long` -> `exit_long`, `enter_short`
- `enter_short` -> `exit_short`, `enter_long` - `enter_short` -> `exit_short`, `enter_long`
## Further strategy ideas ## Further strategy ideas

View File

@ -11,6 +11,8 @@ If you intend on using markets other than spot markets, please migrate your stra
* `populate_buy_trend()` -> `populate_entry_trend()` * `populate_buy_trend()` -> `populate_entry_trend()`
* `populate_sell_trend()` -> `populate_exit_trend()` * `populate_sell_trend()` -> `populate_exit_trend()`
* `custom_sell()` -> `custom_exit()` * `custom_sell()` -> `custom_exit()`
* `check_buy_timeout()` -> `check_entry_timeout()`
* `check_sell_timeout()` -> `check_exit_timeout()`
* Dataframe columns: * Dataframe columns:
* `buy` -> `enter_long` * `buy` -> `enter_long`
* `sell` -> `exit_long` * `sell` -> `exit_long`
@ -20,6 +22,7 @@ If you intend on using markets other than spot markets, please migrate your stra
* New `side` argument to callbacks without trade object * New `side` argument to callbacks without trade object
* `custom_stake_amount` * `custom_stake_amount`
* `confirm_trade_entry` * `confirm_trade_entry`
* Changed argument name in `confirm_trade_exit`
* Renamed `trade.nr_of_successful_buys` to `trade.nr_of_successful_entries` (mostly relevant for `adjust_trade_position()`). * Renamed `trade.nr_of_successful_buys` to `trade.nr_of_successful_entries` (mostly relevant for `adjust_trade_position()`).
* Introduced new [`leverage` callback](strategy-callbacks.md#leverage-callback). * Introduced new [`leverage` callback](strategy-callbacks.md#leverage-callback).
* Informative pairs can now pass a 3rd element in the Tuple, defining the candle type. * Informative pairs can now pass a 3rd element in the Tuple, defining the candle type.
@ -29,6 +32,7 @@ If you intend on using markets other than spot markets, please migrate your stra
* Strategy/Configuration settings. * Strategy/Configuration settings.
* `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.
## Extensive explanation ## Extensive explanation
@ -123,6 +127,32 @@ class AwesomeStrategy(IStrategy):
# ... # ...
``` ```
### `custom_entry_timeout`
`check_buy_timeout()` has been renamed to `check_entry_timeout()`, and `check_sell_timeout()` has been renamed to `check_exit_timeout()`.
``` python hl_lines="2 6"
class AwesomeStrategy(IStrategy):
def check_buy_timeout(self, pair: str, trade: 'Trade', order: dict,
current_time: datetime, **kwargs) -> bool:
return False
def check_sell_timeout(self, pair: str, trade: 'Trade', order: dict,
current_time: datetime, **kwargs) -> bool:
return False
```
``` python hl_lines="2 6"
class AwesomeStrategy(IStrategy):
def check_entry_timeout(self, pair: str, trade: 'Trade', order: dict,
current_time: datetime, **kwargs) -> bool:
return False
def check_exit_timeout(self, pair: str, trade: 'Trade', order: dict,
current_time: datetime, **kwargs) -> bool:
return False
```
### Custom-stake-amount ### Custom-stake-amount
New string argument `side` - which can be either `"long"` or `"short"`. New string argument `side` - which can be either `"long"` or `"short"`.
@ -149,16 +179,17 @@ class AwesomeStrategy(IStrategy):
New string argument `side` - which can be either `"long"` or `"short"`. New string argument `side` - which can be either `"long"` or `"short"`.
``` python hl_lines="5" ``` python hl_lines="4"
class AwesomeStrategy(IStrategy): class AwesomeStrategy(IStrategy):
def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
time_in_force: str, current_time: datetime, entry_tag: Optional[str], time_in_force: str, current_time: datetime, entry_tag: Optional[str],
**kwargs) -> bool: **kwargs) -> bool:
return True return True
``` ```
After: After:
``` python hl_lines="5" ``` python hl_lines="4"
class AwesomeStrategy(IStrategy): class AwesomeStrategy(IStrategy):
def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
time_in_force: str, current_time: datetime, entry_tag: Optional[str], time_in_force: str, current_time: datetime, entry_tag: Optional[str],
@ -166,6 +197,29 @@ class AwesomeStrategy(IStrategy):
return True return True
``` ```
### `confirm_trade_exit`
Changed argument `sell_reason` to `exit_reason`.
For compatibility, `sell_reason` will still be provided for a limited time.
``` python hl_lines="3"
class AwesomeStrategy(IStrategy):
def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float,
rate: float, time_in_force: str, sell_reason: str,
current_time: datetime, **kwargs) -> bool:
return True
```
After:
``` python hl_lines="3"
class AwesomeStrategy(IStrategy):
def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float,
rate: float, time_in_force: str, exit_reason: str,
current_time: datetime, **kwargs) -> bool:
return True
```
### Adjust trade position changes ### Adjust trade position changes
While adjust-trade-position itself did not change, you should no longer use `trade.nr_of_successful_buys` - and instead use `trade.nr_of_successful_entries`, which will also include short entries. While adjust-trade-position itself did not change, you should no longer use `trade.nr_of_successful_buys` - and instead use `trade.nr_of_successful_entries`, which will also include short entries.
@ -234,6 +288,7 @@ This should be given the value of `trade.is_short`.
"stoploss": "market", "stoploss": "market",
"stoploss_on_exchange": false, "stoploss_on_exchange": false,
"stoploss_on_exchange_interval": 60 "stoploss_on_exchange_interval": 60
}
``` ```
``` python hl_lines="2-6" ``` python hl_lines="2-6"
@ -246,4 +301,27 @@ This should be given the value of `trade.is_short`.
"stoploss": "market", "stoploss": "market",
"stoploss_on_exchange": false, "stoploss_on_exchange": false,
"stoploss_on_exchange_interval": 60 "stoploss_on_exchange_interval": 60
}
```
#### `unfilledtimeout`
`unfilledtimeout` have changed all wordings from `buy` to `entry` - and `sell` to `exit`.
``` python hl_lines="2-3"
unfilledtimeout = {
"buy": 10,
"sell": 10,
"exit_timeout_count": 0,
"unit": "minutes"
}
```
``` python hl_lines="2-3"
unfilledtimeout = {
"entry": 10,
"exit": 10,
"exit_timeout_count": 0,
"unit": "minutes"
}
``` ```

View File

@ -216,6 +216,7 @@ def validate_migrated_strategy_settings(conf: Dict[str, Any]) -> None:
_validate_time_in_force(conf) _validate_time_in_force(conf)
_validate_order_types(conf) _validate_order_types(conf)
_validate_unfilledtimeout(conf)
def _validate_time_in_force(conf: Dict[str, Any]) -> None: def _validate_time_in_force(conf: Dict[str, Any]) -> None:
@ -258,3 +259,23 @@ def _validate_order_types(conf: Dict[str, Any]) -> None:
]: ]:
process_deprecated_setting(conf, 'order_types', o, 'order_types', n) process_deprecated_setting(conf, 'order_types', o, 'order_types', n)
def _validate_unfilledtimeout(conf: Dict[str, Any]) -> None:
unfilledtimeout = conf.get('unfilledtimeout', {})
if any(x in unfilledtimeout for x in ['buy', 'sell']):
if conf.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT:
raise OperationalException(
"Please migrate your unfilledtimeout settings to use the new wording.")
else:
logger.warning(
"DEPRECATED: Using 'buy' and 'sell' for unfilledtimeout is deprecated."
"Please migrate your unfilledtimeout settings to use 'entry' and 'exit' wording."
)
for o, n in [
('buy', 'entry'),
('sell', 'exit'),
]:
process_deprecated_setting(conf, 'unfilledtimeout', o, 'unfilledtimeout', n)

View File

@ -165,8 +165,8 @@ CONF_SCHEMA = {
'unfilledtimeout': { 'unfilledtimeout': {
'type': 'object', 'type': 'object',
'properties': { 'properties': {
'buy': {'type': 'number', 'minimum': 1}, 'entry': {'type': 'number', 'minimum': 1},
'sell': {'type': 'number', 'minimum': 1}, 'exit': {'type': 'number', 'minimum': 1},
'exit_timeout_count': {'type': 'number', 'minimum': 0, 'default': 0}, 'exit_timeout_count': {'type': 'number', 'minimum': 0, 'default': 0},
'unit': {'type': 'string', 'enum': TIMEOUT_UNITS, 'default': 'minutes'} 'unit': {'type': 'string', 'enum': TIMEOUT_UNITS, 'default': 'minutes'}
} }

View File

@ -13,8 +13,7 @@ from pandas import DataFrame
from freqtrade.configuration import TimeRange from freqtrade.configuration import TimeRange
from freqtrade.constants import DATETIME_PRINT_FORMAT, UNLIMITED_STAKE_AMOUNT from freqtrade.constants import DATETIME_PRINT_FORMAT, UNLIMITED_STAKE_AMOUNT
from freqtrade.data.history import get_timerange, load_data, refresh_data from freqtrade.data.history import get_timerange, load_data, refresh_data
from freqtrade.enums import RunMode, SellType from freqtrade.enums import CandleType, ExitType, RunMode
from freqtrade.enums.candletype import CandleType
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.exchange.exchange import timeframe_to_seconds from freqtrade.exchange.exchange import timeframe_to_seconds
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
@ -461,7 +460,7 @@ class Edge:
if stop_index <= sell_index: if stop_index <= sell_index:
exit_index = open_trade_index + stop_index exit_index = open_trade_index + stop_index
exit_type = SellType.STOP_LOSS exit_type = ExitType.STOP_LOSS
exit_price = stop_price exit_price = stop_price
elif stop_index > sell_index: elif stop_index > sell_index:
# If exit is SELL then we exit at the next candle # If exit is SELL then we exit at the next candle
@ -471,7 +470,7 @@ class Edge:
if len(ohlc_columns) - 1 < exit_index: if len(ohlc_columns) - 1 < exit_index:
break break
exit_type = SellType.SELL_SIGNAL exit_type = ExitType.SELL_SIGNAL
exit_price = ohlc_columns[exit_index, 0] exit_price = ohlc_columns[exit_index, 0]
trade = {'pair': pair, trade = {'pair': pair,

View File

@ -1,11 +1,12 @@
# flake8: noqa: F401 # flake8: noqa: F401
from freqtrade.enums.backteststate import BacktestState from freqtrade.enums.backteststate import BacktestState
from freqtrade.enums.candletype import CandleType from freqtrade.enums.candletype import CandleType
from freqtrade.enums.exitchecktuple import ExitCheckTuple
from freqtrade.enums.exittype import ExitType
from freqtrade.enums.marginmode import MarginMode from freqtrade.enums.marginmode import MarginMode
from freqtrade.enums.ordertypevalue import OrderTypeValues from freqtrade.enums.ordertypevalue import OrderTypeValues
from freqtrade.enums.rpcmessagetype import RPCMessageType from freqtrade.enums.rpcmessagetype import RPCMessageType
from freqtrade.enums.runmode import NON_UTIL_MODES, OPTIMIZE_MODES, TRADING_MODES, RunMode from freqtrade.enums.runmode import NON_UTIL_MODES, OPTIMIZE_MODES, TRADING_MODES, RunMode
from freqtrade.enums.selltype import SellType
from freqtrade.enums.signaltype import SignalDirection, SignalTagType, SignalType from freqtrade.enums.signaltype import SignalDirection, SignalTagType, SignalType
from freqtrade.enums.state import State from freqtrade.enums.state import State
from freqtrade.enums.tradingmode import TradingMode from freqtrade.enums.tradingmode import TradingMode

View File

@ -0,0 +1,17 @@
from freqtrade.enums.exittype import ExitType
class ExitCheckTuple:
"""
NamedTuple for Exit type + reason
"""
exit_type: ExitType
exit_reason: str = ''
def __init__(self, exit_type: ExitType, exit_reason: str = ''):
self.exit_type = exit_type
self.exit_reason = exit_reason or exit_type.value
@property
def exit_flag(self):
return self.exit_type != ExitType.NONE

View File

@ -1,7 +1,7 @@
from enum import Enum from enum import Enum
class SellType(Enum): class ExitType(Enum):
""" """
Enum to distinguish between sell reasons Enum to distinguish between sell reasons
""" """

View File

@ -401,7 +401,7 @@ class Exchange:
return trades return trades
def _order_contracts_to_amount(self, order: Dict) -> Dict: def _order_contracts_to_amount(self, order: Dict) -> Dict:
if 'symbol' in order: if 'symbol' in order and order['symbol'] is not None:
contract_size = self._get_contract_size(order['symbol']) contract_size = self._get_contract_size(order['symbol'])
if contract_size != 1: if contract_size != 1:
for prop in ['amount', 'cost', 'filled', 'remaining']: for prop in ['amount', 'cost', 'filled', 'remaining']:
@ -1102,14 +1102,15 @@ class Exchange:
if self.trading_mode == TradingMode.FUTURES: if self.trading_mode == TradingMode.FUTURES:
params['reduceOnly'] = True params['reduceOnly'] = True
amount = self.amount_to_precision(pair, amount) amount = self.amount_to_precision(pair, self._amount_to_contracts(pair, amount))
self._lev_prep(pair, leverage, side) self._lev_prep(pair, leverage, side)
order = self._api.create_order(symbol=pair, type=ordertype, side=side, order = self._api.create_order(symbol=pair, type=ordertype, side=side,
amount=amount, price=rate, params=params) amount=amount, price=rate, params=params)
self._log_exchange_response('create_stoploss_order', order)
order = self._order_contracts_to_amount(order)
logger.info(f"stoploss {user_order_type} order added for {pair}. " logger.info(f"stoploss {user_order_type} order added for {pair}. "
f"stop price: {stop_price}. limit: {rate}") f"stop price: {stop_price}. limit: {rate}")
self._log_exchange_response('create_stoploss_order', order)
return order return order
except ccxt.InsufficientFunds as e: except ccxt.InsufficientFunds as e:
raise InsufficientFundsError( raise InsufficientFundsError(
@ -2301,11 +2302,10 @@ class Exchange:
timeframe = self._ft_has['mark_ohlcv_timeframe'] timeframe = self._ft_has['mark_ohlcv_timeframe']
timeframe_ff = self._ft_has.get('funding_fee_timeframe', timeframe_ff = self._ft_has.get('funding_fee_timeframe',
self._ft_has['mark_ohlcv_timeframe']) self._ft_has['mark_ohlcv_timeframe'])
open_date = timeframe_to_prev_date(timeframe, open_date)
if not close_date: if not close_date:
close_date = datetime.now(timezone.utc) close_date = datetime.now(timezone.utc)
open_timestamp = int(open_date.timestamp()) * 1000 open_timestamp = int(timeframe_to_prev_date(timeframe, open_date).timestamp()) * 1000
# close_timestamp = int(close_date.timestamp()) * 1000 # close_timestamp = int(close_date.timestamp()) * 1000
mark_comb: PairWithTimeframe = ( mark_comb: PairWithTimeframe = (

View File

@ -16,7 +16,8 @@ from freqtrade.configuration import validate_config_consistency
from freqtrade.data.converter import order_book_to_dataframe from freqtrade.data.converter import order_book_to_dataframe
from freqtrade.data.dataprovider import DataProvider from freqtrade.data.dataprovider import DataProvider
from freqtrade.edge import Edge from freqtrade.edge import Edge
from freqtrade.enums import RPCMessageType, RunMode, SellType, SignalDirection, State, TradingMode from freqtrade.enums import (ExitCheckTuple, ExitType, RPCMessageType, RunMode, SignalDirection,
State, TradingMode)
from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError, from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError,
InvalidOrderException, PricingError) InvalidOrderException, PricingError)
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds
@ -27,7 +28,7 @@ from freqtrade.plugins.pairlistmanager import PairListManager
from freqtrade.plugins.protectionmanager import ProtectionManager from freqtrade.plugins.protectionmanager import ProtectionManager
from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.resolvers import ExchangeResolver, StrategyResolver
from freqtrade.rpc import RPCManager from freqtrade.rpc import RPCManager
from freqtrade.strategy.interface import IStrategy, SellCheckTuple from freqtrade.strategy.interface import IStrategy
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
from freqtrade.wallets import Wallets from freqtrade.wallets import Wallets
@ -272,7 +273,7 @@ class FreqtradeBot(LoggingMixin):
pair=trade.pair, pair=trade.pair,
amount=trade.amount, amount=trade.amount,
is_short=trade.is_short, is_short=trade.is_short,
open_date=trade.open_date open_date=trade.open_date_utc
) )
trade.funding_fees = funding_fees trade.funding_fees = funding_fees
else: else:
@ -976,8 +977,8 @@ class FreqtradeBot(LoggingMixin):
trade.stoploss_order_id = None trade.stoploss_order_id = None
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, sell_reason=SellCheckTuple( self.execute_trade_exit(trade, trade.stop_loss, exit_check=ExitCheckTuple(
sell_type=SellType.EMERGENCY_SELL)) exit_type=ExitType.EMERGENCY_SELL))
except ExchangeError: except ExchangeError:
trade.stoploss_order_id = None trade.stoploss_order_id = None
@ -989,7 +990,8 @@ class FreqtradeBot(LoggingMixin):
Check if trade is fulfilled in which case the stoploss Check if trade is fulfilled in which case the stoploss
on exchange should be added immediately if stoploss on exchange on exchange should be added immediately if stoploss on exchange
is enabled. is enabled.
# TODO-lev: liquidation price always on exchange, even without stoploss_on_exchange # TODO: liquidation price always on exchange, even without stoploss_on_exchange
# Therefore fetching account liquidations for open pairs may make sense.
""" """
logger.debug('Handling stoploss on exchange %s ...', trade) logger.debug('Handling stoploss on exchange %s ...', trade)
@ -1008,7 +1010,7 @@ class FreqtradeBot(LoggingMixin):
# We check if stoploss order is fulfilled # We check if stoploss order is fulfilled
if stoploss_order and stoploss_order['status'] in ('closed', 'triggered'): if stoploss_order and stoploss_order['status'] in ('closed', 'triggered'):
trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value trade.sell_reason = ExitType.STOPLOSS_ON_EXCHANGE.value
self.update_trade_state(trade, trade.stoploss_order_id, stoploss_order, self.update_trade_state(trade, trade.stoploss_order_id, stoploss_order,
stoploss_order=True) stoploss_order=True)
# Lock pair for one candle to prevent immediate rebuys # Lock pair for one candle to prevent immediate rebuys
@ -1101,7 +1103,7 @@ class FreqtradeBot(LoggingMixin):
""" """
Check and execute trade exit Check and execute trade exit
""" """
should_exit: SellCheckTuple = self.strategy.should_exit( should_exit: ExitCheckTuple = self.strategy.should_exit(
trade, trade,
exit_rate, exit_rate,
datetime.now(timezone.utc), datetime.now(timezone.utc),
@ -1110,8 +1112,8 @@ class FreqtradeBot(LoggingMixin):
force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0 force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0
) )
if should_exit.sell_flag: if should_exit.exit_flag:
logger.info(f'Exit for {trade.pair} detected. Reason: {should_exit.sell_type}' logger.info(f'Exit for {trade.pair} detected. Reason: {should_exit.exit_type}'
f'Tag: {exit_tag if exit_tag is not None else "None"}') f'Tag: {exit_tag if exit_tag is not None else "None"}')
self.execute_trade_exit(trade, exit_rate, should_exit, exit_tag=exit_tag) self.execute_trade_exit(trade, exit_rate, should_exit, exit_tag=exit_tag)
return True return True
@ -1136,13 +1138,12 @@ class FreqtradeBot(LoggingMixin):
fully_cancelled = self.update_trade_state(trade, trade.open_order_id, order) fully_cancelled = self.update_trade_state(trade, trade.open_order_id, order)
is_entering = order['side'] == trade.enter_side is_entering = order['side'] == trade.enter_side
not_closed = order['status'] == 'open' or fully_cancelled not_closed = order['status'] == 'open' or fully_cancelled
time_method = 'sell' if order['side'] == 'sell' else 'buy'
max_timeouts = self.config.get('unfilledtimeout', {}).get('exit_timeout_count', 0) max_timeouts = self.config.get('unfilledtimeout', {}).get('exit_timeout_count', 0)
order_obj = trade.select_order_by_order_id(trade.open_order_id) order_obj = trade.select_order_by_order_id(trade.open_order_id)
if not_closed and (fully_cancelled or (order_obj and self.strategy.ft_check_timed_out( if not_closed and (fully_cancelled or (order_obj and self.strategy.ft_check_timed_out(
time_method, trade, order_obj, datetime.now(timezone.utc))) trade, order_obj, datetime.now(timezone.utc)))
): ):
if is_entering: if is_entering:
self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['TIMEOUT']) self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['TIMEOUT'])
@ -1158,7 +1159,7 @@ class FreqtradeBot(LoggingMixin):
try: try:
self.execute_trade_exit( self.execute_trade_exit(
trade, order.get('price'), trade, order.get('price'),
sell_reason=SellCheckTuple(sell_type=SellType.EMERGENCY_SELL)) exit_check=ExitCheckTuple(exit_type=ExitType.EMERGENCY_SELL))
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}')
@ -1333,7 +1334,7 @@ class FreqtradeBot(LoggingMixin):
self, self,
trade: Trade, trade: Trade,
limit: float, limit: float,
sell_reason: SellCheckTuple, exit_check: ExitCheckTuple,
*, *,
exit_tag: Optional[str] = None, exit_tag: Optional[str] = None,
ordertype: Optional[str] = None, ordertype: Optional[str] = None,
@ -1342,17 +1343,17 @@ class FreqtradeBot(LoggingMixin):
Executes a trade exit for the given trade and limit Executes a trade exit for the given trade and limit
:param trade: Trade instance :param trade: Trade instance
:param limit: limit rate for the sell order :param limit: limit rate for the sell order
:param sell_reason: Reason the sell was triggered :param exit_check: CheckTuple with signal and reason
:return: True if it succeeds (supported) False (not supported) :return: True if it succeeds (supported) False (not supported)
""" """
trade.funding_fees = self.exchange.get_funding_fees( trade.funding_fees = self.exchange.get_funding_fees(
pair=trade.pair, pair=trade.pair,
amount=trade.amount, amount=trade.amount,
is_short=trade.is_short, is_short=trade.is_short,
open_date=trade.open_date, open_date=trade.open_date_utc,
) )
exit_type = 'exit' exit_type = 'exit'
if sell_reason.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS): if exit_check.exit_type in (ExitType.STOP_LOSS, ExitType.TRAILING_STOP_LOSS):
exit_type = 'stoploss' exit_type = 'stoploss'
# if stoploss is on exchange and we are on dry_run mode, # if stoploss is on exchange and we are on dry_run mode,
@ -1376,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 sell_reason.sell_type == SellType.EMERGENCY_SELL: if exit_check.exit_type == ExitType.EMERGENCY_SELL:
# 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")
@ -1385,7 +1386,8 @@ class FreqtradeBot(LoggingMixin):
if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)( if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)(
pair=trade.pair, trade=trade, order_type=order_type, amount=amount, rate=limit, pair=trade.pair, trade=trade, order_type=order_type, amount=amount, rate=limit,
time_in_force=time_in_force, sell_reason=sell_reason.sell_reason, time_in_force=time_in_force, exit_reason=exit_check.exit_reason,
sell_reason=exit_check.exit_reason, # sellreason -> compatibility
current_time=datetime.now(timezone.utc)): current_time=datetime.now(timezone.utc)):
logger.info(f"User requested abortion of exiting {trade.pair}") logger.info(f"User requested abortion of exiting {trade.pair}")
return False return False
@ -1414,7 +1416,7 @@ class FreqtradeBot(LoggingMixin):
trade.open_order_id = order['id'] trade.open_order_id = order['id']
trade.sell_order_status = '' trade.sell_order_status = ''
trade.close_rate_requested = limit trade.close_rate_requested = limit
trade.sell_reason = exit_tag or sell_reason.sell_reason trade.sell_reason = exit_tag or exit_check.exit_reason
# Lock pair for one candle to prevent immediate re-trading # Lock pair for one candle to prevent immediate re-trading
self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc), self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc),

View File

@ -19,7 +19,7 @@ from freqtrade.data import history
from freqtrade.data.btanalysis import find_existing_backtest_stats, trade_list_to_dataframe from freqtrade.data.btanalysis import find_existing_backtest_stats, trade_list_to_dataframe
from freqtrade.data.converter import trim_dataframe, trim_dataframes from freqtrade.data.converter import trim_dataframe, trim_dataframes
from freqtrade.data.dataprovider import DataProvider from freqtrade.data.dataprovider import DataProvider
from freqtrade.enums import BacktestState, CandleType, MarginMode, SellType, TradingMode from freqtrade.enums import BacktestState, CandleType, ExitCheckTuple, ExitType, TradingMode
from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.exceptions import DependencyException, OperationalException
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds
from freqtrade.misc import get_strategy_run_id from freqtrade.misc import get_strategy_run_id
@ -31,7 +31,7 @@ from freqtrade.persistence import LocalTrade, Order, PairLocks, Trade
from freqtrade.plugins.pairlistmanager import PairListManager from freqtrade.plugins.pairlistmanager import PairListManager
from freqtrade.plugins.protectionmanager import ProtectionManager from freqtrade.plugins.protectionmanager import ProtectionManager
from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.resolvers import ExchangeResolver, StrategyResolver
from freqtrade.strategy.interface import IStrategy, SellCheckTuple from freqtrade.strategy.interface import IStrategy
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
from freqtrade.wallets import Wallets from freqtrade.wallets import Wallets
@ -128,12 +128,9 @@ class Backtesting:
self.exchange.validate_required_startup_candles(self.required_startup, self.timeframe) self.exchange.validate_required_startup_candles(self.required_startup, self.timeframe)
self.trading_mode: TradingMode = config.get('trading_mode', TradingMode.SPOT) self.trading_mode: TradingMode = config.get('trading_mode', TradingMode.SPOT)
self.margin_mode: MarginMode = config.get('margin_mode', MarginMode.NONE)
# strategies which define "can_short=True" will fail to load in Spot mode. # strategies which define "can_short=True" will fail to load in Spot mode.
self._can_short = self.trading_mode != TradingMode.SPOT self._can_short = self.trading_mode != TradingMode.SPOT
self.progress = BTProgress()
self.abort = False
self.init_backtest() self.init_backtest()
def __del__(self): def __del__(self):
@ -352,20 +349,20 @@ class Backtesting:
data[pair] = df_analyzed[headers].values.tolist() data[pair] = df_analyzed[headers].values.tolist()
return data return data
def _get_close_rate(self, sell_row: Tuple, trade: LocalTrade, sell: SellCheckTuple, def _get_close_rate(self, sell_row: Tuple, trade: LocalTrade, sell: ExitCheckTuple,
trade_dur: int) -> float: trade_dur: int) -> float:
""" """
Get close rate for backtesting result Get close rate for backtesting result
""" """
# Special handling if high or low hit STOP_LOSS or ROI # Special handling if high or low hit STOP_LOSS or ROI
if sell.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS): if sell.exit_type in (ExitType.STOP_LOSS, ExitType.TRAILING_STOP_LOSS):
return self._get_close_rate_for_stoploss(sell_row, trade, sell, trade_dur) return self._get_close_rate_for_stoploss(sell_row, trade, sell, trade_dur)
elif sell.sell_type == (SellType.ROI): elif sell.exit_type == (ExitType.ROI):
return self._get_close_rate_for_roi(sell_row, trade, sell, trade_dur) return self._get_close_rate_for_roi(sell_row, trade, sell, trade_dur)
else: else:
return sell_row[OPEN_IDX] return sell_row[OPEN_IDX]
def _get_close_rate_for_stoploss(self, sell_row: Tuple, trade: LocalTrade, sell: SellCheckTuple, def _get_close_rate_for_stoploss(self, sell_row: Tuple, trade: LocalTrade, sell: ExitCheckTuple,
trade_dur: int) -> float: trade_dur: int) -> float:
# our stoploss was already lower than candle high, # our stoploss was already lower than candle high,
# possibly due to a cancelled trade exit. # possibly due to a cancelled trade exit.
@ -383,7 +380,7 @@ class Backtesting:
# Special case: trailing triggers within same candle as trade opened. Assume most # Special case: trailing triggers within same candle as trade opened. Assume most
# pessimistic price movement, which is moving just enough to arm stoploss and # pessimistic price movement, which is moving just enough to arm stoploss and
# immediately going down to stop price. # immediately going down to stop price.
if sell.sell_type == SellType.TRAILING_STOP_LOSS and trade_dur == 0: if sell.exit_type == ExitType.TRAILING_STOP_LOSS and trade_dur == 0:
if ( if (
not self.strategy.use_custom_stoploss and self.strategy.trailing_stop not self.strategy.use_custom_stoploss and self.strategy.trailing_stop
and self.strategy.trailing_only_offset_is_reached and self.strategy.trailing_only_offset_is_reached
@ -413,7 +410,7 @@ class Backtesting:
# Set close_rate to stoploss # Set close_rate to stoploss
return trade.stop_loss return trade.stop_loss
def _get_close_rate_for_roi(self, sell_row: Tuple, trade: LocalTrade, sell: SellCheckTuple, def _get_close_rate_for_roi(self, sell_row: Tuple, trade: LocalTrade, sell: ExitCheckTuple,
trade_dur: int) -> float: trade_dur: int) -> float:
is_short = trade.is_short or False is_short = trade.is_short or False
leverage = trade.leverage or 1.0 leverage = trade.leverage or 1.0
@ -521,7 +518,7 @@ class Backtesting:
low=sell_row[LOW_IDX], high=sell_row[HIGH_IDX] low=sell_row[LOW_IDX], high=sell_row[HIGH_IDX]
) )
if sell.sell_flag: if sell.exit_flag:
trade.close_date = sell_candle_time trade.close_date = sell_candle_time
trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60) trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60)
@ -532,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.sell_type in (SellType.SELL_SIGNAL, SellType.CUSTOM_SELL): if sell.exit_type in (ExitType.SELL_SIGNAL, ExitType.CUSTOM_SELL):
# 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,
@ -553,11 +550,12 @@ class Backtesting:
pair=trade.pair, trade=trade, order_type='limit', amount=trade.amount, pair=trade.pair, trade=trade, order_type='limit', amount=trade.amount,
rate=closerate, rate=closerate,
time_in_force=time_in_force, time_in_force=time_in_force,
sell_reason=sell.sell_reason, sell_reason=sell.exit_reason, # deprecated
exit_reason=sell.exit_reason,
current_time=sell_candle_time): current_time=sell_candle_time):
return None return None
trade.sell_reason = sell.sell_reason trade.sell_reason = sell.exit_reason
# Checks and adds an exit tag, after checking that the length of the # Checks and adds an exit tag, after checking that the length of the
# sell_row has the length for an exit tag column # sell_row has the length for an exit tag column
@ -812,7 +810,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.sell_reason = SellType.FORCE_SELL.value trade.sell_reason = ExitType.FORCE_SELL.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
@ -855,7 +853,7 @@ class Backtesting:
""" """
for order in [o for o in trade.orders if o.ft_is_open]: for order in [o for o in trade.orders if o.ft_is_open]:
timedout = self.strategy.ft_check_timed_out(order.side, trade, order, current_time) timedout = self.strategy.ft_check_timed_out(trade, order, current_time)
if timedout: if timedout:
if order.side == trade.enter_side: if order.side == trade.enter_side:
self.timedout_entry_orders += 1 self.timedout_entry_orders += 1

View File

@ -14,7 +14,7 @@ from sqlalchemy.pool import StaticPool
from sqlalchemy.sql.schema import UniqueConstraint from sqlalchemy.sql.schema import UniqueConstraint
from freqtrade.constants import DATETIME_PRINT_FORMAT, NON_OPEN_EXCHANGE_STATES from freqtrade.constants import DATETIME_PRINT_FORMAT, NON_OPEN_EXCHANGE_STATES
from freqtrade.enums import SellType, TradingMode from freqtrade.enums import ExitType, TradingMode
from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.exceptions import DependencyException, OperationalException
from freqtrade.leverage import interest from freqtrade.leverage import interest
from freqtrade.persistence.migrations import check_migrate from freqtrade.persistence.migrations import check_migrate
@ -625,7 +625,7 @@ class LocalTrade():
elif order.ft_order_side == 'stoploss': elif order.ft_order_side == 'stoploss':
self.stoploss_order_id = None self.stoploss_order_id = None
self.close_rate_requested = self.stop_loss self.close_rate_requested = self.stop_loss
self.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value self.sell_reason = ExitType.STOPLOSS_ON_EXCHANGE.value
if self.is_open: if self.is_open:
logger.info(f'{order.order_type.upper()} is hit for {self}.') logger.info(f'{order.order_type.upper()} is hit for {self}.')
self.close(order.safe_price) self.close(order.safe_price)
@ -688,7 +688,7 @@ class LocalTrade():
Get amount of failed exiting orders Get amount of failed exiting orders
assumes full exits. assumes full exits.
""" """
return len([o for o in self.orders if o.ft_order_side == 'sell']) return len([o for o in self.orders if o.ft_order_side == self.exit_side])
def _calc_open_trade_value(self) -> float: def _calc_open_trade_value(self) -> float:
""" """
@ -706,16 +706,14 @@ class LocalTrade():
""" """
Recalculate open_trade_value. Recalculate open_trade_value.
Must be called whenever open_rate, fee_open or is_short is changed. Must be called whenever open_rate, fee_open or is_short is changed.
""" """
self.open_trade_value = self._calc_open_trade_value() self.open_trade_value = self._calc_open_trade_value()
def calculate_interest(self, interest_rate: Optional[float] = None) -> Decimal: def calculate_interest(self, interest_rate: Optional[float] = None) -> Decimal:
""" """
: param interest_rate: interest_charge for borrowing this coin(optional). :param interest_rate: interest_charge for borrowing this coin(optional).
If interest_rate is not set self.interest_rate will be used If interest_rate is not set self.interest_rate will be used
""" """
zero = Decimal(0.0) zero = Decimal(0.0)
# If nothing was borrowed # If nothing was borrowed
if self.trading_mode != TradingMode.MARGIN or self.has_no_leverage: if self.trading_mode != TradingMode.MARGIN or self.has_no_leverage:

View File

@ -3,7 +3,7 @@ import logging
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Any, Dict from typing import Any, Dict
from freqtrade.enums import SellType from freqtrade.enums import ExitType
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.plugins.protections import IProtection, ProtectionReturn from freqtrade.plugins.protections import IProtection, ProtectionReturn
@ -44,8 +44,8 @@ class StoplossGuard(IProtection):
# filters = [ # filters = [
# Trade.is_open.is_(False), # Trade.is_open.is_(False),
# Trade.close_date > look_back_until, # Trade.close_date > look_back_until,
# or_(Trade.sell_reason == SellType.STOP_LOSS.value, # or_(Trade.sell_reason == ExitType.STOP_LOSS.value,
# and_(Trade.sell_reason == SellType.TRAILING_STOP_LOSS.value, # and_(Trade.sell_reason == ExitType.TRAILING_STOP_LOSS.value,
# Trade.close_profit < 0)) # Trade.close_profit < 0))
# ] # ]
# if pair: # if pair:
@ -54,8 +54,8 @@ class StoplossGuard(IProtection):
trades1 = Trade.get_trades_proxy(pair=pair, is_open=False, close_date=look_back_until) trades1 = Trade.get_trades_proxy(pair=pair, is_open=False, close_date=look_back_until)
trades = [trade for trade in trades1 if (str(trade.sell_reason) in ( trades = [trade for trade in trades1 if (str(trade.sell_reason) in (
SellType.TRAILING_STOP_LOSS.value, SellType.STOP_LOSS.value, ExitType.TRAILING_STOP_LOSS.value, ExitType.STOP_LOSS.value,
SellType.STOPLOSS_ON_EXCHANGE.value) ExitType.STOPLOSS_ON_EXCHANGE.value)
and trade.close_profit and trade.close_profit < 0)] and trade.close_profit and trade.close_profit < 0)]
if len(trades) < self._trade_limit: if len(trades) < self._trade_limit:

View File

@ -169,6 +169,51 @@ class StrategyResolver(IResolver):
" in your strategy. Please note that short signals will be ignored in that case." " in your strategy. Please note that short signals will be ignored in that case."
) )
@staticmethod
def validate_strategy(strategy: IStrategy) -> IStrategy:
if strategy.config.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT:
# Require new method
if not check_override(strategy, IStrategy, 'populate_entry_trend'):
raise OperationalException("`populate_entry_trend` must be implemented.")
if not check_override(strategy, IStrategy, 'populate_exit_trend'):
raise OperationalException("`populate_exit_trend` must be implemented.")
if check_override(strategy, IStrategy, 'check_buy_timeout'):
raise OperationalException("Please migrate your implementation "
"of `check_buy_timeout` to `check_entry_timeout`.")
if check_override(strategy, IStrategy, 'check_sell_timeout'):
raise OperationalException("Please migrate your implementation "
"of `check_sell_timeout` to `check_exit_timeout`.")
if check_override(strategy, IStrategy, 'custom_sell'):
raise OperationalException(
"Please migrate your implementation of `custom_sell` to `custom_exit`.")
else:
# TODO: Implementing one of the following methods should show a deprecation warning
# buy_trend and sell_trend, custom_sell
if (
not check_override(strategy, IStrategy, 'populate_buy_trend')
and not check_override(strategy, IStrategy, 'populate_entry_trend')
):
raise OperationalException(
"`populate_entry_trend` or `populate_buy_trend` must be implemented.")
if (
not check_override(strategy, IStrategy, 'populate_sell_trend')
and not check_override(strategy, IStrategy, 'populate_exit_trend')
):
raise OperationalException(
"`populate_exit_trend` or `populate_sell_trend` must be implemented.")
strategy._populate_fun_len = len(getfullargspec(strategy.populate_indicators).args)
strategy._buy_fun_len = len(getfullargspec(strategy.populate_buy_trend).args)
strategy._sell_fun_len = len(getfullargspec(strategy.populate_sell_trend).args)
if any(x == 2 for x in [
strategy._populate_fun_len,
strategy._buy_fun_len,
strategy._sell_fun_len
]):
strategy.INTERFACE_VERSION = 1
return strategy
@staticmethod @staticmethod
def _load_strategy(strategy_name: str, def _load_strategy(strategy_name: str,
config: dict, extra_dir: Optional[str] = None) -> IStrategy: config: dict, extra_dir: Optional[str] = None) -> IStrategy:
@ -208,42 +253,8 @@ class StrategyResolver(IResolver):
) )
if strategy: if strategy:
if strategy.config.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT:
# Require new method
if not check_override(strategy, IStrategy, 'populate_entry_trend'):
raise OperationalException("`populate_entry_trend` must be implemented.")
if not check_override(strategy, IStrategy, 'populate_exit_trend'):
raise OperationalException("`populate_exit_trend` must be implemented.")
if check_override(strategy, IStrategy, 'custom_sell'):
raise OperationalException(
"Please migrate your implementation of `custom_sell` to `custom_exit`.")
else:
# TODO: Implementing one of the following methods should show a deprecation warning
# buy_trend and sell_trend, custom_sell
if (
not check_override(strategy, IStrategy, 'populate_buy_trend')
and not check_override(strategy, IStrategy, 'populate_entry_trend')
):
raise OperationalException(
"`populate_entry_trend` or `populate_buy_trend` must be implemented.")
if (
not check_override(strategy, IStrategy, 'populate_sell_trend')
and not check_override(strategy, IStrategy, 'populate_exit_trend')
):
raise OperationalException(
"`populate_exit_trend` or `populate_sell_trend` must be implemented.")
strategy._populate_fun_len = len(getfullargspec(strategy.populate_indicators).args) return StrategyResolver.validate_strategy(strategy)
strategy._buy_fun_len = len(getfullargspec(strategy.populate_buy_trend).args)
strategy._sell_fun_len = len(getfullargspec(strategy.populate_sell_trend).args)
if any(x == 2 for x in [
strategy._populate_fun_len,
strategy._buy_fun_len,
strategy._sell_fun_len
]):
strategy.INTERFACE_VERSION = 1
return strategy
raise OperationalException( raise OperationalException(
f"Impossible to load Strategy '{strategy_name}'. This class does not exist " f"Impossible to load Strategy '{strategy_name}'. This class does not exist "

View File

@ -131,8 +131,8 @@ class Daily(BaseModel):
class UnfilledTimeout(BaseModel): class UnfilledTimeout(BaseModel):
buy: Optional[int] entry: Optional[int]
sell: Optional[int] exit: Optional[int]
unit: Optional[str] unit: Optional[str]
exit_timeout_count: Optional[int] exit_timeout_count: Optional[int]

View File

@ -18,7 +18,7 @@ from freqtrade import __version__
from freqtrade.configuration.timerange import TimeRange from freqtrade.configuration.timerange import TimeRange
from freqtrade.constants import CANCEL_REASON, DATETIME_PRINT_FORMAT from freqtrade.constants import CANCEL_REASON, DATETIME_PRINT_FORMAT
from freqtrade.data.history import load_data from freqtrade.data.history import load_data
from freqtrade.enums import SellType, SignalDirection, State, TradingMode from freqtrade.enums import ExitCheckTuple, ExitType, SignalDirection, State, TradingMode
from freqtrade.exceptions import ExchangeError, PricingError from freqtrade.exceptions import ExchangeError, PricingError
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs
from freqtrade.loggers import bufferHandler from freqtrade.loggers import bufferHandler
@ -27,7 +27,6 @@ from freqtrade.persistence import PairLocks, Trade
from freqtrade.persistence.models import PairLock from freqtrade.persistence.models import PairLock
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
from freqtrade.strategy.interface import SellCheckTuple
from freqtrade.wallets import PositionWallet, Wallet from freqtrade.wallets import PositionWallet, Wallet
@ -156,7 +155,7 @@ class RPC:
""" """
# Fetch open trades # Fetch open trades
if trade_ids: if trade_ids:
trades = Trade.get_trades(trade_filter=Trade.id.in_(trade_ids)).all() trades: List[Trade] = Trade.get_trades(trade_filter=Trade.id.in_(trade_ids)).all()
else: else:
trades = Trade.get_open_trades() trades = Trade.get_open_trades()
@ -171,9 +170,8 @@ class RPC:
# calculate profit and send message to user # calculate profit and send message to user
if trade.is_open: if trade.is_open:
try: try:
closing_side = trade.exit_side
current_rate = self._freqtrade.exchange.get_rate( current_rate = self._freqtrade.exchange.get_rate(
trade.pair, refresh=False, side=closing_side) trade.pair, refresh=False, side=trade.exit_side)
except (ExchangeError, PricingError): except (ExchangeError, PricingError):
current_rate = NAN current_rate = NAN
else: else:
@ -223,7 +221,7 @@ class RPC:
def _rpc_status_table(self, stake_currency: str, def _rpc_status_table(self, stake_currency: str,
fiat_display_currency: str) -> Tuple[List, List, float]: fiat_display_currency: str) -> Tuple[List, List, float]:
trades = Trade.get_open_trades() trades: List[Trade] = Trade.get_open_trades()
if not trades: if not trades:
raise RPCException('no active trade') raise RPCException('no active trade')
else: else:
@ -232,9 +230,8 @@ class RPC:
for trade in trades: for trade in trades:
# calculate profit and send message to user # calculate profit and send message to user
try: try:
closing_side = "buy" if trade.is_short else "sell"
current_rate = self._freqtrade.exchange.get_rate( current_rate = self._freqtrade.exchange.get_rate(
trade.pair, refresh=False, side=closing_side) trade.pair, refresh=False, side=trade.exit_side)
except (PricingError, ExchangeError): except (PricingError, ExchangeError):
current_rate = NAN current_rate = NAN
trade_profit = trade.calc_profit(current_rate) trade_profit = trade.calc_profit(current_rate)
@ -458,7 +455,7 @@ class RPC:
""" Returns cumulative profit statistics """ """ Returns cumulative profit statistics """
trade_filter = ((Trade.is_open.is_(False) & (Trade.close_date >= start_date)) | trade_filter = ((Trade.is_open.is_(False) & (Trade.close_date >= start_date)) |
Trade.is_open.is_(True)) Trade.is_open.is_(True))
trades = Trade.get_trades(trade_filter).order_by(Trade.id).all() trades: List[Trade] = Trade.get_trades(trade_filter).order_by(Trade.id).all()
profit_all_coin = [] profit_all_coin = []
profit_all_ratio = [] profit_all_ratio = []
@ -487,9 +484,8 @@ class RPC:
else: else:
# Get current rate # Get current rate
try: try:
closing_side = "buy" if trade.is_short else "sell"
current_rate = self._freqtrade.exchange.get_rate( current_rate = self._freqtrade.exchange.get_rate(
trade.pair, refresh=False, side=closing_side) trade.pair, refresh=False, side=trade.exit_side)
except (PricingError, ExchangeError): except (PricingError, ExchangeError):
current_rate = NAN current_rate = NAN
profit_ratio = trade.calc_profit_ratio(rate=current_rate) profit_ratio = trade.calc_profit_ratio(rate=current_rate)
@ -710,12 +706,12 @@ class RPC:
# 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, refresh=False, side=trade.exit_side) trade.pair, refresh=False, side=trade.exit_side)
sell_reason = SellCheckTuple(sell_type=SellType.FORCE_SELL) exit_check = ExitCheckTuple(exit_type=ExitType.FORCE_SELL)
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"])
self._freqtrade.execute_trade_exit( self._freqtrade.execute_trade_exit(
trade, current_rate, sell_reason, ordertype=order_type) trade, current_rate, exit_check, ordertype=order_type)
# ---- EOF def _exec_forcesell ---- # ---- EOF def _exec_forcesell ----
if self._freqtrade.state != State.RUNNING: if self._freqtrade.state != State.RUNNING:

View File

@ -13,8 +13,8 @@ from pandas import DataFrame
from freqtrade.constants import ListPairsWithTimeframes from freqtrade.constants import ListPairsWithTimeframes
from freqtrade.data.dataprovider import DataProvider from freqtrade.data.dataprovider import DataProvider
from freqtrade.enums import (CandleType, SellType, SignalDirection, SignalTagType, SignalType, from freqtrade.enums import (CandleType, ExitCheckTuple, ExitType, SignalDirection, SignalTagType,
TradingMode) SignalType, TradingMode)
from freqtrade.exceptions import OperationalException, StrategyError from freqtrade.exceptions import OperationalException, StrategyError
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds
from freqtrade.exchange.exchange import timeframe_to_next_date from freqtrade.exchange.exchange import timeframe_to_next_date
@ -32,22 +32,6 @@ logger = logging.getLogger(__name__)
CUSTOM_EXIT_MAX_LENGTH = 64 CUSTOM_EXIT_MAX_LENGTH = 64
class SellCheckTuple:
"""
NamedTuple for Sell type + reason
"""
sell_type: SellType
sell_reason: str = ''
def __init__(self, sell_type: SellType, sell_reason: str = ''):
self.sell_type = sell_type
self.sell_reason = sell_reason or sell_type.value
@property
def sell_flag(self):
return self.sell_type != SellType.NONE
class IStrategy(ABC, HyperStrategyMixin): class IStrategy(ABC, HyperStrategyMixin):
""" """
Interface for freqtrade strategies Interface for freqtrade strategies
@ -152,8 +136,7 @@ class IStrategy(ABC, HyperStrategyMixin):
cls_method = getattr(self.__class__, attr_name) cls_method = getattr(self.__class__, attr_name)
if not callable(cls_method): if not callable(cls_method):
continue continue
informative_data_list = getattr( informative_data_list = getattr(cls_method, '_ft_informative', None)
cls_method, '_ft_informative', None)
if not isinstance(informative_data_list, list): if not isinstance(informative_data_list, list):
# Type check is required because mocker would return a mock object that evaluates to # Type check is required because mocker would return a mock object that evaluates to
# True, confusing this code. # True, confusing this code.
@ -226,7 +209,14 @@ class IStrategy(ABC, HyperStrategyMixin):
def check_buy_timeout(self, pair: str, trade: Trade, order: dict, def check_buy_timeout(self, pair: str, trade: Trade, order: dict,
current_time: datetime, **kwargs) -> bool: current_time: datetime, **kwargs) -> bool:
""" """
Check buy timeout function callback. DEPRECATED: Please use `check_entry_timeout` instead.
"""
return False
def check_entry_timeout(self, pair: str, trade: Trade, order: dict,
current_time: datetime, **kwargs) -> bool:
"""
Check entry timeout function callback.
This method can be used to override the enter-timeout. This method can be used to override the enter-timeout.
It is called whenever a limit entry order has been created, It is called whenever a limit entry order has been created,
and is not yet fully filled. and is not yet fully filled.
@ -241,11 +231,19 @@ class IStrategy(ABC, HyperStrategyMixin):
: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 entry order is cancelled. :return bool: When True is returned, then the entry order is cancelled.
""" """
return False return self.check_buy_timeout(
pair=pair, trade=trade, order=order, current_time=current_time)
def check_sell_timeout(self, pair: str, trade: Trade, order: dict, def check_sell_timeout(self, pair: str, trade: Trade, order: dict,
current_time: datetime, **kwargs) -> bool: current_time: datetime, **kwargs) -> bool:
""" """
DEPRECATED: Please use `check_exit_timeout` instead.
"""
return False
def check_exit_timeout(self, pair: str, trade: Trade, order: dict,
current_time: datetime, **kwargs) -> bool:
"""
Check sell timeout function callback. Check sell timeout function callback.
This method can be used to override the exit-timeout. This method can be used to override the exit-timeout.
It is called whenever a (long) limit sell order or (short) limit buy It is called whenever a (long) limit sell order or (short) limit buy
@ -261,7 +259,8 @@ class IStrategy(ABC, HyperStrategyMixin):
: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 (long)sell/(short)buy-order is cancelled. :return bool: When True is returned, then the (long)sell/(short)buy-order is cancelled.
""" """
return False return self.check_sell_timeout(
pair=pair, trade=trade, order=order, current_time=current_time)
def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
time_in_force: str, current_time: datetime, entry_tag: Optional[str], time_in_force: str, current_time: datetime, entry_tag: Optional[str],
@ -290,7 +289,7 @@ class IStrategy(ABC, HyperStrategyMixin):
return True return True
def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float, def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float,
rate: float, time_in_force: str, sell_reason: str, rate: float, time_in_force: str, exit_reason: str,
current_time: datetime, **kwargs) -> bool: current_time: datetime, **kwargs) -> bool:
""" """
Called right before placing a regular exit order. Called right before placing a regular exit order.
@ -307,7 +306,7 @@ class IStrategy(ABC, HyperStrategyMixin):
:param amount: Amount in quote currency. :param amount: Amount in quote currency.
:param rate: Rate that's going to be used when using limit orders :param rate: Rate that's going to be used when using limit orders
: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 sell_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'] 'sell_signal', 'force_sell', 'emergency_sell']
:param current_time: datetime object, containing the current datetime :param current_time: datetime object, containing the current datetime
@ -848,7 +847,7 @@ class IStrategy(ABC, HyperStrategyMixin):
def should_exit(self, trade: Trade, rate: float, current_time: datetime, *, def should_exit(self, trade: Trade, rate: float, current_time: datetime, *,
enter: bool, exit_: bool, enter: bool, exit_: bool,
low: float = None, high: float = None, low: float = None, high: float = None,
force_stoploss: float = 0) -> SellCheckTuple: force_stoploss: float = 0) -> ExitCheckTuple:
""" """
This function evaluates if one of the conditions required to trigger an exit order This function evaluates if one of the conditions required to trigger an exit order
has been reached, which can either be a stop-loss, ROI or exit-signal. has been reached, which can either be a stop-loss, ROI or exit-signal.
@ -877,7 +876,7 @@ class IStrategy(ABC, HyperStrategyMixin):
and self.min_roi_reached(trade=trade, current_profit=current_profit, and self.min_roi_reached(trade=trade, current_profit=current_profit,
current_time=current_time)) current_time=current_time))
sell_signal = SellType.NONE sell_signal = ExitType.NONE
custom_reason = '' custom_reason = ''
# use provided rate in backtesting, not high/low. # use provided rate in backtesting, not high/low.
current_rate = rate current_rate = rate
@ -888,14 +887,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_:
sell_signal = SellType.SELL_SIGNAL sell_signal = ExitType.SELL_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:
sell_signal = SellType.CUSTOM_SELL sell_signal = ExitType.CUSTOM_SELL
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,33 +903,33 @@ 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 sell_signal in (SellType.CUSTOM_SELL, SellType.SELL_SIGNAL): if sell_signal in (ExitType.CUSTOM_SELL, ExitType.SELL_SIGNAL):
logger.debug(f"{trade.pair} - Sell signal received. " logger.debug(f"{trade.pair} - Sell signal received. "
f"sell_type=SellType.{sell_signal.name}" + f"sell_type=ExitType.{sell_signal.name}" +
(f", custom_reason={custom_reason}" if custom_reason else "")) (f", custom_reason={custom_reason}" if custom_reason else ""))
return SellCheckTuple(sell_type=sell_signal, sell_reason=custom_reason) return ExitCheckTuple(exit_type=sell_signal, exit_reason=custom_reason)
# Sequence: # Sequence:
# Exit-signal # Exit-signal
# ROI (if not stoploss) # ROI (if not stoploss)
# Stoploss # Stoploss
if roi_reached and stoplossflag.sell_type != SellType.STOP_LOSS: if roi_reached and stoplossflag.exit_type != ExitType.STOP_LOSS:
logger.debug(f"{trade.pair} - Required profit reached. sell_type=SellType.ROI") logger.debug(f"{trade.pair} - Required profit reached. sell_type=ExitType.ROI")
return SellCheckTuple(sell_type=SellType.ROI) return ExitCheckTuple(exit_type=ExitType.ROI)
if stoplossflag.sell_flag: if stoplossflag.exit_flag:
logger.debug(f"{trade.pair} - Stoploss hit. sell_type={stoplossflag.sell_type}") logger.debug(f"{trade.pair} - Stoploss hit. sell_type={stoplossflag.exit_type}")
return stoplossflag return stoplossflag
# This one is noisy, commented out... # This one is noisy, commented out...
# logger.debug(f"{trade.pair} - No exit signal.") # logger.debug(f"{trade.pair} - No exit signal.")
return SellCheckTuple(sell_type=SellType.NONE) return ExitCheckTuple(exit_type=ExitType.NONE)
def stop_loss_reached(self, current_rate: float, trade: Trade, def stop_loss_reached(self, current_rate: float, trade: Trade,
current_time: datetime, current_profit: float, current_time: datetime, current_profit: float,
force_stoploss: float, low: float = None, force_stoploss: float, low: float = None,
high: float = None) -> SellCheckTuple: high: float = None) -> ExitCheckTuple:
""" """
Based on current profit of the trade and configured (trailing) stoploss, Based on current profit of the trade and configured (trailing) stoploss,
decides to exit or not decides to exit or not
@ -961,9 +960,9 @@ class IStrategy(ABC, HyperStrategyMixin):
else: else:
logger.warning("CustomStoploss function did not return valid stoploss") logger.warning("CustomStoploss function did not return valid stoploss")
sl_lower_short = (trade.stop_loss < (low or current_rate) and not trade.is_short) sl_lower_long = (trade.stop_loss < (low or current_rate) and not trade.is_short)
sl_higher_long = (trade.stop_loss > (high or current_rate) and trade.is_short) sl_higher_short = (trade.stop_loss > (high or current_rate) and trade.is_short)
if self.trailing_stop and (sl_lower_short or sl_higher_long): if self.trailing_stop and (sl_lower_long or sl_higher_short):
# trailing stoploss handling # trailing stoploss handling
sl_offset = self.trailing_stop_positive_offset sl_offset = self.trailing_stop_positive_offset
@ -989,11 +988,11 @@ class IStrategy(ABC, HyperStrategyMixin):
if ((sl_higher_long or sl_lower_short) and if ((sl_higher_long or sl_lower_short) and
(not self.order_types.get('stoploss_on_exchange') or self.config['dry_run'])): (not self.order_types.get('stoploss_on_exchange') or self.config['dry_run'])):
sell_type = SellType.STOP_LOSS sell_type = ExitType.STOP_LOSS
# If initial stoploss is not the same as current one then it is trailing. # If initial stoploss is not the same as current one then it is trailing.
if trade.initial_stop_loss != trade.stop_loss: if trade.initial_stop_loss != trade.stop_loss:
sell_type = SellType.TRAILING_STOP_LOSS sell_type = ExitType.TRAILING_STOP_LOSS
logger.debug( logger.debug(
f"{trade.pair} - HIT STOP: current price at " f"{trade.pair} - HIT STOP: current price at "
f"{((high if trade.is_short else low) or current_rate):.6f}, " f"{((high if trade.is_short else low) or current_rate):.6f}, "
@ -1008,9 +1007,9 @@ class IStrategy(ABC, HyperStrategyMixin):
logger.debug(f"{trade.pair} - Trailing stop saved " logger.debug(f"{trade.pair} - Trailing stop saved "
f"{new_stoploss:.6f}") f"{new_stoploss:.6f}")
return SellCheckTuple(sell_type=sell_type) return ExitCheckTuple(exit_type=sell_type)
return SellCheckTuple(sell_type=SellType.NONE) return ExitCheckTuple(exit_type=ExitType.NONE)
def min_roi_reached_entry(self, trade_dur: int) -> Tuple[Optional[int], Optional[float]]: def min_roi_reached_entry(self, trade_dur: int) -> Tuple[Optional[int], Optional[float]]:
""" """
@ -1040,22 +1039,24 @@ class IStrategy(ABC, HyperStrategyMixin):
else: else:
return current_profit > roi return current_profit > roi
def ft_check_timed_out(self, side: str, trade: LocalTrade, order: Order, def ft_check_timed_out(self, trade: LocalTrade, order: Order,
current_time: datetime) -> bool: current_time: datetime) -> bool:
""" """
FT Internal method. FT Internal method.
Check if timeout is active, and if the order is still open and timed out Check if timeout is active, and if the order is still open and timed out
""" """
side = 'entry' if order.ft_order_side == trade.enter_side else 'exit'
timeout = self.config.get('unfilledtimeout', {}).get(side) timeout = self.config.get('unfilledtimeout', {}).get(side)
if timeout is not None: if timeout is not None:
timeout_unit = self.config.get('unfilledtimeout', {}).get('unit', 'minutes') timeout_unit = self.config.get('unfilledtimeout', {}).get('unit', 'minutes')
timeout_kwargs = {timeout_unit: -timeout} timeout_kwargs = {timeout_unit: -timeout}
timeout_threshold = current_time + timedelta(**timeout_kwargs) timeout_threshold = current_time + timedelta(**timeout_kwargs)
timedout = (order.status == 'open' and order.side == side timedout = (order.status == 'open' and order.order_date_utc < timeout_threshold)
and order.order_date_utc < timeout_threshold)
if timedout: if timedout:
return True return True
time_method = self.check_sell_timeout if order.side == 'sell' else self.check_buy_timeout time_method = (self.check_exit_timeout if order.side == trade.exit_side
else self.check_entry_timeout)
return strategy_safe_wrapper(time_method, return strategy_safe_wrapper(time_method,
default_retval=False)( default_retval=False)(

View File

@ -16,8 +16,8 @@
"trading_mode": "{{ trading_mode }}", "trading_mode": "{{ trading_mode }}",
"margin_mode": "{{ margin_mode }}", "margin_mode": "{{ margin_mode }}",
"unfilledtimeout": { "unfilledtimeout": {
"buy": 10, "entry": 10,
"sell": 10, "exit": 10,
"exit_timeout_count": 0, "exit_timeout_count": 0,
"unit": "minutes" "unit": "minutes"
}, },

View File

@ -143,7 +143,7 @@ def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: f
return True return True
def confirm_trade_exit(self, pair: str, trade: 'Trade', order_type: str, amount: float, def confirm_trade_exit(self, pair: str, trade: 'Trade', order_type: str, amount: float,
rate: float, time_in_force: str, sell_reason: str, rate: float, time_in_force: str, exit_reason: str,
current_time: 'datetime', **kwargs) -> bool: current_time: 'datetime', **kwargs) -> bool:
""" """
Called right before placing a regular sell order. Called right before placing a regular sell order.
@ -160,7 +160,7 @@ def confirm_trade_exit(self, pair: str, trade: 'Trade', order_type: str, amount:
:param amount: Amount in quote currency. :param amount: Amount in quote currency.
:param rate: Rate that's going to be used when using limit orders :param rate: Rate that's going to be used when using limit orders
: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 sell_reason: Sell 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'] 'sell_signal', 'force_sell', 'emergency_sell']
:param current_time: datetime object, containing the current datetime :param current_time: datetime object, containing the current datetime
@ -170,11 +170,11 @@ def confirm_trade_exit(self, pair: str, trade: 'Trade', order_type: str, amount:
""" """
return True return True
def check_buy_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -> bool: def check_entry_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -> bool:
""" """
Check buy timeout function callback. Check entry timeout function callback.
This method can be used to override the buy-timeout. This method can be used to override the entry-timeout.
It is called whenever a limit buy order has been created, It is called whenever a limit entry order has been created,
and is not yet fully filled. and is not yet fully filled.
Configuration options in `unfilledtimeout` will be verified before this, Configuration options in `unfilledtimeout` will be verified before this,
so ensure to set these timeouts high enough. so ensure to set these timeouts high enough.
@ -190,11 +190,11 @@ def check_buy_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) ->
""" """
return False return False
def check_sell_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -> bool: def check_exit_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -> bool:
""" """
Check sell timeout function callback. Check exit timeout function callback.
This method can be used to override the sell-timeout. This method can be used to override the exit-timeout.
It is called whenever a limit sell order has been created, It is called whenever a limit exit order has been created,
and is not yet fully filled. and is not yet fully filled.
Configuration options in `unfilledtimeout` will be verified before this, Configuration options in `unfilledtimeout` will be verified before this,
so ensure to set these timeouts high enough. so ensure to set these timeouts high enough.

View File

@ -5,7 +5,7 @@ import re
from copy import deepcopy from copy import deepcopy
from datetime import datetime, timedelta from datetime import datetime, timedelta
from pathlib import Path from pathlib import Path
from typing import Optional, Tuple from typing import Optional
from unittest.mock import MagicMock, Mock, PropertyMock from unittest.mock import MagicMock, Mock, PropertyMock
import arrow import arrow
@ -360,10 +360,6 @@ def create_mock_trades_usdt(fee, use_db: bool = True):
Trade.commit() Trade.commit()
def get_sides(is_short: bool) -> Tuple[str, str]:
return ("sell", "buy") if is_short else ("buy", "sell")
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def patch_coingekko(mocker) -> None: def patch_coingekko(mocker) -> None:
""" """
@ -420,8 +416,8 @@ def get_default_conf(testdatadir):
"dry_run_wallet": 1000, "dry_run_wallet": 1000,
"stoploss": -0.10, "stoploss": -0.10,
"unfilledtimeout": { "unfilledtimeout": {
"buy": 10, "entry": 10,
"sell": 30 "exit": 30
}, },
"bid_strategy": { "bid_strategy": {
"ask_last_balance": 0.0, "ask_last_balance": 0.0,

View File

@ -12,7 +12,7 @@ from pandas import DataFrame, to_datetime
from freqtrade.data.converter import ohlcv_to_dataframe from freqtrade.data.converter import ohlcv_to_dataframe
from freqtrade.edge import Edge, PairInfo from freqtrade.edge import Edge, PairInfo
from freqtrade.enums import SellType from freqtrade.enums import ExitType
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from tests.conftest import get_patched_freqtradebot, log_has from tests.conftest import get_patched_freqtradebot, log_has
from tests.optimize import (BTContainer, BTrade, _build_backtest_dataframe, from tests.optimize import (BTContainer, BTrade, _build_backtest_dataframe,
@ -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(sell_reason=SellType.SELL_SIGNAL, open_tick=1, close_tick=2), trades=[BTrade(sell_reason=ExitType.SELL_SIGNAL, open_tick=1, close_tick=2),
BTrade(sell_reason=SellType.SELL_SIGNAL, open_tick=4, close_tick=6)] BTrade(sell_reason=ExitType.SELL_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
@ -107,7 +107,7 @@ tc2 = BTContainer(data=[
[2, 5000, 5025, 4975, 4987, 6172, 0, 0], [2, 5000, 5025, 4975, 4987, 6172, 0, 0],
], ],
stop_loss=-0.01, roi={"0": float('inf')}, profit_perc=-0.01, stop_loss=-0.01, roi={"0": float('inf')}, profit_perc=-0.01,
trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=1)] trades=[BTrade(sell_reason=ExitType.STOP_LOSS, open_tick=1, close_tick=1)]
) )
# 4) Entered, sl 3 %, candle drops 4%, recovers to 1 % = > Trade closed, 3 % loss # 4) Entered, sl 3 %, candle drops 4%, recovers to 1 % = > Trade closed, 3 % loss
@ -118,7 +118,7 @@ tc3 = BTContainer(data=[
[2, 5000, 5025, 4975, 4987, 6172, 0, 0], [2, 5000, 5025, 4975, 4987, 6172, 0, 0],
], ],
stop_loss=-0.03, roi={"0": float('inf')}, profit_perc=-0.03, stop_loss=-0.03, roi={"0": float('inf')}, profit_perc=-0.03,
trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=1)] trades=[BTrade(sell_reason=ExitType.STOP_LOSS, open_tick=1, close_tick=1)]
) )
# 5) Stoploss and sell are hit. should sell on stoploss # 5) Stoploss and sell are hit. should sell on stoploss
@ -129,7 +129,7 @@ tc4 = BTContainer(data=[
[2, 5000, 5025, 4975, 4987, 6172, 0, 0], [2, 5000, 5025, 4975, 4987, 6172, 0, 0],
], ],
stop_loss=-0.03, roi={"0": float('inf')}, profit_perc=-0.03, stop_loss=-0.03, roi={"0": float('inf')}, profit_perc=-0.03,
trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=1)] trades=[BTrade(sell_reason=ExitType.STOP_LOSS, open_tick=1, close_tick=1)]
) )
TESTS = [ TESTS = [

View File

@ -3877,13 +3877,14 @@ def test_get_or_calculate_liquidation_price(mocker, default_conf):
@pytest.mark.parametrize('exchange,rate_start,rate_end,d1,d2,amount,expected_fees', [ @pytest.mark.parametrize('exchange,rate_start,rate_end,d1,d2,amount,expected_fees', [
('binance', 0, 2, "2021-09-01 00:00:00", "2021-09-01 08:00:00", 30.0, -0.0009140999999999999), ('binance', 0, 2, "2021-09-01 01:00:00", "2021-09-01 04:00:00", 30.0, 0.0),
('binance', 0, 2, "2021-09-01 00:00:15", "2021-09-01 08:00:00", 30.0, -0.0009140999999999999), ('binance', 0, 2, "2021-09-01 00:00:00", "2021-09-01 08:00:00", 30.0, -0.00091409999),
('binance', 0, 2, "2021-09-01 00:00:15", "2021-09-01 08:00:00", 30.0, -0.0002493),
('binance', 1, 2, "2021-09-01 01:00:14", "2021-09-01 08:00:00", 30.0, -0.0002493), ('binance', 1, 2, "2021-09-01 01:00:14", "2021-09-01 08:00:00", 30.0, -0.0002493),
('binance', 1, 2, "2021-09-01 00:00:16", "2021-09-01 08:00:00", 30.0, -0.0002493), ('binance', 1, 2, "2021-09-01 00:00:16", "2021-09-01 08:00:00", 30.0, -0.0002493),
('binance', 0, 1, "2021-09-01 00:00:00", "2021-09-01 07:59:59", 30.0, -0.0006647999999999999), ('binance', 0, 1, "2021-09-01 00:00:00", "2021-09-01 07:59:59", 30.0, -0.00066479999),
('binance', 0, 2, "2021-09-01 00:00:00", "2021-09-01 12:00:00", 30.0, -0.0009140999999999999), ('binance', 0, 2, "2021-09-01 00:00:00", "2021-09-01 12:00:00", 30.0, -0.00091409999),
('binance', 0, 2, "2021-09-01 00:00:01", "2021-09-01 08:00:00", 30.0, -0.0009140999999999999), ('binance', 0, 2, "2021-09-01 00:00:01", "2021-09-01 08:00:00", 30.0, -0.0002493),
# TODO: Uncoment once _calculate_funding_fees can pas time_in_ratio to exchange._get_funding_fee # TODO: Uncoment once _calculate_funding_fees can pas time_in_ratio to exchange._get_funding_fee
# ('kraken', "2021-09-01 00:00:00", "2021-09-01 08:00:00", 30.0, -0.0014937), # ('kraken', "2021-09-01 00:00:00", "2021-09-01 08:00:00", 30.0, -0.0014937),
# ('kraken', "2021-09-01 00:00:15", "2021-09-01 08:00:00", 30.0, -0.0008289), # ('kraken', "2021-09-01 00:00:15", "2021-09-01 08:00:00", 30.0, -0.0008289),
@ -3891,16 +3892,18 @@ def test_get_or_calculate_liquidation_price(mocker, default_conf):
# ('kraken', "2021-09-01 00:00:00", "2021-09-01 07:59:59", 30.0, -0.0012443999999999999), # ('kraken', "2021-09-01 00:00:00", "2021-09-01 07:59:59", 30.0, -0.0012443999999999999),
# ('kraken', "2021-09-01 00:00:00", "2021-09-01 12:00:00", 30.0, 0.0045759), # ('kraken', "2021-09-01 00:00:00", "2021-09-01 12:00:00", 30.0, 0.0045759),
# ('kraken', "2021-09-01 00:00:01", "2021-09-01 08:00:00", 30.0, -0.0008289), # ('kraken', "2021-09-01 00:00:01", "2021-09-01 08:00:00", 30.0, -0.0008289),
('ftx', 0, 9, "2021-09-01 00:00:00", "2021-09-01 08:00:00", 30.0, 0.0010008000000000003), ('ftx', 0, 2, "2021-09-01 00:10:00", "2021-09-01 00:30:00", 30.0, 0.0),
('ftx', 0, 9, "2021-09-01 00:00:00", "2021-09-01 08:00:00", 30.0, 0.0010008),
('ftx', 0, 13, "2021-09-01 00:00:00", "2021-09-01 12:00:00", 30.0, 0.0146691), ('ftx', 0, 13, "2021-09-01 00:00:00", "2021-09-01 12:00:00", 30.0, 0.0146691),
('ftx', 1, 9, "2021-09-01 00:00:01", "2021-09-01 08:00:00", 30.0, 0.0016656000000000002), ('ftx', 0, 9, "2021-09-01 00:00:00", "2021-09-01 08:00:00", 50.0, 0.001668),
('gateio', 0, 2, "2021-09-01 00:00:00", "2021-09-01 08:00:00", 30.0, -0.0009140999999999999), ('ftx', 1, 9, "2021-09-01 00:00:01", "2021-09-01 08:00:00", 30.0, 0.0019932),
('gateio', 0, 2, "2021-09-01 00:00:00", "2021-09-01 12:00:00", 30.0, -0.0009140999999999999), ('gateio', 0, 2, "2021-09-01 00:10:00", "2021-09-01 04:00:00", 30.0, 0.0),
('gateio', 0, 2, "2021-09-01 00:00:00", "2021-09-01 08:00:00", 30.0, -0.0009140999),
('gateio', 0, 2, "2021-09-01 00:00:00", "2021-09-01 12:00:00", 30.0, -0.0009140999),
('gateio', 1, 2, "2021-09-01 00:00:01", "2021-09-01 08:00:00", 30.0, -0.0002493), ('gateio', 1, 2, "2021-09-01 00:00:01", "2021-09-01 08:00:00", 30.0, -0.0002493),
('binance', 0, 2, "2021-09-01 00:00:00", "2021-09-01 08:00:00", 50.0, -0.0015235000000000001), ('binance', 0, 2, "2021-09-01 00:00:00", "2021-09-01 08:00:00", 50.0, -0.0015235),
# TODO: Uncoment once _calculate_funding_fees can pas time_in_ratio to exchange._get_funding_fee # TODO: Uncoment once _calculate_funding_fees can pas time_in_ratio to exchange._get_funding_fee
# ('kraken', "2021-09-01 00:00:00", "2021-09-01 08:00:00", 50.0, -0.0024895), # ('kraken', "2021-09-01 00:00:00", "2021-09-01 08:00:00", 50.0, -0.0024895),
('ftx', 0, 9, "2021-09-01 00:00:00", "2021-09-01 08:00:00", 50.0, 0.0016680000000000002),
]) ])
def test__fetch_and_calculate_funding_fees( def test__fetch_and_calculate_funding_fees(
mocker, mocker,
@ -3966,7 +3969,7 @@ def test__fetch_and_calculate_funding_fees(
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange)
mocker.patch('freqtrade.exchange.Exchange.timeframes', PropertyMock( mocker.patch('freqtrade.exchange.Exchange.timeframes', PropertyMock(
return_value=['1h', '4h', '8h'])) return_value=['1h', '4h', '8h']))
funding_fees = exchange._fetch_and_calculate_funding_fees( funding_fees = exchange._fetch_and_calculate_funding_fees(
pair='ADA/USDT', amount=amount, is_short=True, open_date=d1, close_date=d2) pair='ADA/USDT', amount=amount, is_short=True, open_date=d1, close_date=d2)
assert pytest.approx(funding_fees) == expected_fees assert pytest.approx(funding_fees) == expected_fees
@ -4111,10 +4114,36 @@ def test__order_contracts_to_amount(
'trades': None, 'trades': None,
'info': {}, 'info': {},
}, },
{
# Realistic stoploss order on gateio.
'id': '123456380',
'clientOrderId': '12345638203',
'timestamp': None,
'datetime': None,
'lastTradeTimestamp': None,
'status': None,
'symbol': None,
'type': None,
'timeInForce': None,
'postOnly': None,
'side': None,
'price': None,
'stopPrice': None,
'average': None,
'amount': None,
'cost': None,
'filled': None,
'remaining': None,
'fee': None,
'fees': [],
'trades': None,
'info': {},
},
] ]
order1 = exchange._order_contracts_to_amount(orders[0]) order1 = exchange._order_contracts_to_amount(orders[0])
order2 = exchange._order_contracts_to_amount(orders[1]) order2 = exchange._order_contracts_to_amount(orders[1])
exchange._order_contracts_to_amount(orders[2])
assert order1['amount'] == 30.0 * contract_size assert order1['amount'] == 30.0 * contract_size
assert order2['amount'] == 40.0 * contract_size assert order2['amount'] == 40.0 * contract_size
@ -4787,3 +4816,46 @@ def test_get_liquidation_price(
buffer_amount = liquidation_buffer * abs(open_rate - expected_liq) buffer_amount = liquidation_buffer * abs(open_rate - expected_liq)
expected_liq = expected_liq - buffer_amount if is_short else expected_liq + buffer_amount expected_liq = expected_liq - buffer_amount if is_short else expected_liq + buffer_amount
isclose(expected_liq, liq) isclose(expected_liq, liq)
@pytest.mark.parametrize('contract_size,order_amount', [
(10, 10),
(0.01, 10000),
])
def test_stoploss_contract_size(mocker, default_conf, contract_size, order_amount):
api_mock = MagicMock()
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
api_mock.create_order = MagicMock(return_value={
'id': order_id,
'info': {
'foo': 'bar'
},
'amount': order_amount,
'cost': order_amount,
'filled': order_amount,
'remaining': order_amount,
'symbol': 'ETH/BTC',
})
default_conf['dry_run'] = False
mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange._get_contract_size = MagicMock(return_value=contract_size)
api_mock.create_order.reset_mock()
order = exchange.stoploss(
pair='ETH/BTC',
amount=100,
stop_price=220,
order_types={},
side='buy',
leverage=1.0
)
assert api_mock.create_order.call_args_list[0][1]['amount'] == order_amount
assert order['amount'] == 100
assert order['cost'] == 100
assert order['filled'] == 100
assert order['remaining'] == 100

View File

@ -3,7 +3,7 @@ from typing import Dict, List, NamedTuple, Optional
import arrow import arrow
from pandas import DataFrame from pandas import DataFrame
from freqtrade.enums import SellType from freqtrade.enums import ExitType
from freqtrade.exchange import timeframe_to_minutes from freqtrade.exchange import timeframe_to_minutes
@ -15,7 +15,7 @@ class BTrade(NamedTuple):
""" """
Minimalistic Trade result used for functional backtesting Minimalistic Trade result used for functional backtesting
""" """
sell_reason: SellType sell_reason: ExitType
open_tick: int open_tick: int
close_tick: int close_tick: int
enter_tag: Optional[str] = None enter_tag: Optional[str] = None

View File

@ -5,7 +5,7 @@ from pathlib import Path
import pandas as pd import pandas as pd
import pytest import pytest
from freqtrade.enums import RunMode, SellType from freqtrade.enums import ExitType, RunMode
from freqtrade.optimize.hyperopt import Hyperopt from freqtrade.optimize.hyperopt import Hyperopt
from tests.conftest import patch_exchange from tests.conftest import patch_exchange
@ -44,7 +44,7 @@ def hyperopt_results():
'profit_abs': [-0.2, 0.4, -0.2, 0.6], 'profit_abs': [-0.2, 0.4, -0.2, 0.6],
'trade_duration': [10, 30, 10, 10], 'trade_duration': [10, 30, 10, 10],
'amount': [0.1, 0.1, 0.1, 0.1], 'amount': [0.1, 0.1, 0.1, 0.1],
'sell_reason': [SellType.STOP_LOSS, SellType.ROI, SellType.STOP_LOSS, SellType.ROI], 'sell_reason': [ExitType.STOP_LOSS, ExitType.ROI, ExitType.STOP_LOSS, ExitType.ROI],
'open_date': 'open_date':
[ [
datetime(2019, 1, 1, 9, 15, 0), datetime(2019, 1, 1, 9, 15, 0),

View File

@ -5,7 +5,7 @@ from unittest.mock import MagicMock
import pytest import pytest
from freqtrade.data.history import get_timerange from freqtrade.data.history import get_timerange
from freqtrade.enums import SellType from freqtrade.enums import ExitType
from freqtrade.optimize.backtesting import Backtesting from freqtrade.optimize.backtesting import Backtesting
from tests.conftest import patch_exchange from tests.conftest import patch_exchange
from tests.optimize import (BTContainer, BTrade, _build_backtest_dataframe, from tests.optimize import (BTContainer, BTrade, _build_backtest_dataframe,
@ -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_sell_signal=True, stop_loss=-0.01, roi={"0": 1}, profit_perc=0.002, use_sell_signal=True,
trades=[BTrade(sell_reason=SellType.SELL_SIGNAL, open_tick=1, close_tick=4)] trades=[BTrade(sell_reason=ExitType.SELL_SIGNAL, open_tick=1, close_tick=4)]
) )
# Test 1: Stop-Loss Triggered 1% loss # Test 1: Stop-Loss Triggered 1% loss
@ -37,7 +37,7 @@ tc1 = BTContainer(data=[
[4, 4977, 4995, 4977, 4995, 6172, 0, 0], [4, 4977, 4995, 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.01, stop_loss=-0.01, roi={"0": 1}, profit_perc=-0.01,
trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=2)] trades=[BTrade(sell_reason=ExitType.STOP_LOSS, open_tick=1, close_tick=2)]
) )
@ -52,7 +52,7 @@ tc2 = BTContainer(data=[
[4, 4962, 4987, 4937, 4950, 6172, 0, 0], [4, 4962, 4987, 4937, 4950, 6172, 0, 0],
[5, 4950, 4975, 4925, 4950, 6172, 0, 0]], [5, 4950, 4975, 4925, 4950, 6172, 0, 0]],
stop_loss=-0.03, roi={"0": 1}, profit_perc=-0.03, stop_loss=-0.03, roi={"0": 1}, profit_perc=-0.03,
trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=3)] trades=[BTrade(sell_reason=ExitType.STOP_LOSS, open_tick=1, close_tick=3)]
) )
@ -72,8 +72,8 @@ tc3 = BTContainer(data=[
[5, 4962, 4987, 4000, 4000, 6172, 0, 0], # exit with stoploss hit [5, 4962, 4987, 4000, 4000, 6172, 0, 0], # exit with stoploss hit
[6, 4950, 4975, 4950, 4950, 6172, 0, 0]], [6, 4950, 4975, 4950, 4950, 6172, 0, 0]],
stop_loss=-0.02, roi={"0": 1}, profit_perc=-0.04, stop_loss=-0.02, roi={"0": 1}, profit_perc=-0.04,
trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=2), trades=[BTrade(sell_reason=ExitType.STOP_LOSS, open_tick=1, close_tick=2),
BTrade(sell_reason=SellType.STOP_LOSS, open_tick=4, close_tick=5)] BTrade(sell_reason=ExitType.STOP_LOSS, open_tick=4, close_tick=5)]
) )
# Test 4: Minus 3% / recovery +15% # Test 4: Minus 3% / recovery +15%
@ -89,7 +89,7 @@ tc4 = BTContainer(data=[
[4, 4962, 4987, 4937, 4950, 6172, 0, 0], [4, 4962, 4987, 4937, 4950, 6172, 0, 0],
[5, 4950, 4975, 4925, 4950, 6172, 0, 0]], [5, 4950, 4975, 4925, 4950, 6172, 0, 0]],
stop_loss=-0.02, roi={"0": 0.06}, profit_perc=-0.02, stop_loss=-0.02, roi={"0": 0.06}, profit_perc=-0.02,
trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=2)] trades=[BTrade(sell_reason=ExitType.STOP_LOSS, open_tick=1, close_tick=2)]
) )
# Test 5: Drops 0.5% Closes +20%, ROI triggers 3% Gain # Test 5: Drops 0.5% Closes +20%, ROI triggers 3% Gain
@ -103,7 +103,7 @@ tc5 = BTContainer(data=[
[4, 4962, 4987, 4962, 4972, 6172, 0, 0], [4, 4962, 4987, 4962, 4972, 6172, 0, 0],
[5, 4950, 4975, 4925, 4950, 6172, 0, 0]], [5, 4950, 4975, 4925, 4950, 6172, 0, 0]],
stop_loss=-0.01, roi={"0": 0.03}, profit_perc=0.03, stop_loss=-0.01, roi={"0": 0.03}, profit_perc=0.03,
trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)] trades=[BTrade(sell_reason=ExitType.ROI, open_tick=1, close_tick=3)]
) )
# Test 6: Drops 3% / Recovers 6% Positive / Closes 1% positve, Stop-Loss triggers 2% Loss # Test 6: Drops 3% / Recovers 6% Positive / Closes 1% positve, Stop-Loss triggers 2% Loss
@ -117,7 +117,7 @@ tc6 = BTContainer(data=[
[4, 4962, 4987, 4950, 4950, 6172, 0, 0], [4, 4962, 4987, 4950, 4950, 6172, 0, 0],
[5, 4950, 4975, 4925, 4950, 6172, 0, 0]], [5, 4950, 4975, 4925, 4950, 6172, 0, 0]],
stop_loss=-0.02, roi={"0": 0.05}, profit_perc=-0.02, stop_loss=-0.02, roi={"0": 0.05}, profit_perc=-0.02,
trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=2)] trades=[BTrade(sell_reason=ExitType.STOP_LOSS, open_tick=1, close_tick=2)]
) )
# Test 7: 6% Positive / 1% Negative / Close 1% Positve, ROI Triggers 3% Gain # Test 7: 6% Positive / 1% Negative / Close 1% Positve, ROI Triggers 3% Gain
@ -131,7 +131,7 @@ tc7 = BTContainer(data=[
[4, 4962, 4987, 4950, 4950, 6172, 0, 0], [4, 4962, 4987, 4950, 4950, 6172, 0, 0],
[5, 4950, 4975, 4925, 4950, 6172, 0, 0]], [5, 4950, 4975, 4925, 4950, 6172, 0, 0]],
stop_loss=-0.02, roi={"0": 0.03}, profit_perc=0.03, stop_loss=-0.02, roi={"0": 0.03}, profit_perc=0.03,
trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=2)] trades=[BTrade(sell_reason=ExitType.ROI, open_tick=1, close_tick=2)]
) )
@ -145,7 +145,7 @@ tc8 = BTContainer(data=[
[3, 4850, 5050, 4650, 4750, 6172, 0, 0], [3, 4850, 5050, 4650, 4750, 6172, 0, 0],
[4, 4750, 4950, 4350, 4750, 6172, 0, 0]], [4, 4750, 4950, 4350, 4750, 6172, 0, 0]],
stop_loss=-0.10, roi={"0": 0.10}, profit_perc=-0.055, trailing_stop=True, stop_loss=-0.10, roi={"0": 0.10}, profit_perc=-0.055, trailing_stop=True,
trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=3)] trades=[BTrade(sell_reason=ExitType.TRAILING_STOP_LOSS, open_tick=1, close_tick=3)]
) )
@ -159,7 +159,7 @@ tc9 = BTContainer(data=[
[3, 5000, 5200, 4550, 4850, 6172, 0, 0], [3, 5000, 5200, 4550, 4850, 6172, 0, 0],
[4, 4750, 4950, 4350, 4750, 6172, 0, 0]], [4, 4750, 4950, 4350, 4750, 6172, 0, 0]],
stop_loss=-0.10, roi={"0": 0.10}, profit_perc=-0.064, trailing_stop=True, stop_loss=-0.10, roi={"0": 0.10}, profit_perc=-0.064, trailing_stop=True,
trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=3)] trades=[BTrade(sell_reason=ExitType.TRAILING_STOP_LOSS, open_tick=1, close_tick=3)]
) )
# Test 10: trailing_stop should raise so candle 3 causes a stoploss # Test 10: trailing_stop should raise so candle 3 causes a stoploss
@ -175,7 +175,7 @@ tc10 = BTContainer(data=[
stop_loss=-0.10, roi={"0": 0.10}, profit_perc=-0.1, trailing_stop=True, stop_loss=-0.10, roi={"0": 0.10}, profit_perc=-0.1, trailing_stop=True,
trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.10, trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.10,
trailing_stop_positive=0.03, trailing_stop_positive=0.03,
trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=4)] trades=[BTrade(sell_reason=ExitType.STOP_LOSS, open_tick=1, close_tick=4)]
) )
# Test 11: trailing_stop should raise so candle 3 causes a stoploss # Test 11: trailing_stop should raise so candle 3 causes a stoploss
@ -191,7 +191,7 @@ tc11 = BTContainer(data=[
stop_loss=-0.10, roi={"0": 0.10}, profit_perc=0.019, trailing_stop=True, stop_loss=-0.10, roi={"0": 0.10}, profit_perc=0.019, trailing_stop=True,
trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.05, trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.05,
trailing_stop_positive=0.03, trailing_stop_positive=0.03,
trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=3)] trades=[BTrade(sell_reason=ExitType.TRAILING_STOP_LOSS, open_tick=1, close_tick=3)]
) )
# Test 12: trailing_stop should raise in candle 2 and cause a stoploss in the same candle # Test 12: trailing_stop should raise in candle 2 and cause a stoploss in the same candle
@ -207,7 +207,7 @@ tc12 = BTContainer(data=[
stop_loss=-0.10, roi={"0": 0.10}, profit_perc=0.019, trailing_stop=True, stop_loss=-0.10, roi={"0": 0.10}, profit_perc=0.019, trailing_stop=True,
trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.05, trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.05,
trailing_stop_positive=0.03, trailing_stop_positive=0.03,
trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=2)] trades=[BTrade(sell_reason=ExitType.TRAILING_STOP_LOSS, open_tick=1, close_tick=2)]
) )
# Test 13: Buy and sell ROI on same candle # Test 13: Buy and sell ROI on same candle
@ -220,7 +220,7 @@ tc13 = BTContainer(data=[
[3, 4850, 5050, 4750, 4750, 6172, 0, 0], [3, 4850, 5050, 4750, 4750, 6172, 0, 0],
[4, 4750, 4950, 4750, 4750, 6172, 0, 0]], [4, 4750, 4950, 4750, 4750, 6172, 0, 0]],
stop_loss=-0.10, roi={"0": 0.01}, profit_perc=0.01, stop_loss=-0.10, roi={"0": 0.01}, profit_perc=0.01,
trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=1)] trades=[BTrade(sell_reason=ExitType.ROI, open_tick=1, close_tick=1)]
) )
# Test 14 - Buy and Stoploss on same candle # Test 14 - Buy and Stoploss on same candle
@ -233,7 +233,7 @@ tc14 = BTContainer(data=[
[3, 4850, 5050, 4750, 4750, 6172, 0, 0], [3, 4850, 5050, 4750, 4750, 6172, 0, 0],
[4, 4750, 4950, 4350, 4750, 6172, 0, 0]], [4, 4750, 4950, 4350, 4750, 6172, 0, 0]],
stop_loss=-0.05, roi={"0": 0.10}, profit_perc=-0.05, stop_loss=-0.05, roi={"0": 0.10}, profit_perc=-0.05,
trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=1)] trades=[BTrade(sell_reason=ExitType.STOP_LOSS, open_tick=1, close_tick=1)]
) )
@ -247,8 +247,8 @@ tc15 = BTContainer(data=[
[3, 4850, 5050, 4750, 4750, 6172, 0, 0], [3, 4850, 5050, 4750, 4750, 6172, 0, 0],
[4, 4750, 4950, 4350, 4750, 6172, 0, 0]], [4, 4750, 4950, 4350, 4750, 6172, 0, 0]],
stop_loss=-0.05, roi={"0": 0.01}, profit_perc=-0.04, stop_loss=-0.05, roi={"0": 0.01}, profit_perc=-0.04,
trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=1), trades=[BTrade(sell_reason=ExitType.ROI, open_tick=1, close_tick=1),
BTrade(sell_reason=SellType.STOP_LOSS, open_tick=2, close_tick=2)] BTrade(sell_reason=ExitType.STOP_LOSS, open_tick=2, close_tick=2)]
) )
# Test 16: Buy, hold for 65 min, then forcesell using roi=-1 # Test 16: Buy, hold for 65 min, then forcesell using roi=-1
@ -263,7 +263,7 @@ tc16 = BTContainer(data=[
[4, 4962, 4987, 4950, 4950, 6172, 0, 0], [4, 4962, 4987, 4950, 4950, 6172, 0, 0],
[5, 4950, 4975, 4925, 4950, 6172, 0, 0]], [5, 4950, 4975, 4925, 4950, 6172, 0, 0]],
stop_loss=-0.10, roi={"0": 0.10, "65": -1}, profit_perc=-0.012, stop_loss=-0.10, roi={"0": 0.10, "65": -1}, profit_perc=-0.012,
trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)] trades=[BTrade(sell_reason=ExitType.ROI, open_tick=1, close_tick=3)]
) )
# Test 17: Buy, hold for 120 mins, then forcesell using roi=-1 # Test 17: Buy, hold for 120 mins, then forcesell using roi=-1
@ -279,7 +279,7 @@ tc17 = BTContainer(data=[
[4, 4962, 4987, 4950, 4950, 6172, 0, 0], [4, 4962, 4987, 4950, 4950, 6172, 0, 0],
[5, 4950, 4975, 4925, 4950, 6172, 0, 0]], [5, 4950, 4975, 4925, 4950, 6172, 0, 0]],
stop_loss=-0.10, roi={"0": 0.10, "120": -1}, profit_perc=-0.004, stop_loss=-0.10, roi={"0": 0.10, "120": -1}, profit_perc=-0.004,
trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)] trades=[BTrade(sell_reason=ExitType.ROI, open_tick=1, close_tick=3)]
) )
@ -295,7 +295,7 @@ tc18 = BTContainer(data=[
[4, 4962, 4987, 4950, 4950, 6172, 0, 0], [4, 4962, 4987, 4950, 4950, 6172, 0, 0],
[5, 4950, 4975, 4925, 4950, 6172, 0, 0]], [5, 4950, 4975, 4925, 4950, 6172, 0, 0]],
stop_loss=-0.10, roi={"0": 0.10, "120": 0.01}, profit_perc=0.04, stop_loss=-0.10, roi={"0": 0.10, "120": 0.01}, profit_perc=0.04,
trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)] trades=[BTrade(sell_reason=ExitType.ROI, open_tick=1, close_tick=3)]
) )
# Test 19: Buy, hold for 119 mins, then drop ROI to 1%, causing a sell in candle 3. # Test 19: Buy, hold for 119 mins, then drop ROI to 1%, causing a sell in candle 3.
@ -310,7 +310,7 @@ tc19 = BTContainer(data=[
[4, 4962, 4987, 4950, 4950, 6172, 0, 0], [4, 4962, 4987, 4950, 4950, 6172, 0, 0],
[5, 4550, 4975, 4550, 4950, 6172, 0, 0]], [5, 4550, 4975, 4550, 4950, 6172, 0, 0]],
stop_loss=-0.10, roi={"0": 0.10, "120": 0.01}, profit_perc=0.01, stop_loss=-0.10, roi={"0": 0.10, "120": 0.01}, profit_perc=0.01,
trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)] trades=[BTrade(sell_reason=ExitType.ROI, open_tick=1, close_tick=3)]
) )
# Test 20: Buy, hold for 119 mins, then drop ROI to 1%, causing a sell in candle 3. # Test 20: Buy, hold for 119 mins, then drop ROI to 1%, causing a sell in candle 3.
@ -325,7 +325,7 @@ tc20 = BTContainer(data=[
[4, 4962, 4987, 4950, 4950, 6172, 0, 0], [4, 4962, 4987, 4950, 4950, 6172, 0, 0],
[5, 4925, 4975, 4925, 4950, 6172, 0, 0]], [5, 4925, 4975, 4925, 4950, 6172, 0, 0]],
stop_loss=-0.10, roi={"0": 0.10, "119": 0.01}, profit_perc=0.01, stop_loss=-0.10, roi={"0": 0.10, "119": 0.01}, profit_perc=0.01,
trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)] trades=[BTrade(sell_reason=ExitType.ROI, open_tick=1, close_tick=3)]
) )
# Test 21: trailing_stop ROI collision. # Test 21: trailing_stop ROI collision.
@ -342,7 +342,7 @@ tc21 = BTContainer(data=[
stop_loss=-0.10, roi={"0": 0.04}, profit_perc=0.04, trailing_stop=True, stop_loss=-0.10, roi={"0": 0.04}, profit_perc=0.04, trailing_stop=True,
trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.05, trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.05,
trailing_stop_positive=0.03, trailing_stop_positive=0.03,
trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=2)] trades=[BTrade(sell_reason=ExitType.ROI, open_tick=1, close_tick=2)]
) )
# Test 22: trailing_stop Raises in candle 2 - but ROI applies at the same time. # Test 22: trailing_stop Raises in candle 2 - but ROI applies at the same time.
@ -358,7 +358,7 @@ tc22 = BTContainer(data=[
stop_loss=-0.10, roi={"0": 0.04}, profit_perc=0.04, trailing_stop=True, stop_loss=-0.10, roi={"0": 0.04}, profit_perc=0.04, trailing_stop=True,
trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.05, trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.05,
trailing_stop_positive=0.03, trailing_stop_positive=0.03,
trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=2)] trades=[BTrade(sell_reason=ExitType.ROI, open_tick=1, close_tick=2)]
) )
@ -375,7 +375,7 @@ tc22s = BTContainer(data=[
stop_loss=-0.10, roi={"0": 0.04}, profit_perc=0.04, trailing_stop=True, stop_loss=-0.10, roi={"0": 0.04}, profit_perc=0.04, trailing_stop=True,
trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.05, trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.05,
trailing_stop_positive=0.03, trailing_stop_positive=0.03,
trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=2, is_short=True)] trades=[BTrade(sell_reason=ExitType.ROI, open_tick=1, close_tick=2, is_short=True)]
) )
# Test 23: trailing_stop Raises in candle 2 (does not trigger) # Test 23: trailing_stop Raises in candle 2 (does not trigger)
@ -394,7 +394,7 @@ tc23 = BTContainer(data=[
stop_loss=-0.10, roi={"0": 0.1, "119": 0.03}, profit_perc=0.03, trailing_stop=True, stop_loss=-0.10, roi={"0": 0.1, "119": 0.03}, profit_perc=0.03, trailing_stop=True,
trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.05, trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.05,
trailing_stop_positive=0.03, trailing_stop_positive=0.03,
trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)] trades=[BTrade(sell_reason=ExitType.ROI, open_tick=1, close_tick=3)]
) )
# Test 24: Sell with signal sell in candle 3 (stoploss also triggers on this candle) # Test 24: Sell with signal sell in candle 3 (stoploss also triggers on this candle)
@ -409,7 +409,7 @@ tc24 = BTContainer(data=[
[4, 5010, 5010, 4977, 4995, 6172, 0, 0], [4, 5010, 5010, 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.01, use_sell_signal=True, stop_loss=-0.01, roi={"0": 1}, profit_perc=-0.01, use_sell_signal=True,
trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=3)] trades=[BTrade(sell_reason=ExitType.STOP_LOSS, open_tick=1, close_tick=3)]
) )
# Test 25: Sell with signal sell in candle 3 (stoploss also triggers on this candle) # Test 25: Sell with signal sell in candle 3 (stoploss also triggers on this candle)
@ -424,7 +424,7 @@ tc25 = 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_sell_signal=True, stop_loss=-0.01, roi={"0": 1}, profit_perc=0.002, use_sell_signal=True,
trades=[BTrade(sell_reason=SellType.SELL_SIGNAL, open_tick=1, close_tick=4)] trades=[BTrade(sell_reason=ExitType.SELL_SIGNAL, open_tick=1, close_tick=4)]
) )
# Test 25l: (copy of test25 with leverage) # Test 25l: (copy of test25 with leverage)
@ -441,7 +441,7 @@ tc25l = 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_sell_signal=True, stop_loss=-0.05, roi={"0": 1}, profit_perc=0.002 * 5.0, use_sell_signal=True,
leverage=5.0, leverage=5.0,
trades=[BTrade(sell_reason=SellType.SELL_SIGNAL, open_tick=1, close_tick=4)] trades=[BTrade(sell_reason=ExitType.SELL_SIGNAL, open_tick=1, close_tick=4)]
) )
# Test 25s: (copy of test25 with leverage and as short) # Test 25s: (copy of test25 with leverage and as short)
@ -458,7 +458,7 @@ tc25s = 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_sell_signal=True, stop_loss=-0.05, roi={"0": 1}, profit_perc=0.002 * 5.0, use_sell_signal=True,
leverage=5.0, leverage=5.0,
trades=[BTrade(sell_reason=SellType.SELL_SIGNAL, open_tick=1, close_tick=4, is_short=True)] trades=[BTrade(sell_reason=ExitType.SELL_SIGNAL, open_tick=1, close_tick=4, is_short=True)]
) )
# Test 26: Sell with signal sell in candle 3 (ROI at signal candle) # Test 26: 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)
@ -472,7 +472,7 @@ tc26 = BTContainer(data=[
[4, 5010, 5010, 4855, 4995, 6172, 0, 0], [4, 5010, 5010, 4855, 4995, 6172, 0, 0],
[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.05, use_sell_signal=True, stop_loss=-0.10, roi={"0": 0.05}, profit_perc=0.05, use_sell_signal=True,
trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)] trades=[BTrade(sell_reason=ExitType.ROI, open_tick=1, close_tick=3)]
) )
# Test 27: Sell with signal sell in candle 3 (ROI at signal candle) # Test 27: Sell with signal sell in candle 3 (ROI at signal candle)
@ -486,7 +486,7 @@ tc27 = 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_sell_signal=True, stop_loss=-0.10, roi={"0": 0.05}, profit_perc=0.002, use_sell_signal=True,
trades=[BTrade(sell_reason=SellType.SELL_SIGNAL, open_tick=1, close_tick=4)] trades=[BTrade(sell_reason=ExitType.SELL_SIGNAL, open_tick=1, close_tick=4)]
) )
# Test 28: trailing_stop should raise so candle 3 causes a stoploss # Test 28: trailing_stop should raise so candle 3 causes a stoploss
@ -503,7 +503,7 @@ tc28 = BTContainer(data=[
stop_loss=-0.10, roi={"0": 0.10}, profit_perc=-0.03, trailing_stop=True, stop_loss=-0.10, roi={"0": 0.10}, profit_perc=-0.03, trailing_stop=True,
trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.05, trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.05,
trailing_stop_positive=0.03, trailing_stop_positive=0.03,
trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=3)] trades=[BTrade(sell_reason=ExitType.TRAILING_STOP_LOSS, open_tick=1, close_tick=3)]
) )
# Test 28s: trailing_stop should raise so candle 3 causes a stoploss # Test 28s: trailing_stop should raise so candle 3 causes a stoploss
@ -521,7 +521,7 @@ tc28s = BTContainer(data=[
trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.05, trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.05,
trailing_stop_positive=0.03, trailing_stop_positive=0.03,
trades=[ trades=[
BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=3, is_short=True) BTrade(sell_reason=ExitType.TRAILING_STOP_LOSS, open_tick=1, close_tick=3, is_short=True)
] ]
) )
@ -537,7 +537,7 @@ tc29 = BTContainer(data=[
[4, 4750, 4950, 4350, 4750, 6172, 0, 0]], [4, 4750, 4950, 4350, 4750, 6172, 0, 0]],
stop_loss=-0.10, roi={"0": 0.10}, profit_perc=-0.02, trailing_stop=True, stop_loss=-0.10, roi={"0": 0.10}, profit_perc=-0.02, trailing_stop=True,
trailing_stop_positive=0.03, trailing_stop_positive=0.03,
trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=2)] trades=[BTrade(sell_reason=ExitType.TRAILING_STOP_LOSS, open_tick=1, close_tick=2)]
) )
# Test 30: trailing_stop should be triggered immediately on trade open candle. # Test 30: trailing_stop should be triggered immediately on trade open candle.
@ -551,7 +551,7 @@ tc30 = BTContainer(data=[
[4, 4750, 4950, 4350, 4750, 6172, 0, 0]], [4, 4750, 4950, 4350, 4750, 6172, 0, 0]],
stop_loss=-0.10, roi={"0": 0.10}, profit_perc=-0.01, trailing_stop=True, stop_loss=-0.10, roi={"0": 0.10}, profit_perc=-0.01, trailing_stop=True,
trailing_stop_positive=0.01, trailing_stop_positive=0.01,
trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=1)] trades=[BTrade(sell_reason=ExitType.TRAILING_STOP_LOSS, open_tick=1, close_tick=1)]
) )
# Test 31: trailing_stop should be triggered immediately on trade open candle. # Test 31: trailing_stop should be triggered immediately on trade open candle.
@ -566,7 +566,7 @@ tc31 = BTContainer(data=[
stop_loss=-0.10, roi={"0": 0.10}, profit_perc=0.01, trailing_stop=True, stop_loss=-0.10, roi={"0": 0.10}, profit_perc=0.01, trailing_stop=True,
trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.02, trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.02,
trailing_stop_positive=0.01, trailing_stop_positive=0.01,
trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=1)] trades=[BTrade(sell_reason=ExitType.TRAILING_STOP_LOSS, open_tick=1, close_tick=1)]
) )
# Test 32: trailing_stop should be triggered immediately on trade open candle. # Test 32: trailing_stop should be triggered immediately on trade open candle.
@ -581,7 +581,7 @@ tc32 = BTContainer(data=[
stop_loss=-0.01, roi={"0": 0.10}, profit_perc=-0.01, trailing_stop=True, stop_loss=-0.01, roi={"0": 0.10}, profit_perc=-0.01, trailing_stop=True,
trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.02, trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.02,
trailing_stop_positive=0.01, use_custom_stoploss=True, trailing_stop_positive=0.01, use_custom_stoploss=True,
trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=1)] trades=[BTrade(sell_reason=ExitType.TRAILING_STOP_LOSS, open_tick=1, close_tick=1)]
) )
# Test 33: trailing_stop should be triggered immediately on trade open candle. # Test 33: trailing_stop should be triggered immediately on trade open candle.
@ -597,7 +597,7 @@ tc33 = BTContainer(data=[
trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.02, trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.02,
trailing_stop_positive=0.01, use_custom_stoploss=True, trailing_stop_positive=0.01, use_custom_stoploss=True,
trades=[BTrade( trades=[BTrade(
sell_reason=SellType.TRAILING_STOP_LOSS, sell_reason=ExitType.TRAILING_STOP_LOSS,
open_tick=1, open_tick=1,
close_tick=1, close_tick=1,
enter_tag='buy_signal_01' enter_tag='buy_signal_01'
@ -617,7 +617,7 @@ tc33s = BTContainer(data=[
trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.02, trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.02,
trailing_stop_positive=0.01, use_custom_stoploss=True, trailing_stop_positive=0.01, use_custom_stoploss=True,
trades=[BTrade( trades=[BTrade(
sell_reason=SellType.TRAILING_STOP_LOSS, sell_reason=ExitType.TRAILING_STOP_LOSS,
open_tick=1, open_tick=1,
close_tick=1, close_tick=1,
enter_tag='short_signal_01', enter_tag='short_signal_01',
@ -647,7 +647,7 @@ tc35 = BTContainer(data=[
[4, 4750, 4950, 4350, 4750, 6172, 0, 0]], [4, 4750, 4950, 4350, 4750, 6172, 0, 0]],
stop_loss=-0.01, roi={"0": 0.10}, profit_perc=-0.01, stop_loss=-0.01, roi={"0": 0.10}, profit_perc=-0.01,
custom_entry_price=7200, trades=[ custom_entry_price=7200, trades=[
BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=1) BTrade(sell_reason=ExitType.STOP_LOSS, open_tick=1, close_tick=1)
]) ])
# Test 35s: Custom-entry-price above all candles should have rate adjusted to "entry candle high" # Test 35s: Custom-entry-price above all candles should have rate adjusted to "entry candle high"
@ -661,7 +661,7 @@ tc35s = BTContainer(data=[
stop_loss=-0.01, roi={"0": 0.10}, profit_perc=-0.01, stop_loss=-0.01, roi={"0": 0.10}, profit_perc=-0.01,
custom_entry_price=4000, custom_entry_price=4000,
trades=[ trades=[
BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=1, is_short=True) BTrade(sell_reason=ExitType.STOP_LOSS, open_tick=1, close_tick=1, is_short=True)
] ]
) )
@ -678,7 +678,7 @@ tc36 = BTContainer(data=[
[4, 4750, 4950, 4350, 4750, 6172, 0, 0]], [4, 4750, 4950, 4350, 4750, 6172, 0, 0]],
stop_loss=-0.10, roi={"0": 0.01}, profit_perc=0.01, stop_loss=-0.10, roi={"0": 0.01}, profit_perc=0.01,
custom_entry_price=4952, custom_entry_price=4952,
trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=2)] trades=[BTrade(sell_reason=ExitType.ROI, open_tick=1, close_tick=2)]
) )
# Test 37: Custom-entry-price around candle low # Test 37: Custom-entry-price around candle low
@ -693,7 +693,7 @@ tc37 = BTContainer(data=[
[4, 4750, 4950, 4350, 4750, 6172, 0, 0]], [4, 4750, 4950, 4350, 4750, 6172, 0, 0]],
stop_loss=-0.10, roi={"0": 0.01}, profit_perc=0.01, stop_loss=-0.10, roi={"0": 0.01}, profit_perc=0.01,
custom_entry_price=4952, custom_entry_price=4952,
trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=1)] trades=[BTrade(sell_reason=ExitType.ROI, open_tick=1, close_tick=1)]
) )
# Test 38: Custom exit price below all candles # Test 38: Custom exit price below all candles
@ -708,7 +708,7 @@ tc38 = 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_sell_signal=True, use_sell_signal=True,
custom_exit_price=4552, custom_exit_price=4552,
trades=[BTrade(sell_reason=SellType.SELL_SIGNAL, open_tick=1, close_tick=3)] trades=[BTrade(sell_reason=ExitType.SELL_SIGNAL, open_tick=1, close_tick=3)]
) )
# Test 39: Custom exit price above all candles # Test 39: Custom exit price above all candles
@ -723,7 +723,7 @@ tc39 = 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_sell_signal=True, use_sell_signal=True,
custom_exit_price=6052, custom_exit_price=6052,
trades=[BTrade(sell_reason=SellType.FORCE_SELL, open_tick=1, close_tick=4)] trades=[BTrade(sell_reason=ExitType.FORCE_SELL, open_tick=1, close_tick=4)]
) )
# Test 39: Custom short exit price above below candles # Test 39: Custom short exit price above below candles
@ -738,7 +738,7 @@ tc39a = 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_sell_signal=True, use_sell_signal=True,
custom_exit_price=4700, custom_exit_price=4700,
trades=[BTrade(sell_reason=SellType.FORCE_SELL, open_tick=1, close_tick=4, is_short=True)] trades=[BTrade(sell_reason=ExitType.FORCE_SELL, open_tick=1, close_tick=4, is_short=True)]
) )
# Test 40: Colliding long and short signal # Test 40: Colliding long and short signal

View File

@ -19,7 +19,7 @@ from freqtrade.data.btanalysis import BT_DATA_COLUMNS, evaluate_result_multi
from freqtrade.data.converter import clean_ohlcv_dataframe from freqtrade.data.converter import clean_ohlcv_dataframe
from freqtrade.data.dataprovider import DataProvider from freqtrade.data.dataprovider import DataProvider
from freqtrade.data.history import get_timerange from freqtrade.data.history import get_timerange
from freqtrade.enums import RunMode, SellType from freqtrade.enums import ExitType, RunMode
from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.exceptions import DependencyException, OperationalException
from freqtrade.exchange.exchange import timeframe_to_next_date from freqtrade.exchange.exchange import timeframe_to_next_date
from freqtrade.misc import get_strategy_run_id from freqtrade.misc import get_strategy_run_id
@ -713,7 +713,7 @@ def test_backtest__get_sell_trade_entry(default_conf, fee, mocker) -> None:
# No data available. # No data available.
res = backtesting._get_sell_trade_entry(trade, row_sell) res = backtesting._get_sell_trade_entry(trade, row_sell)
assert res is not None assert res is not None
assert res.sell_reason == SellType.ROI.value assert res.sell_reason == ExitType.ROI.value
assert res.close_date_utc == datetime(2020, 1, 1, 5, 0, tzinfo=timezone.utc) assert res.close_date_utc == datetime(2020, 1, 1, 5, 0, tzinfo=timezone.utc)
# Enter new trade # Enter new trade
@ -732,7 +732,7 @@ def test_backtest__get_sell_trade_entry(default_conf, fee, mocker) -> None:
res = backtesting._get_sell_trade_entry(trade, row_sell) res = backtesting._get_sell_trade_entry(trade, row_sell)
assert res is not None assert res is not None
assert res.sell_reason == SellType.ROI.value assert res.sell_reason == ExitType.ROI.value
# Sell at minute 3 (not available above!) # Sell at minute 3 (not available above!)
assert res.close_date_utc == datetime(2020, 1, 1, 5, 3, tzinfo=timezone.utc) assert res.close_date_utc == datetime(2020, 1, 1, 5, 3, tzinfo=timezone.utc)
sell_order = res.select_order('sell', True) sell_order = res.select_order('sell', True)
@ -781,7 +781,7 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None:
'trade_duration': [235, 40], 'trade_duration': [235, 40],
'profit_ratio': [0.0, 0.0], 'profit_ratio': [0.0, 0.0],
'profit_abs': [0.0, 0.0], 'profit_abs': [0.0, 0.0],
'sell_reason': [SellType.ROI.value, SellType.ROI.value], 'sell_reason': [ExitType.ROI.value, ExitType.ROI.value],
'initial_stop_loss_abs': [0.0940005, 0.09272236], 'initial_stop_loss_abs': [0.0940005, 0.09272236],
'initial_stop_loss_ratio': [-0.1, -0.1], 'initial_stop_loss_ratio': [-0.1, -0.1],
'stop_loss_abs': [0.0940005, 0.09272236], 'stop_loss_abs': [0.0940005, 0.09272236],
@ -1219,7 +1219,7 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat
'close_rate': [0.104969, 0.103541], 'close_rate': [0.104969, 0.103541],
"is_short": [False, False], "is_short": [False, False],
'sell_reason': [SellType.ROI, SellType.ROI] 'sell_reason': [ExitType.ROI, ExitType.ROI]
}) })
result2 = pd.DataFrame({'pair': ['XRP/BTC', 'LTC/BTC', 'ETH/BTC'], result2 = pd.DataFrame({'pair': ['XRP/BTC', 'LTC/BTC', 'ETH/BTC'],
'profit_ratio': [0.03, 0.01, 0.1], 'profit_ratio': [0.03, 0.01, 0.1],
@ -1237,7 +1237,7 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat
'open_rate': [0.104445, 0.10302485, 0.122541], 'open_rate': [0.104445, 0.10302485, 0.122541],
'close_rate': [0.104969, 0.103541, 0.123541], 'close_rate': [0.104969, 0.103541, 0.123541],
"is_short": [False, False, False], "is_short": [False, False, False],
'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS] 'sell_reason': [ExitType.ROI, ExitType.ROI, ExitType.STOP_LOSS]
}) })
backtestmock = MagicMock(side_effect=[ backtestmock = MagicMock(side_effect=[
{ {
@ -1337,7 +1337,7 @@ def test_backtest_start_nomock_futures(default_conf_usdt, mocker,
'stake_amount': [0.01, 0.01], 'stake_amount': [0.01, 0.01],
'open_rate': [0.104445, 0.10302485], 'open_rate': [0.104445, 0.10302485],
'close_rate': [0.104969, 0.103541], 'close_rate': [0.104969, 0.103541],
'sell_reason': [SellType.ROI, SellType.ROI] 'sell_reason': [ExitType.ROI, ExitType.ROI]
}) })
result2 = pd.DataFrame({'pair': ['XRP/USDT', 'XRP/USDT', 'XRP/USDT'], result2 = pd.DataFrame({'pair': ['XRP/USDT', 'XRP/USDT', 'XRP/USDT'],
'profit_ratio': [0.03, 0.01, 0.1], 'profit_ratio': [0.03, 0.01, 0.1],
@ -1355,7 +1355,7 @@ def test_backtest_start_nomock_futures(default_conf_usdt, mocker,
'stake_amount': [0.01, 0.01, 0.01], 'stake_amount': [0.01, 0.01, 0.01],
'open_rate': [0.104445, 0.10302485, 0.122541], 'open_rate': [0.104445, 0.10302485, 0.122541],
'close_rate': [0.104969, 0.103541, 0.123541], 'close_rate': [0.104969, 0.103541, 0.123541],
'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS] 'sell_reason': [ExitType.ROI, ExitType.ROI, ExitType.STOP_LOSS]
}) })
backtestmock = MagicMock(side_effect=[ backtestmock = MagicMock(side_effect=[
{ {
@ -1440,7 +1440,7 @@ def test_backtest_start_multi_strat_nomock_detail(default_conf, mocker,
'stake_amount': [0.01, 0.01], 'stake_amount': [0.01, 0.01],
'open_rate': [0.104445, 0.10302485], 'open_rate': [0.104445, 0.10302485],
'close_rate': [0.104969, 0.103541], 'close_rate': [0.104969, 0.103541],
'sell_reason': [SellType.ROI, SellType.ROI] 'sell_reason': [ExitType.ROI, ExitType.ROI]
}) })
result2 = pd.DataFrame({'pair': ['XRP/BTC', 'LTC/BTC', 'ETH/BTC'], result2 = pd.DataFrame({'pair': ['XRP/BTC', 'LTC/BTC', 'ETH/BTC'],
'profit_ratio': [0.03, 0.01, 0.1], 'profit_ratio': [0.03, 0.01, 0.1],
@ -1458,7 +1458,7 @@ def test_backtest_start_multi_strat_nomock_detail(default_conf, mocker,
'stake_amount': [0.01, 0.01, 0.01], 'stake_amount': [0.01, 0.01, 0.01],
'open_rate': [0.104445, 0.10302485, 0.122541], 'open_rate': [0.104445, 0.10302485, 0.122541],
'close_rate': [0.104969, 0.103541, 0.123541], 'close_rate': [0.104969, 0.103541, 0.123541],
'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS] 'sell_reason': [ExitType.ROI, ExitType.ROI, ExitType.STOP_LOSS]
}) })
backtestmock = MagicMock(side_effect=[ backtestmock = MagicMock(side_effect=[
{ {

View File

@ -8,7 +8,7 @@ from arrow import Arrow
from freqtrade.configuration import TimeRange from freqtrade.configuration import TimeRange
from freqtrade.data import history from freqtrade.data import history
from freqtrade.data.history import get_timerange from freqtrade.data.history import get_timerange
from freqtrade.enums import SellType from freqtrade.enums import ExitType
from freqtrade.optimize.backtesting import Backtesting from freqtrade.optimize.backtesting import Backtesting
from tests.conftest import patch_exchange from tests.conftest import patch_exchange
@ -60,7 +60,7 @@ def test_backtest_position_adjustment(default_conf, fee, mocker, testdatadir) ->
'trade_duration': [200, 40], 'trade_duration': [200, 40],
'profit_ratio': [0.0, 0.0], 'profit_ratio': [0.0, 0.0],
'profit_abs': [0.0, 0.0], 'profit_abs': [0.0, 0.0],
'sell_reason': [SellType.ROI.value, SellType.ROI.value], 'sell_reason': [ExitType.ROI.value, ExitType.ROI.value],
'initial_stop_loss_abs': [0.0940005, 0.09272236], 'initial_stop_loss_abs': [0.0940005, 0.09272236],
'initial_stop_loss_ratio': [-0.1, -0.1], 'initial_stop_loss_ratio': [-0.1, -0.1],
'stop_loss_abs': [0.0940005, 0.09272236], 'stop_loss_abs': [0.0940005, 0.09272236],

View File

@ -10,7 +10,7 @@ from filelock import Timeout
from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_hyperopt from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_hyperopt
from freqtrade.data.history import load_data from freqtrade.data.history import load_data
from freqtrade.enums import RunMode, SellType from freqtrade.enums import ExitType, RunMode
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.optimize.hyperopt import Hyperopt from freqtrade.optimize.hyperopt import Hyperopt
from freqtrade.optimize.hyperopt_auto import HyperOptAuto from freqtrade.optimize.hyperopt_auto import HyperOptAuto
@ -357,8 +357,8 @@ def test_hyperopt_format_results(hyperopt):
"is_open": [False, False, False, True], "is_open": [False, False, False, True],
"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],
"sell_reason": [SellType.ROI, SellType.STOP_LOSS, "sell_reason": [ExitType.ROI, ExitType.STOP_LOSS,
SellType.ROI, SellType.FORCE_SELL] ExitType.ROI, ExitType.FORCE_SELL]
}), }),
'config': hyperopt.config, 'config': hyperopt.config,
'locks': [], 'locks': [],
@ -428,8 +428,8 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None:
"is_open": [False, False, False, True], "is_open": [False, False, False, True],
"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],
"sell_reason": [SellType.ROI, SellType.STOP_LOSS, "sell_reason": [ExitType.ROI, ExitType.STOP_LOSS,
SellType.ROI, SellType.FORCE_SELL] ExitType.ROI, ExitType.FORCE_SELL]
}), }),
'config': hyperopt_conf, 'config': hyperopt_conf,
'locks': [], 'locks': [],

View File

@ -12,7 +12,7 @@ from freqtrade.data import history
from freqtrade.data.btanalysis import (get_latest_backtest_filename, load_backtest_data, from freqtrade.data.btanalysis import (get_latest_backtest_filename, load_backtest_data,
load_backtest_stats) load_backtest_stats)
from freqtrade.edge import PairInfo from freqtrade.edge import PairInfo
from freqtrade.enums import SellType from freqtrade.enums import ExitType
from freqtrade.optimize.optimize_reports import (_get_resample_from_period, generate_backtest_stats, from freqtrade.optimize.optimize_reports import (_get_resample_from_period, generate_backtest_stats,
generate_daily_stats, generate_edge_table, generate_daily_stats, generate_edge_table,
generate_pair_metrics, generate_pair_metrics,
@ -77,8 +77,8 @@ def test_generate_backtest_stats(default_conf, testdatadir, tmpdir):
"is_open": [False, False, False, True], "is_open": [False, False, False, True],
"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],
"sell_reason": [SellType.ROI, SellType.STOP_LOSS, "sell_reason": [ExitType.ROI, ExitType.STOP_LOSS,
SellType.ROI, SellType.FORCE_SELL] ExitType.ROI, ExitType.FORCE_SELL]
}), }),
'config': default_conf, 'config': default_conf,
'locks': [], 'locks': [],
@ -129,8 +129,8 @@ def test_generate_backtest_stats(default_conf, testdatadir, tmpdir):
"is_open": [False, False, False, True], "is_open": [False, False, False, True],
"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],
"sell_reason": [SellType.ROI, SellType.ROI, "sell_reason": [ExitType.ROI, ExitType.ROI,
SellType.STOP_LOSS, SellType.FORCE_SELL] ExitType.STOP_LOSS, ExitType.FORCE_SELL]
}), }),
'config': default_conf, 'config': default_conf,
'locks': [], 'locks': [],
@ -276,7 +276,7 @@ def test_text_table_sell_reason():
'wins': [2, 0, 0], 'wins': [2, 0, 0],
'draws': [0, 0, 0], 'draws': [0, 0, 0],
'losses': [0, 0, 1], 'losses': [0, 0, 1],
'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS] 'sell_reason': [ExitType.ROI, ExitType.ROI, ExitType.STOP_LOSS]
} }
) )
@ -308,7 +308,7 @@ def test_generate_sell_reason_stats():
'wins': [2, 0, 0], 'wins': [2, 0, 0],
'draws': [0, 0, 0], 'draws': [0, 0, 0],
'losses': [0, 0, 1], 'losses': [0, 0, 1],
'sell_reason': [SellType.ROI.value, SellType.ROI.value, SellType.STOP_LOSS.value] 'sell_reason': [ExitType.ROI.value, ExitType.ROI.value, ExitType.STOP_LOSS.value]
} }
) )

View File

@ -4,14 +4,14 @@ from datetime import datetime, timedelta
import pytest import pytest
from freqtrade import constants from freqtrade import constants
from freqtrade.enums import SellType from freqtrade.enums import ExitType
from freqtrade.persistence import PairLocks, Trade from freqtrade.persistence import PairLocks, Trade
from freqtrade.plugins.protectionmanager import ProtectionManager from freqtrade.plugins.protectionmanager import ProtectionManager
from tests.conftest import get_patched_freqtradebot, log_has_re 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 = SellType.SELL_SIGNAL, sell_reason: str = ExitType.SELL_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
): ):
@ -91,7 +91,7 @@ def test_stoploss_guard(mocker, default_conf, fee, caplog):
caplog.clear() caplog.clear()
Trade.query.session.add(generate_mock_trade( Trade.query.session.add(generate_mock_trade(
'XRP/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, 'XRP/BTC', fee.return_value, False, sell_reason=ExitType.STOP_LOSS.value,
min_ago_open=200, min_ago_close=30, min_ago_open=200, min_ago_close=30,
)) ))
@ -100,12 +100,12 @@ def test_stoploss_guard(mocker, default_conf, fee, caplog):
caplog.clear() caplog.clear()
# This trade does not count, as it's closed too long ago # This trade does not count, as it's closed too long ago
Trade.query.session.add(generate_mock_trade( Trade.query.session.add(generate_mock_trade(
'BCH/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, 'BCH/BTC', fee.return_value, False, sell_reason=ExitType.STOP_LOSS.value,
min_ago_open=250, min_ago_close=100, min_ago_open=250, min_ago_close=100,
)) ))
Trade.query.session.add(generate_mock_trade( Trade.query.session.add(generate_mock_trade(
'ETH/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, 'ETH/BTC', fee.return_value, False, sell_reason=ExitType.STOP_LOSS.value,
min_ago_open=240, min_ago_close=30, min_ago_open=240, min_ago_close=30,
)) ))
# 3 Trades closed - but the 2nd has been closed too long ago. # 3 Trades closed - but the 2nd has been closed too long ago.
@ -114,7 +114,7 @@ def test_stoploss_guard(mocker, default_conf, fee, caplog):
caplog.clear() caplog.clear()
Trade.query.session.add(generate_mock_trade( Trade.query.session.add(generate_mock_trade(
'LTC/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, 'LTC/BTC', fee.return_value, False, sell_reason=ExitType.STOP_LOSS.value,
min_ago_open=180, min_ago_close=30, min_ago_open=180, min_ago_close=30,
)) ))
@ -148,7 +148,7 @@ def test_stoploss_guard_perpair(mocker, default_conf, fee, caplog, only_per_pair
caplog.clear() caplog.clear()
Trade.query.session.add(generate_mock_trade( Trade.query.session.add(generate_mock_trade(
pair, fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, pair, fee.return_value, False, sell_reason=ExitType.STOP_LOSS.value,
min_ago_open=200, min_ago_close=30, profit_rate=0.9, min_ago_open=200, min_ago_close=30, profit_rate=0.9,
)) ))
@ -158,12 +158,12 @@ def test_stoploss_guard_perpair(mocker, default_conf, fee, caplog, only_per_pair
caplog.clear() caplog.clear()
# This trade does not count, as it's closed too long ago # This trade does not count, as it's closed too long ago
Trade.query.session.add(generate_mock_trade( Trade.query.session.add(generate_mock_trade(
pair, fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, pair, fee.return_value, False, sell_reason=ExitType.STOP_LOSS.value,
min_ago_open=250, min_ago_close=100, profit_rate=0.9, min_ago_open=250, min_ago_close=100, profit_rate=0.9,
)) ))
# Trade does not count for per pair stop as it's the wrong pair. # Trade does not count for per pair stop as it's the wrong pair.
Trade.query.session.add(generate_mock_trade( Trade.query.session.add(generate_mock_trade(
'ETH/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, 'ETH/BTC', fee.return_value, False, sell_reason=ExitType.STOP_LOSS.value,
min_ago_open=240, min_ago_close=30, profit_rate=0.9, min_ago_open=240, min_ago_close=30, profit_rate=0.9,
)) ))
# 3 Trades closed - but the 2nd has been closed too long ago. # 3 Trades closed - but the 2nd has been closed too long ago.
@ -178,7 +178,7 @@ def test_stoploss_guard_perpair(mocker, default_conf, fee, caplog, only_per_pair
# 2nd Trade that counts with correct pair # 2nd Trade that counts with correct pair
Trade.query.session.add(generate_mock_trade( Trade.query.session.add(generate_mock_trade(
pair, fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, pair, fee.return_value, False, sell_reason=ExitType.STOP_LOSS.value,
min_ago_open=180, min_ago_close=30, profit_rate=0.9, min_ago_open=180, min_ago_close=30, profit_rate=0.9,
)) ))
@ -203,7 +203,7 @@ def test_CooldownPeriod(mocker, default_conf, fee, caplog):
caplog.clear() caplog.clear()
Trade.query.session.add(generate_mock_trade( Trade.query.session.add(generate_mock_trade(
'XRP/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, 'XRP/BTC', fee.return_value, False, sell_reason=ExitType.STOP_LOSS.value,
min_ago_open=200, min_ago_close=30, min_ago_open=200, min_ago_close=30,
)) ))
@ -213,7 +213,7 @@ def test_CooldownPeriod(mocker, default_conf, fee, caplog):
assert not PairLocks.is_global_lock() assert not PairLocks.is_global_lock()
Trade.query.session.add(generate_mock_trade( Trade.query.session.add(generate_mock_trade(
'ETH/BTC', fee.return_value, False, sell_reason=SellType.ROI.value, 'ETH/BTC', fee.return_value, False, sell_reason=ExitType.ROI.value,
min_ago_open=205, min_ago_close=35, min_ago_open=205, min_ago_close=35,
)) ))
@ -242,7 +242,7 @@ def test_LowProfitPairs(mocker, default_conf, fee, caplog):
caplog.clear() caplog.clear()
Trade.query.session.add(generate_mock_trade( Trade.query.session.add(generate_mock_trade(
'XRP/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, 'XRP/BTC', fee.return_value, False, sell_reason=ExitType.STOP_LOSS.value,
min_ago_open=800, min_ago_close=450, profit_rate=0.9, min_ago_open=800, min_ago_close=450, profit_rate=0.9,
)) ))
@ -253,7 +253,7 @@ def test_LowProfitPairs(mocker, default_conf, fee, caplog):
assert not PairLocks.is_global_lock() assert not PairLocks.is_global_lock()
Trade.query.session.add(generate_mock_trade( Trade.query.session.add(generate_mock_trade(
'XRP/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, 'XRP/BTC', fee.return_value, False, sell_reason=ExitType.STOP_LOSS.value,
min_ago_open=200, min_ago_close=120, profit_rate=0.9, min_ago_open=200, min_ago_close=120, profit_rate=0.9,
)) ))
@ -265,14 +265,14 @@ def test_LowProfitPairs(mocker, default_conf, fee, caplog):
# Add positive trade # Add positive trade
Trade.query.session.add(generate_mock_trade( Trade.query.session.add(generate_mock_trade(
'XRP/BTC', fee.return_value, False, sell_reason=SellType.ROI.value, 'XRP/BTC', fee.return_value, False, sell_reason=ExitType.ROI.value,
min_ago_open=20, min_ago_close=10, profit_rate=1.15, min_ago_open=20, min_ago_close=10, profit_rate=1.15,
)) ))
assert not freqtrade.protections.stop_per_pair('XRP/BTC') assert not freqtrade.protections.stop_per_pair('XRP/BTC')
assert not PairLocks.is_pair_locked('XRP/BTC') assert not PairLocks.is_pair_locked('XRP/BTC')
Trade.query.session.add(generate_mock_trade( Trade.query.session.add(generate_mock_trade(
'XRP/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, 'XRP/BTC', fee.return_value, False, sell_reason=ExitType.STOP_LOSS.value,
min_ago_open=110, min_ago_close=20, profit_rate=0.8, min_ago_open=110, min_ago_close=20, profit_rate=0.8,
)) ))
@ -300,15 +300,15 @@ def test_MaxDrawdown(mocker, default_conf, fee, caplog):
caplog.clear() caplog.clear()
Trade.query.session.add(generate_mock_trade( Trade.query.session.add(generate_mock_trade(
'XRP/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, 'XRP/BTC', fee.return_value, False, sell_reason=ExitType.STOP_LOSS.value,
min_ago_open=1000, min_ago_close=900, profit_rate=1.1, min_ago_open=1000, min_ago_close=900, profit_rate=1.1,
)) ))
Trade.query.session.add(generate_mock_trade( Trade.query.session.add(generate_mock_trade(
'ETH/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, 'ETH/BTC', fee.return_value, False, sell_reason=ExitType.STOP_LOSS.value,
min_ago_open=1000, min_ago_close=900, profit_rate=1.1, min_ago_open=1000, min_ago_close=900, profit_rate=1.1,
)) ))
Trade.query.session.add(generate_mock_trade( Trade.query.session.add(generate_mock_trade(
'NEO/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, 'NEO/BTC', fee.return_value, False, sell_reason=ExitType.STOP_LOSS.value,
min_ago_open=1000, min_ago_close=900, profit_rate=1.1, min_ago_open=1000, min_ago_close=900, profit_rate=1.1,
)) ))
# No losing trade yet ... so max_drawdown will raise exception # No losing trade yet ... so max_drawdown will raise exception
@ -316,7 +316,7 @@ def test_MaxDrawdown(mocker, default_conf, fee, caplog):
assert not freqtrade.protections.stop_per_pair('XRP/BTC') assert not freqtrade.protections.stop_per_pair('XRP/BTC')
Trade.query.session.add(generate_mock_trade( Trade.query.session.add(generate_mock_trade(
'XRP/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, 'XRP/BTC', fee.return_value, False, sell_reason=ExitType.STOP_LOSS.value,
min_ago_open=500, min_ago_close=400, profit_rate=0.9, min_ago_open=500, min_ago_close=400, profit_rate=0.9,
)) ))
# Not locked with one trade # Not locked with one trade
@ -326,7 +326,7 @@ def test_MaxDrawdown(mocker, default_conf, fee, caplog):
assert not PairLocks.is_global_lock() assert not PairLocks.is_global_lock()
Trade.query.session.add(generate_mock_trade( Trade.query.session.add(generate_mock_trade(
'XRP/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, 'XRP/BTC', fee.return_value, False, sell_reason=ExitType.STOP_LOSS.value,
min_ago_open=1200, min_ago_close=1100, profit_rate=0.5, min_ago_open=1200, min_ago_close=1100, profit_rate=0.5,
)) ))
@ -339,7 +339,7 @@ def test_MaxDrawdown(mocker, default_conf, fee, caplog):
# Winning trade ... (should not lock, does not change drawdown!) # Winning trade ... (should not lock, does not change drawdown!)
Trade.query.session.add(generate_mock_trade( Trade.query.session.add(generate_mock_trade(
'XRP/BTC', fee.return_value, False, sell_reason=SellType.ROI.value, 'XRP/BTC', fee.return_value, False, sell_reason=ExitType.ROI.value,
min_ago_open=320, min_ago_close=410, profit_rate=1.5, min_ago_open=320, min_ago_close=410, profit_rate=1.5,
)) ))
assert not freqtrade.protections.global_stop() assert not freqtrade.protections.global_stop()
@ -349,7 +349,7 @@ def test_MaxDrawdown(mocker, default_conf, fee, caplog):
# Add additional negative trade, causing a loss of > 15% # Add additional negative trade, causing a loss of > 15%
Trade.query.session.add(generate_mock_trade( Trade.query.session.add(generate_mock_trade(
'XRP/BTC', fee.return_value, False, sell_reason=SellType.ROI.value, 'XRP/BTC', fee.return_value, False, sell_reason=ExitType.ROI.value,
min_ago_open=20, min_ago_close=10, profit_rate=0.8, min_ago_open=20, min_ago_close=10, profit_rate=0.8,
)) ))
assert not freqtrade.protections.stop_per_pair('XRP/BTC') assert not freqtrade.protections.stop_per_pair('XRP/BTC')

View File

@ -18,7 +18,7 @@ from telegram.error import BadRequest, NetworkError, TelegramError
from freqtrade import __version__ from freqtrade import __version__
from freqtrade.constants import CANCEL_REASON from freqtrade.constants import CANCEL_REASON
from freqtrade.edge import PairInfo from freqtrade.edge import PairInfo
from freqtrade.enums import RPCMessageType, RunMode, SellType, SignalDirection, State from freqtrade.enums import ExitType, RPCMessageType, RunMode, SignalDirection, State
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.freqtradebot import FreqtradeBot from freqtrade.freqtradebot import FreqtradeBot
from freqtrade.loggers import setup_logging from freqtrade.loggers import setup_logging
@ -1059,7 +1059,7 @@ 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': SellType.FORCE_SELL.value, 'sell_reason': ExitType.FORCE_SELL.value,
'open_date': ANY, 'open_date': ANY,
'close_date': ANY, 'close_date': ANY,
'close_rate': ANY, 'close_rate': ANY,
@ -1127,7 +1127,7 @@ 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': SellType.FORCE_SELL.value, 'sell_reason': ExitType.FORCE_SELL.value,
'open_date': ANY, 'open_date': ANY,
'close_date': ANY, 'close_date': ANY,
'close_rate': ANY, 'close_rate': ANY,
@ -1185,7 +1185,7 @@ 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': SellType.FORCE_SELL.value, 'sell_reason': ExitType.FORCE_SELL.value,
'open_date': ANY, 'open_date': ANY,
'close_date': ANY, 'close_date': ANY,
'close_rate': ANY, 'close_rate': ANY,
@ -1932,7 +1932,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
'stake_currency': 'ETH', 'stake_currency': 'ETH',
'fiat_currency': 'USD', 'fiat_currency': 'USD',
'enter_tag': 'buy_signal1', 'enter_tag': 'buy_signal1',
'sell_reason': SellType.STOP_LOSS.value, 'sell_reason': ExitType.STOP_LOSS.value,
'open_date': arrow.utcnow().shift(hours=-1), 'open_date': arrow.utcnow().shift(hours=-1),
'close_date': arrow.utcnow(), 'close_date': arrow.utcnow(),
}) })
@ -1966,7 +1966,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
'profit_ratio': -0.57405275, 'profit_ratio': -0.57405275,
'stake_currency': 'ETH', 'stake_currency': 'ETH',
'enter_tag': 'buy_signal1', 'enter_tag': 'buy_signal1',
'sell_reason': SellType.STOP_LOSS.value, 'sell_reason': ExitType.STOP_LOSS.value,
'open_date': arrow.utcnow().shift(days=-1, hours=-2, minutes=-30), 'open_date': arrow.utcnow().shift(days=-1, hours=-2, minutes=-30),
'close_date': arrow.utcnow(), 'close_date': arrow.utcnow(),
}) })
@ -2045,7 +2045,7 @@ def test_send_msg_sell_fill_notification(default_conf, mocker, direction,
'profit_ratio': -0.57405275, 'profit_ratio': -0.57405275,
'stake_currency': 'ETH', 'stake_currency': 'ETH',
'enter_tag': enter_signal, 'enter_tag': enter_signal,
'sell_reason': SellType.STOP_LOSS.value, 'sell_reason': ExitType.STOP_LOSS.value,
'open_date': arrow.utcnow().shift(days=-1, hours=-2, minutes=-30), 'open_date': arrow.utcnow().shift(days=-1, hours=-2, minutes=-30),
'close_date': arrow.utcnow(), 'close_date': arrow.utcnow(),
}) })
@ -2169,7 +2169,7 @@ def test_send_msg_sell_notification_no_fiat(
'stake_currency': 'ETH', 'stake_currency': 'ETH',
'fiat_currency': 'USD', 'fiat_currency': 'USD',
'enter_tag': enter_signal, 'enter_tag': enter_signal,
'sell_reason': SellType.STOP_LOSS.value, 'sell_reason': ExitType.STOP_LOSS.value,
'open_date': arrow.utcnow().shift(hours=-2, minutes=-35, seconds=-3), 'open_date': arrow.utcnow().shift(hours=-2, minutes=-35, seconds=-3),
'close_date': arrow.utcnow(), 'close_date': arrow.utcnow(),
}) })

View File

@ -5,7 +5,7 @@ from unittest.mock import MagicMock
import pytest import pytest
from requests import RequestException from requests import RequestException
from freqtrade.enums import RPCMessageType, SellType from freqtrade.enums import ExitType, RPCMessageType
from freqtrade.rpc import RPC from freqtrade.rpc import RPC
from freqtrade.rpc.webhook import Webhook from freqtrade.rpc.webhook import Webhook
from tests.conftest import get_patched_freqtradebot, log_has from tests.conftest import get_patched_freqtradebot, log_has
@ -244,7 +244,7 @@ def test_send_msg_webhook(default_conf, mocker):
'profit_amount': 0.001, 'profit_amount': 0.001,
'profit_ratio': 0.20, 'profit_ratio': 0.20,
'stake_currency': 'BTC', 'stake_currency': 'BTC',
'sell_reason': SellType.STOP_LOSS.value 'sell_reason': ExitType.STOP_LOSS.value
} }
webhook.send_msg(msg=msg) webhook.send_msg(msg=msg)
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
@ -269,7 +269,7 @@ def test_send_msg_webhook(default_conf, mocker):
'profit_amount': 0.001, 'profit_amount': 0.001,
'profit_ratio': 0.20, 'profit_ratio': 0.20,
'stake_currency': 'BTC', 'stake_currency': 'BTC',
'sell_reason': SellType.STOP_LOSS.value 'sell_reason': ExitType.STOP_LOSS.value
} }
webhook.send_msg(msg=msg) webhook.send_msg(msg=msg)
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
@ -294,7 +294,7 @@ def test_send_msg_webhook(default_conf, mocker):
'profit_amount': 0.001, 'profit_amount': 0.001,
'profit_ratio': 0.20, 'profit_ratio': 0.20,
'stake_currency': 'BTC', 'stake_currency': 'BTC',
'sell_reason': SellType.STOP_LOSS.value 'sell_reason': ExitType.STOP_LOSS.value
} }
webhook.send_msg(msg=msg) webhook.send_msg(msg=msg)
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1

View File

@ -29,3 +29,21 @@ class TestStrategyImplementCustomSell(TestStrategyNoImplementSell):
current_rate: float, current_profit: float, current_rate: float, current_profit: float,
**kwargs): **kwargs):
return False return False
class TestStrategyImplementBuyTimeout(TestStrategyNoImplementSell):
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
return super().populate_exit_trend(dataframe, metadata)
def check_buy_timeout(self, pair: str, trade, order: dict,
current_time: datetime, **kwargs) -> bool:
return False
class TestStrategyImplementSellTimeout(TestStrategyNoImplementSell):
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
return super().populate_exit_trend(dataframe, metadata)
def check_sell_timeout(self, pair: str, trade, order: dict,
current_time: datetime, **kwargs) -> bool:
return False

View File

@ -46,7 +46,8 @@ def test_strategy_test_v3(result, fee, is_short, side):
current_time=datetime.utcnow(), current_time=datetime.utcnow(),
side=side, entry_tag=None) is True side=side, entry_tag=None) is True
assert strategy.confirm_trade_exit(pair='ETH/BTC', trade=trade, order_type='limit', amount=0.1, assert strategy.confirm_trade_exit(pair='ETH/BTC', trade=trade, order_type='limit', amount=0.1,
rate=20000, time_in_force='gtc', sell_reason='roi', rate=20000, time_in_force='gtc', exit_reason='roi',
sell_reason='roi',
current_time=datetime.utcnow(), current_time=datetime.utcnow(),
side=side) is True side=side) is True

View File

@ -11,14 +11,13 @@ from pandas import DataFrame
from freqtrade.configuration import TimeRange from freqtrade.configuration import TimeRange
from freqtrade.data.dataprovider import DataProvider from freqtrade.data.dataprovider import DataProvider
from freqtrade.data.history import load_data from freqtrade.data.history import load_data
from freqtrade.enums import SellType, SignalDirection from freqtrade.enums import ExitCheckTuple, ExitType, SignalDirection
from freqtrade.exceptions import OperationalException, StrategyError from freqtrade.exceptions import OperationalException, StrategyError
from freqtrade.optimize.space import SKDecimal from freqtrade.optimize.space import SKDecimal
from freqtrade.persistence import PairLocks, Trade from freqtrade.persistence import PairLocks, Trade
from freqtrade.resolvers import StrategyResolver from freqtrade.resolvers import StrategyResolver
from freqtrade.strategy.hyper import (BaseParameter, BooleanParameter, CategoricalParameter, from freqtrade.strategy.hyper import (BaseParameter, BooleanParameter, CategoricalParameter,
DecimalParameter, IntParameter, RealParameter) DecimalParameter, IntParameter, RealParameter)
from freqtrade.strategy.interface import SellCheckTuple
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
from tests.conftest import CURRENT_TEST_STRATEGY, TRADE_SIDES, log_has, log_has_re from tests.conftest import CURRENT_TEST_STRATEGY, TRADE_SIDES, log_has, log_has_re
@ -410,22 +409,22 @@ def test_min_roi_reached3(default_conf, fee) -> None:
'profit,adjusted,expected,trailing,custom,profit2,adjusted2,expected2,custom_stop', [ 'profit,adjusted,expected,trailing,custom,profit2,adjusted2,expected2,custom_stop', [
# Profit, adjusted stoploss(absolute), profit for 2nd call, enable trailing, # Profit, adjusted stoploss(absolute), profit for 2nd call, enable trailing,
# enable custom stoploss, expected after 1st call, expected after 2nd call # enable custom stoploss, expected after 1st call, expected after 2nd call
(0.2, 0.9, SellType.NONE, False, False, 0.3, 0.9, SellType.NONE, None), (0.2, 0.9, ExitType.NONE, False, False, 0.3, 0.9, ExitType.NONE, None),
(0.2, 0.9, SellType.NONE, False, False, -0.2, 0.9, SellType.STOP_LOSS, None), (0.2, 0.9, ExitType.NONE, False, False, -0.2, 0.9, ExitType.STOP_LOSS, None),
(0.2, 1.14, SellType.NONE, True, False, 0.05, 1.14, SellType.TRAILING_STOP_LOSS, None), (0.2, 1.14, ExitType.NONE, True, False, 0.05, 1.14, ExitType.TRAILING_STOP_LOSS, None),
(0.01, 0.96, SellType.NONE, True, False, 0.05, 1, SellType.NONE, None), (0.01, 0.96, ExitType.NONE, True, False, 0.05, 1, ExitType.NONE, None),
(0.05, 1, SellType.NONE, True, False, -0.01, 1, SellType.TRAILING_STOP_LOSS, None), (0.05, 1, ExitType.NONE, True, False, -0.01, 1, ExitType.TRAILING_STOP_LOSS, None),
# Default custom case - trails with 10% # Default custom case - trails with 10%
(0.05, 0.95, SellType.NONE, False, True, -0.02, 0.95, SellType.NONE, None), (0.05, 0.95, ExitType.NONE, False, True, -0.02, 0.95, ExitType.NONE, None),
(0.05, 0.95, SellType.NONE, False, True, -0.06, 0.95, SellType.TRAILING_STOP_LOSS, None), (0.05, 0.95, ExitType.NONE, False, True, -0.06, 0.95, ExitType.TRAILING_STOP_LOSS, None),
(0.05, 1, SellType.NONE, False, True, -0.06, 1, SellType.TRAILING_STOP_LOSS, (0.05, 1, ExitType.NONE, False, True, -0.06, 1, ExitType.TRAILING_STOP_LOSS,
lambda **kwargs: -0.05), lambda **kwargs: -0.05),
(0.05, 1, SellType.NONE, False, True, 0.09, 1.04, SellType.NONE, (0.05, 1, ExitType.NONE, False, True, 0.09, 1.04, ExitType.NONE,
lambda **kwargs: -0.05), lambda **kwargs: -0.05),
(0.05, 0.95, SellType.NONE, False, True, 0.09, 0.98, SellType.NONE, (0.05, 0.95, ExitType.NONE, False, True, 0.09, 0.98, ExitType.NONE,
lambda current_profit, **kwargs: -0.1 if current_profit < 0.6 else -(current_profit * 2)), lambda current_profit, **kwargs: -0.1 if current_profit < 0.6 else -(current_profit * 2)),
# Error case - static stoploss in place # Error case - static stoploss in place
(0.05, 0.9, SellType.NONE, False, True, 0.09, 0.9, SellType.NONE, (0.05, 0.9, ExitType.NONE, False, True, 0.09, 0.9, ExitType.NONE,
lambda **kwargs: None), lambda **kwargs: None),
]) ])
def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, trailing, custom, def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, trailing, custom,
@ -455,23 +454,23 @@ def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, traili
sl_flag = strategy.stop_loss_reached(current_rate=current_rate, trade=trade, sl_flag = strategy.stop_loss_reached(current_rate=current_rate, trade=trade,
current_time=now, current_profit=profit, current_time=now, current_profit=profit,
force_stoploss=0, high=None) force_stoploss=0, high=None)
assert isinstance(sl_flag, SellCheckTuple) assert isinstance(sl_flag, ExitCheckTuple)
assert sl_flag.sell_type == expected assert sl_flag.exit_type == expected
if expected == SellType.NONE: if expected == ExitType.NONE:
assert sl_flag.sell_flag is False assert sl_flag.exit_flag is False
else: else:
assert sl_flag.sell_flag is True assert sl_flag.exit_flag is True
assert round(trade.stop_loss, 2) == adjusted assert round(trade.stop_loss, 2) == adjusted
current_rate2 = trade.open_rate * (1 + profit2) current_rate2 = trade.open_rate * (1 + profit2)
sl_flag = strategy.stop_loss_reached(current_rate=current_rate2, trade=trade, sl_flag = strategy.stop_loss_reached(current_rate=current_rate2, trade=trade,
current_time=now, current_profit=profit2, current_time=now, current_profit=profit2,
force_stoploss=0, high=None) force_stoploss=0, high=None)
assert sl_flag.sell_type == expected2 assert sl_flag.exit_type == expected2
if expected2 == SellType.NONE: if expected2 == ExitType.NONE:
assert sl_flag.sell_flag is False assert sl_flag.exit_flag is False
else: else:
assert sl_flag.sell_flag is True assert sl_flag.exit_flag is True
assert round(trade.stop_loss, 2) == adjusted2 assert round(trade.stop_loss, 2) == adjusted2
strategy.custom_stoploss = original_stopvalue strategy.custom_stoploss = original_stopvalue
@ -496,34 +495,34 @@ 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.sell_flag is False assert res.exit_flag is False
assert res.sell_type == SellType.NONE assert res.exit_type == ExitType.NONE
strategy.custom_exit = MagicMock(return_value=True) strategy.custom_exit = MagicMock(return_value=True)
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.sell_flag is True assert res.exit_flag is True
assert res.sell_type == SellType.CUSTOM_SELL assert res.exit_type == ExitType.CUSTOM_SELL
assert res.sell_reason == 'custom_sell' assert res.exit_reason == 'custom_sell'
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.sell_type == SellType.CUSTOM_SELL assert res.exit_type == ExitType.CUSTOM_SELL
assert res.sell_flag is True assert res.exit_flag is True
assert res.sell_reason == 'hello world' assert res.exit_reason == 'hello world'
caplog.clear() caplog.clear()
strategy.custom_exit = MagicMock(return_value='h' * 100) strategy.custom_exit = MagicMock(return_value='h' * 100)
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.sell_type == SellType.CUSTOM_SELL assert res.exit_type == ExitType.CUSTOM_SELL
assert res.sell_flag is True assert res.exit_flag is True
assert res.sell_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

@ -418,11 +418,20 @@ def test_missing_implements(default_conf):
StrategyResolver.load_strategy(default_conf) StrategyResolver.load_strategy(default_conf)
default_conf['strategy'] = 'TestStrategyImplementCustomSell' default_conf['strategy'] = 'TestStrategyImplementCustomSell'
with pytest.raises(OperationalException, with pytest.raises(OperationalException,
match=r"Please migrate your implementation of `custom_sell`.*"): match=r"Please migrate your implementation of `custom_sell`.*"):
StrategyResolver.load_strategy(default_conf) StrategyResolver.load_strategy(default_conf)
default_conf['strategy'] = 'TestStrategyImplementBuyTimeout'
with pytest.raises(OperationalException,
match=r"Please migrate your implementation of `check_buy_timeout`.*"):
StrategyResolver.load_strategy(default_conf)
default_conf['strategy'] = 'TestStrategyImplementSellTimeout'
with pytest.raises(OperationalException,
match=r"Please migrate your implementation of `check_sell_timeout`.*"):
StrategyResolver.load_strategy(default_conf)
@pytest.mark.filterwarnings("ignore:deprecated") @pytest.mark.filterwarnings("ignore:deprecated")
def test_call_deprecated_function(result, default_conf, caplog): def test_call_deprecated_function(result, default_conf, caplog):

View File

@ -963,7 +963,7 @@ def test_validate_time_in_force(default_conf, caplog) -> None:
validate_config_consistency(conf) validate_config_consistency(conf)
def test_validate_order_types(default_conf, caplog) -> None: def test__validate_order_types(default_conf, caplog) -> None:
conf = deepcopy(default_conf) conf = deepcopy(default_conf)
conf['order_types'] = { conf['order_types'] = {
'buy': 'limit', 'buy': 'limit',
@ -998,6 +998,31 @@ def test_validate_order_types(default_conf, caplog) -> None:
validate_config_consistency(conf) validate_config_consistency(conf)
def test__validate_unfilledtimeout(default_conf, caplog) -> None:
conf = deepcopy(default_conf)
conf['unfilledtimeout'] = {
'buy': 30,
'sell': 35,
}
validate_config_consistency(conf)
assert log_has_re(r"DEPRECATED: Using 'buy' and 'sell' for unfilledtimeout is.*", caplog)
assert conf['unfilledtimeout']['entry'] == 30
assert conf['unfilledtimeout']['exit'] == 35
assert 'buy' not in conf['unfilledtimeout']
assert 'sell' not in conf['unfilledtimeout']
conf = deepcopy(default_conf)
conf['unfilledtimeout'] = {
'buy': 30,
'sell': 35,
}
conf['trading_mode'] = 'futures'
with pytest.raises(
OperationalException,
match=r"Please migrate your unfilledtimeout settings to use the new wording\."):
validate_config_consistency(conf)
def test_load_config_test_comments() -> None: def test_load_config_test_comments() -> None:
""" """
Load config with comments Load config with comments

View File

@ -13,14 +13,14 @@ import pytest
from pandas import DataFrame from pandas import DataFrame
from freqtrade.constants import CANCEL_REASON, MATH_CLOSE_PREC, UNLIMITED_STAKE_AMOUNT from freqtrade.constants import CANCEL_REASON, MATH_CLOSE_PREC, UNLIMITED_STAKE_AMOUNT
from freqtrade.enums import CandleType, RPCMessageType, RunMode, SellType, SignalDirection, State from freqtrade.enums import (CandleType, ExitCheckTuple, ExitType, RPCMessageType, RunMode,
SignalDirection, State)
from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError, from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError,
InvalidOrderException, OperationalException, PricingError, InvalidOrderException, OperationalException, PricingError,
TemporaryError) TemporaryError)
from freqtrade.freqtradebot import FreqtradeBot from freqtrade.freqtradebot import FreqtradeBot
from freqtrade.persistence import Order, PairLocks, Trade from freqtrade.persistence import Order, PairLocks, Trade
from freqtrade.persistence.models import PairLock from freqtrade.persistence.models import PairLock
from freqtrade.strategy.interface import SellCheckTuple
from freqtrade.worker import Worker from freqtrade.worker import Worker
from tests.conftest import (create_mock_trades, get_patched_freqtradebot, get_patched_worker, from tests.conftest import (create_mock_trades, get_patched_freqtradebot, get_patched_worker,
log_has, log_has_re, patch_edge, patch_exchange, patch_get_signal, log_has, log_has_re, patch_edge, patch_exchange, patch_get_signal,
@ -236,7 +236,7 @@ def test_edge_overrides_stoploss(limit_order, fee, caplog, mocker,
assert freqtrade.handle_trade(trade) is not ignore_strat_sl assert freqtrade.handle_trade(trade) is not ignore_strat_sl
if not ignore_strat_sl: if not ignore_strat_sl:
assert log_has_re('Exit for NEO/BTC detected. Reason: stop_loss.*', caplog) assert log_has_re('Exit for NEO/BTC detected. Reason: stop_loss.*', caplog)
assert trade.sell_reason == SellType.STOP_LOSS.value assert trade.sell_reason == ExitType.STOP_LOSS.value
def test_total_open_trades_stakes(mocker, default_conf_usdt, ticker_usdt, fee) -> None: def test_total_open_trades_stakes(mocker, default_conf_usdt, ticker_usdt, fee) -> None:
@ -1209,7 +1209,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.sell_reason == str(SellType.EMERGENCY_SELL) assert trade.sell_reason == str(ExitType.EMERGENCY_SELL)
@pytest.mark.parametrize("is_short", [False, True]) @pytest.mark.parametrize("is_short", [False, True])
@ -1292,7 +1292,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.sell_reason == SellType.EMERGENCY_SELL.value assert trade.sell_reason == ExitType.EMERGENCY_SELL.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)
@ -1304,7 +1304,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'] == SellType.EMERGENCY_SELL.value assert rpc_mock.call_args_list[1][0][0]['sell_reason'] == ExitType.EMERGENCY_SELL.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'
@ -2274,7 +2274,7 @@ def test_handle_trade_roi(default_conf_usdt, ticker_usdt, limit_order_open, fee,
caplog.clear() caplog.clear()
patch_get_signal(freqtrade) patch_get_signal(freqtrade)
assert freqtrade.handle_trade(trade) assert freqtrade.handle_trade(trade)
assert log_has("ETH/USDT - Required profit reached. sell_type=SellType.ROI", assert log_has("ETH/USDT - Required profit reached. sell_type=ExitType.ROI",
caplog) caplog)
@ -2316,7 +2316,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. sell_type=SellType.SELL_SIGNAL", assert log_has("ETH/USDT - Sell signal received. sell_type=ExitType.SELL_SIGNAL",
caplog) caplog)
@ -2370,7 +2370,7 @@ def test_bot_loop_start_called_once(mocker, default_conf_usdt, caplog):
@pytest.mark.parametrize("is_short", [False, True]) @pytest.mark.parametrize("is_short", [False, True])
def test_check_handle_timedout_buy_usercustom( def test_check_handle_timedout_entry_usercustom(
default_conf_usdt, ticker_usdt, limit_buy_order_old, open_trade, default_conf_usdt, ticker_usdt, limit_buy_order_old, open_trade,
limit_sell_order_old, fee, mocker, is_short limit_sell_order_old, fee, mocker, is_short
) -> None: ) -> None:
@ -2378,8 +2378,7 @@ def test_check_handle_timedout_buy_usercustom(
old_order = limit_sell_order_old if is_short else limit_buy_order_old old_order = limit_sell_order_old if is_short else limit_buy_order_old
old_order['id'] = open_trade.open_order_id old_order['id'] = open_trade.open_order_id
default_conf_usdt["unfilledtimeout"] = {"buy": 30, default_conf_usdt["unfilledtimeout"] = {"entry": 1400, "exit": 30}
"sell": 1400} if is_short else {"buy": 1400, "sell": 30}
rpc_mock = patch_RPCManager(mocker) rpc_mock = patch_RPCManager(mocker)
cancel_order_mock = MagicMock(return_value=old_order) cancel_order_mock = MagicMock(return_value=old_order)
@ -2399,6 +2398,7 @@ def test_check_handle_timedout_buy_usercustom(
freqtrade = FreqtradeBot(default_conf_usdt) freqtrade = FreqtradeBot(default_conf_usdt)
open_trade.is_short = is_short open_trade.is_short = is_short
open_trade.orders[0].side = 'sell' if is_short else 'buy' open_trade.orders[0].side = 'sell' if is_short else 'buy'
open_trade.orders[0].ft_order_side = 'sell' if is_short else 'buy'
Trade.query.session.add(open_trade) Trade.query.session.add(open_trade)
# Ensure default is to return empty (so not mocked yet) # Ensure default is to return empty (so not mocked yet)
@ -2406,34 +2406,23 @@ def test_check_handle_timedout_buy_usercustom(
assert cancel_order_mock.call_count == 0 assert cancel_order_mock.call_count == 0
# Return false - trade remains open # Return false - trade remains open
if is_short: freqtrade.strategy.check_entry_timeout = MagicMock(return_value=False)
freqtrade.strategy.check_sell_timeout = MagicMock(return_value=False)
else:
freqtrade.strategy.check_buy_timeout = MagicMock(return_value=False)
freqtrade.check_handle_timedout() freqtrade.check_handle_timedout()
assert cancel_order_mock.call_count == 0 assert cancel_order_mock.call_count == 0
trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all() trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all()
nb_trades = len(trades) nb_trades = len(trades)
assert nb_trades == 1 assert nb_trades == 1
if is_short: assert freqtrade.strategy.check_entry_timeout.call_count == 1
assert freqtrade.strategy.check_sell_timeout.call_count == 1 freqtrade.strategy.check_entry_timeout = MagicMock(side_effect=KeyError)
# Raise Keyerror ... (no impact on trade)
freqtrade.strategy.check_sell_timeout = MagicMock(side_effect=KeyError)
else:
assert freqtrade.strategy.check_buy_timeout.call_count == 1
freqtrade.strategy.check_buy_timeout = MagicMock(side_effect=KeyError)
freqtrade.check_handle_timedout() freqtrade.check_handle_timedout()
assert cancel_order_mock.call_count == 0 assert cancel_order_mock.call_count == 0
trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all() trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all()
nb_trades = len(trades) nb_trades = len(trades)
assert nb_trades == 1 assert nb_trades == 1
if is_short: assert freqtrade.strategy.check_entry_timeout.call_count == 1
assert freqtrade.strategy.check_sell_timeout.call_count == 1 freqtrade.strategy.check_entry_timeout = MagicMock(return_value=True)
freqtrade.strategy.check_sell_timeout = MagicMock(return_value=True)
else:
assert freqtrade.strategy.check_buy_timeout.call_count == 1
freqtrade.strategy.check_buy_timeout = MagicMock(return_value=True)
# Trade should be closed since the function returns true # Trade should be closed since the function returns true
freqtrade.check_handle_timedout() freqtrade.check_handle_timedout()
assert cancel_order_wr_mock.call_count == 1 assert cancel_order_wr_mock.call_count == 1
@ -2441,10 +2430,7 @@ def test_check_handle_timedout_buy_usercustom(
trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all() trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all()
nb_trades = len(trades) nb_trades = len(trades)
assert nb_trades == 0 assert nb_trades == 0
if is_short: assert freqtrade.strategy.check_entry_timeout.call_count == 1
assert freqtrade.strategy.check_sell_timeout.call_count == 1
else:
assert freqtrade.strategy.check_buy_timeout.call_count == 1
@pytest.mark.parametrize("is_short", [False, True]) @pytest.mark.parametrize("is_short", [False, True])
@ -2472,9 +2458,9 @@ def test_check_handle_timedout_buy(
Trade.query.session.add(open_trade) Trade.query.session.add(open_trade)
if is_short: if is_short:
freqtrade.strategy.check_sell_timeout = MagicMock(return_value=False) freqtrade.strategy.check_exit_timeout = MagicMock(return_value=False)
else: else:
freqtrade.strategy.check_buy_timeout = MagicMock(return_value=False) freqtrade.strategy.check_entry_timeout = MagicMock(return_value=False)
# check it does cancel buy orders over the time limit # check it does cancel buy orders over the time limit
freqtrade.check_handle_timedout() freqtrade.check_handle_timedout()
assert cancel_order_mock.call_count == 1 assert cancel_order_mock.call_count == 1
@ -2484,9 +2470,9 @@ def test_check_handle_timedout_buy(
assert nb_trades == 0 assert nb_trades == 0
# Custom user buy-timeout is never called # Custom user buy-timeout is never called
if is_short: if is_short:
assert freqtrade.strategy.check_sell_timeout.call_count == 0 assert freqtrade.strategy.check_exit_timeout.call_count == 0
else: else:
assert freqtrade.strategy.check_buy_timeout.call_count == 0 assert freqtrade.strategy.check_entry_timeout.call_count == 0
@pytest.mark.parametrize("is_short", [False, True]) @pytest.mark.parametrize("is_short", [False, True])
@ -2553,11 +2539,11 @@ def test_check_handle_timedout_buy_exception(
@pytest.mark.parametrize("is_short", [False, True]) @pytest.mark.parametrize("is_short", [False, True])
def test_check_handle_timedout_sell_usercustom( def test_check_handle_timedout_exit_usercustom(
default_conf_usdt, ticker_usdt, limit_sell_order_old, mocker, default_conf_usdt, ticker_usdt, limit_sell_order_old, mocker,
is_short, open_trade_usdt, caplog is_short, open_trade_usdt, caplog
) -> None: ) -> None:
default_conf_usdt["unfilledtimeout"] = {"buy": 1440, "sell": 1440, "exit_timeout_count": 1} default_conf_usdt["unfilledtimeout"] = {"entry": 1440, "exit": 1440, "exit_timeout_count": 1}
limit_sell_order_old['id'] = open_trade_usdt.open_order_id limit_sell_order_old['id'] = open_trade_usdt.open_order_id
if is_short: if is_short:
limit_sell_order_old['side'] = 'buy' limit_sell_order_old['side'] = 'buy'
@ -2585,35 +2571,35 @@ def test_check_handle_timedout_sell_usercustom(
freqtrade.check_handle_timedout() freqtrade.check_handle_timedout()
assert cancel_order_mock.call_count == 0 assert cancel_order_mock.call_count == 0
freqtrade.strategy.check_sell_timeout = MagicMock(return_value=False) freqtrade.strategy.check_exit_timeout = MagicMock(return_value=False)
freqtrade.strategy.check_buy_timeout = MagicMock(return_value=False) freqtrade.strategy.check_entry_timeout = MagicMock(return_value=False)
# Return false - No impact # Return false - No impact
freqtrade.check_handle_timedout() freqtrade.check_handle_timedout()
assert cancel_order_mock.call_count == 0 assert cancel_order_mock.call_count == 0
assert rpc_mock.call_count == 0 assert rpc_mock.call_count == 0
assert open_trade_usdt.is_open is False assert open_trade_usdt.is_open is False
assert freqtrade.strategy.check_sell_timeout.call_count == (0 if is_short else 1) assert freqtrade.strategy.check_exit_timeout.call_count == 1
assert freqtrade.strategy.check_buy_timeout.call_count == (1 if is_short else 0) assert freqtrade.strategy.check_entry_timeout.call_count == 0
freqtrade.strategy.check_sell_timeout = MagicMock(side_effect=KeyError) freqtrade.strategy.check_exit_timeout = MagicMock(side_effect=KeyError)
freqtrade.strategy.check_buy_timeout = MagicMock(side_effect=KeyError) freqtrade.strategy.check_entry_timeout = MagicMock(side_effect=KeyError)
# Return Error - No impact # Return Error - No impact
freqtrade.check_handle_timedout() freqtrade.check_handle_timedout()
assert cancel_order_mock.call_count == 0 assert cancel_order_mock.call_count == 0
assert rpc_mock.call_count == 0 assert rpc_mock.call_count == 0
assert open_trade_usdt.is_open is False assert open_trade_usdt.is_open is False
assert freqtrade.strategy.check_sell_timeout.call_count == (0 if is_short else 1) assert freqtrade.strategy.check_exit_timeout.call_count == 1
assert freqtrade.strategy.check_buy_timeout.call_count == (1 if is_short else 0) assert freqtrade.strategy.check_entry_timeout.call_count == 0
# Return True - sells! # Return True - sells!
freqtrade.strategy.check_sell_timeout = MagicMock(return_value=True) freqtrade.strategy.check_exit_timeout = MagicMock(return_value=True)
freqtrade.strategy.check_buy_timeout = MagicMock(return_value=True) freqtrade.strategy.check_entry_timeout = MagicMock(return_value=True)
freqtrade.check_handle_timedout() freqtrade.check_handle_timedout()
assert cancel_order_mock.call_count == 1 assert cancel_order_mock.call_count == 1
assert rpc_mock.call_count == 1 assert rpc_mock.call_count == 1
assert open_trade_usdt.is_open is True assert open_trade_usdt.is_open is True
assert freqtrade.strategy.check_sell_timeout.call_count == (0 if is_short else 1) assert freqtrade.strategy.check_exit_timeout.call_count == 1
assert freqtrade.strategy.check_buy_timeout.call_count == (1 if is_short else 0) assert freqtrade.strategy.check_entry_timeout.call_count == 0
# 2nd canceled trade - Fail execute sell # 2nd canceled trade - Fail execute sell
caplog.clear() caplog.clear()
@ -2665,16 +2651,16 @@ def test_check_handle_timedout_sell(
Trade.query.session.add(open_trade_usdt) Trade.query.session.add(open_trade_usdt)
freqtrade.strategy.check_sell_timeout = MagicMock(return_value=False) freqtrade.strategy.check_exit_timeout = MagicMock(return_value=False)
freqtrade.strategy.check_buy_timeout = MagicMock(return_value=False) freqtrade.strategy.check_entry_timeout = MagicMock(return_value=False)
# check it does cancel sell orders over the time limit # check it does cancel sell orders over the time limit
freqtrade.check_handle_timedout() freqtrade.check_handle_timedout()
assert cancel_order_mock.call_count == 1 assert cancel_order_mock.call_count == 1
assert rpc_mock.call_count == 1 assert rpc_mock.call_count == 1
assert open_trade_usdt.is_open is True assert open_trade_usdt.is_open is True
# Custom user sell-timeout is never called # Custom user sell-timeout is never called
assert freqtrade.strategy.check_sell_timeout.call_count == 0 assert freqtrade.strategy.check_exit_timeout.call_count == 0
assert freqtrade.strategy.check_buy_timeout.call_count == 0 assert freqtrade.strategy.check_entry_timeout.call_count == 0
@pytest.mark.parametrize("is_short", [False, True]) @pytest.mark.parametrize("is_short", [False, True])
@ -3100,7 +3086,7 @@ def test_execute_trade_exit_up(default_conf_usdt, ticker_usdt, fee, ticker_usdt_
freqtrade.execute_trade_exit( freqtrade.execute_trade_exit(
trade=trade, trade=trade,
limit=(ticker_usdt_sell_down()['ask'] if is_short else ticker_usdt_sell_up()['bid']), limit=(ticker_usdt_sell_down()['ask'] if is_short else ticker_usdt_sell_up()['bid']),
sell_reason=SellCheckTuple(sell_type=SellType.ROI) exit_check=ExitCheckTuple(exit_type=ExitType.ROI)
) )
assert rpc_mock.call_count == 0 assert rpc_mock.call_count == 0
assert freqtrade.strategy.confirm_trade_exit.call_count == 1 assert freqtrade.strategy.confirm_trade_exit.call_count == 1
@ -3112,7 +3098,7 @@ def test_execute_trade_exit_up(default_conf_usdt, ticker_usdt, fee, ticker_usdt_
freqtrade.execute_trade_exit( freqtrade.execute_trade_exit(
trade=trade, trade=trade,
limit=(ticker_usdt_sell_down()['ask'] if is_short else ticker_usdt_sell_up()['bid']), limit=(ticker_usdt_sell_down()['ask'] if is_short else ticker_usdt_sell_up()['bid']),
sell_reason=SellCheckTuple(sell_type=SellType.ROI) exit_check=ExitCheckTuple(exit_type=ExitType.ROI)
) )
assert freqtrade.strategy.confirm_trade_exit.call_count == 1 assert freqtrade.strategy.confirm_trade_exit.call_count == 1
@ -3137,7 +3123,7 @@ def test_execute_trade_exit_up(default_conf_usdt, ticker_usdt, fee, ticker_usdt_
'profit_ratio': 0.00493809 if is_short else 0.09451372, 'profit_ratio': 0.00493809 if is_short else 0.09451372,
'stake_currency': 'USDT', 'stake_currency': 'USDT',
'fiat_currency': 'USD', 'fiat_currency': 'USD',
'sell_reason': SellType.ROI.value, 'sell_reason': ExitType.ROI.value,
'open_date': ANY, 'open_date': ANY,
'close_date': ANY, 'close_date': ANY,
'close_rate': ANY, 'close_rate': ANY,
@ -3173,7 +3159,7 @@ def test_execute_trade_exit_down(default_conf_usdt, ticker_usdt, fee, ticker_usd
) )
freqtrade.execute_trade_exit( freqtrade.execute_trade_exit(
trade=trade, limit=(ticker_usdt_sell_up if is_short else ticker_usdt_sell_down)()['bid'], trade=trade, limit=(ticker_usdt_sell_up if is_short else ticker_usdt_sell_down)()['bid'],
sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS)) exit_check=ExitCheckTuple(exit_type=ExitType.STOP_LOSS))
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]
@ -3196,7 +3182,7 @@ def test_execute_trade_exit_down(default_conf_usdt, ticker_usdt, fee, ticker_usd
'profit_ratio': -0.0945681 if is_short else -1.247e-05, 'profit_ratio': -0.0945681 if is_short else -1.247e-05,
'stake_currency': 'USDT', 'stake_currency': 'USDT',
'fiat_currency': 'USD', 'fiat_currency': 'USD',
'sell_reason': SellType.STOP_LOSS.value, 'sell_reason': ExitType.STOP_LOSS.value,
'open_date': ANY, 'open_date': ANY,
'close_date': ANY, 'close_date': ANY,
'close_rate': ANY, 'close_rate': ANY,
@ -3248,7 +3234,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'],
sell_reason=SellCheckTuple(sell_type=SellType.SELL_SIGNAL) exit_check=ExitCheckTuple(exit_type=ExitType.SELL_SIGNAL)
) )
# Sell price must be different to default bid price # Sell price must be different to default bid price
@ -3276,7 +3262,7 @@ 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': SellType.SELL_SIGNAL.value, 'sell_reason': ExitType.SELL_SIGNAL.value,
'open_date': ANY, 'open_date': ANY,
'close_date': ANY, 'close_date': ANY,
'close_rate': ANY, 'close_rate': ANY,
@ -3319,7 +3305,7 @@ def test_execute_trade_exit_down_stoploss_on_exchange_dry_run(
trade.stop_loss = 2.0 * 1.01 if is_short else 2.0 * 0.99 trade.stop_loss = 2.0 * 1.01 if is_short else 2.0 * 0.99
freqtrade.execute_trade_exit( freqtrade.execute_trade_exit(
trade=trade, limit=(ticker_usdt_sell_up if is_short else ticker_usdt_sell_down())['bid'], trade=trade, limit=(ticker_usdt_sell_up if is_short else ticker_usdt_sell_down())['bid'],
sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS)) exit_check=ExitCheckTuple(exit_type=ExitType.STOP_LOSS))
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]
@ -3343,7 +3329,7 @@ def test_execute_trade_exit_down_stoploss_on_exchange_dry_run(
'profit_ratio': -0.00501253 if is_short else -0.01493766, 'profit_ratio': -0.00501253 if is_short else -0.01493766,
'stake_currency': 'USDT', 'stake_currency': 'USDT',
'fiat_currency': 'USD', 'fiat_currency': 'USD',
'sell_reason': SellType.STOP_LOSS.value, 'sell_reason': ExitType.STOP_LOSS.value,
'open_date': ANY, 'open_date': ANY,
'close_date': ANY, 'close_date': ANY,
'close_rate': ANY, 'close_rate': ANY,
@ -3379,7 +3365,7 @@ def test_execute_trade_exit_sloe_cancel_exception(
trade.stoploss_order_id = "abcd" trade.stoploss_order_id = "abcd"
freqtrade.execute_trade_exit(trade=trade, limit=1234, freqtrade.execute_trade_exit(trade=trade, limit=1234,
sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS)) exit_check=ExitCheckTuple(exit_type=ExitType.STOP_LOSS))
assert create_order_mock.call_count == 2 assert create_order_mock.call_count == 2
assert log_has('Could not cancel stoploss order abcd', caplog) assert log_has('Could not cancel stoploss order abcd', caplog)
@ -3434,7 +3420,7 @@ def test_execute_trade_exit_with_stoploss_on_exchange(
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'],
sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS) exit_check=ExitCheckTuple(exit_type=ExitType.STOP_LOSS)
) )
trade = Trade.query.first() trade = Trade.query.first()
@ -3510,7 +3496,7 @@ def test_may_execute_trade_exit_after_stoploss_on_exchange_hit(
freqtrade.exit_positions(trades) freqtrade.exit_positions(trades)
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.sell_reason == SellType.STOPLOSS_ON_EXCHANGE.value assert trade.sell_reason == ExitType.STOPLOSS_ON_EXCHANGE.value
assert rpc_mock.call_count == 3 assert rpc_mock.call_count == 3
if is_short: if is_short:
assert rpc_mock.call_args_list[0][0][0]['type'] == RPCMessageType.SHORT assert rpc_mock.call_args_list[0][0][0]['type'] == RPCMessageType.SHORT
@ -3579,7 +3565,7 @@ def test_execute_trade_exit_market_order(
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'],
sell_reason=SellCheckTuple(sell_type=SellType.ROI) exit_check=ExitCheckTuple(exit_type=ExitType.ROI)
) )
assert not trade.is_open assert not trade.is_open
@ -3606,7 +3592,7 @@ def test_execute_trade_exit_market_order(
'profit_ratio': profit_ratio, 'profit_ratio': profit_ratio,
'stake_currency': 'USDT', 'stake_currency': 'USDT',
'fiat_currency': 'USD', 'fiat_currency': 'USD',
'sell_reason': SellType.ROI.value, 'sell_reason': ExitType.ROI.value,
'open_date': ANY, 'open_date': ANY,
'close_date': ANY, 'close_date': ANY,
'close_rate': ANY, 'close_rate': ANY,
@ -3643,29 +3629,29 @@ def test_execute_trade_exit_insufficient_funds_error(default_conf_usdt, ticker_u
fetch_ticker=ticker_usdt_sell_up fetch_ticker=ticker_usdt_sell_up
) )
sell_reason = SellCheckTuple(sell_type=SellType.ROI) sell_reason = ExitCheckTuple(exit_type=ExitType.ROI)
assert not freqtrade.execute_trade_exit( assert not 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'],
sell_reason=sell_reason exit_check=sell_reason
) )
assert mock_insuf.call_count == 1 assert mock_insuf.call_count == 1
@pytest.mark.parametrize('profit_only,bid,ask,handle_first,handle_second,sell_type,is_short', [ @pytest.mark.parametrize('profit_only,bid,ask,handle_first,handle_second,sell_type,is_short', [
# Enable profit # Enable profit
(True, 2.18, 2.2, False, True, SellType.SELL_SIGNAL.value, False), (True, 2.18, 2.2, False, True, ExitType.SELL_SIGNAL.value, False),
(True, 2.18, 2.2, False, True, SellType.SELL_SIGNAL.value, True), (True, 2.18, 2.2, False, True, ExitType.SELL_SIGNAL.value, True),
# # Disable profit # # Disable profit
(False, 3.19, 3.2, True, False, SellType.SELL_SIGNAL.value, False), (False, 3.19, 3.2, True, False, ExitType.SELL_SIGNAL.value, False),
(False, 3.19, 3.2, True, False, SellType.SELL_SIGNAL.value, True), (False, 3.19, 3.2, True, False, ExitType.SELL_SIGNAL.value, True),
# # Enable loss # # Enable loss
# # * Shouldn't this be SellType.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, SellType.SELL_SIGNAL.value, False), (False, 0.10, 0.22, True, False, ExitType.SELL_SIGNAL.value, False),
(False, 0.10, 0.22, True, False, SellType.SELL_SIGNAL.value, True), (False, 0.10, 0.22, True, False, ExitType.SELL_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,
@ -3693,11 +3679,11 @@ 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 sell_type == SellType.SELL_SIGNAL.value: if sell_type == ExitType.SELL_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=SellCheckTuple( freqtrade.strategy.stop_loss_reached = MagicMock(return_value=ExitCheckTuple(
sell_type=SellType.NONE)) exit_type=ExitType.NONE))
freqtrade.enter_positions() freqtrade.enter_positions()
trade = Trade.query.first() trade = Trade.query.first()
@ -3816,7 +3802,7 @@ def test_locked_pairs(default_conf_usdt, ticker_usdt, fee,
freqtrade.execute_trade_exit( freqtrade.execute_trade_exit(
trade=trade, trade=trade,
limit=ticker_usdt_sell_down()['ask' if is_short else 'bid'], limit=ticker_usdt_sell_down()['ask' if is_short else 'bid'],
sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS) exit_check=ExitCheckTuple(exit_type=ExitType.STOP_LOSS)
) )
trade.close(ticker_usdt_sell_down()['bid']) trade.close(ticker_usdt_sell_down()['bid'])
assert freqtrade.strategy.is_pair_locked(trade.pair) assert freqtrade.strategy.is_pair_locked(trade.pair)
@ -3874,7 +3860,7 @@ def test_ignore_roi_if_buy_signal(default_conf_usdt, limit_order, limit_order_op
else: else:
patch_get_signal(freqtrade, enter_long=False, exit_long=False) patch_get_signal(freqtrade, enter_long=False, exit_long=False)
assert freqtrade.handle_trade(trade) is True assert freqtrade.handle_trade(trade) is True
assert trade.sell_reason == SellType.ROI.value assert trade.sell_reason == ExitType.ROI.value
@pytest.mark.parametrize("is_short,val1,val2", [ @pytest.mark.parametrize("is_short,val1,val2", [
@ -3936,7 +3922,7 @@ def test_trailing_stop_loss(default_conf_usdt, limit_order_open,
f"stoploss is {(2.0 * val1 * stop_multi):6f}, " f"stoploss is {(2.0 * val1 * stop_multi):6f}, "
f"initial stoploss was at {(2.0 * stop_multi):6f}, trade opened at 2.000000", f"initial stoploss was at {(2.0 * stop_multi):6f}, trade opened at 2.000000",
caplog) caplog)
assert trade.sell_reason == SellType.TRAILING_STOP_LOSS.value assert trade.sell_reason == ExitType.TRAILING_STOP_LOSS.value
@pytest.mark.parametrize('offset,trail_if_reached,second_sl,is_short', [ @pytest.mark.parametrize('offset,trail_if_reached,second_sl,is_short', [
@ -4042,7 +4028,7 @@ def test_trailing_stop_loss_positive(
f"initial stoploss was at {'2.42' if is_short else '1.80'}0000, " f"initial stoploss was at {'2.42' if is_short else '1.80'}0000, "
f"trade opened at {2.2 if is_short else 2.0}00000", f"trade opened at {2.2 if is_short else 2.0}00000",
caplog) caplog)
assert trade.sell_reason == SellType.TRAILING_STOP_LOSS.value assert trade.sell_reason == ExitType.TRAILING_STOP_LOSS.value
@pytest.mark.parametrize("is_short", [False, True]) @pytest.mark.parametrize("is_short", [False, True])
@ -4088,7 +4074,7 @@ def test_disable_ignore_roi_if_buy_signal(default_conf_usdt, limit_order, limit_
# Test if entry-signal is absent # Test if entry-signal is absent
patch_get_signal(freqtrade) patch_get_signal(freqtrade)
assert freqtrade.handle_trade(trade) is True assert freqtrade.handle_trade(trade) is True
assert trade.sell_reason == SellType.ROI.value assert trade.sell_reason == ExitType.ROI.value
def test_get_real_amount_quote(default_conf_usdt, trades_for_order, buy_order_fee, fee, caplog, def test_get_real_amount_quote(default_conf_usdt, trades_for_order, buy_order_fee, fee, caplog,
@ -5047,9 +5033,9 @@ def test_update_funding_fees(
default_conf['trading_mode'] = 'futures' default_conf['trading_mode'] = 'futures'
default_conf['margin_mode'] = 'isolated' default_conf['margin_mode'] = 'isolated'
date_midnight = arrow.get('2021-09-01 00:00:00') date_midnight = arrow.get('2021-09-01 00:00:00').datetime
date_eight = arrow.get('2021-09-01 08:00:00') date_eight = arrow.get('2021-09-01 08:00:00').datetime
date_sixteen = arrow.get('2021-09-01 16:00:00') date_sixteen = arrow.get('2021-09-01 16:00:00').datetime
columns = ['date', 'open', 'high', 'low', 'close', 'volume'] columns = ['date', 'open', 'high', 'low', 'close', 'volume']
# 16:00 entry is actually never used # 16:00 entry is actually never used
# But should be kept in the test to ensure we're filtering correctly. # But should be kept in the test to ensure we're filtering correctly.
@ -5132,11 +5118,7 @@ def test_update_funding_fees(
trades = Trade.get_open_trades() trades = Trade.get_open_trades()
assert len(trades) == 3 assert len(trades) == 3
for trade in trades: for trade in trades:
assert pytest.approx(trade.funding_fees) == ( assert pytest.approx(trade.funding_fees) == 0
trade.amount *
mark_prices[trade.pair].iloc[0]['open'] *
funding_rates[trade.pair].iloc[0]['open'] * multipl
)
mocker.patch('freqtrade.exchange.Exchange.create_order', return_value=open_exit_order) mocker.patch('freqtrade.exchange.Exchange.create_order', return_value=open_exit_order)
time_machine.move_to("2021-09-01 08:00:00 +00:00") time_machine.move_to("2021-09-01 08:00:00 +00:00")
if schedule_off: if schedule_off:
@ -5145,12 +5127,12 @@ def test_update_funding_fees(
trade=trade, trade=trade,
# The values of the next 2 params are irrelevant for this test # The values of the next 2 params are irrelevant for this test
limit=ticker_usdt_sell_up()['bid'], limit=ticker_usdt_sell_up()['bid'],
sell_reason=SellCheckTuple(sell_type=SellType.ROI) exit_check=ExitCheckTuple(exit_type=ExitType.ROI)
) )
assert trade.funding_fees == pytest.approx(sum( assert trade.funding_fees == pytest.approx(sum(
trade.amount * trade.amount *
mark_prices[trade.pair].iloc[0:2]['open'] * mark_prices[trade.pair].iloc[1:2]['open'] *
funding_rates[trade.pair].iloc[0:2]['open'] * multipl funding_rates[trade.pair].iloc[1:2]['open'] * multipl
)) ))
else: else:
@ -5160,8 +5142,8 @@ def test_update_funding_fees(
for trade in trades: for trade in trades:
assert trade.funding_fees == pytest.approx(sum( assert trade.funding_fees == pytest.approx(sum(
trade.amount * trade.amount *
mark_prices[trade.pair].iloc[0:2]['open'] * mark_prices[trade.pair].iloc[1:2]['open'] *
funding_rates[trade.pair].iloc[0:2]['open'] * funding_rates[trade.pair].iloc[1:2]['open'] *
multipl multipl
)) ))

View File

@ -2,11 +2,10 @@ from unittest.mock import MagicMock
import pytest import pytest
from freqtrade.enums import SellType from freqtrade.enums import ExitCheckTuple, ExitType
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.persistence.models import Order from freqtrade.persistence.models import Order
from freqtrade.rpc.rpc import RPC from freqtrade.rpc.rpc import RPC
from freqtrade.strategy.interface import SellCheckTuple
from tests.conftest import get_patched_freqtradebot, patch_get_signal from tests.conftest import get_patched_freqtradebot, patch_get_signal
@ -53,8 +52,8 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee,
side_effect=[stoploss_order_closed, stoploss_order_open, stoploss_order_open]) side_effect=[stoploss_order_closed, stoploss_order_open, stoploss_order_open])
# 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=[
SellCheckTuple(sell_type=SellType.NONE), ExitCheckTuple(exit_type=ExitType.NONE),
SellCheckTuple(sell_type=SellType.SELL_SIGNAL)] ExitCheckTuple(exit_type=ExitType.SELL_SIGNAL)]
) )
cancel_order_mock = MagicMock() cancel_order_mock = MagicMock()
mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss) mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss)
@ -116,7 +115,7 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee,
assert wallets_mock.call_count == 4 assert wallets_mock.call_count == 4
trade = trades[0] trade = trades[0]
assert trade.sell_reason == SellType.STOPLOSS_ON_EXCHANGE.value assert trade.sell_reason == ExitType.STOPLOSS_ON_EXCHANGE.value
assert not trade.is_open assert not trade.is_open
trade = trades[1] trade = trades[1]
@ -124,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.sell_reason == SellType.SELL_SIGNAL.value assert trade.sell_reason == ExitType.SELL_SIGNAL.value
assert not trade.is_open assert not trade.is_open
@ -161,11 +160,11 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, mocker, balance_rati
_notify_exit=MagicMock(), _notify_exit=MagicMock(),
) )
should_sell_mock = MagicMock(side_effect=[ should_sell_mock = MagicMock(side_effect=[
SellCheckTuple(sell_type=SellType.NONE), ExitCheckTuple(exit_type=ExitType.NONE),
SellCheckTuple(sell_type=SellType.SELL_SIGNAL), ExitCheckTuple(exit_type=ExitType.SELL_SIGNAL),
SellCheckTuple(sell_type=SellType.NONE), ExitCheckTuple(exit_type=ExitType.NONE),
SellCheckTuple(sell_type=SellType.NONE), ExitCheckTuple(exit_type=ExitType.NONE),
SellCheckTuple(sell_type=SellType.NONE)] ExitCheckTuple(exit_type=ExitType.NONE)]
) )
mocker.patch("freqtrade.strategy.interface.IStrategy.should_exit", should_sell_mock) mocker.patch("freqtrade.strategy.interface.IStrategy.should_exit", should_sell_mock)

View File

@ -15,8 +15,7 @@ from freqtrade.enums import TradingMode
from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.exceptions import DependencyException, OperationalException
from freqtrade.persistence import LocalTrade, Order, Trade, clean_dry_run_db, init_db from freqtrade.persistence import LocalTrade, Order, Trade, clean_dry_run_db, init_db
from freqtrade.persistence.migrations import get_last_sequence_ids, set_sequence_ids from freqtrade.persistence.migrations import get_last_sequence_ids, set_sequence_ids
from tests.conftest import (create_mock_trades, create_mock_trades_usdt, from tests.conftest import create_mock_trades, create_mock_trades_with_leverage, log_has, log_has_re
create_mock_trades_with_leverage, get_sides, log_has, log_has_re)
spot, margin, futures = TradingMode.SPOT, TradingMode.MARGIN, TradingMode.FUTURES spot, margin, futures = TradingMode.SPOT, TradingMode.MARGIN, TradingMode.FUTURES
@ -77,7 +76,7 @@ def test_init_dryrun_db(default_conf, tmpdir):
@pytest.mark.parametrize('is_short', [False, True]) @pytest.mark.parametrize('is_short', [False, True])
@pytest.mark.usefixtures("init_persistence") @pytest.mark.usefixtures("init_persistence")
def test_enter_exit_side(fee, is_short): def test_enter_exit_side(fee, is_short):
enter_side, exit_side = get_sides(is_short) enter_side, exit_side = ("sell", "buy") if is_short else ("buy", "sell")
trade = Trade( trade = Trade(
id=2, id=2,
pair='ADA/USDT', pair='ADA/USDT',
@ -456,7 +455,7 @@ def test_update_limit_order(fee, caplog, limit_buy_order_usdt, limit_sell_order_
enter_order = limit_sell_order_usdt if is_short else limit_buy_order_usdt enter_order = limit_sell_order_usdt if is_short else limit_buy_order_usdt
exit_order = limit_buy_order_usdt if is_short else limit_sell_order_usdt exit_order = limit_buy_order_usdt if is_short else limit_sell_order_usdt
enter_side, exit_side = get_sides(is_short) enter_side, exit_side = ("sell", "buy") if is_short else ("buy", "sell")
trade = Trade( trade = Trade(
id=2, id=2,
@ -2057,10 +2056,11 @@ def test_get_best_pair_lev(fee):
@pytest.mark.usefixtures("init_persistence") @pytest.mark.usefixtures("init_persistence")
def test_get_exit_order_count(fee): @pytest.mark.parametrize('is_short', [True, False])
def test_get_exit_order_count(fee, is_short):
create_mock_trades_usdt(fee) create_mock_trades(fee, is_short=is_short)
trade = Trade.get_trades([Trade.pair == 'ETC/USDT']).first() trade = Trade.get_trades([Trade.pair == 'ETC/BTC']).first()
assert trade.get_exit_order_count() == 1 assert trade.get_exit_order_count() == 1