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:
Matthias 2021-04-29 06:50:56 +02:00 committed by GitHub
commit a3d2e68312
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 249 additions and 129 deletions

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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}")

View File

@ -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:

View File

@ -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]]:
""" """

View File

@ -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
""" """

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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)