Merge pull request #4089 from freqtrade/feat/stoploss_custom
introduce custom stoploss
This commit is contained in:
commit
9e3224ccc0
@ -78,6 +78,7 @@ At this stage the bot contains the following stoploss support modes:
|
|||||||
2. Trailing stop loss.
|
2. Trailing stop loss.
|
||||||
3. Trailing stop loss, custom positive loss.
|
3. Trailing stop loss, custom positive loss.
|
||||||
4. Trailing stop loss only once the trade has reached a certain offset.
|
4. Trailing stop loss only once the trade has reached a certain offset.
|
||||||
|
5. [Custom stoploss function](strategy-advanced.md#custom-stoploss)
|
||||||
|
|
||||||
### Static Stop Loss
|
### Static Stop Loss
|
||||||
|
|
||||||
|
@ -8,11 +8,127 @@ If you're just getting started, please be familiar with the methods described in
|
|||||||
!!! Note
|
!!! Note
|
||||||
All callback methods described below should only be implemented in a strategy if they are actually used.
|
All callback methods described below should only be implemented in a strategy if they are actually used.
|
||||||
|
|
||||||
|
!!! Tip
|
||||||
|
You can get a strategy template containing all below methods by running `freqtrade new-strategy --strategy MyAwesomeStrategy --template advanced`
|
||||||
|
|
||||||
|
## Custom stoploss
|
||||||
|
|
||||||
|
A stoploss can only ever move upwards - so if you set it to an absolute profit of 2%, you can never move it below this price.
|
||||||
|
Also, the traditional `stoploss` value serves as an absolute lower level and will be instated as the initial stoploss.
|
||||||
|
|
||||||
|
The usage of the custom stoploss method must be enabled by setting `use_custom_stoploss=True` on the strategy object.
|
||||||
|
The method must return a stoploss value (float / number) with a relative ratio below the current price.
|
||||||
|
E.g. `current_profit = 0.05` (5% profit) - stoploss returns `0.02` - then you "locked in" a profit of 3% (`0.05 - 0.02 = 0.03`).
|
||||||
|
|
||||||
|
To simulate a regular trailing stoploss of 4% (trailing 4% behind the maximum reached price) you would use the following very simple method:
|
||||||
|
|
||||||
|
``` python
|
||||||
|
use_custom_stoploss = True
|
||||||
|
|
||||||
|
def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, current_rate: float,
|
||||||
|
current_profit: float, **kwargs) -> float:
|
||||||
|
"""
|
||||||
|
Custom stoploss logic, returning the new distance relative to current_rate (as ratio).
|
||||||
|
e.g. returning -0.05 would create a stoploss 5% below current_rate.
|
||||||
|
The custom stoploss can never be below self.stoploss, which serves as a hard maximum loss.
|
||||||
|
|
||||||
|
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
|
||||||
|
|
||||||
|
When not implemented by a strategy, returns the initial stoploss value
|
||||||
|
Only called when use_custom_stoploss is set to True.
|
||||||
|
|
||||||
|
:param pair: Pair that's currently analyzed
|
||||||
|
:param trade: trade object.
|
||||||
|
:param current_time: datetime object, containing the current datetime
|
||||||
|
:param current_rate: Rate, calculated based on pricing settings in ask_strategy.
|
||||||
|
:param current_profit: Current profit (as ratio), calculated based on current_rate.
|
||||||
|
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||||
|
:return float: New stoploss value, relative to the currentrate
|
||||||
|
"""
|
||||||
|
return -0.04
|
||||||
|
```
|
||||||
|
|
||||||
|
Stoploss on exchange works similar to `trailing_stop`, and the stoploss on exchange is updated as configured in `stoploss_on_exchange_interval` ([More details about stoploss on exchange](stoploss.md#stop-loss-on-exchange-freqtrade)).
|
||||||
|
|
||||||
|
!!! Note "Use of dates"
|
||||||
|
All time-based calculations should be done based on `current_time` - using `datetime.now()` or `datetime.utcnow()` is discouraged, as this will break backtesting support.
|
||||||
|
|
||||||
|
!!! Tip "Trailing stoploss"
|
||||||
|
It's recommended to disable `trailing_stop` when using custom stoploss values. Both can work in tandem, but you might encounter the trailing stop to move the price higher while your custom function would not want this, causing conflicting behavior.
|
||||||
|
|
||||||
|
### Custom stoploss examples
|
||||||
|
|
||||||
|
The next section will show some examples on what's possible with the custom stoploss function.
|
||||||
|
Of course, many more things are possible, and all examples can be combined at will.
|
||||||
|
|
||||||
|
#### Time based trailing stop
|
||||||
|
|
||||||
|
Use the initial stoploss for the first 60 minutes, after this change to 10% trailing stoploss, and after 2 hours (120 minutes) we use a 5% trailing stoploss.
|
||||||
|
|
||||||
|
``` python
|
||||||
|
use_custom_stoploss = True
|
||||||
|
|
||||||
|
def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, current_rate: float,
|
||||||
|
current_profit: float, **kwargs) -> float:
|
||||||
|
|
||||||
|
# Make sure you have the longest interval first - these conditions are evaluated from top to bottom.
|
||||||
|
if current_time - timedelta(minutes=120) > trade.open_date:
|
||||||
|
return -0.05
|
||||||
|
elif current_time - timedelta(minutes=60) > trade.open_date:
|
||||||
|
return -0.10
|
||||||
|
return 1
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Different stoploss per pair
|
||||||
|
|
||||||
|
Use a different stoploss depending on the pair.
|
||||||
|
In this example, we'll trail the highest price with 10% trailing stoploss for `ETH/BTC` and `XRP/BTC`, with 5% trailing stoploss for `LTC/BTC` and with 15% for all other pairs.
|
||||||
|
|
||||||
|
``` python
|
||||||
|
use_custom_stoploss = True
|
||||||
|
|
||||||
|
def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, current_rate: float,
|
||||||
|
current_profit: float, **kwargs) -> float:
|
||||||
|
|
||||||
|
if pair in ('ETH/BTC', 'XRP/BTC'):
|
||||||
|
return -0.10
|
||||||
|
elif pair in ('LTC/BTC'):
|
||||||
|
return -0.05
|
||||||
|
return -0.15
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Absolute stoploss
|
||||||
|
|
||||||
|
The below example sets absolute profit levels based on the current profit.
|
||||||
|
|
||||||
|
* Use the regular stoploss until 20% profit is reached
|
||||||
|
* Once profit is > 40%, stoploss will be at 25%, locking in at least 25% of the profit.
|
||||||
|
* Once profit is > 25% - stoploss will be 15%.
|
||||||
|
* Once profit is > 20% - stoploss will be set to 7%.
|
||||||
|
|
||||||
|
``` python
|
||||||
|
use_custom_stoploss = True
|
||||||
|
|
||||||
|
def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, current_rate: float,
|
||||||
|
current_profit: float, **kwargs) -> float:
|
||||||
|
|
||||||
|
# Calculate as `-desired_stop_from_open + current_profit` to get the distance between current_profit and initial price
|
||||||
|
if current_profit > 0.40:
|
||||||
|
return (-0.25 + current_profit)
|
||||||
|
if current_profit > 0.25:
|
||||||
|
return (-0.15 + current_profit)
|
||||||
|
if current_profit > 0.20:
|
||||||
|
return (-0.7 + current_profit)
|
||||||
|
return 1
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Custom order timeout rules
|
## Custom order timeout rules
|
||||||
|
|
||||||
Simple, timebased order-timeouts can be configured either via strategy or in the configuration in the `unfilledtimeout` section.
|
Simple, time-based order-timeouts can be configured either via strategy or in the configuration in the `unfilledtimeout` section.
|
||||||
|
|
||||||
However, freqtrade also offers a custom callback for both ordertypes, which allows you to decide based on custom criteria if a order did time out or not.
|
However, freqtrade also offers a custom callback for both order types, which allows you to decide based on custom criteria if a order did time out or not.
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
Unfilled order timeouts are not relevant during backtesting or hyperopt, and are only relevant during real (live) trading. Therefore these methods are only called in these circumstances.
|
Unfilled order timeouts are not relevant during backtesting or hyperopt, and are only relevant during real (live) trading. Therefore these methods are only called in these circumstances.
|
||||||
@ -28,7 +144,7 @@ The function must return either `True` (cancel order) or `False` (keep order ali
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
|
|
||||||
class Awesomestrategy(IStrategy):
|
class AwesomeStrategy(IStrategy):
|
||||||
|
|
||||||
# ... populate_* methods
|
# ... populate_* methods
|
||||||
|
|
||||||
@ -67,7 +183,7 @@ class Awesomestrategy(IStrategy):
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
|
|
||||||
class Awesomestrategy(IStrategy):
|
class AwesomeStrategy(IStrategy):
|
||||||
|
|
||||||
# ... populate_* methods
|
# ... populate_* methods
|
||||||
|
|
||||||
@ -95,6 +211,8 @@ class Awesomestrategy(IStrategy):
|
|||||||
return False
|
return False
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Bot loop start callback
|
## Bot loop start callback
|
||||||
|
|
||||||
A simple callback which is called once at the start of every bot throttling iteration.
|
A simple callback which is called once at the start of every bot throttling iteration.
|
||||||
@ -103,7 +221,7 @@ This can be used to perform calculations which are pair independent (apply to al
|
|||||||
``` python
|
``` python
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
class Awesomestrategy(IStrategy):
|
class AwesomeStrategy(IStrategy):
|
||||||
|
|
||||||
# ... populate_* methods
|
# ... populate_* methods
|
||||||
|
|
||||||
@ -128,7 +246,7 @@ class Awesomestrategy(IStrategy):
|
|||||||
`confirm_trade_entry()` can be used to abort a trade entry at the latest second (maybe because the price is not what we expect).
|
`confirm_trade_entry()` can be used to abort a trade entry at the latest second (maybe because the price is not what we expect).
|
||||||
|
|
||||||
``` python
|
``` python
|
||||||
class Awesomestrategy(IStrategy):
|
class AwesomeStrategy(IStrategy):
|
||||||
|
|
||||||
# ... populate_* methods
|
# ... populate_* methods
|
||||||
|
|
||||||
@ -164,7 +282,7 @@ class Awesomestrategy(IStrategy):
|
|||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
|
|
||||||
|
|
||||||
class Awesomestrategy(IStrategy):
|
class AwesomeStrategy(IStrategy):
|
||||||
|
|
||||||
# ... populate_* methods
|
# ... populate_* methods
|
||||||
|
|
||||||
@ -200,6 +318,8 @@ class Awesomestrategy(IStrategy):
|
|||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Derived strategies
|
## Derived strategies
|
||||||
|
|
||||||
The strategies can be derived from other strategies. This avoids duplication of your custom strategy code. You can use this technique to override small parts of your main strategy, leaving the rest untouched:
|
The strategies can be derived from other strategies. This avoids duplication of your custom strategy code. You can use this technique to override small parts of your main strategy, leaving the rest untouched:
|
||||||
@ -219,4 +339,4 @@ class MyAwesomeStrategy2(MyAwesomeStrategy):
|
|||||||
trailing_stop = True
|
trailing_stop = True
|
||||||
```
|
```
|
||||||
|
|
||||||
Both attributes and methods may be overriden, altering behavior of the original strategy in a way you need.
|
Both attributes and methods may be overridden, altering behavior of the original strategy in a way you need.
|
||||||
|
@ -309,7 +309,7 @@ Storing information can be accomplished by creating a new dictionary within the
|
|||||||
The name of the variable can be chosen at will, but should be prefixed with `cust_` to avoid naming collisions with predefined strategy variables.
|
The name of the variable can be chosen at will, but should be prefixed with `cust_` to avoid naming collisions with predefined strategy variables.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
class Awesomestrategy(IStrategy):
|
class AwesomeStrategy(IStrategy):
|
||||||
# Create custom dictionary
|
# Create custom dictionary
|
||||||
cust_info = {}
|
cust_info = {}
|
||||||
|
|
||||||
|
@ -342,6 +342,12 @@ class Trade(_DECL_BASE):
|
|||||||
self.max_rate = max(current_price, self.max_rate or self.open_rate)
|
self.max_rate = max(current_price, self.max_rate or self.open_rate)
|
||||||
self.min_rate = min(current_price, self.min_rate or self.open_rate)
|
self.min_rate = min(current_price, self.min_rate or self.open_rate)
|
||||||
|
|
||||||
|
def _set_new_stoploss(self, new_loss: float, stoploss: float):
|
||||||
|
"""Assign new stop value"""
|
||||||
|
self.stop_loss = new_loss
|
||||||
|
self.stop_loss_pct = -1 * abs(stoploss)
|
||||||
|
self.stoploss_last_update = datetime.utcnow()
|
||||||
|
|
||||||
def adjust_stop_loss(self, current_price: float, stoploss: float,
|
def adjust_stop_loss(self, current_price: float, stoploss: float,
|
||||||
initial: bool = False) -> None:
|
initial: bool = False) -> None:
|
||||||
"""
|
"""
|
||||||
@ -360,19 +366,15 @@ class Trade(_DECL_BASE):
|
|||||||
# no stop loss assigned yet
|
# no stop loss assigned yet
|
||||||
if not self.stop_loss:
|
if not self.stop_loss:
|
||||||
logger.debug(f"{self.pair} - Assigning new stoploss...")
|
logger.debug(f"{self.pair} - Assigning new stoploss...")
|
||||||
self.stop_loss = new_loss
|
self._set_new_stoploss(new_loss, stoploss)
|
||||||
self.stop_loss_pct = -1 * abs(stoploss)
|
|
||||||
self.initial_stop_loss = new_loss
|
self.initial_stop_loss = new_loss
|
||||||
self.initial_stop_loss_pct = -1 * abs(stoploss)
|
self.initial_stop_loss_pct = -1 * abs(stoploss)
|
||||||
self.stoploss_last_update = datetime.utcnow()
|
|
||||||
|
|
||||||
# evaluate if the stop loss needs to be updated
|
# evaluate if the stop loss needs to be updated
|
||||||
else:
|
else:
|
||||||
if new_loss > self.stop_loss: # stop losses only walk up, never down!
|
if new_loss > self.stop_loss: # stop losses only walk up, never down!
|
||||||
logger.debug(f"{self.pair} - Adjusting stoploss...")
|
logger.debug(f"{self.pair} - Adjusting stoploss...")
|
||||||
self.stop_loss = new_loss
|
self._set_new_stoploss(new_loss, stoploss)
|
||||||
self.stop_loss_pct = -1 * abs(stoploss)
|
|
||||||
self.stoploss_last_update = datetime.utcnow()
|
|
||||||
else:
|
else:
|
||||||
logger.debug(f"{self.pair} - Keeping current stoploss...")
|
logger.debug(f"{self.pair} - Keeping current stoploss...")
|
||||||
|
|
||||||
|
@ -89,6 +89,7 @@ class IStrategy(ABC):
|
|||||||
trailing_stop_positive: Optional[float] = None
|
trailing_stop_positive: Optional[float] = None
|
||||||
trailing_stop_positive_offset: float = 0.0
|
trailing_stop_positive_offset: float = 0.0
|
||||||
trailing_only_offset_is_reached = False
|
trailing_only_offset_is_reached = False
|
||||||
|
use_custom_stoploss: bool = False
|
||||||
|
|
||||||
# associated timeframe
|
# associated timeframe
|
||||||
ticker_interval: str # DEPRECATED
|
ticker_interval: str # DEPRECATED
|
||||||
@ -254,6 +255,28 @@ class IStrategy(ABC):
|
|||||||
"""
|
"""
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, current_rate: float,
|
||||||
|
current_profit: float, **kwargs) -> float:
|
||||||
|
"""
|
||||||
|
Custom stoploss logic, returning the new distance relative to current_rate (as ratio).
|
||||||
|
e.g. returning -0.05 would create a stoploss 5% below current_rate.
|
||||||
|
The custom stoploss can never be below self.stoploss, which serves as a hard maximum loss.
|
||||||
|
|
||||||
|
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
|
||||||
|
|
||||||
|
When not implemented by a strategy, returns the initial stoploss value
|
||||||
|
Only called when use_custom_stoploss is set to True.
|
||||||
|
|
||||||
|
:param pair: Pair that's currently analyzed
|
||||||
|
:param trade: trade object.
|
||||||
|
:param current_time: datetime object, containing the current datetime
|
||||||
|
:param current_rate: Rate, calculated based on pricing settings in ask_strategy.
|
||||||
|
:param current_profit: Current profit (as ratio), calculated based on current_rate.
|
||||||
|
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||||
|
:return float: New stoploss value, relative to the currentrate
|
||||||
|
"""
|
||||||
|
return self.stoploss
|
||||||
|
|
||||||
def informative_pairs(self) -> ListPairsWithTimeframes:
|
def informative_pairs(self) -> ListPairsWithTimeframes:
|
||||||
"""
|
"""
|
||||||
Define additional, informative pair/interval combinations to be cached from the exchange.
|
Define additional, informative pair/interval combinations to be cached from the exchange.
|
||||||
@ -531,6 +554,19 @@ class IStrategy(ABC):
|
|||||||
# Initiate stoploss with open_rate. Does nothing if stoploss is already set.
|
# Initiate stoploss with open_rate. Does nothing if stoploss is already set.
|
||||||
trade.adjust_stop_loss(trade.open_rate, stop_loss_value, initial=True)
|
trade.adjust_stop_loss(trade.open_rate, stop_loss_value, initial=True)
|
||||||
|
|
||||||
|
if self.use_custom_stoploss:
|
||||||
|
stop_loss_value = strategy_safe_wrapper(self.custom_stoploss, default_retval=None
|
||||||
|
)(pair=trade.pair, trade=trade,
|
||||||
|
current_time=current_time,
|
||||||
|
current_rate=current_rate,
|
||||||
|
current_profit=current_profit)
|
||||||
|
# Sanity check - error cases will return None
|
||||||
|
if stop_loss_value:
|
||||||
|
# logger.info(f"{trade.pair} {stop_loss_value=} {current_profit=}")
|
||||||
|
trade.adjust_stop_loss(current_rate, stop_loss_value)
|
||||||
|
else:
|
||||||
|
logger.warning("CustomStoploss function did not return valid stoploss")
|
||||||
|
|
||||||
if self.trailing_stop:
|
if self.trailing_stop:
|
||||||
# trailing stoploss handling
|
# trailing stoploss handling
|
||||||
sl_offset = self.trailing_stop_positive_offset
|
sl_offset = self.trailing_stop_positive_offset
|
||||||
|
@ -12,6 +12,30 @@ def bot_loop_start(self, **kwargs) -> None:
|
|||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
use_custom_stoploss = True
|
||||||
|
|
||||||
|
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float,
|
||||||
|
current_profit: float, **kwargs) -> float:
|
||||||
|
"""
|
||||||
|
Custom stoploss logic, returning the new distance relative to current_rate (as ratio).
|
||||||
|
e.g. returning -0.05 would create a stoploss 5% below current_rate.
|
||||||
|
The custom stoploss can never be below self.stoploss, which serves as a hard maximum loss.
|
||||||
|
|
||||||
|
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
|
||||||
|
|
||||||
|
When not implemented by a strategy, returns the initial stoploss value
|
||||||
|
Only called when use_custom_stoploss is set to True.
|
||||||
|
|
||||||
|
:param pair: Pair that's about to be sold.
|
||||||
|
:param trade: trade object.
|
||||||
|
:param current_time: datetime object, containing the current datetime
|
||||||
|
:param current_rate: Rate, calculated based on pricing settings in ask_strategy.
|
||||||
|
:param current_profit: Current profit (as ratio), calculated based on current_rate.
|
||||||
|
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||||
|
:return float: New stoploss value, relative to the currentrate
|
||||||
|
"""
|
||||||
|
return self.stoploss
|
||||||
|
|
||||||
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, **kwargs) -> bool:
|
time_in_force: str, **kwargs) -> bool:
|
||||||
"""
|
"""
|
||||||
@ -45,7 +69,7 @@ def confirm_trade_exit(self, pair: str, trade: 'Trade', order_type: str, amount:
|
|||||||
|
|
||||||
When not implemented by a strategy, returns True (always confirming).
|
When not implemented by a strategy, returns True (always confirming).
|
||||||
|
|
||||||
:param pair: Pair that's about to be sold.
|
:param pair: Pair that's currently analyzed
|
||||||
:param trade: trade object.
|
:param trade: trade object.
|
||||||
:param order_type: Order type (as configured in order_types). usually limit or market.
|
:param order_type: Order type (as configured in order_types). usually limit or market.
|
||||||
:param amount: Amount in quote currency.
|
:param amount: Amount in quote currency.
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
|
from freqtrade.persistence.models import Trade
|
||||||
|
|
||||||
from .strats.default_strategy import DefaultStrategy
|
from .strats.default_strategy import DefaultStrategy
|
||||||
|
|
||||||
|
|
||||||
@ -12,7 +16,7 @@ def test_default_strategy_structure():
|
|||||||
assert hasattr(DefaultStrategy, 'populate_sell_trend')
|
assert hasattr(DefaultStrategy, 'populate_sell_trend')
|
||||||
|
|
||||||
|
|
||||||
def test_default_strategy(result):
|
def test_default_strategy(result, fee):
|
||||||
strategy = DefaultStrategy({})
|
strategy = DefaultStrategy({})
|
||||||
|
|
||||||
metadata = {'pair': 'ETH/BTC'}
|
metadata = {'pair': 'ETH/BTC'}
|
||||||
@ -23,3 +27,18 @@ def test_default_strategy(result):
|
|||||||
assert type(indicators) is DataFrame
|
assert type(indicators) is DataFrame
|
||||||
assert type(strategy.populate_buy_trend(indicators, metadata)) is DataFrame
|
assert type(strategy.populate_buy_trend(indicators, metadata)) is DataFrame
|
||||||
assert type(strategy.populate_sell_trend(indicators, metadata)) is DataFrame
|
assert type(strategy.populate_sell_trend(indicators, metadata)) is DataFrame
|
||||||
|
|
||||||
|
trade = Trade(
|
||||||
|
open_rate=19_000,
|
||||||
|
amount=0.1,
|
||||||
|
pair='ETH/BTC',
|
||||||
|
fee_open=fee.return_value
|
||||||
|
)
|
||||||
|
|
||||||
|
assert strategy.confirm_trade_entry(pair='ETH/BTC', order_type='limit', amount=0.1,
|
||||||
|
rate=20000, time_in_force='gtc') is True
|
||||||
|
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') is True
|
||||||
|
|
||||||
|
assert strategy.custom_stoploss(pair='ETH/BTC', trade=trade, current_time=datetime.now(),
|
||||||
|
current_rate=20_000, current_profit=0.05) == strategy.stoploss
|
||||||
|
@ -13,6 +13,7 @@ from freqtrade.data.history import load_data
|
|||||||
from freqtrade.exceptions import StrategyError
|
from freqtrade.exceptions import StrategyError
|
||||||
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.interface import SellCheckTuple, SellType
|
||||||
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
|
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
|
||||||
from tests.conftest import log_has, log_has_re
|
from tests.conftest import log_has, log_has_re
|
||||||
|
|
||||||
@ -105,9 +106,7 @@ def test_get_signal_old_dataframe(default_conf, mocker, caplog, ohlcv_history):
|
|||||||
assert log_has('Outdated history for pair xyz. Last tick is 16 minutes old', caplog)
|
assert log_has('Outdated history for pair xyz. Last tick is 16 minutes old', caplog)
|
||||||
|
|
||||||
|
|
||||||
def test_assert_df_raise(default_conf, mocker, caplog, ohlcv_history):
|
def test_assert_df_raise(mocker, caplog, ohlcv_history):
|
||||||
# default_conf defines a 5m interval. we check interval * 2 + 5m
|
|
||||||
# this is necessary as the last candle is removed (partial candles) by default
|
|
||||||
ohlcv_history.loc[1, 'date'] = arrow.utcnow().shift(minutes=-16)
|
ohlcv_history.loc[1, 'date'] = arrow.utcnow().shift(minutes=-16)
|
||||||
# Take a copy to correctly modify the call
|
# Take a copy to correctly modify the call
|
||||||
mocked_history = ohlcv_history.copy()
|
mocked_history = ohlcv_history.copy()
|
||||||
@ -127,7 +126,7 @@ def test_assert_df_raise(default_conf, mocker, caplog, ohlcv_history):
|
|||||||
caplog)
|
caplog)
|
||||||
|
|
||||||
|
|
||||||
def test_assert_df(default_conf, mocker, ohlcv_history, caplog):
|
def test_assert_df(ohlcv_history, caplog):
|
||||||
df_len = len(ohlcv_history) - 1
|
df_len = len(ohlcv_history) - 1
|
||||||
# Ensure it's running when passed correctly
|
# Ensure it's running when passed correctly
|
||||||
_STRATEGY.assert_df(ohlcv_history, len(ohlcv_history),
|
_STRATEGY.assert_df(ohlcv_history, len(ohlcv_history),
|
||||||
@ -288,6 +287,77 @@ def test_min_roi_reached3(default_conf, fee) -> None:
|
|||||||
assert strategy.min_roi_reached(trade, 0.31, arrow.utcnow().shift(minutes=-2).datetime)
|
assert strategy.min_roi_reached(trade, 0.31, arrow.utcnow().shift(minutes=-2).datetime)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
'profit,adjusted,expected,trailing,custom,profit2,adjusted2,expected2,custom_stop', [
|
||||||
|
# Profit, adjusted stoploss(absolute), profit for 2nd call, enable trailing,
|
||||||
|
# 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, SellType.NONE, False, False, -0.2, 0.9, SellType.STOP_LOSS, None),
|
||||||
|
(0.2, 1.14, SellType.NONE, True, False, 0.05, 1.14, SellType.TRAILING_STOP_LOSS, None),
|
||||||
|
(0.01, 0.96, SellType.NONE, True, False, 0.05, 1, SellType.NONE, None),
|
||||||
|
(0.05, 1, SellType.NONE, True, False, -0.01, 1, SellType.TRAILING_STOP_LOSS, None),
|
||||||
|
# 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, SellType.NONE, False, True, -0.06, 0.95, SellType.TRAILING_STOP_LOSS, None),
|
||||||
|
(0.05, 1, SellType.NONE, False, True, -0.06, 1, SellType.TRAILING_STOP_LOSS,
|
||||||
|
lambda **kwargs: -0.05),
|
||||||
|
(0.05, 1, SellType.NONE, False, True, 0.09, 1.04, SellType.NONE,
|
||||||
|
lambda **kwargs: -0.05),
|
||||||
|
(0.05, 0.95, SellType.NONE, False, True, 0.09, 0.98, SellType.NONE,
|
||||||
|
lambda current_profit, **kwargs: -0.1 if current_profit < 0.6 else -(current_profit * 2)),
|
||||||
|
# Error case - static stoploss in place
|
||||||
|
(0.05, 0.9, SellType.NONE, False, True, 0.09, 0.9, SellType.NONE,
|
||||||
|
lambda **kwargs: None),
|
||||||
|
])
|
||||||
|
def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, trailing, custom,
|
||||||
|
profit2, adjusted2, expected2, custom_stop) -> None:
|
||||||
|
|
||||||
|
default_conf.update({'strategy': 'DefaultStrategy'})
|
||||||
|
|
||||||
|
strategy = StrategyResolver.load_strategy(default_conf)
|
||||||
|
trade = Trade(
|
||||||
|
pair='ETH/BTC',
|
||||||
|
stake_amount=0.01,
|
||||||
|
amount=1,
|
||||||
|
open_date=arrow.utcnow().shift(hours=-1).datetime,
|
||||||
|
fee_open=fee.return_value,
|
||||||
|
fee_close=fee.return_value,
|
||||||
|
exchange='bittrex',
|
||||||
|
open_rate=1,
|
||||||
|
)
|
||||||
|
trade.adjust_min_max_rates(trade.open_rate)
|
||||||
|
strategy.trailing_stop = trailing
|
||||||
|
strategy.trailing_stop_positive = -0.05
|
||||||
|
strategy.use_custom_stoploss = custom
|
||||||
|
original_stopvalue = strategy.custom_stoploss
|
||||||
|
if custom_stop:
|
||||||
|
strategy.custom_stoploss = custom_stop
|
||||||
|
|
||||||
|
now = arrow.utcnow().datetime
|
||||||
|
sl_flag = strategy.stop_loss_reached(current_rate=trade.open_rate * (1 + profit), trade=trade,
|
||||||
|
current_time=now, current_profit=profit,
|
||||||
|
force_stoploss=0, high=None)
|
||||||
|
assert isinstance(sl_flag, SellCheckTuple)
|
||||||
|
assert sl_flag.sell_type == expected
|
||||||
|
if expected == SellType.NONE:
|
||||||
|
assert sl_flag.sell_flag is False
|
||||||
|
else:
|
||||||
|
assert sl_flag.sell_flag is True
|
||||||
|
assert round(trade.stop_loss, 2) == adjusted
|
||||||
|
|
||||||
|
sl_flag = strategy.stop_loss_reached(current_rate=trade.open_rate * (1 + profit2), trade=trade,
|
||||||
|
current_time=now, current_profit=profit2,
|
||||||
|
force_stoploss=0, high=None)
|
||||||
|
assert sl_flag.sell_type == expected2
|
||||||
|
if expected2 == SellType.NONE:
|
||||||
|
assert sl_flag.sell_flag is False
|
||||||
|
else:
|
||||||
|
assert sl_flag.sell_flag is True
|
||||||
|
assert round(trade.stop_loss, 2) == adjusted2
|
||||||
|
|
||||||
|
strategy.custom_stoploss = original_stopvalue
|
||||||
|
|
||||||
|
|
||||||
def test_analyze_ticker_default(ohlcv_history, mocker, caplog) -> None:
|
def test_analyze_ticker_default(ohlcv_history, mocker, caplog) -> None:
|
||||||
caplog.set_level(logging.DEBUG)
|
caplog.set_level(logging.DEBUG)
|
||||||
ind_mock = MagicMock(side_effect=lambda x, meta: x)
|
ind_mock = MagicMock(side_effect=lambda x, meta: x)
|
||||||
|
Loading…
Reference in New Issue
Block a user