diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 35c0a1705..65dab15dd 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -20,6 +20,7 @@ from freqtrade.fiat_convert import CryptoToFiatConverter from freqtrade.persistence import Trade from freqtrade.rpc import RPCManager, RPCMessageType from freqtrade.state import State +from freqtrade.strategy.interface import SellType from freqtrade.strategy.resolver import IStrategy, StrategyResolver logger = logging.getLogger(__name__) @@ -505,8 +506,9 @@ class FreqtradeBot(object): (buy, sell) = self.strategy.get_signal(self.exchange, trade.pair, self.strategy.ticker_interval) - if self.strategy.should_sell(trade, current_rate, datetime.utcnow(), buy, sell): - self.execute_sell(trade, current_rate) + should_sell = self.strategy.should_sell(trade, current_rate, datetime.utcnow(), buy, sell) + if should_sell[0]: + self.execute_sell(trade, current_rate, should_sell[1]) return True logger.info('Found no sell signals for whitelisted currencies. Trying again..') return False @@ -607,17 +609,19 @@ class FreqtradeBot(object): # TODO: figure out how to handle partially complete sell orders return False - def execute_sell(self, trade: Trade, limit: float) -> None: + def execute_sell(self, trade: Trade, limit: float, sellreason: SellType) -> None: """ Executes a limit sell for the given trade and limit :param trade: Trade instance :param limit: limit rate for the sell order + :param sellrason: Reaseon the sell was triggered :return: None """ # Execute sell and update trade record order_id = self.exchange.sell(str(trade.pair), limit, trade.amount)['id'] trade.open_order_id = order_id trade.close_rate_requested = limit + trade.sell_reason = sellreason.value profit_trade = trade.calc_profit(rate=limit) current_rate = self.exchange.get_ticker(trade.pair)['bid'] diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index 0e0b22e82..086e8c2ea 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -88,6 +88,7 @@ def check_migrate(engine) -> None: stop_loss = get_column_def(cols, 'stop_loss', '0.0') initial_stop_loss = get_column_def(cols, 'initial_stop_loss', '0.0') max_rate = get_column_def(cols, 'max_rate', '0.0') + sell_reason = get_column_def(cols, 'sell_reason', 'null') # Schema migration necessary engine.execute(f"alter table trades rename to {table_back_name}") @@ -99,7 +100,7 @@ def check_migrate(engine) -> None: (id, exchange, pair, is_open, fee_open, fee_close, open_rate, open_rate_requested, close_rate, close_rate_requested, close_profit, stake_amount, amount, open_date, close_date, open_order_id, - stop_loss, initial_stop_loss, max_rate + stop_loss, initial_stop_loss, max_rate, sell_reason ) select id, lower(exchange), case @@ -114,7 +115,7 @@ def check_migrate(engine) -> None: {close_rate_requested} close_rate_requested, close_profit, stake_amount, amount, open_date, close_date, open_order_id, {stop_loss} stop_loss, {initial_stop_loss} initial_stop_loss, - {max_rate} max_rate + {max_rate} max_rate, {sell_reason} sell_reason from {table_back_name} """) @@ -170,6 +171,7 @@ class Trade(_DECL_BASE): initial_stop_loss = Column(Float, nullable=True, default=0.0) # absolute value of the highest reached price max_rate = Column(Float, nullable=True, default=0.0) + sell_reason = Column(String, nullable=True) def __repr__(self): open_since = arrow.get(self.open_date).humanize() if self.is_open else 'closed' diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 9411e983b..96556df78 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -13,6 +13,7 @@ import sqlalchemy as sql from numpy import mean, nan_to_num from pandas import DataFrame +from freqtrade.analyze import SellType from freqtrade.misc import shorten_date from freqtrade.persistence import Trade from freqtrade.state import State @@ -344,7 +345,7 @@ class RPC(object): # Get current rate and execute sell current_rate = self._freqtrade.exchange.get_ticker(trade.pair, False)['bid'] - self._freqtrade.execute_sell(trade, current_rate) + self._freqtrade.execute_sell(trade, current_rate, SellType.FORCE_SELL) # ---- EOF def _exec_forcesell ---- if self._freqtrade.state != State.RUNNING: diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 811f3232e..c5d58dd46 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -6,7 +6,7 @@ import logging from abc import ABC, abstractmethod from datetime import datetime from enum import Enum -from typing import Dict, List, Tuple +from typing import Dict, List, Tuple, Optional import arrow from pandas import DataFrame @@ -35,6 +35,7 @@ class SellType(Enum): STOP_LOSS = "stop_loss" TRAILING_STOP_LOSS = "trailing_stop_loss" SELL_SIGNAL = "sell_signal" + FORCE_SELL = "force_sell" class IStrategy(ABC): @@ -147,40 +148,42 @@ class IStrategy(ABC): ) return buy, sell - def should_sell(self, trade: Trade, rate: float, date: datetime, buy: bool, sell: bool) -> bool: + def should_sell(self, trade: Trade, rate: float, date: datetime, buy: bool, + sell: bool) -> Tuple[bool, Optional[SellType]]: """ This function evaluate if on the condition required to trigger a sell has been reached if the threshold is reached and updates the trade record. :return: True if trade should be sold, False otherwise """ current_profit = trade.calc_profit_percent(rate) - if self.stop_loss_reached(current_rate=rate, trade=trade, current_time=date, - current_profit=current_profit): - return True + stoplossflag = self.stop_loss_reached(current_rate=rate, trade=trade, current_time=date, + current_profit=current_profit) + if stoplossflag[0]: + return (True, stoplossflag[1]) experimental = self.config.get('experimental', {}) if buy and experimental.get('ignore_roi_if_buy_signal', False): logger.debug('Buy signal still active - not selling.') - return False + return (False, None) # Check if minimal roi has been reached and no longer in buy conditions (avoiding a fee) if self.min_roi_reached(trade=trade, current_profit=current_profit, current_time=date): logger.debug('Required profit reached. Selling..') - return True + return (True, SellType.ROI) if experimental.get('sell_profit_only', False): logger.debug('Checking if trade is profitable..') if trade.calc_profit(rate=rate) <= 0: - return False + return (False, None) if sell and not buy and experimental.get('use_sell_signal', False): logger.debug('Sell signal received. Selling..') - return True + return (True, SellType.SELL_SIGNAL) - return False + return (False, None) def stop_loss_reached(self, current_rate: float, trade: Trade, current_time: datetime, - current_profit: float) -> bool: + current_profit: float) -> Tuple[bool, Optional[SellType]]: """ Based on current profit of the trade and configured (trailing) stoploss, decides to sell or not @@ -192,8 +195,9 @@ class IStrategy(ABC): # evaluate if the stoploss was hit if self.stoploss is not None and trade.stop_loss >= current_rate: - + selltype = SellType.STOP_LOSS if trailing_stop: + selltype = SellType.TRAILING_STOP_LOSS logger.debug( f"HIT STOP: current price at {current_rate:.6f}, " f"stop loss is {trade.stop_loss:.6f}, " @@ -202,7 +206,7 @@ class IStrategy(ABC): logger.debug(f"trailing stop saved {trade.stop_loss - trade.initial_stop_loss:.6f}") logger.debug('Stop loss hit.') - return True + return (True, selltype) # update the stop loss afterwards, after all by definition it's supposed to be hanging if trailing_stop: @@ -219,7 +223,7 @@ class IStrategy(ABC): trade.adjust_stop_loss(current_rate, stop_loss_value) - return False + return (False, None) def min_roi_reached(self, trade: Trade, current_profit: float, current_time: datetime) -> bool: """