Merge pull request #4750 from rokups/rk/custom_sell
Add IStrategy.custom_sell method which allows per-trade sell signal evaluation
This commit is contained in:
commit
a3d2e68312
@ -42,32 +42,38 @@ class AwesomeStrategy(IStrategy):
|
|||||||
|
|
||||||
***
|
***
|
||||||
|
|
||||||
### Storing custom information using DatetimeIndex from `dataframe`
|
## Custom sell signal
|
||||||
|
|
||||||
Imagine you need to store an indicator like `ATR` or `RSI` into `custom_info`. To use this in a meaningful way, you will not only need the raw data of the indicator, but probably also need to keep the right timestamps.
|
It is possible to define custom sell signals. This is very useful when we need to customize sell conditions for each individual trade, or if you need the trade profit to take the sell decision.
|
||||||
|
|
||||||
|
An example of how we can use different indicators depending on the current profit and also sell trades that were open longer than 1 day:
|
||||||
|
|
||||||
|
``` python
|
||||||
|
from freqtrade.strategy import IStrategy, timeframe_to_prev_date
|
||||||
|
|
||||||
```python
|
|
||||||
import talib.abstract as ta
|
|
||||||
class AwesomeStrategy(IStrategy):
|
class AwesomeStrategy(IStrategy):
|
||||||
# Create custom dictionary
|
def custom_sell(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float,
|
||||||
custom_info = {}
|
current_profit: float, dataframe: DataFrame, **kwargs):
|
||||||
|
trade_open_date = timeframe_to_prev_date(self.timeframe, trade.open_date_utc)
|
||||||
|
trade_row = dataframe.loc[dataframe['date'] == trade_open_date].squeeze()
|
||||||
|
|
||||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
# Sell when price falls below value in stoploss column of taken buy signal.
|
||||||
# using "ATR" here as example
|
# above 20% profit, sell when rsi < 80
|
||||||
dataframe['atr'] = ta.ATR(dataframe)
|
if current_profit > 0.2:
|
||||||
if self.dp.runmode.value in ('backtest', 'hyperopt'):
|
if trade_row['rsi'] < 80:
|
||||||
# add indicator mapped to correct DatetimeIndex to custom_info
|
return 'rsi_below_80'
|
||||||
self.custom_info[metadata['pair']] = dataframe[['date', 'atr']].set_index('date')
|
|
||||||
return dataframe
|
# Between 2% and 10%, sell if EMA-long above EMA-short
|
||||||
|
if 0.02 < current_profit < 0.1:
|
||||||
|
if trade_row['emalong'] > trade_row['emashort']:
|
||||||
|
return 'ema_long_below_80'
|
||||||
|
|
||||||
|
# Sell any positions at a loss if they are held for more than two days.
|
||||||
|
if current_profit < 0.0 and (current_time - trade.open_date_utc).days >= 1:
|
||||||
|
return 'unclog'
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! Warning
|
See [Custom stoploss using an indicator from dataframe example](#custom-stoploss-using-an-indicator-from-dataframe-example) for explanation on how to use `dataframe` parameter.
|
||||||
The data is not persisted after a bot-restart (or config-reload). Also, the amount of data should be kept smallish (no DataFrames and such), otherwise the bot will start to consume a lot of memory and eventually run out of memory and crash.
|
|
||||||
|
|
||||||
!!! Note
|
|
||||||
If the data is pair-specific, make sure to use pair as one of the keys in the dictionary.
|
|
||||||
|
|
||||||
See `custom_stoploss` examples below on how to access the saved dataframe columns
|
|
||||||
|
|
||||||
## Custom stoploss
|
## Custom stoploss
|
||||||
|
|
||||||
@ -93,7 +99,8 @@ class AwesomeStrategy(IStrategy):
|
|||||||
use_custom_stoploss = True
|
use_custom_stoploss = True
|
||||||
|
|
||||||
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
||||||
current_rate: float, current_profit: float, **kwargs) -> float:
|
current_rate: float, current_profit: float, dataframe: DataFrame,
|
||||||
|
**kwargs) -> float:
|
||||||
"""
|
"""
|
||||||
Custom stoploss logic, returning the new distance relative to current_rate (as ratio).
|
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.
|
e.g. returning -0.05 would create a stoploss 5% below current_rate.
|
||||||
@ -110,7 +117,7 @@ class AwesomeStrategy(IStrategy):
|
|||||||
:param current_rate: Rate, calculated based on pricing settings in ask_strategy.
|
: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 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.
|
: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 float: New stoploss value, relative to the current rate
|
||||||
"""
|
"""
|
||||||
return -0.04
|
return -0.04
|
||||||
```
|
```
|
||||||
@ -143,7 +150,8 @@ class AwesomeStrategy(IStrategy):
|
|||||||
use_custom_stoploss = True
|
use_custom_stoploss = True
|
||||||
|
|
||||||
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
||||||
current_rate: float, current_profit: float, **kwargs) -> float:
|
current_rate: float, current_profit: float, dataframe: DataFrame,
|
||||||
|
**kwargs) -> float:
|
||||||
|
|
||||||
# Make sure you have the longest interval first - these conditions are evaluated from top to bottom.
|
# 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_utc:
|
if current_time - timedelta(minutes=120) > trade.open_date_utc:
|
||||||
@ -169,7 +177,8 @@ class AwesomeStrategy(IStrategy):
|
|||||||
use_custom_stoploss = True
|
use_custom_stoploss = True
|
||||||
|
|
||||||
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
||||||
current_rate: float, current_profit: float, **kwargs) -> float:
|
current_rate: float, current_profit: float, dataframe: DataFrame,
|
||||||
|
**kwargs) -> float:
|
||||||
|
|
||||||
if pair in ('ETH/BTC', 'XRP/BTC'):
|
if pair in ('ETH/BTC', 'XRP/BTC'):
|
||||||
return -0.10
|
return -0.10
|
||||||
@ -195,7 +204,8 @@ class AwesomeStrategy(IStrategy):
|
|||||||
use_custom_stoploss = True
|
use_custom_stoploss = True
|
||||||
|
|
||||||
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
||||||
current_rate: float, current_profit: float, **kwargs) -> float:
|
current_rate: float, current_profit: float, dataframe: DataFrame,
|
||||||
|
**kwargs) -> float:
|
||||||
|
|
||||||
if current_profit < 0.04:
|
if current_profit < 0.04:
|
||||||
return -1 # return a value bigger than the inital stoploss to keep using the inital stoploss
|
return -1 # return a value bigger than the inital stoploss to keep using the inital stoploss
|
||||||
@ -222,7 +232,6 @@ Instead of continuously trailing behind the current price, this example sets fix
|
|||||||
* Once profit is > 25% - set stoploss to 15% above open price.
|
* Once profit is > 25% - set stoploss to 15% above open price.
|
||||||
* Once profit is > 40% - set stoploss to 25% above open price.
|
* Once profit is > 40% - set stoploss to 25% above open price.
|
||||||
|
|
||||||
|
|
||||||
``` python
|
``` python
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
@ -235,7 +244,8 @@ class AwesomeStrategy(IStrategy):
|
|||||||
use_custom_stoploss = True
|
use_custom_stoploss = True
|
||||||
|
|
||||||
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
||||||
current_rate: float, current_profit: float, **kwargs) -> float:
|
current_rate: float, current_profit: float, dataframe: DataFrame,
|
||||||
|
**kwargs) -> float:
|
||||||
|
|
||||||
# evaluate highest to lowest, so that highest possible stop is used
|
# evaluate highest to lowest, so that highest possible stop is used
|
||||||
if current_profit > 0.40:
|
if current_profit > 0.40:
|
||||||
@ -248,18 +258,25 @@ class AwesomeStrategy(IStrategy):
|
|||||||
# return maximum stoploss value, keeping current stoploss price unchanged
|
# return maximum stoploss value, keeping current stoploss price unchanged
|
||||||
return 1
|
return 1
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Custom stoploss using an indicator from dataframe example
|
#### Custom stoploss using an indicator from dataframe example
|
||||||
|
|
||||||
Imagine you want to use `custom_stoploss()` to use a trailing indicator like e.g. "ATR"
|
Imagine you want to use `custom_stoploss()` to use a trailing indicator like e.g. "ATR"
|
||||||
|
|
||||||
See: "Storing custom information using DatetimeIndex from `dataframe`" example above) on how to store the indicator into `custom_info`
|
|
||||||
|
|
||||||
!!! Warning
|
!!! Warning
|
||||||
only use .iat[-1] in live mode, not in backtesting/hyperopt
|
Only use `dataframe` values up until and including `current_time` value. Reading past
|
||||||
otherwise you will look into the future
|
`current_time` you will look into the future, which will produce incorrect backtesting results
|
||||||
|
and throw an exception in dry/live runs.
|
||||||
see [Common mistakes when developing strategies](strategy-customization.md#common-mistakes-when-developing-strategies) for more info.
|
see [Common mistakes when developing strategies](strategy-customization.md#common-mistakes-when-developing-strategies) for more info.
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
`dataframe['date']` contains the candle's open date. During dry/live runs `current_time` and
|
||||||
|
`trade.open_date_utc` will not match the candle date precisely and using them directly will throw
|
||||||
|
an error. Use `date = timeframe_to_prev_date(self.timeframe, date)` to round a date to the candle's open date
|
||||||
|
before using it to access `dataframe`.
|
||||||
|
|
||||||
``` python
|
``` python
|
||||||
|
from freqtrade.exchange import timeframe_to_prev_date
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
from freqtrade.state import RunMode
|
from freqtrade.state import RunMode
|
||||||
|
|
||||||
@ -270,28 +287,20 @@ class AwesomeStrategy(IStrategy):
|
|||||||
use_custom_stoploss = True
|
use_custom_stoploss = True
|
||||||
|
|
||||||
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
||||||
current_rate: float, current_profit: float, **kwargs) -> float:
|
current_rate: float, current_profit: float, dataframe: DataFrame,
|
||||||
|
**kwargs) -> float:
|
||||||
|
|
||||||
|
# Default return value
|
||||||
result = 1
|
result = 1
|
||||||
if self.custom_info and pair in self.custom_info and trade:
|
if trade:
|
||||||
# using current_time directly (like below) will only work in backtesting.
|
# Using current_time directly would only work in backtesting. Live/dry runs need time to
|
||||||
# so check "runmode" to make sure that it's only used in backtesting/hyperopt
|
# be rounded to previous candle to be used as dataframe index. Rounding must also be
|
||||||
if self.dp and self.dp.runmode.value in ('backtest', 'hyperopt'):
|
# applied to `trade.open_date(_utc)` if it is used for `dataframe` indexing.
|
||||||
relative_sl = self.custom_info[pair].loc[current_time]['atr']
|
current_time = timeframe_to_prev_date(self.timeframe, current_time)
|
||||||
# in live / dry-run, it'll be really the current time
|
current_row = dataframe.loc[dataframe['date'] == current_time].squeeze()
|
||||||
else:
|
if 'atr' in current_row:
|
||||||
# but we can just use the last entry from an already analyzed dataframe instead
|
|
||||||
dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=pair,
|
|
||||||
timeframe=self.timeframe)
|
|
||||||
# WARNING
|
|
||||||
# only use .iat[-1] in live mode, not in backtesting/hyperopt
|
|
||||||
# otherwise you will look into the future
|
|
||||||
# see: https://www.freqtrade.io/en/latest/strategy-customization/#common-mistakes-when-developing-strategies
|
|
||||||
relative_sl = dataframe['atr'].iat[-1]
|
|
||||||
|
|
||||||
if (relative_sl is not None):
|
|
||||||
# new stoploss relative to current_rate
|
# new stoploss relative to current_rate
|
||||||
new_stoploss = (current_rate-relative_sl)/current_rate
|
new_stoploss = (current_rate - current_row['atr']) / current_rate
|
||||||
# turn into relative negative offset required by `custom_stoploss` return implementation
|
# turn into relative negative offset required by `custom_stoploss` return implementation
|
||||||
result = new_stoploss - 1
|
result = new_stoploss - 1
|
||||||
|
|
||||||
|
@ -631,9 +631,10 @@ Stoploss values returned from `custom_stoploss` must specify a percentage relati
|
|||||||
use_custom_stoploss = True
|
use_custom_stoploss = True
|
||||||
|
|
||||||
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
||||||
current_rate: float, current_profit: float, **kwargs) -> float:
|
current_rate: float, current_profit: float, dataframe: DataFrame,
|
||||||
|
**kwargs) -> float:
|
||||||
|
|
||||||
# once the profit has risin above 10%, keep the stoploss at 7% above the open price
|
# once the profit has risen above 10%, keep the stoploss at 7% above the open price
|
||||||
if current_profit > 0.10:
|
if current_profit > 0.10:
|
||||||
return stoploss_from_open(0.07, current_profit)
|
return stoploss_from_open(0.07, current_profit)
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@ from typing import Any, Dict, List, Optional
|
|||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
from cachetools import TTLCache
|
from cachetools import TTLCache
|
||||||
|
from pandas import DataFrame
|
||||||
|
|
||||||
from freqtrade import __version__, constants
|
from freqtrade import __version__, constants
|
||||||
from freqtrade.configuration import validate_config_consistency
|
from freqtrade.configuration import validate_config_consistency
|
||||||
@ -28,7 +29,7 @@ from freqtrade.plugins.protectionmanager import ProtectionManager
|
|||||||
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
||||||
from freqtrade.rpc import RPCManager, RPCMessageType
|
from freqtrade.rpc import RPCManager, RPCMessageType
|
||||||
from freqtrade.state import State
|
from freqtrade.state import State
|
||||||
from freqtrade.strategy.interface import IStrategy, SellType
|
from freqtrade.strategy.interface import IStrategy, SellCheckTuple, SellType
|
||||||
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
|
||||||
|
|
||||||
@ -783,10 +784,10 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
|
|
||||||
config_ask_strategy = self.config.get('ask_strategy', {})
|
config_ask_strategy = self.config.get('ask_strategy', {})
|
||||||
|
|
||||||
|
analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair,
|
||||||
|
self.strategy.timeframe)
|
||||||
if (config_ask_strategy.get('use_sell_signal', True) or
|
if (config_ask_strategy.get('use_sell_signal', True) or
|
||||||
config_ask_strategy.get('ignore_roi_if_buy_signal', False)):
|
config_ask_strategy.get('ignore_roi_if_buy_signal', False)):
|
||||||
analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair,
|
|
||||||
self.strategy.timeframe)
|
|
||||||
|
|
||||||
(buy, sell) = self.strategy.get_signal(trade.pair, self.strategy.timeframe, analyzed_df)
|
(buy, sell) = self.strategy.get_signal(trade.pair, self.strategy.timeframe, analyzed_df)
|
||||||
|
|
||||||
@ -813,13 +814,13 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
# resulting in outdated RPC messages
|
# resulting in outdated RPC messages
|
||||||
self._sell_rate_cache[trade.pair] = sell_rate
|
self._sell_rate_cache[trade.pair] = sell_rate
|
||||||
|
|
||||||
if self._check_and_execute_sell(trade, sell_rate, buy, sell):
|
if self._check_and_execute_sell(analyzed_df, trade, sell_rate, buy, sell):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
else:
|
else:
|
||||||
logger.debug('checking sell')
|
logger.debug('checking sell')
|
||||||
sell_rate = self.get_sell_rate(trade.pair, True)
|
sell_rate = self.get_sell_rate(trade.pair, True)
|
||||||
if self._check_and_execute_sell(trade, sell_rate, buy, sell):
|
if self._check_and_execute_sell(analyzed_df, trade, sell_rate, buy, sell):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
logger.debug('Found no sell signal for %s.', trade)
|
logger.debug('Found no sell signal for %s.', trade)
|
||||||
@ -850,7 +851,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('Selling the trade forcefully')
|
logger.warning('Selling the trade forcefully')
|
||||||
self.execute_sell(trade, trade.stop_loss, sell_reason=SellType.EMERGENCY_SELL)
|
self.execute_sell(trade, trade.stop_loss, sell_reason=SellCheckTuple(
|
||||||
|
sell_type=SellType.EMERGENCY_SELL))
|
||||||
|
|
||||||
except ExchangeError:
|
except ExchangeError:
|
||||||
trade.stoploss_order_id = None
|
trade.stoploss_order_id = None
|
||||||
@ -949,19 +951,19 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
logger.warning(f"Could not create trailing stoploss order "
|
logger.warning(f"Could not create trailing stoploss order "
|
||||||
f"for pair {trade.pair}.")
|
f"for pair {trade.pair}.")
|
||||||
|
|
||||||
def _check_and_execute_sell(self, trade: Trade, sell_rate: float,
|
def _check_and_execute_sell(self, dataframe: DataFrame, trade: Trade, sell_rate: float,
|
||||||
buy: bool, sell: bool) -> bool:
|
buy: bool, sell: bool) -> bool:
|
||||||
"""
|
"""
|
||||||
Check and execute sell
|
Check and execute sell
|
||||||
"""
|
"""
|
||||||
should_sell = self.strategy.should_sell(
|
should_sell = self.strategy.should_sell(
|
||||||
trade, sell_rate, datetime.now(timezone.utc), buy, sell,
|
dataframe, trade, sell_rate, datetime.now(timezone.utc), buy, sell,
|
||||||
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_sell.sell_flag:
|
if should_sell.sell_flag:
|
||||||
logger.info(f'Executing Sell for {trade.pair}. Reason: {should_sell.sell_type}')
|
logger.info(f'Executing Sell for {trade.pair}. Reason: {should_sell.sell_type}')
|
||||||
self.execute_sell(trade, sell_rate, should_sell.sell_type)
|
self.execute_sell(trade, sell_rate, should_sell)
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -1150,16 +1152,16 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
raise DependencyException(
|
raise DependencyException(
|
||||||
f"Not enough amount to sell. Trade-amount: {amount}, Wallet: {wallet_amount}")
|
f"Not enough amount to sell. Trade-amount: {amount}, Wallet: {wallet_amount}")
|
||||||
|
|
||||||
def execute_sell(self, trade: Trade, limit: float, sell_reason: SellType) -> bool:
|
def execute_sell(self, trade: Trade, limit: float, sell_reason: SellCheckTuple) -> bool:
|
||||||
"""
|
"""
|
||||||
Executes a limit sell for the given trade and limit
|
Executes a limit sell 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 sellreason: Reason the sell was triggered
|
:param sell_reason: Reason the sell was triggered
|
||||||
:return: True if it succeeds (supported) False (not supported)
|
:return: True if it succeeds (supported) False (not supported)
|
||||||
"""
|
"""
|
||||||
sell_type = 'sell'
|
sell_type = 'sell'
|
||||||
if sell_reason in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS):
|
if sell_reason.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS):
|
||||||
sell_type = 'stoploss'
|
sell_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,
|
||||||
@ -1176,10 +1178,10 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}")
|
logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}")
|
||||||
|
|
||||||
order_type = self.strategy.order_types[sell_type]
|
order_type = self.strategy.order_types[sell_type]
|
||||||
if sell_reason == SellType.EMERGENCY_SELL:
|
if sell_reason.sell_type == SellType.EMERGENCY_SELL:
|
||||||
# Emergency sells (default to market!)
|
# Emergency sells (default to market!)
|
||||||
order_type = self.strategy.order_types.get("emergencysell", "market")
|
order_type = self.strategy.order_types.get("emergencysell", "market")
|
||||||
if sell_reason == SellType.FORCE_SELL:
|
if sell_reason.sell_type == SellType.FORCE_SELL:
|
||||||
# Force sells (default to the sell_type defined in the strategy,
|
# Force sells (default to the sell_type defined in the strategy,
|
||||||
# but we allow this value to be changed)
|
# but we allow this value to be changed)
|
||||||
order_type = self.strategy.order_types.get("forcesell", order_type)
|
order_type = self.strategy.order_types.get("forcesell", order_type)
|
||||||
@ -1190,7 +1192,7 @@ 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,
|
time_in_force=time_in_force,
|
||||||
sell_reason=sell_reason.value):
|
sell_reason=sell_reason.sell_reason):
|
||||||
logger.info(f"User requested abortion of selling {trade.pair}")
|
logger.info(f"User requested abortion of selling {trade.pair}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -1213,7 +1215,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 = sell_reason.value
|
trade.sell_reason = sell_reason.sell_reason
|
||||||
# In case of market sell orders the order can be closed immediately
|
# In case of market sell orders the order can be closed immediately
|
||||||
if order.get('status', 'unknown') == 'closed':
|
if order.get('status', 'unknown') == 'closed':
|
||||||
self.update_trade_state(trade, trade.open_order_id, order)
|
self.update_trade_state(trade, trade.open_order_id, order)
|
||||||
|
@ -247,15 +247,17 @@ class Backtesting:
|
|||||||
else:
|
else:
|
||||||
return sell_row[OPEN_IDX]
|
return sell_row[OPEN_IDX]
|
||||||
|
|
||||||
def _get_sell_trade_entry(self, trade: LocalTrade, sell_row: Tuple) -> Optional[LocalTrade]:
|
def _get_sell_trade_entry(self, dataframe: DataFrame, trade: LocalTrade,
|
||||||
|
sell_row: Tuple) -> Optional[LocalTrade]:
|
||||||
|
|
||||||
sell = self.strategy.should_sell(trade, sell_row[OPEN_IDX], # type: ignore
|
sell = self.strategy.should_sell(dataframe, trade, sell_row[OPEN_IDX], # type: ignore
|
||||||
sell_row[DATE_IDX], sell_row[BUY_IDX], sell_row[SELL_IDX],
|
sell_row[DATE_IDX].to_pydatetime(), sell_row[BUY_IDX],
|
||||||
|
sell_row[SELL_IDX],
|
||||||
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.sell_flag:
|
||||||
trade.close_date = sell_row[DATE_IDX]
|
trade.close_date = sell_row[DATE_IDX].to_pydatetime()
|
||||||
trade.sell_reason = sell.sell_type.value
|
trade.sell_reason = sell.sell_reason
|
||||||
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)
|
||||||
closerate = self._get_close_rate(sell_row, trade, sell, trade_dur)
|
closerate = self._get_close_rate(sell_row, trade, sell, trade_dur)
|
||||||
|
|
||||||
@ -265,7 +267,7 @@ 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_type.value):
|
sell_reason=sell.sell_reason):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
trade.close(closerate, show_msg=False)
|
trade.close(closerate, show_msg=False)
|
||||||
@ -293,7 +295,7 @@ class Backtesting:
|
|||||||
trade = LocalTrade(
|
trade = LocalTrade(
|
||||||
pair=pair,
|
pair=pair,
|
||||||
open_rate=row[OPEN_IDX],
|
open_rate=row[OPEN_IDX],
|
||||||
open_date=row[DATE_IDX],
|
open_date=row[DATE_IDX].to_pydatetime(),
|
||||||
stake_amount=stake_amount,
|
stake_amount=stake_amount,
|
||||||
amount=round(stake_amount / row[OPEN_IDX], 8),
|
amount=round(stake_amount / row[OPEN_IDX], 8),
|
||||||
fee_open=self.fee,
|
fee_open=self.fee,
|
||||||
@ -315,7 +317,7 @@ class Backtesting:
|
|||||||
for trade in open_trades[pair]:
|
for trade in open_trades[pair]:
|
||||||
sell_row = data[pair][-1]
|
sell_row = data[pair][-1]
|
||||||
|
|
||||||
trade.close_date = sell_row[DATE_IDX]
|
trade.close_date = sell_row[DATE_IDX].to_pydatetime()
|
||||||
trade.sell_reason = SellType.FORCE_SELL.value
|
trade.sell_reason = SellType.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)
|
||||||
@ -396,7 +398,7 @@ class Backtesting:
|
|||||||
|
|
||||||
for trade in open_trades[pair]:
|
for trade in open_trades[pair]:
|
||||||
# also check the buying candle for sell conditions.
|
# also check the buying candle for sell conditions.
|
||||||
trade_entry = self._get_sell_trade_entry(trade, row)
|
trade_entry = self._get_sell_trade_entry(processed[pair], trade, row)
|
||||||
# Sell occured
|
# Sell occured
|
||||||
if trade_entry:
|
if trade_entry:
|
||||||
# logger.debug(f"{pair} - Backtesting sell {trade}")
|
# logger.debug(f"{pair} - Backtesting sell {trade}")
|
||||||
|
@ -24,7 +24,7 @@ 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.state import State
|
from freqtrade.state import State
|
||||||
from freqtrade.strategy.interface import SellType
|
from freqtrade.strategy.interface import SellCheckTuple, SellType
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -554,7 +554,8 @@ class RPC:
|
|||||||
if not fully_canceled:
|
if not fully_canceled:
|
||||||
# Get current rate and execute sell
|
# Get current rate and execute sell
|
||||||
current_rate = self._freqtrade.get_sell_rate(trade.pair, False)
|
current_rate = self._freqtrade.get_sell_rate(trade.pair, False)
|
||||||
self._freqtrade.execute_sell(trade, current_rate, SellType.FORCE_SELL)
|
sell_reason = SellCheckTuple(sell_type=SellType.FORCE_SELL)
|
||||||
|
self._freqtrade.execute_sell(trade, current_rate, sell_reason)
|
||||||
# ---- EOF def _exec_forcesell ----
|
# ---- EOF def _exec_forcesell ----
|
||||||
|
|
||||||
if self._freqtrade.state != State.RUNNING:
|
if self._freqtrade.state != State.RUNNING:
|
||||||
|
@ -7,7 +7,7 @@ import warnings
|
|||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Dict, List, NamedTuple, Optional, Tuple
|
from typing import Dict, List, Optional, Tuple, Union
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
@ -24,6 +24,7 @@ from freqtrade.wallets import Wallets
|
|||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
CUSTOM_SELL_MAX_LENGTH = 64
|
||||||
|
|
||||||
|
|
||||||
class SignalType(Enum):
|
class SignalType(Enum):
|
||||||
@ -45,6 +46,7 @@ class SellType(Enum):
|
|||||||
SELL_SIGNAL = "sell_signal"
|
SELL_SIGNAL = "sell_signal"
|
||||||
FORCE_SELL = "force_sell"
|
FORCE_SELL = "force_sell"
|
||||||
EMERGENCY_SELL = "emergency_sell"
|
EMERGENCY_SELL = "emergency_sell"
|
||||||
|
CUSTOM_SELL = "custom_sell"
|
||||||
NONE = ""
|
NONE = ""
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
@ -52,12 +54,20 @@ class SellType(Enum):
|
|||||||
return self.value
|
return self.value
|
||||||
|
|
||||||
|
|
||||||
class SellCheckTuple(NamedTuple):
|
class SellCheckTuple(object):
|
||||||
"""
|
"""
|
||||||
NamedTuple for Sell type + reason
|
NamedTuple for Sell type + reason
|
||||||
"""
|
"""
|
||||||
sell_flag: bool
|
|
||||||
sell_type: SellType
|
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):
|
||||||
@ -264,7 +274,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, current_rate: float,
|
def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, current_rate: float,
|
||||||
current_profit: float, **kwargs) -> float:
|
current_profit: float, dataframe: DataFrame, **kwargs) -> float:
|
||||||
"""
|
"""
|
||||||
Custom stoploss logic, returning the new distance relative to current_rate (as ratio).
|
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.
|
e.g. returning -0.05 would create a stoploss 5% below current_rate.
|
||||||
@ -280,11 +290,37 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
:param current_time: datetime object, containing the current datetime
|
:param current_time: datetime object, containing the current datetime
|
||||||
:param current_rate: Rate, calculated based on pricing settings in ask_strategy.
|
: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 current_profit: Current profit (as ratio), calculated based on current_rate.
|
||||||
|
:param dataframe: Analyzed dataframe for this pair. Can contain future data in backtesting.
|
||||||
: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 float: New stoploss value, relative to the currentrate
|
:return float: New stoploss value, relative to the currentrate
|
||||||
"""
|
"""
|
||||||
return self.stoploss
|
return self.stoploss
|
||||||
|
|
||||||
|
def custom_sell(self, pair: str, trade: Trade, current_time: datetime, current_rate: float,
|
||||||
|
current_profit: float, dataframe: DataFrame,
|
||||||
|
**kwargs) -> Optional[Union[str, bool]]:
|
||||||
|
"""
|
||||||
|
Custom sell signal logic indicating that specified position should be sold. Returning a
|
||||||
|
string or True from this method is equal to setting sell signal on a candle at specified
|
||||||
|
time. This method is not called when sell signal is set.
|
||||||
|
|
||||||
|
This method should be overridden to create sell signals that depend on trade parameters. For
|
||||||
|
example you could implement a stoploss relative to candle when trade was opened, or a custom
|
||||||
|
1:2 risk-reward ROI.
|
||||||
|
|
||||||
|
Custom sell reason max length is 64. Exceeding this limit will raise OperationalException.
|
||||||
|
|
||||||
|
: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: To execute sell, return a string with custom sell reason or True. Otherwise return
|
||||||
|
None or False.
|
||||||
|
"""
|
||||||
|
return None
|
||||||
|
|
||||||
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.
|
||||||
@ -500,8 +536,8 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def should_sell(self, trade: Trade, rate: float, date: datetime, buy: bool,
|
def should_sell(self, dataframe: DataFrame, trade: Trade, rate: float, date: datetime,
|
||||||
sell: bool, low: float = None, high: float = None,
|
buy: bool, sell: bool, low: float = None, high: float = None,
|
||||||
force_stoploss: float = 0) -> SellCheckTuple:
|
force_stoploss: float = 0) -> SellCheckTuple:
|
||||||
"""
|
"""
|
||||||
This function evaluates if one of the conditions required to trigger a sell
|
This function evaluates if one of the conditions required to trigger a sell
|
||||||
@ -517,8 +553,9 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
|
|
||||||
trade.adjust_min_max_rates(high or current_rate)
|
trade.adjust_min_max_rates(high or current_rate)
|
||||||
|
|
||||||
stoplossflag = self.stop_loss_reached(current_rate=current_rate, trade=trade,
|
stoplossflag = self.stop_loss_reached(dataframe=dataframe, current_rate=current_rate,
|
||||||
current_time=date, current_profit=current_profit,
|
trade=trade, current_time=date,
|
||||||
|
current_profit=current_profit,
|
||||||
force_stoploss=force_stoploss, high=high)
|
force_stoploss=force_stoploss, high=high)
|
||||||
|
|
||||||
# Set current rate to high for backtesting sell
|
# Set current rate to high for backtesting sell
|
||||||
@ -531,12 +568,29 @@ 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=date))
|
current_time=date))
|
||||||
|
|
||||||
|
sell_signal = SellType.NONE
|
||||||
|
custom_reason = ''
|
||||||
if (ask_strategy.get('sell_profit_only', False)
|
if (ask_strategy.get('sell_profit_only', False)
|
||||||
and current_profit <= ask_strategy.get('sell_profit_offset', 0)):
|
and current_profit <= ask_strategy.get('sell_profit_offset', 0)):
|
||||||
# sell_profit_only and profit doesn't reach the offset - ignore sell signal
|
# sell_profit_only and profit doesn't reach the offset - ignore sell signal
|
||||||
sell_signal = False
|
pass
|
||||||
else:
|
elif ask_strategy.get('use_sell_signal', True) and not buy:
|
||||||
sell_signal = sell and not buy and ask_strategy.get('use_sell_signal', True)
|
if sell:
|
||||||
|
sell_signal = SellType.SELL_SIGNAL
|
||||||
|
else:
|
||||||
|
custom_reason = strategy_safe_wrapper(self.custom_sell, default_retval=False)(
|
||||||
|
pair=trade.pair, trade=trade, current_time=date, current_rate=current_rate,
|
||||||
|
current_profit=current_profit, dataframe=dataframe)
|
||||||
|
if custom_reason:
|
||||||
|
sell_signal = SellType.CUSTOM_SELL
|
||||||
|
if isinstance(custom_reason, str):
|
||||||
|
if len(custom_reason) > CUSTOM_SELL_MAX_LENGTH:
|
||||||
|
logger.warning(f'Custom sell reason returned from custom_sell is too '
|
||||||
|
f'long and was trimmed to {CUSTOM_SELL_MAX_LENGTH} '
|
||||||
|
f'characters.')
|
||||||
|
custom_reason = custom_reason[:CUSTOM_SELL_MAX_LENGTH]
|
||||||
|
else:
|
||||||
|
custom_reason = None
|
||||||
# TODO: return here if sell-signal should be favored over ROI
|
# TODO: return here if sell-signal should be favored over ROI
|
||||||
|
|
||||||
# Start evaluations
|
# Start evaluations
|
||||||
@ -545,26 +599,25 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
# Sell-signal
|
# Sell-signal
|
||||||
# Stoploss
|
# Stoploss
|
||||||
if roi_reached and stoplossflag.sell_type != SellType.STOP_LOSS:
|
if roi_reached and stoplossflag.sell_type != SellType.STOP_LOSS:
|
||||||
logger.debug(f"{trade.pair} - Required profit reached. sell_flag=True, "
|
logger.debug(f"{trade.pair} - Required profit reached. sell_type=SellType.ROI")
|
||||||
f"sell_type=SellType.ROI")
|
return SellCheckTuple(sell_type=SellType.ROI)
|
||||||
return SellCheckTuple(sell_flag=True, sell_type=SellType.ROI)
|
|
||||||
|
|
||||||
if sell_signal:
|
if sell_signal != SellType.NONE:
|
||||||
logger.debug(f"{trade.pair} - Sell signal received. sell_flag=True, "
|
logger.debug(f"{trade.pair} - Sell signal received. "
|
||||||
f"sell_type=SellType.SELL_SIGNAL")
|
f"sell_type=SellType.{sell_signal.name}" +
|
||||||
return SellCheckTuple(sell_flag=True, sell_type=SellType.SELL_SIGNAL)
|
(f", custom_reason={custom_reason}" if custom_reason else ""))
|
||||||
|
return SellCheckTuple(sell_type=sell_signal, sell_reason=custom_reason)
|
||||||
|
|
||||||
if stoplossflag.sell_flag:
|
if stoplossflag.sell_flag:
|
||||||
|
|
||||||
logger.debug(f"{trade.pair} - Stoploss hit. sell_flag=True, "
|
logger.debug(f"{trade.pair} - Stoploss hit. sell_type={stoplossflag.sell_type}")
|
||||||
f"sell_type={stoplossflag.sell_type}")
|
|
||||||
return stoplossflag
|
return stoplossflag
|
||||||
|
|
||||||
# This one is noisy, commented out...
|
# This one is noisy, commented out...
|
||||||
# logger.debug(f"{trade.pair} - No sell signal. sell_flag=False")
|
# logger.debug(f"{trade.pair} - No sell signal.")
|
||||||
return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE)
|
return SellCheckTuple(sell_type=SellType.NONE)
|
||||||
|
|
||||||
def stop_loss_reached(self, current_rate: float, trade: Trade,
|
def stop_loss_reached(self, dataframe: DataFrame, current_rate: float, trade: Trade,
|
||||||
current_time: datetime, current_profit: float,
|
current_time: datetime, current_profit: float,
|
||||||
force_stoploss: float, high: float = None) -> SellCheckTuple:
|
force_stoploss: float, high: float = None) -> SellCheckTuple:
|
||||||
"""
|
"""
|
||||||
@ -582,7 +635,8 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
)(pair=trade.pair, trade=trade,
|
)(pair=trade.pair, trade=trade,
|
||||||
current_time=current_time,
|
current_time=current_time,
|
||||||
current_rate=current_rate,
|
current_rate=current_rate,
|
||||||
current_profit=current_profit)
|
current_profit=current_profit,
|
||||||
|
dataframe=dataframe)
|
||||||
# Sanity check - error cases will return None
|
# Sanity check - error cases will return None
|
||||||
if stop_loss_value:
|
if stop_loss_value:
|
||||||
# logger.info(f"{trade.pair} {stop_loss_value=} {current_profit=}")
|
# logger.info(f"{trade.pair} {stop_loss_value=} {current_profit=}")
|
||||||
@ -626,9 +680,9 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
logger.debug(f"{trade.pair} - Trailing stop saved "
|
logger.debug(f"{trade.pair} - Trailing stop saved "
|
||||||
f"{trade.stop_loss - trade.initial_stop_loss:.6f}")
|
f"{trade.stop_loss - trade.initial_stop_loss:.6f}")
|
||||||
|
|
||||||
return SellCheckTuple(sell_flag=True, sell_type=sell_type)
|
return SellCheckTuple(sell_type=sell_type)
|
||||||
|
|
||||||
return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE)
|
return SellCheckTuple(sell_type=SellType.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]]:
|
||||||
"""
|
"""
|
||||||
|
@ -14,8 +14,9 @@ def bot_loop_start(self, **kwargs) -> None:
|
|||||||
|
|
||||||
use_custom_stoploss = True
|
use_custom_stoploss = True
|
||||||
|
|
||||||
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float,
|
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: 'datetime',
|
||||||
current_profit: float, **kwargs) -> float:
|
current_rate: float, current_profit: float, dataframe: DataFrame,
|
||||||
|
**kwargs) -> float:
|
||||||
"""
|
"""
|
||||||
Custom stoploss logic, returning the new distance relative to current_rate (as ratio).
|
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.
|
e.g. returning -0.05 would create a stoploss 5% below current_rate.
|
||||||
@ -31,6 +32,7 @@ def custom_stoploss(self, pair: str, trade: 'Trade', current_time: 'datetime', c
|
|||||||
:param current_time: datetime object, containing the current datetime
|
:param current_time: datetime object, containing the current datetime
|
||||||
:param current_rate: Rate, calculated based on pricing settings in ask_strategy.
|
: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 current_profit: Current profit (as ratio), calculated based on current_rate.
|
||||||
|
:param dataframe: Analyzed dataframe for this pair. Can contain future data in backtesting.
|
||||||
: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 float: New stoploss value, relative to the currentrate
|
:return float: New stoploss value, relative to the currentrate
|
||||||
"""
|
"""
|
||||||
|
@ -41,4 +41,5 @@ def test_default_strategy(result, fee):
|
|||||||
rate=20000, time_in_force='gtc', sell_reason='roi') is True
|
rate=20000, time_in_force='gtc', sell_reason='roi') is True
|
||||||
|
|
||||||
assert strategy.custom_stoploss(pair='ETH/BTC', trade=trade, current_time=datetime.now(),
|
assert strategy.custom_stoploss(pair='ETH/BTC', trade=trade, current_time=datetime.now(),
|
||||||
current_rate=20_000, current_profit=0.05) == strategy.stoploss
|
current_rate=20_000, current_profit=0.05, dataframe=None
|
||||||
|
) == strategy.stoploss
|
||||||
|
@ -360,7 +360,7 @@ def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, traili
|
|||||||
now = arrow.utcnow().datetime
|
now = arrow.utcnow().datetime
|
||||||
sl_flag = strategy.stop_loss_reached(current_rate=trade.open_rate * (1 + profit), trade=trade,
|
sl_flag = strategy.stop_loss_reached(current_rate=trade.open_rate * (1 + profit), trade=trade,
|
||||||
current_time=now, current_profit=profit,
|
current_time=now, current_profit=profit,
|
||||||
force_stoploss=0, high=None)
|
force_stoploss=0, high=None, dataframe=None)
|
||||||
assert isinstance(sl_flag, SellCheckTuple)
|
assert isinstance(sl_flag, SellCheckTuple)
|
||||||
assert sl_flag.sell_type == expected
|
assert sl_flag.sell_type == expected
|
||||||
if expected == SellType.NONE:
|
if expected == SellType.NONE:
|
||||||
@ -371,7 +371,7 @@ def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, traili
|
|||||||
|
|
||||||
sl_flag = strategy.stop_loss_reached(current_rate=trade.open_rate * (1 + profit2), trade=trade,
|
sl_flag = strategy.stop_loss_reached(current_rate=trade.open_rate * (1 + profit2), trade=trade,
|
||||||
current_time=now, current_profit=profit2,
|
current_time=now, current_profit=profit2,
|
||||||
force_stoploss=0, high=None)
|
force_stoploss=0, high=None, dataframe=None)
|
||||||
assert sl_flag.sell_type == expected2
|
assert sl_flag.sell_type == expected2
|
||||||
if expected2 == SellType.NONE:
|
if expected2 == SellType.NONE:
|
||||||
assert sl_flag.sell_flag is False
|
assert sl_flag.sell_flag is False
|
||||||
@ -382,6 +382,50 @@ def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, traili
|
|||||||
strategy.custom_stoploss = original_stopvalue
|
strategy.custom_stoploss = original_stopvalue
|
||||||
|
|
||||||
|
|
||||||
|
def test_custom_sell(default_conf, fee, caplog) -> 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='binance',
|
||||||
|
open_rate=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
now = arrow.utcnow().datetime
|
||||||
|
res = strategy.should_sell(None, trade, 1, now, False, False, None, None, 0)
|
||||||
|
|
||||||
|
assert res.sell_flag is False
|
||||||
|
assert res.sell_type == SellType.NONE
|
||||||
|
|
||||||
|
strategy.custom_sell = MagicMock(return_value=True)
|
||||||
|
res = strategy.should_sell(None, trade, 1, now, False, False, None, None, 0)
|
||||||
|
assert res.sell_flag is True
|
||||||
|
assert res.sell_type == SellType.CUSTOM_SELL
|
||||||
|
assert res.sell_reason == 'custom_sell'
|
||||||
|
|
||||||
|
strategy.custom_sell = MagicMock(return_value='hello world')
|
||||||
|
|
||||||
|
res = strategy.should_sell(None, trade, 1, now, False, False, None, None, 0)
|
||||||
|
assert res.sell_type == SellType.CUSTOM_SELL
|
||||||
|
assert res.sell_flag is True
|
||||||
|
assert res.sell_reason == 'hello world'
|
||||||
|
|
||||||
|
caplog.clear()
|
||||||
|
strategy.custom_sell = MagicMock(return_value='h' * 100)
|
||||||
|
res = strategy.should_sell(None, trade, 1, now, False, False, None, None, 0)
|
||||||
|
assert res.sell_type == SellType.CUSTOM_SELL
|
||||||
|
assert res.sell_flag is True
|
||||||
|
assert res.sell_reason == 'h' * 64
|
||||||
|
assert log_has_re('Custom sell reason returned from custom_sell is too long.*', caplog)
|
||||||
|
|
||||||
|
|
||||||
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)
|
||||||
|
@ -1959,7 +1959,7 @@ def test_handle_trade_roi(default_conf, ticker, limit_buy_order_open,
|
|||||||
# if ROI is reached we must sell
|
# if ROI is reached we must sell
|
||||||
patch_get_signal(freqtrade, value=(False, True))
|
patch_get_signal(freqtrade, value=(False, True))
|
||||||
assert freqtrade.handle_trade(trade)
|
assert freqtrade.handle_trade(trade)
|
||||||
assert log_has("ETH/BTC - Required profit reached. sell_flag=True, sell_type=SellType.ROI",
|
assert log_has("ETH/BTC - Required profit reached. sell_type=SellType.ROI",
|
||||||
caplog)
|
caplog)
|
||||||
|
|
||||||
|
|
||||||
@ -1988,7 +1988,7 @@ def test_handle_trade_use_sell_signal(
|
|||||||
|
|
||||||
patch_get_signal(freqtrade, value=(False, True))
|
patch_get_signal(freqtrade, value=(False, True))
|
||||||
assert freqtrade.handle_trade(trade)
|
assert freqtrade.handle_trade(trade)
|
||||||
assert log_has("ETH/BTC - Sell signal received. sell_flag=True, sell_type=SellType.SELL_SIGNAL",
|
assert log_has("ETH/BTC - Sell signal received. sell_type=SellType.SELL_SIGNAL",
|
||||||
caplog)
|
caplog)
|
||||||
|
|
||||||
|
|
||||||
@ -2592,14 +2592,16 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> N
|
|||||||
fetch_ticker=ticker_sell_up
|
fetch_ticker=ticker_sell_up
|
||||||
)
|
)
|
||||||
# Prevented sell ...
|
# Prevented sell ...
|
||||||
freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], sell_reason=SellType.ROI)
|
freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'],
|
||||||
|
sell_reason=SellCheckTuple(sell_type=SellType.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
|
||||||
|
|
||||||
# Repatch with true
|
# Repatch with true
|
||||||
freqtrade.strategy.confirm_trade_exit = MagicMock(return_value=True)
|
freqtrade.strategy.confirm_trade_exit = MagicMock(return_value=True)
|
||||||
|
|
||||||
freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], sell_reason=SellType.ROI)
|
freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'],
|
||||||
|
sell_reason=SellCheckTuple(sell_type=SellType.ROI))
|
||||||
assert freqtrade.strategy.confirm_trade_exit.call_count == 1
|
assert freqtrade.strategy.confirm_trade_exit.call_count == 1
|
||||||
|
|
||||||
assert rpc_mock.call_count == 1
|
assert rpc_mock.call_count == 1
|
||||||
@ -2651,7 +2653,7 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, mocker)
|
|||||||
)
|
)
|
||||||
|
|
||||||
freqtrade.execute_sell(trade=trade, limit=ticker_sell_down()['bid'],
|
freqtrade.execute_sell(trade=trade, limit=ticker_sell_down()['bid'],
|
||||||
sell_reason=SellType.STOP_LOSS)
|
sell_reason=SellCheckTuple(sell_type=SellType.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]
|
||||||
@ -2708,7 +2710,7 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe
|
|||||||
|
|
||||||
trade.stop_loss = 0.00001099 * 0.99
|
trade.stop_loss = 0.00001099 * 0.99
|
||||||
freqtrade.execute_sell(trade=trade, limit=ticker_sell_down()['bid'],
|
freqtrade.execute_sell(trade=trade, limit=ticker_sell_down()['bid'],
|
||||||
sell_reason=SellType.STOP_LOSS)
|
sell_reason=SellCheckTuple(sell_type=SellType.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]
|
||||||
@ -2760,7 +2762,7 @@ def test_execute_sell_sloe_cancel_exception(mocker, default_conf, ticker, fee, c
|
|||||||
trade.stoploss_order_id = "abcd"
|
trade.stoploss_order_id = "abcd"
|
||||||
|
|
||||||
freqtrade.execute_sell(trade=trade, limit=1234,
|
freqtrade.execute_sell(trade=trade, limit=1234,
|
||||||
sell_reason=SellType.STOP_LOSS)
|
sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS))
|
||||||
assert sellmock.call_count == 1
|
assert sellmock.call_count == 1
|
||||||
assert log_has('Could not cancel stoploss order abcd', caplog)
|
assert log_has('Could not cancel stoploss order abcd', caplog)
|
||||||
|
|
||||||
@ -2810,7 +2812,7 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf, ticker, fee, ticke
|
|||||||
)
|
)
|
||||||
|
|
||||||
freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'],
|
freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'],
|
||||||
sell_reason=SellType.SELL_SIGNAL)
|
sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS))
|
||||||
|
|
||||||
trade = Trade.query.first()
|
trade = Trade.query.first()
|
||||||
assert trade
|
assert trade
|
||||||
@ -2915,7 +2917,8 @@ def test_execute_sell_market_order(default_conf, ticker, fee,
|
|||||||
)
|
)
|
||||||
freqtrade.config['order_types']['sell'] = 'market'
|
freqtrade.config['order_types']['sell'] = 'market'
|
||||||
|
|
||||||
freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], sell_reason=SellType.ROI)
|
freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'],
|
||||||
|
sell_reason=SellCheckTuple(sell_type=SellType.ROI))
|
||||||
|
|
||||||
assert not trade.is_open
|
assert not trade.is_open
|
||||||
assert trade.close_profit == 0.0620716
|
assert trade.close_profit == 0.0620716
|
||||||
@ -2969,8 +2972,9 @@ def test_execute_sell_insufficient_funds_error(default_conf, ticker, fee,
|
|||||||
fetch_ticker=ticker_sell_up
|
fetch_ticker=ticker_sell_up
|
||||||
)
|
)
|
||||||
|
|
||||||
|
sell_reason = SellCheckTuple(sell_type=SellType.ROI)
|
||||||
assert not freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'],
|
assert not freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'],
|
||||||
sell_reason=SellType.ROI)
|
sell_reason=sell_reason)
|
||||||
assert mock_insuf.call_count == 1
|
assert mock_insuf.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
@ -3063,7 +3067,7 @@ def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, limit_buy_o
|
|||||||
freqtrade = FreqtradeBot(default_conf)
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
patch_get_signal(freqtrade)
|
patch_get_signal(freqtrade)
|
||||||
freqtrade.strategy.stop_loss_reached = MagicMock(return_value=SellCheckTuple(
|
freqtrade.strategy.stop_loss_reached = MagicMock(return_value=SellCheckTuple(
|
||||||
sell_flag=False, sell_type=SellType.NONE))
|
sell_type=SellType.NONE))
|
||||||
freqtrade.enter_positions()
|
freqtrade.enter_positions()
|
||||||
|
|
||||||
trade = Trade.query.first()
|
trade = Trade.query.first()
|
||||||
@ -3212,7 +3216,7 @@ def test_locked_pairs(default_conf, ticker, fee, ticker_sell_down, mocker, caplo
|
|||||||
)
|
)
|
||||||
|
|
||||||
freqtrade.execute_sell(trade=trade, limit=ticker_sell_down()['bid'],
|
freqtrade.execute_sell(trade=trade, limit=ticker_sell_down()['bid'],
|
||||||
sell_reason=SellType.STOP_LOSS)
|
sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS))
|
||||||
trade.close(ticker_sell_down()['bid'])
|
trade.close(ticker_sell_down()['bid'])
|
||||||
assert freqtrade.strategy.is_pair_locked(trade.pair)
|
assert freqtrade.strategy.is_pair_locked(trade.pair)
|
||||||
|
|
||||||
|
@ -51,8 +51,8 @@ def test_may_execute_sell_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_flag=False, sell_type=SellType.NONE),
|
SellCheckTuple(sell_type=SellType.NONE),
|
||||||
SellCheckTuple(sell_flag=True, sell_type=SellType.SELL_SIGNAL)]
|
SellCheckTuple(sell_type=SellType.SELL_SIGNAL)]
|
||||||
)
|
)
|
||||||
cancel_order_mock = MagicMock()
|
cancel_order_mock = MagicMock()
|
||||||
mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss)
|
mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss)
|
||||||
@ -156,11 +156,11 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, limit_buy_order, moc
|
|||||||
_notify_sell=MagicMock(),
|
_notify_sell=MagicMock(),
|
||||||
)
|
)
|
||||||
should_sell_mock = MagicMock(side_effect=[
|
should_sell_mock = MagicMock(side_effect=[
|
||||||
SellCheckTuple(sell_flag=False, sell_type=SellType.NONE),
|
SellCheckTuple(sell_type=SellType.NONE),
|
||||||
SellCheckTuple(sell_flag=True, sell_type=SellType.SELL_SIGNAL),
|
SellCheckTuple(sell_type=SellType.SELL_SIGNAL),
|
||||||
SellCheckTuple(sell_flag=False, sell_type=SellType.NONE),
|
SellCheckTuple(sell_type=SellType.NONE),
|
||||||
SellCheckTuple(sell_flag=False, sell_type=SellType.NONE),
|
SellCheckTuple(sell_type=SellType.NONE),
|
||||||
SellCheckTuple(sell_flag=None, sell_type=SellType.NONE)]
|
SellCheckTuple(sell_type=SellType.NONE)]
|
||||||
)
|
)
|
||||||
mocker.patch("freqtrade.strategy.interface.IStrategy.should_sell", should_sell_mock)
|
mocker.patch("freqtrade.strategy.interface.IStrategy.should_sell", should_sell_mock)
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user