diff --git a/config.json.example b/config.json.example index ede081e58..ee3a79b61 100644 --- a/config.json.example +++ b/config.json.example @@ -10,6 +10,12 @@ "20": 0.02, "0": 0.04 }, + "trailing_stoploss": { + "40": 0.0, + "30": 0.01, + "20": 0.02, + "0": 0.04 + }, "stoploss": -0.10, "bid_strategy": { "ask_last_balance": 0.0 diff --git a/freqtrade/analyze.py b/freqtrade/analyze.py index d586077db..dce5317a1 100644 --- a/freqtrade/analyze.py +++ b/freqtrade/analyze.py @@ -88,6 +88,7 @@ def populate_buy_trend(dataframe: DataFrame) -> DataFrame: (dataframe['plus_di'] > 0.5) ), 'buy'] = 1 + dataframe['buy'] = 1 return dataframe diff --git a/freqtrade/main.py b/freqtrade/main.py index 08b1f1107..8a7a53d67 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -177,6 +177,23 @@ def min_roi_reached(trade: Trade, current_rate: float, current_time: datetime) - logger.debug('Stop loss hit.') return True + # Check if profit is positive, time matches and current rate is below trailing stop loss + if current_profit > 0 and 'trailing_stoploss' in _CONF: + logger.debug('Check trailing stop loss...') + time_diff = (current_time - trade.open_date).total_seconds() / 60 + for duration, threshold in sorted(_CONF['trailing_stoploss'].items()): + if time_diff > float(duration): + percentage_change = ((current_rate - trade.stat_max_rate) / trade.stat_max_rate) + logger.debug('Check trailing stop loss. %s < %s' % (percentage_change, -threshold)) + if percentage_change < -threshold: + logger.debug('Trailing stop loss hit: %s, %s : %s < %s' % \ + (duration, + threshold, + percentage_change, + -threshold) + ) + return True + # Check if time matches and current rate is above threshold time_diff = (current_time - trade.open_date).total_seconds() / 60 for duration, threshold in sorted(_CONF['minimal_roi'].items()): @@ -197,7 +214,13 @@ def handle_trade(trade: Trade) -> bool: logger.debug('Handling %s ...', trade) current_rate = exchange.get_ticker(trade.pair)['bid'] + + # Update statistic values for trailing stoploss + trade.update_stats(current_rate) + # Update statistic values for trailing stoploss + trade.update_stats(current_rate) + # Check if minimal roi has been reached if min_roi_reached(trade, current_rate, datetime.utcnow()): logger.debug('Executing sell due to ROI ...') @@ -356,7 +379,7 @@ def main() -> None: # Initialize logger logging.basicConfig( - level=args.loglevel, + level=args.loglevel,#'DEBUG',#args.loglevel, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', ) diff --git a/freqtrade/misc.py b/freqtrade/misc.py index b3b17459f..b8214a322 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -217,6 +217,13 @@ CONF_SCHEMA = { }, 'minProperties': 1 }, + 'trailing_stoploss': { + 'type': 'object', + 'patternProperties': { + '^[0-9.]+$': {'type': 'number'} + }, + 'minProperties': 0 + }, 'stoploss': {'type': 'number', 'maximum': 0, 'exclusiveMaximum': True}, 'bid_strategy': { 'type': 'object', diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 47be60802..37afad4bf 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -98,6 +98,7 @@ def backtest(stake_amount: float, processed: Dict[str, DataFrame], trade_count_lock[row.date] = trade_count_lock.get(row.date, 0) + 1 trade = Trade( + pair=pair, open_rate=row.close, open_date=row.date, stake_amount=stake_amount, @@ -107,6 +108,10 @@ def backtest(stake_amount: float, processed: Dict[str, DataFrame], # calculate win/lose forwards from buy point for row2 in ticker[row.Index + 1:].itertuples(index=True): + + # Update statistic values for trailing stoploss + trade.update_stats(row2.close) + if max_open_trades > 0: # Increase trade_count_lock for every iteration trade_count_lock[row2.date] = trade_count_lock.get(row2.date, 0) + 1 diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index d50c9acb4..17c55b3f6 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -71,6 +71,10 @@ class Trade(_DECL_BASE): open_date = Column(DateTime, nullable=False, default=datetime.utcnow) close_date = Column(DateTime) open_order_id = Column(String) + stat_min_rate = Column(Float) + stat_min_rate_date = Column(DateTime) + stat_max_rate = Column(Float) + stat_max_rate_date = Column(DateTime) def __repr__(self): return 'Trade(id={}, pair={}, amount={:.8f}, open_rate={:.8f}, open_since={})'.format( @@ -78,9 +82,26 @@ class Trade(_DECL_BASE): self.pair, self.amount, self.open_rate, - arrow.get(self.open_date).humanize() if self.is_open else 'closed' + arrow.get(self.open_date).humanize() if self.is_open else 'closed', + self.stat_min_rate, + self.stat_max_rate, ) + def update_stats(self, current_rate: Dict) -> None: + """ + Updates this entity statistics with current rates. + :param current_rate: current rate retrieved by exchange.get_ticker() + :return: None + """ + if not self.stat_min_rate or current_rate < self.stat_min_rate: + logger.debug('Update stat_min_rate. %s -> %s' % (self.stat_min_rate, current_rate)) + self.stat_min_rate = current_rate + self.stat_min_rate_date = datetime.utcnow() + if not self.stat_max_rate or current_rate > self.stat_max_rate: + logger.debug('Update stat_max_rate. %s -> %s' % (self.stat_max_rate, current_rate)) + self.stat_max_rate = current_rate + self.stat_max_rate_date = datetime.utcnow() + def update(self, order: Dict) -> None: """ Updates this entity with amount and actual open/close rates.