def execute_sell(trade: Trade, limit: float) -> None: """ Executes a limit sell for the given trade and limit :param trade: Trade instance :param limit: limit rate for the sell order :return: None """ # Execute sell and update trade record order_id = exchange.sell(str(trade.pair), limit, trade.amount) trade.open_order_id = order_id fmt_exp_profit = round(trade.calc_profit_percent(rate=limit) * 100, 2) profit_trade = trade.calc_profit(rate=limit) message = '*{exchange}:* Selling [{pair}]({pair_url}) with limit `{limit:.8f}`'.format( exchange=trade.exchange, pair=trade.pair.replace('_', '/'), pair_url=exchange.get_pair_detail_url(trade.pair), limit=limit ) # For regular case, when the configuration exists if 'stake_currency' in _CONF and 'fiat_display_currency' in _CONF: fiat_converter = CryptoToFiatConverter() profit_fiat = fiat_converter.convert_amount( profit_trade, _CONF['stake_currency'], _CONF['fiat_display_currency'] ) message += '` ({gain}: {profit_percent:.2f}%, {profit_coin:.8f} {coin}`' \ '` / {profit_fiat:.3f} {fiat})`'.format( gain="profit" if fmt_exp_profit > 0 else "loss", profit_percent=fmt_exp_profit, profit_coin=profit_trade, coin=_CONF['stake_currency'], profit_fiat=profit_fiat, fiat=_CONF['fiat_display_currency'], ) # Because telegram._forcesell does not have the configuration # Ignore the FIAT value and does not show the stake_currency as well else: message += '` ({gain}: {profit_percent:.2f}%, {profit_coin:.8f})`'.format( gain="profit" if fmt_exp_profit > 0 else "loss", profit_percent=fmt_exp_profit, profit_coin=profit_trade ) # Send the message rpc.send_msg(message) Trade.session.flush()
def handle_trade(trade: Trade) -> bool: """ Sells the current pair if the threshold is reached and updates the trade record. :return: True if trade has been sold, False otherwise """ if not trade.is_open: raise ValueError('attempt to handle closed trade: {}'.format(trade)) logger.debug('Handling %s ...', trade) current_rate = exchange.get_ticker(trade.pair)['bid'] # Check if minimal roi has been reached if min_roi_reached(trade, current_rate, datetime.utcnow()): logger.debug('Executing sell due to ROI ...') execute_sell(trade, current_rate) return True # Experimental: Check if sell signal has been enabled and triggered if _CONF.get('experimental', {}).get('use_sell_signal'): # Experimental: Check if the trade is profitable before selling it (avoid selling at loss) if _CONF.get('experimental', {}).get('sell_profit_only'): logger.debug('Checking if trade is profitable ...') if trade.calc_profit(rate=current_rate) <= 0: return False logger.debug('Checking sell_signal ...') if get_signal(trade.pair, SignalType.SELL): logger.debug('Executing sell due to sell signal ...') execute_sell(trade, current_rate) return True return False
def _exec_forcesell(trade: Trade) -> None: # Check if there is there is an open order if trade.open_order_id: order = exchange.get_order(trade.open_order_id) # Cancel open LIMIT_BUY orders and close trade if order and not order['closed'] and order['type'] == 'LIMIT_BUY': exchange.cancel_order(trade.open_order_id) trade.close(order.get('rate') or trade.open_rate) # TODO: sell amount which has been bought already return # Ignore trades with an attached LIMIT_SELL order if order and not order['closed'] and order['type'] == 'LIMIT_SELL': return # Get current rate and execute sell current_rate = exchange.get_ticker(trade.pair, False)['bid'] from freqtrade.main import execute_sell execute_sell(trade, current_rate)
def min_roi_reached(trade: Trade, current_rate: float, current_time: datetime) -> bool: """ Based an earlier trade and current price and ROI configuration, decides whether bot should sell :return True if bot should sell at current rate """ current_profit = trade.calc_profit_percent(current_rate) if 'stoploss' in _CONF and current_profit < float(_CONF['stoploss']): logger.debug('Stop loss hit.') 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()): if time_diff > float(duration) and current_profit > threshold: return True logger.debug('Threshold not reached. (cur_profit: %1.2f%%)', float(current_profit) * 100.0) return False
def execute_sell(self, trade: Trade, limit: float, sell_reason: 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 sellreason: Reason the sell was triggered :return: None """ sell_type = 'sell' if sell_reason in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS): sell_type = 'stoploss' # if stoploss is on exchange and we are on dry_run mode, # we consider the sell price stop price if self.config.get('dry_run', False) and sell_type == 'stoploss' \ and self.strategy.order_types['stoploss_on_exchange']: limit = trade.stop_loss # First cancelling stoploss on exchange ... if self.strategy.order_types.get( 'stoploss_on_exchange') and trade.stoploss_order_id: self.exchange.cancel_order(trade.stoploss_order_id, trade.pair) # Execute sell and update trade record order_id = self.exchange.sell( pair=str(trade.pair), ordertype=self.strategy.order_types[sell_type], amount=trade.amount, rate=limit, time_in_force=self.strategy.order_time_in_force['sell'])['id'] trade.open_order_id = order_id trade.close_rate_requested = limit trade.sell_reason = sell_reason.value profit_trade = trade.calc_profit(rate=limit) current_rate = self.exchange.get_ticker(trade.pair)['bid'] profit_percent = trade.calc_profit_percent(limit) pair_url = self.exchange.get_pair_detail_url(trade.pair) gain = "profit" if profit_percent > 0 else "loss" msg = { 'type': RPCMessageType.SELL_NOTIFICATION, 'exchange': trade.exchange.capitalize(), 'pair': trade.pair, 'gain': gain, 'market_url': pair_url, 'limit': limit, 'amount': trade.amount, 'open_rate': trade.open_rate, 'current_rate': current_rate, 'profit_amount': profit_trade, 'profit_percent': profit_percent, 'sell_reason': sell_reason.value } # For regular case, when the configuration exists if 'stake_currency' in self.config and 'fiat_display_currency' in self.config: stake_currency = self.config['stake_currency'] fiat_currency = self.config['fiat_display_currency'] msg.update({ 'stake_currency': stake_currency, 'fiat_currency': fiat_currency, }) # Send the message self.rpc.send_msg(msg) Trade.session.flush()
def min_roi_reached_dynamic( self, trade: Trade, current_profit: float, current_time: datetime, trade_dur: int) -> Tuple[Optional[int], Optional[float]]: dynamic_roi = self.get_pair_params(trade.pair, 'dynamic_roi') minimal_roi = self.get_pair_params(trade.pair, 'minimal_roi') if not dynamic_roi or not minimal_roi: return None, None _, table_roi = self.min_roi_reached_entry(trade_dur, trade.pair) # see if we have the data we need to do this, otherwise fall back to the standard table if self.custom_trade_info and trade and trade.pair in self.custom_trade_info: if self.config['runmode'].value in ('live', 'dry_run'): dataframe, last_updated = self.dp.get_analyzed_dataframe( pair=trade.pair, timeframe=self.timeframe) roc = dataframe['roc'].iat[-1] atr = dataframe['atr'].iat[-1] rmi_slow = dataframe['rmi-slow'].iat[-1] rmi_trend = dataframe['rmi-up-trend'].iat[-1] # If in backtest or hyperopt, get the indicator values out of the trades dict (Thanks @JoeSchr!) else: roc = self.custom_trade_info[ trade.pair]['roc'].loc[current_time]['roc'] atr = self.custom_trade_info[ trade.pair]['atr'].loc[current_time]['atr'] rmi_slow = self.custom_trade_info[ trade.pair]['rmi-slow'].loc[current_time]['rmi-slow'] rmi_trend = self.custom_trade_info[trade.pair][ 'rmi-up-trend'].loc[current_time]['rmi-up-trend'] d = dynamic_roi profit_factor = (1 - (rmi_slow / d['profit-factor'])) rmi_grow = cta.linear_growth(d['rmi-start'], d['rmi-end'], d['grow-delay'], d['grow-time'], trade_dur) max_profit = trade.calc_profit_ratio(trade.max_rate) open_rate = trade.open_rate atr_roi = max(d['min-roc-atr'], ((open_rate + atr) / open_rate) - 1) roc_roi = max(d['min-roc-atr'], (roc / 100)) # atr as the fallback (if > min-roc-atr) if d['fallback'] == 'atr': min_roi = atr_roi # roc as the fallback (if > min-roc-atr) elif d['fallback'] == 'roc': min_roi = roc_roi # atr or table as the fallback (whichever is larger) elif d['fallback'] == 'atr-table': min_roi = max(table_roi, atr_roi) # roc or table as the fallback (whichever is larger) elif d['fallback'] == 'roc-table': min_roi = max(table_roi, roc_roi) # default to table else: min_roi = table_roi # If we observe a strong upward trend and our current profit has not retreated from the peak by much, hold if (rmi_trend == 1) and (rmi_slow > rmi_grow): if current_profit > min_roi and (current_profit < (max_profit * profit_factor)): min_roi = min_roi else: min_roi = 100 """ # If we observe a strong upward trend and our current profit has not retreated from the peak by much, hold if (current_profit > (max_profit * profit_factor)) and (rmi_trend == 1) and (rmi_slow > rmi_grow): min_roi = 100 """ else: min_roi = table_roi # Attempting to wedge the dynamic roi value into a thing so we can trick backtesting... if self.config['runmode'].value not in ('live', 'dry_run'): # Theoretically, if backtesting uses this value, ROI was triggered so we need to trick it with a sell # rate other than what is on the standard ROI table... self.custom_trade_info['backtest']['roi'] = max( min_roi, current_profit) return trade_dur, min_roi
def backtest(stake_amount: float, processed: Dict[str, DataFrame], max_open_trades: int = 0, realistic: bool = True, sell_profit_only: bool = False, stoploss: int = -1.00, use_sell_signal: bool = False) -> DataFrame: """ Implements backtesting functionality :param stake_amount: btc amount to use for each trade :param processed: a processed dictionary with format {pair, data} :param max_open_trades: maximum number of concurrent trades (default: 0, disabled) :param realistic: do we try to simulate realistic trades? (default: True) :return: DataFrame """ trades = [] trade_count_lock: dict = {} exchange._API = Bittrex({'key': '', 'secret': ''}) for pair, pair_data in processed.items(): pair_data['buy'], pair_data['sell'] = 0, 0 ticker = populate_sell_trend(populate_buy_trend(pair_data)) # for each buy point lock_pair_until = None buy_subset = ticker[ticker.buy == 1][['buy', 'open', 'close', 'date', 'sell']] for row in buy_subset.itertuples(index=True): if realistic: if lock_pair_until is not None and row.Index <= lock_pair_until: continue if max_open_trades > 0: # Check if max_open_trades has already been reached for the given date if not trade_count_lock.get(row.date, 0) < max_open_trades: continue if max_open_trades > 0: # Increase lock trade_count_lock[row.date] = trade_count_lock.get(row.date, 0) + 1 trade = Trade( open_rate=row.close, open_date=row.date, stake_amount=stake_amount, amount=stake_amount / row.open, fee=exchange.get_fee() ) # calculate win/lose forwards from buy point sell_subset = ticker[row.Index + 1:][['close', 'date', 'sell']] for row2 in sell_subset.itertuples(index=True): 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 current_profit_percent = trade.calc_profit_percent(rate=row2.close) if (sell_profit_only and current_profit_percent < 0): continue if min_roi_reached(trade, row2.close, row2.date) or \ (row2.sell == 1 and use_sell_signal) or \ current_profit_percent <= stoploss: current_profit_btc = trade.calc_profit(rate=row2.close) lock_pair_until = row2.Index trades.append( ( pair, current_profit_percent, current_profit_btc, row2.Index - row.Index, current_profit_btc > 0, current_profit_btc < 0 ) ) break labels = ['currency', 'profit_percent', 'profit_BTC', 'duration', 'profit', 'loss'] return DataFrame.from_records(trades, columns=labels)
def _rpc_forcesell(self, trade_id: str) -> Dict[str, str]: """ Handler for forcesell <id>. Sells the given trade at current price """ def _exec_forcesell(trade: Trade) -> None: # Check if there is there is an open order if trade.open_order_id: order = self._freqtrade.exchange.get_order( trade.open_order_id, trade.pair) # Cancel open LIMIT_BUY orders and close trade if order and order['status'] == 'open' \ and order['type'] == 'limit' \ and order['side'] == 'buy': self._freqtrade.exchange.cancel_order( trade.open_order_id, trade.pair) trade.close(order.get('price') or trade.open_rate) # Do the best effort, if we don't know 'filled' amount, don't try selling if order['filled'] is None: return trade.amount = order['filled'] # Ignore trades with an attached LIMIT_SELL order if order and order['status'] == 'open' \ and order['type'] == 'limit' \ and order['side'] == 'sell': return # Get current rate and execute sell current_rate = self._freqtrade.get_sell_rate(trade.pair, False) self._freqtrade.execute_sell(trade, current_rate, SellType.FORCE_SELL) # ---- EOF def _exec_forcesell ---- if self._freqtrade.state != State.RUNNING: raise RPCException('trader is not running') with self._freqtrade._sell_lock: if trade_id == 'all': # Execute sell for all open orders for trade in Trade.get_open_trades(): _exec_forcesell(trade) Trade.session.flush() self._freqtrade.wallets.update() return {'result': 'Created sell orders for all open trades.'} # Query for trade trade = Trade.get_trades(trade_filter=[ Trade.id == trade_id, Trade.is_open.is_(True), ]).first() if not trade: logger.warning('forcesell: Invalid argument received') raise RPCException('invalid argument') _exec_forcesell(trade) Trade.session.flush() self._freqtrade.wallets.update() return {'result': f'Created sell order for trade {trade_id}.'}
def _rpc_trade_statistics( self, stake_currency: str, fiat_display_currency: str, start_date: datetime = datetime.fromtimestamp(0) ) -> Dict[str, Any]: """ Returns cumulative profit statistics """ trade_filter = ((Trade.is_open.is_(False) & (Trade.close_date >= start_date)) | Trade.is_open.is_(True)) trades = Trade.get_trades(trade_filter).order_by(Trade.id).all() profit_all_coin = [] profit_all_ratio = [] profit_closed_coin = [] profit_closed_ratio = [] durations = [] winning_trades = 0 losing_trades = 0 for trade in trades: current_rate: float = 0.0 if not trade.open_rate: continue if trade.close_date: durations.append( (trade.close_date - trade.open_date).total_seconds()) if not trade.is_open: profit_ratio = trade.close_profit profit_closed_coin.append(trade.close_profit_abs) profit_closed_ratio.append(profit_ratio) if trade.close_profit >= 0: winning_trades += 1 else: losing_trades += 1 else: # Get current rate try: current_rate = self._freqtrade.exchange.get_sell_rate( trade.pair, False) except (PricingError, ExchangeError): current_rate = NAN profit_ratio = trade.calc_profit_ratio(rate=current_rate) profit_all_coin.append( trade.calc_profit(rate=trade.close_rate or current_rate)) profit_all_ratio.append(profit_ratio) best_pair = Trade.get_best_pair(start_date) # Prepare data to display profit_closed_coin_sum = round(sum(profit_closed_coin), 8) profit_closed_ratio_mean = float( mean(profit_closed_ratio) if profit_closed_ratio else 0.0) profit_closed_ratio_sum = sum( profit_closed_ratio) if profit_closed_ratio else 0.0 profit_closed_fiat = self._fiat_converter.convert_amount( profit_closed_coin_sum, stake_currency, fiat_display_currency) if self._fiat_converter else 0 profit_all_coin_sum = round(sum(profit_all_coin), 8) profit_all_ratio_mean = float( mean(profit_all_ratio) if profit_all_ratio else 0.0) profit_all_ratio_sum = sum( profit_all_ratio) if profit_all_ratio else 0.0 profit_all_fiat = self._fiat_converter.convert_amount( profit_all_coin_sum, stake_currency, fiat_display_currency) if self._fiat_converter else 0 first_date = trades[0].open_date if trades else None last_date = trades[-1].open_date if trades else None num = float(len(durations) or 1) return { 'profit_closed_coin': profit_closed_coin_sum, 'profit_closed_percent_mean': round(profit_closed_ratio_mean * 100, 2), 'profit_closed_ratio_mean': profit_closed_ratio_mean, 'profit_closed_percent_sum': round(profit_closed_ratio_sum * 100, 2), 'profit_closed_ratio_sum': profit_closed_ratio_sum, 'profit_closed_fiat': profit_closed_fiat, 'profit_all_coin': profit_all_coin_sum, 'profit_all_percent_mean': round(profit_all_ratio_mean * 100, 2), 'profit_all_ratio_mean': profit_all_ratio_mean, 'profit_all_percent_sum': round(profit_all_ratio_sum * 100, 2), 'profit_all_ratio_sum': profit_all_ratio_sum, 'profit_all_fiat': profit_all_fiat, 'trade_count': len(trades), 'closed_trade_count': len([t for t in trades if not t.is_open]), 'first_trade_date': arrow.get(first_date).humanize() if first_date else '', 'first_trade_timestamp': int(first_date.timestamp() * 1000) if first_date else 0, 'latest_trade_date': arrow.get(last_date).humanize() if last_date else '', 'latest_trade_timestamp': int(last_date.timestamp() * 1000) if last_date else 0, 'avg_duration': str(timedelta(seconds=sum(durations) / num)).split('.')[0], 'best_pair': best_pair[0] if best_pair else '', 'best_rate': round(best_pair[1] * 100, 2) if best_pair else 0, 'winning_trades': winning_trades, 'losing_trades': losing_trades, }
def stop_loss_reached(self, current_rate: float, trade: Trade, current_time: datetime, current_profit: float, force_stoploss: float, high: float = None) -> SellCheckTuple: """ Based on current profit of the trade and configured (trailing) stoploss, decides to sell or not :param current_profit: current profit in percent """ trailing_stop = self.config.get('trailing_stop', False) stop_loss_value = force_stoploss if force_stoploss else self.stoploss # Initiate stoploss with open_rate. Does nothing if stoploss is already set. trade.adjust_stop_loss(trade.open_rate, stop_loss_value, initial=True) if trailing_stop: # trailing stoploss handling sl_offset = self.config.get('trailing_stop_positive_offset') or 0.0 tsl_only_offset = self.config.get( 'trailing_only_offset_is_reached', False) # Make sure current_profit is calculated using high for backtesting. high_profit = current_profit if not high else trade.calc_profit_percent( high) # Don't update stoploss if trailing_only_offset_is_reached is true. if not (tsl_only_offset and high_profit < sl_offset): # Specific handling for trailing_stop_positive if 'trailing_stop_positive' in self.config and high_profit > sl_offset: # Ignore mypy error check in configuration that this is a float stop_loss_value = self.config.get( 'trailing_stop_positive') # type: ignore logger.debug( f"{trade.pair} - Using positive stoploss: {stop_loss_value} " f"offset: {sl_offset:.4g} profit: {current_profit:.4f}%" ) trade.adjust_stop_loss(high or current_rate, stop_loss_value) # evaluate if the stoploss was hit if stoploss is not on exchange if ((self.stoploss is not None) and (trade.stop_loss >= current_rate) and (not self.order_types.get('stoploss_on_exchange'))): sell_type = SellType.STOP_LOSS # If initial stoploss is not the same as current one then it is trailing. if trade.initial_stop_loss != trade.stop_loss: sell_type = SellType.TRAILING_STOP_LOSS logger.debug( f"{trade.pair} - HIT STOP: current price at {current_rate:.6f}, " f"stoploss is {trade.stop_loss:.6f}, " f"initial stoploss was at {trade.initial_stop_loss:.6f}, " f"trade opened at {trade.open_rate:.6f}") logger.debug( f"{trade.pair} - Trailing stop saved " f"{trade.stop_loss - trade.initial_stop_loss:.6f}") return SellCheckTuple(sell_flag=True, sell_type=sell_type) return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE)
def test_calc_profit_ratio(limit_buy_order, limit_sell_order, fee): trade = Trade( pair='ETH/BTC', stake_amount=0.001, amount=5, open_rate=0.00001099, fee_open=fee.return_value, fee_close=fee.return_value, exchange='bittrex', ) trade.open_order_id = 'something' trade.update(limit_buy_order) # Buy @ 0.00001099 # Get percent of profit with a custom rate (Higher than open rate) assert trade.calc_profit_ratio(rate=0.00001234) == 0.11723875 # Get percent of profit with a custom rate (Lower than open rate) assert trade.calc_profit_ratio(rate=0.00000123) == -0.88863828 # Test when we apply a Sell order. Sell higher than open rate @ 0.00001173 trade.update(limit_sell_order) assert trade.calc_profit_ratio() == 0.06201058 # Test with a custom fee rate on the close trade assert trade.calc_profit_ratio(fee=0.003) == 0.06147824
def test_update_with_bittrex(limit_buy_order, limit_sell_order, fee, caplog): """ On this test we will buy and sell a crypto currency. Buy - Buy: 90.99181073 Crypto at 0.00001099 BTC (90.99181073*0.00001099 = 0.0009999 BTC) - Buying fee: 0.25% - Total cost of buy trade: 0.001002500 BTC ((90.99181073*0.00001099) + ((90.99181073*0.00001099)*0.0025)) Sell - Sell: 90.99181073 Crypto at 0.00001173 BTC (90.99181073*0.00001173 = 0,00106733394 BTC) - Selling fee: 0.25% - Total cost of sell trade: 0.001064666 BTC ((90.99181073*0.00001173) - ((90.99181073*0.00001173)*0.0025)) Profit/Loss: +0.000062166 BTC (Sell:0.001064666 - Buy:0.001002500) Profit/Loss percentage: 0.0620 ((0.001064666/0.001002500)-1 = 6.20%) :param limit_buy_order: :param limit_sell_order: :return: """ trade = Trade( id=2, pair='ETH/BTC', stake_amount=0.001, open_rate=0.01, amount=5, is_open=True, open_date=arrow.utcnow().datetime, fee_open=fee.return_value, fee_close=fee.return_value, exchange='bittrex', ) assert trade.open_order_id is None assert trade.close_profit is None assert trade.close_date is None trade.open_order_id = 'something' trade.update(limit_buy_order) assert trade.open_order_id is None assert trade.open_rate == 0.00001099 assert trade.close_profit is None assert trade.close_date is None assert log_has_re( r"LIMIT_BUY has been fulfilled for Trade\(id=2, " r"pair=ETH/BTC, amount=90.99181073, open_rate=0.00001099, open_since=.*\).", caplog) caplog.clear() trade.open_order_id = 'something' trade.update(limit_sell_order) assert trade.open_order_id is None assert trade.close_rate == 0.00001173 assert trade.close_profit == 0.06201058 assert trade.close_date is not None assert log_has_re( r"LIMIT_SELL has been fulfilled for Trade\(id=2, " r"pair=ETH/BTC, amount=90.99181073, open_rate=0.00001099, open_since=.*\).", caplog)
def test_calc_open_close_trade_price(limit_buy_order, limit_sell_order, fee): trade = Trade( pair='ETH/BTC', stake_amount=0.001, open_rate=0.01, amount=5, fee_open=fee.return_value, fee_close=fee.return_value, exchange='bittrex', ) trade.open_order_id = 'something' trade.update(limit_buy_order) assert trade._calc_open_trade_value() == 0.0010024999999225068 trade.update(limit_sell_order) assert trade.calc_close_trade_value() == 0.0010646656050132426 # Profit in BTC assert trade.calc_profit() == 0.00006217 # Profit in percent assert trade.calc_profit_ratio() == 0.06201058
def test_calc_profit(limit_buy_order, limit_sell_order, fee): trade = Trade( pair='ETH/BTC', stake_amount=0.001, amount=5, open_rate=0.00001099, fee_open=fee.return_value, fee_close=fee.return_value, exchange='bittrex', ) trade.open_order_id = 'something' trade.update(limit_buy_order) # Buy @ 0.00001099 # Custom closing rate and regular fee rate # Higher than open rate assert trade.calc_profit(rate=0.00001234) == 0.00011753 # Lower than open rate assert trade.calc_profit(rate=0.00000123) == -0.00089086 # Custom closing rate and custom fee rate # Higher than open rate assert trade.calc_profit(rate=0.00001234, fee=0.003) == 0.00011697 # Lower than open rate assert trade.calc_profit(rate=0.00000123, fee=0.003) == -0.00089092 # Test when we apply a Sell order. Sell higher than open rate @ 0.00001173 trade.update(limit_sell_order) assert trade.calc_profit() == 0.00006217 # Test with a custom fee rate on the close trade assert trade.calc_profit(fee=0.003) == 0.00006163
def test_fee_updated(fee): trade = Trade( pair='ETH/BTC', stake_amount=0.001, fee_open=fee.return_value, open_date=arrow.utcnow().shift(hours=-2).datetime, amount=10, fee_close=fee.return_value, exchange='bittrex', open_rate=1, max_rate=1, ) assert trade.fee_open_currency is None assert not trade.fee_updated('buy') assert not trade.fee_updated('sell') assert not trade.fee_updated('asdf') trade.update_fee(0.15, 'BTC', 0.0075, 'buy') assert trade.fee_updated('buy') assert not trade.fee_updated('sell') assert trade.fee_open_currency is not None assert trade.fee_close_currency is None trade.update_fee(0.15, 'ABC', 0.0075, 'sell') assert trade.fee_updated('buy') assert trade.fee_updated('sell') assert not trade.fee_updated('asfd')
def stop_loss_reached(self, current_rate: float, trade: Trade, current_time: datetime, current_profit: float, force_stoploss: float, low: float = None, high: float = None) -> SellCheckTuple: """ Based on current profit of the trade and configured (trailing) stoploss, decides to sell or not :param current_profit: current profit as ratio :param low: Low value of this candle, only set in backtesting :param high: High value of this candle, only set in backtesting """ stop_loss_value = force_stoploss if force_stoploss else self.stoploss # Initiate stoploss with open_rate. Does nothing if stoploss is already set. trade.adjust_stop_loss(trade.open_rate, stop_loss_value, initial=True) if self.use_custom_stoploss and trade.stop_loss < (low or current_rate): stop_loss_value = strategy_safe_wrapper(self.custom_stoploss, default_retval=None )(pair=trade.pair, trade=trade, current_time=current_time, current_rate=current_rate, current_profit=current_profit) # Sanity check - error cases will return None if stop_loss_value: # logger.info(f"{trade.pair} {stop_loss_value=} {current_profit=}") trade.adjust_stop_loss(current_rate, stop_loss_value) else: logger.warning("CustomStoploss function did not return valid stoploss") if self.trailing_stop and trade.stop_loss < (low or current_rate): # trailing stoploss handling sl_offset = self.trailing_stop_positive_offset # Make sure current_profit is calculated using high for backtesting. high_profit = current_profit if not high else trade.calc_profit_ratio(high) # Don't update stoploss if trailing_only_offset_is_reached is true. if not (self.trailing_only_offset_is_reached and high_profit < sl_offset): # Specific handling for trailing_stop_positive if self.trailing_stop_positive is not None and high_profit > sl_offset: stop_loss_value = self.trailing_stop_positive logger.debug(f"{trade.pair} - Using positive stoploss: {stop_loss_value} " f"offset: {sl_offset:.4g} profit: {current_profit:.2%}") trade.adjust_stop_loss(high or current_rate, stop_loss_value) # evaluate if the stoploss was hit if stoploss is not on exchange # in Dry-Run, this handles stoploss logic as well, as the logic will not be different to # regular stoploss handling. if ((trade.stop_loss >= (low or current_rate)) and (not self.order_types.get('stoploss_on_exchange') or self.config['dry_run'])): sell_type = SellType.STOP_LOSS # If initial stoploss is not the same as current one then it is trailing. if trade.initial_stop_loss != trade.stop_loss: sell_type = SellType.TRAILING_STOP_LOSS logger.debug( f"{trade.pair} - HIT STOP: current price at {(low or current_rate):.6f}, " f"stoploss is {trade.stop_loss:.6f}, " f"initial stoploss was at {trade.initial_stop_loss:.6f}, " f"trade opened at {trade.open_rate:.6f}") logger.debug(f"{trade.pair} - Trailing stop saved " f"{trade.stop_loss - trade.initial_stop_loss:.6f}") return SellCheckTuple(sell_type=sell_type) return SellCheckTuple(sell_type=SellType.NONE)
def should_sell(self, trade: Trade, rate: float, date: datetime, buy: bool, sell: bool, low: float = None, high: float = None, force_stoploss: float = 0) -> SellCheckTuple: """ This function evaluates if one of the conditions required to trigger a sell has been reached, which can either be a stop-loss, ROI or sell-signal. :param low: Only used during backtesting to simulate stoploss :param high: Only used during backtesting, to simulate ROI :param force_stoploss: Externally provided stoploss :return: True if trade should be sold, False otherwise """ current_rate = rate current_profit = trade.calc_profit_ratio(current_rate) trade.adjust_min_max_rates(high or current_rate, low or current_rate) stoplossflag = self.stop_loss_reached(current_rate=current_rate, trade=trade, current_time=date, current_profit=current_profit, force_stoploss=force_stoploss, low=low, high=high) # Set current rate to high for backtesting sell current_rate = high or rate current_profit = trade.calc_profit_ratio(current_rate) # if buy signal and ignore_roi is set, we don't need to evaluate min_roi. roi_reached = (not (buy and self.ignore_roi_if_buy_signal) and self.min_roi_reached(trade=trade, current_profit=current_profit, current_time=date)) sell_signal = SellType.NONE custom_reason = '' # use provided rate in backtesting, not high/low. current_rate = rate current_profit = trade.calc_profit_ratio(current_rate) if (self.sell_profit_only and current_profit <= self.sell_profit_offset): # sell_profit_only and profit doesn't reach the offset - ignore sell signal pass elif self.use_sell_signal and not buy: 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) 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 # Start evaluations # Sequence: # ROI (if not stoploss) # Sell-signal # Stoploss if roi_reached and stoplossflag.sell_type != SellType.STOP_LOSS: logger.debug(f"{trade.pair} - Required profit reached. sell_type=SellType.ROI") return SellCheckTuple(sell_type=SellType.ROI) if sell_signal != SellType.NONE: logger.debug(f"{trade.pair} - Sell signal received. " f"sell_type=SellType.{sell_signal.name}" + (f", custom_reason={custom_reason}" if custom_reason else "")) return SellCheckTuple(sell_type=sell_signal, sell_reason=custom_reason) if stoplossflag.sell_flag: logger.debug(f"{trade.pair} - Stoploss hit. sell_type={stoplossflag.sell_type}") return stoplossflag # This one is noisy, commented out... # logger.debug(f"{trade.pair} - No sell signal.") return SellCheckTuple(sell_type=SellType.NONE)
def __init__(self, config: Dict[str, Any]) -> None: LoggingMixin.show_output = False self.config = config # Reset keys for backtesting remove_credentials(self.config) self.strategylist: List[IStrategy] = [] self.all_results: Dict[str, Dict] = {} self.exchange = ExchangeResolver.load_exchange( self.config['exchange']['name'], self.config) self.dataprovider = DataProvider(self.config, None) if self.config.get('strategy_list', None): for strat in list(self.config['strategy_list']): stratconf = deepcopy(self.config) stratconf['strategy'] = strat self.strategylist.append( StrategyResolver.load_strategy(stratconf)) validate_config_consistency(stratconf) else: # No strategy list specified, only one strategy self.strategylist.append( StrategyResolver.load_strategy(self.config)) validate_config_consistency(self.config) if "timeframe" not in self.config: raise OperationalException( "Timeframe (ticker interval) needs to be set in either " "configuration or as cli argument `--timeframe 5m`") self.timeframe = str(self.config.get('timeframe')) self.timeframe_min = timeframe_to_minutes(self.timeframe) self.pairlists = PairListManager(self.exchange, self.config) if 'VolumePairList' in self.pairlists.name_list: raise OperationalException( "VolumePairList not allowed for backtesting.") if 'PerformanceFilter' in self.pairlists.name_list: raise OperationalException( "PerformanceFilter not allowed for backtesting.") if len(self.strategylist ) > 1 and 'PrecisionFilter' in self.pairlists.name_list: raise OperationalException( "PrecisionFilter not allowed for backtesting multiple strategies." ) self.dataprovider.add_pairlisthandler(self.pairlists) self.pairlists.refresh_pairlist() if len(self.pairlists.whitelist) == 0: raise OperationalException("No pair in whitelist.") if config.get('fee', None) is not None: self.fee = config['fee'] else: self.fee = self.exchange.get_fee( symbol=self.pairlists.whitelist[0]) Trade.use_db = False Trade.reset_trades() PairLocks.timeframe = self.config['timeframe'] PairLocks.use_db = False PairLocks.reset_locks() self.wallets = Wallets(self.config, self.exchange, log=False) # Get maximum required startup period self.required_startup = max( [strat.startup_candle_count for strat in self.strategylist])
def test_adjust_stop_loss(fee): trade = Trade( pair='ETH/BTC', stake_amount=0.001, amount=5, fee_open=fee.return_value, fee_close=fee.return_value, exchange='bittrex', open_rate=1, max_rate=1, ) trade.adjust_stop_loss(trade.open_rate, 0.05, True) assert trade.stop_loss == 0.95 assert trade.stop_loss_pct == -0.05 assert trade.initial_stop_loss == 0.95 assert trade.initial_stop_loss_pct == -0.05 # Get percent of profit with a lower rate trade.adjust_stop_loss(0.96, 0.05) assert trade.stop_loss == 0.95 assert trade.stop_loss_pct == -0.05 assert trade.initial_stop_loss == 0.95 assert trade.initial_stop_loss_pct == -0.05 # Get percent of profit with a custom rate (Higher than open rate) trade.adjust_stop_loss(1.3, -0.1) assert round(trade.stop_loss, 8) == 1.17 assert trade.stop_loss_pct == -0.1 assert trade.initial_stop_loss == 0.95 assert trade.initial_stop_loss_pct == -0.05 # current rate lower again ... should not change trade.adjust_stop_loss(1.2, 0.1) assert round(trade.stop_loss, 8) == 1.17 assert trade.initial_stop_loss == 0.95 assert trade.initial_stop_loss_pct == -0.05 # current rate higher... should raise stoploss trade.adjust_stop_loss(1.4, 0.1) assert round(trade.stop_loss, 8) == 1.26 assert trade.initial_stop_loss == 0.95 assert trade.initial_stop_loss_pct == -0.05 # Initial is true but stop_loss set - so doesn't do anything trade.adjust_stop_loss(1.7, 0.1, True) assert round(trade.stop_loss, 8) == 1.26 assert trade.initial_stop_loss == 0.95 assert trade.initial_stop_loss_pct == -0.05 assert trade.stop_loss_pct == -0.1
def _rpc_trade_status(self, trade_ids: List[int] = []) -> List[Dict[str, Any]]: """ Below follows the RPC backend it is prefixed with rpc_ to raise awareness that it is a remotely exposed function """ # Fetch open trades if trade_ids: trades = Trade.get_trades( trade_filter=Trade.id.in_(trade_ids)).all() else: trades = Trade.get_open_trades() if not trades: raise RPCException('no active trade') else: results = [] for trade in trades: order = None if trade.open_order_id: order = self._freqtrade.exchange.fetch_order( trade.open_order_id, trade.pair) # calculate profit and send message to user if trade.is_open: try: current_rate = self._freqtrade.exchange.get_sell_rate( trade.pair, False) except (ExchangeError, PricingError): current_rate = NAN else: current_rate = trade.close_rate current_profit = trade.calc_profit_ratio(current_rate) current_profit_abs = trade.calc_profit(current_rate) current_profit_fiat: Optional[float] = None # Calculate fiat profit if self._fiat_converter: current_profit_fiat = self._fiat_converter.convert_amount( current_profit_abs, self._freqtrade.config['stake_currency'], self._freqtrade.config['fiat_display_currency']) # Calculate guaranteed profit (in case of trailing stop) stoploss_entry_dist = trade.calc_profit(trade.stop_loss) stoploss_entry_dist_ratio = trade.calc_profit_ratio( trade.stop_loss) # calculate distance to stoploss stoploss_current_dist = trade.stop_loss - current_rate stoploss_current_dist_ratio = stoploss_current_dist / current_rate trade_dict = trade.to_json() trade_dict.update( dict( base_currency=self._freqtrade.config['stake_currency'], close_profit=trade.close_profit if trade.close_profit is not None else None, current_rate=current_rate, current_profit=current_profit, # Deprecated current_profit_pct=round(current_profit * 100, 2), # Deprecated current_profit_abs=current_profit_abs, # Deprecated profit_ratio=current_profit, profit_pct=round(current_profit * 100, 2), profit_abs=current_profit_abs, profit_fiat=current_profit_fiat, stoploss_current_dist=stoploss_current_dist, stoploss_current_dist_ratio=round( stoploss_current_dist_ratio, 8), stoploss_current_dist_pct=round( stoploss_current_dist_ratio * 100, 2), stoploss_entry_dist=stoploss_entry_dist, stoploss_entry_dist_ratio=round( stoploss_entry_dist_ratio, 8), open_order='({} {} rem={:.8f})'.format( order['type'], order['side'], order['remaining']) if order else None, )) results.append(trade_dict) return results
def test_api_performance(botclient, fee): ftbot, client = botclient patch_get_signal(ftbot, (True, False, None)) trade = Trade( pair='LTC/ETH', amount=1, exchange='binance', stake_amount=1, open_rate=0.245441, open_order_id="123456", is_open=False, fee_close=fee.return_value, fee_open=fee.return_value, close_rate=0.265441, ) trade.close_profit = trade.calc_profit_ratio() trade.close_profit_abs = trade.calc_profit() Trade.query.session.add(trade) trade = Trade( pair='XRP/ETH', amount=5, stake_amount=1, exchange='binance', open_rate=0.412, open_order_id="123456", is_open=False, fee_close=fee.return_value, fee_open=fee.return_value, close_rate=0.391 ) trade.close_profit = trade.calc_profit_ratio() trade.close_profit_abs = trade.calc_profit() Trade.query.session.add(trade) Trade.query.session.flush() rc = client_get(client, f"{BASE_URI}/performance") assert_response(rc) assert len(rc.json()) == 2 assert rc.json() == [{'count': 1, 'pair': 'LTC/ETH', 'profit': 7.61, 'profit_abs': 0.01872279}, {'count': 1, 'pair': 'XRP/ETH', 'profit': -5.57, 'profit_abs': -0.1150375}]
def should_sell(self, trade: Trade, rate: float, date: datetime, buy: bool, sell: bool, low: float = None, high: float = None, force_stoploss: float = 0) -> SellCheckTuple: """ This function evaluates if one of the conditions required to trigger a sell has been reached, which can either be a stop-loss, ROI or sell-signal. :param low: Only used during backtesting to simulate stoploss :param high: Only used during backtesting, to simulate ROI :param force_stoploss: Externally provided stoploss :return: True if trade should be sold, False otherwise """ # Set current rate to low for backtesting sell current_rate = low or rate current_profit = trade.calc_profit_percent(current_rate) trade.adjust_min_max_rates(high or current_rate) stoplossflag = self.stop_loss_reached(current_rate=current_rate, trade=trade, current_time=date, current_profit=current_profit, force_stoploss=force_stoploss, high=high) if stoplossflag.sell_flag: logger.debug(f"{trade.pair} - Stoploss hit. sell_flag=True, " f"sell_type={stoplossflag.sell_type}") return stoplossflag # Set current rate to high for backtesting sell current_rate = high or rate current_profit = trade.calc_profit_percent(current_rate) experimental = self.config.get('experimental', {}) if buy and experimental.get('ignore_roi_if_buy_signal', False): # This one is noisy, commented out # logger.debug(f"{trade.pair} - Buy signal still active. sell_flag=False") return SellCheckTuple(sell_flag=False, sell_type=SellType.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( f"{trade.pair} - Required profit reached. sell_flag=True, " f"sell_type=SellType.ROI") return SellCheckTuple(sell_flag=True, sell_type=SellType.ROI) if experimental.get('sell_profit_only', False): # This one is noisy, commented out # logger.debug(f"{trade.pair} - Checking if trade is profitable...") if trade.calc_profit(rate=rate) <= 0: # This one is noisy, commented out # logger.debug(f"{trade.pair} - Trade is not profitable. sell_flag=False") return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE) if sell and not buy and experimental.get('use_sell_signal', False): logger.debug( f"{trade.pair} - Sell signal received. sell_flag=True, " f"sell_type=SellType.SELL_SIGNAL") return SellCheckTuple(sell_flag=True, sell_type=SellType.SELL_SIGNAL) # This one is noisy, commented out... # logger.debug(f"{trade.pair} - No sell signal. sell_flag=False") return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE)
def test_api_forcebuy(botclient, mocker, fee): ftbot, client = botclient rc = client_post(client, f"{BASE_URI}/forcebuy", data='{"pair": "ETH/BTC"}') assert_response(rc, 502) assert rc.json == {"error": "Error querying _forcebuy: Forcebuy not enabled."} # enable forcebuy ftbot.config["forcebuy_enable"] = True fbuy_mock = MagicMock(return_value=None) mocker.patch("freqtrade.rpc.RPC._rpc_forcebuy", fbuy_mock) rc = client_post(client, f"{BASE_URI}/forcebuy", data='{"pair": "ETH/BTC"}') assert_response(rc) assert rc.json == {"status": "Error buying pair ETH/BTC."} # Test creating trae fbuy_mock = MagicMock(return_value=Trade( pair='ETH/ETH', amount=1, exchange='bittrex', stake_amount=1, open_rate=0.245441, open_order_id="123456", open_date=datetime.utcnow(), is_open=False, fee_close=fee.return_value, fee_open=fee.return_value, close_rate=0.265441, )) mocker.patch("freqtrade.rpc.RPC._rpc_forcebuy", fbuy_mock) rc = client_post(client, f"{BASE_URI}/forcebuy", data='{"pair": "ETH/BTC"}') assert_response(rc) assert rc.json == {'amount': 1, 'trade_id': None, 'close_date': None, 'close_date_hum': None, 'close_timestamp': None, 'close_rate': 0.265441, 'open_date': ANY, 'open_date_hum': 'just now', 'open_timestamp': ANY, 'open_rate': 0.245441, 'pair': 'ETH/ETH', 'stake_amount': 1, 'stop_loss': None, 'stop_loss_abs': None, 'stop_loss_pct': None, 'stop_loss_ratio': None, 'stoploss_order_id': None, 'stoploss_last_update': None, 'stoploss_last_update_timestamp': None, 'initial_stop_loss': None, 'initial_stop_loss_abs': None, 'initial_stop_loss_pct': None, 'initial_stop_loss_ratio': None, 'close_profit': None, 'close_profit_abs': None, 'close_rate_requested': None, 'fee_close': 0.0025, 'fee_close_cost': None, 'fee_close_currency': None, 'fee_open': 0.0025, 'fee_open_cost': None, 'fee_open_currency': None, 'is_open': False, 'max_rate': None, 'min_rate': None, 'open_order_id': '123456', 'open_rate_requested': None, 'open_trade_price': 0.2460546025, 'sell_reason': None, 'sell_order_status': None, 'strategy': None, 'ticker_interval': None, 'timeframe': None, 'exchange': 'bittrex', }
def handle_stoploss_on_exchange(self, trade: Trade) -> bool: """ Check if trade is fulfilled in which case the stoploss on exchange should be added immediately if stoploss on exchange is enabled. """ logger.debug('Handling stoploss on exchange %s ...', trade) stoploss_order = None try: # First we check if there is already a stoploss on exchange stoploss_order = self.exchange.get_order(trade.stoploss_order_id, trade.pair) \ if trade.stoploss_order_id else None except InvalidOrderException as exception: logger.warning('Unable to fetch stoploss order: %s', exception) # If trade open order id does not exist: buy order is fulfilled buy_order_fulfilled = not trade.open_order_id # Limit price threshold: As limit price should always be below price limit_price_pct = 0.99 # If buy order is fulfilled but there is no stoploss, we add a stoploss on exchange if (buy_order_fulfilled and not stoploss_order): if self.edge: stoploss = self.edge.stoploss(pair=trade.pair) else: stoploss = self.strategy.stoploss stop_price = trade.open_rate * (1 + stoploss) # limit price should be less than stop price. limit_price = stop_price * limit_price_pct try: stoploss_order_id = self.exchange.stoploss_limit( pair=trade.pair, amount=trade.amount, stop_price=stop_price, rate=limit_price)['id'] trade.stoploss_order_id = str(stoploss_order_id) trade.stoploss_last_update = datetime.now() return False except DependencyException as exception: logger.warning( 'Unable to place a stoploss order on exchange: %s', exception) # If stoploss order is canceled for some reason we add it if stoploss_order and stoploss_order['status'] == 'canceled': try: stoploss_order_id = self.exchange.stoploss_limit( pair=trade.pair, amount=trade.amount, stop_price=trade.stop_loss, rate=trade.stop_loss * limit_price_pct)['id'] trade.stoploss_order_id = str(stoploss_order_id) return False except DependencyException as exception: logger.warning( 'Stoploss order was cancelled, ' 'but unable to recreate one: %s', exception) # We check if stoploss order is fulfilled if stoploss_order and stoploss_order['status'] == 'closed': trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value trade.update(stoploss_order) self._notify_sell(trade) return True # Finally we check if stoploss on exchange should be moved up because of trailing. if stoploss_order and self.config.get('trailing_stop', False): # if trailing stoploss is enabled we check if stoploss value has changed # in which case we cancel stoploss order and put another one with new # value immediately self.handle_trailing_stoploss_on_exchange(trade, stoploss_order) return False
def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, get_fee=fee, ) freqtradebot = get_patched_freqtradebot(mocker, default_conf) patch_get_signal(freqtradebot) rpc = RPC(freqtradebot) freqtradebot.state = State.RUNNING with pytest.raises(RPCException, match=r'.*no active trade*'): rpc._rpc_trade_status() freqtradebot.enter_positions() trades = Trade.get_open_trades() trades[0].open_order_id = None freqtradebot.exit_positions(trades) results = rpc._rpc_trade_status() assert results[0] == { 'trade_id': 1, 'pair': 'ETH/BTC', 'base_currency': 'BTC', 'open_date': ANY, 'open_timestamp': ANY, 'is_open': ANY, 'fee_open': ANY, 'fee_open_cost': ANY, 'fee_open_currency': ANY, 'fee_close': fee.return_value, 'fee_close_cost': ANY, 'fee_close_currency': ANY, 'open_rate_requested': ANY, 'open_trade_value': 0.0010025, 'close_rate_requested': ANY, 'sell_reason': ANY, 'sell_order_status': ANY, 'min_rate': ANY, 'max_rate': ANY, 'strategy': ANY, 'buy_tag': ANY, 'timeframe': 5, 'open_order_id': ANY, 'close_date': None, 'close_timestamp': None, 'open_rate': 1.098e-05, 'close_rate': None, 'current_rate': 1.099e-05, 'amount': 91.07468123, 'amount_requested': 91.07468123, 'stake_amount': 0.001, 'trade_duration': None, 'trade_duration_s': None, 'close_profit': None, 'close_profit_pct': None, 'close_profit_abs': None, 'current_profit': -0.00408133, 'current_profit_pct': -0.41, 'current_profit_abs': -4.09e-06, 'profit_ratio': -0.00408133, 'profit_pct': -0.41, 'profit_abs': -4.09e-06, 'profit_fiat': ANY, 'stop_loss_abs': 9.882e-06, 'stop_loss_pct': -10.0, 'stop_loss_ratio': -0.1, 'stoploss_order_id': None, 'stoploss_last_update': ANY, 'stoploss_last_update_timestamp': ANY, 'initial_stop_loss_abs': 9.882e-06, 'initial_stop_loss_pct': -10.0, 'initial_stop_loss_ratio': -0.1, 'stoploss_current_dist': -1.1080000000000002e-06, 'stoploss_current_dist_ratio': -0.10081893, 'stoploss_current_dist_pct': -10.08, 'stoploss_entry_dist': -0.00010475, 'stoploss_entry_dist_ratio': -0.10448878, 'open_order': None, 'exchange': 'binance', } mocker.patch( 'freqtrade.exchange.Exchange.get_rate', MagicMock(side_effect=ExchangeError("Pair 'ETH/BTC' not available"))) results = rpc._rpc_trade_status() assert isnan(results[0]['current_profit']) assert isnan(results[0]['current_rate']) assert results[0] == { 'trade_id': 1, 'pair': 'ETH/BTC', 'base_currency': 'BTC', 'open_date': ANY, 'open_timestamp': ANY, 'is_open': ANY, 'fee_open': ANY, 'fee_open_cost': ANY, 'fee_open_currency': ANY, 'fee_close': fee.return_value, 'fee_close_cost': ANY, 'fee_close_currency': ANY, 'open_rate_requested': ANY, 'open_trade_value': ANY, 'close_rate_requested': ANY, 'sell_reason': ANY, 'sell_order_status': ANY, 'min_rate': ANY, 'max_rate': ANY, 'strategy': ANY, 'buy_tag': ANY, 'timeframe': ANY, 'open_order_id': ANY, 'close_date': None, 'close_timestamp': None, 'open_rate': 1.098e-05, 'close_rate': None, 'current_rate': ANY, 'amount': 91.07468123, 'amount_requested': 91.07468123, 'trade_duration': ANY, 'trade_duration_s': ANY, 'stake_amount': 0.001, 'close_profit': None, 'close_profit_pct': None, 'close_profit_abs': None, 'current_profit': ANY, 'current_profit_pct': ANY, 'current_profit_abs': ANY, 'profit_ratio': ANY, 'profit_pct': ANY, 'profit_abs': ANY, 'profit_fiat': ANY, 'stop_loss_abs': 9.882e-06, 'stop_loss_pct': -10.0, 'stop_loss_ratio': -0.1, 'stoploss_order_id': None, 'stoploss_last_update': ANY, 'stoploss_last_update_timestamp': ANY, 'initial_stop_loss_abs': 9.882e-06, 'initial_stop_loss_pct': -10.0, 'initial_stop_loss_ratio': -0.1, 'stoploss_current_dist': ANY, 'stoploss_current_dist_ratio': ANY, 'stoploss_current_dist_pct': ANY, 'stoploss_entry_dist': -0.00010475, 'stoploss_entry_dist_ratio': -0.10448878, 'open_order': None, 'exchange': 'binance', }
def test_get_open(fee): create_mock_trades(fee) assert len(Trade.get_open_trades()) == 4
def _rpc_trade_statistics(self, stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]: """ Returns cumulative profit statistics """ trades = Trade.get_trades().order_by(Trade.id).all() profit_all_coin = [] profit_all_ratio = [] profit_closed_coin = [] profit_closed_ratio = [] durations = [] for trade in trades: current_rate: float = 0.0 if not trade.open_rate: continue if trade.close_date: durations.append( (trade.close_date - trade.open_date).total_seconds()) if not trade.is_open: profit_ratio = trade.close_profit profit_closed_coin.append(trade.close_profit_abs) profit_closed_ratio.append(profit_ratio) else: # Get current rate try: current_rate = self._freqtrade.get_sell_rate( trade.pair, False) except DependencyException: current_rate = NAN profit_ratio = trade.calc_profit_ratio(rate=current_rate) profit_all_coin.append( trade.calc_profit(rate=trade.close_rate or current_rate)) profit_all_ratio.append(profit_ratio) best_pair = Trade.get_best_pair() if not best_pair: raise RPCException('no closed trade') bp_pair, bp_rate = best_pair # Prepare data to display profit_closed_coin_sum = round(sum(profit_closed_coin), 8) profit_closed_percent = (round(mean(profit_closed_ratio) * 100, 2) if profit_closed_ratio else 0.0) profit_closed_fiat = self._fiat_converter.convert_amount( profit_closed_coin_sum, stake_currency, fiat_display_currency) if self._fiat_converter else 0 profit_all_coin_sum = round(sum(profit_all_coin), 8) profit_all_percent = round(mean(profit_all_ratio) * 100, 2) if profit_all_ratio else 0.0 profit_all_fiat = self._fiat_converter.convert_amount( profit_all_coin_sum, stake_currency, fiat_display_currency) if self._fiat_converter else 0 num = float(len(durations) or 1) return { 'profit_closed_coin': profit_closed_coin_sum, 'profit_closed_percent': profit_closed_percent, 'profit_closed_fiat': profit_closed_fiat, 'profit_all_coin': profit_all_coin_sum, 'profit_all_percent': profit_all_percent, 'profit_all_fiat': profit_all_fiat, 'trade_count': len(trades), 'first_trade_date': arrow.get(trades[0].open_date).humanize(), 'latest_trade_date': arrow.get(trades[-1].open_date).humanize(), 'avg_duration': str(timedelta(seconds=sum(durations) / num)).split('.')[0], 'best_pair': bp_pair, 'best_rate': round(bp_rate * 100, 2), }
def test_api_status(botclient, mocker, ticker, fee, markets): ftbot, client = botclient patch_get_signal(ftbot, (True, False)) mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_balances=MagicMock(return_value=ticker), fetch_ticker=ticker, get_fee=fee, markets=PropertyMock(return_value=markets) ) rc = client_get(client, f"{BASE_URI}/status") assert_response(rc, 200) assert rc.json == [] ftbot.enter_positions() trades = Trade.get_open_trades() trades[0].open_order_id = None ftbot.exit_positions(trades) rc = client_get(client, f"{BASE_URI}/status") assert_response(rc) assert len(rc.json) == 1 assert rc.json == [{'amount': 91.07468124, 'base_currency': 'BTC', 'close_date': None, 'close_date_hum': None, 'close_timestamp': None, 'close_profit': None, 'close_profit_pct': None, 'close_profit_abs': None, 'close_rate': None, 'current_profit': -0.00408133, 'current_profit_pct': -0.41, 'current_profit_abs': -4.09e-06, 'current_rate': 1.099e-05, 'open_date': ANY, 'open_date_hum': 'just now', 'open_timestamp': ANY, 'open_order': None, 'open_rate': 1.098e-05, 'pair': 'ETH/BTC', 'stake_amount': 0.001, 'stop_loss': 9.882e-06, 'stop_loss_abs': 9.882e-06, 'stop_loss_pct': -10.0, 'stop_loss_ratio': -0.1, 'stoploss_order_id': None, 'stoploss_last_update': ANY, 'stoploss_last_update_timestamp': ANY, 'initial_stop_loss': 9.882e-06, 'initial_stop_loss_abs': 9.882e-06, 'initial_stop_loss_pct': -10.0, 'initial_stop_loss_ratio': -0.1, 'stoploss_current_dist': -1.1080000000000002e-06, 'stoploss_current_dist_ratio': -0.10081893, 'stoploss_entry_dist': -0.00010475, 'stoploss_entry_dist_ratio': -0.10448878, 'trade_id': 1, 'close_rate_requested': None, 'current_rate': 1.099e-05, 'fee_close': 0.0025, 'fee_close_cost': None, 'fee_close_currency': None, 'fee_open': 0.0025, 'fee_open_cost': None, 'fee_open_currency': None, 'open_date': ANY, 'is_open': True, 'max_rate': 1.099e-05, 'min_rate': 1.098e-05, 'open_order_id': None, 'open_rate_requested': 1.098e-05, 'open_trade_price': 0.0010025, 'sell_reason': None, 'sell_order_status': None, 'strategy': 'DefaultStrategy', 'ticker_interval': 5, 'timeframe': 5, 'exchange': 'bittrex', }]
def test_to_json(default_conf, fee): # Simulate dry_run entries trade = Trade(pair='ETH/BTC', stake_amount=0.001, amount=123.0, amount_requested=123.0, fee_open=fee.return_value, fee_close=fee.return_value, open_date=arrow.utcnow().shift(hours=-2).datetime, open_rate=0.123, exchange='bittrex', open_order_id='dry_run_buy_12345') result = trade.to_json() assert isinstance(result, dict) assert result == { 'trade_id': None, 'pair': 'ETH/BTC', 'is_open': None, 'open_date': trade.open_date.strftime("%Y-%m-%d %H:%M:%S"), 'open_timestamp': int(trade.open_date.timestamp() * 1000), 'open_order_id': 'dry_run_buy_12345', 'close_date': None, 'close_timestamp': None, 'open_rate': 0.123, 'open_rate_requested': None, 'open_trade_value': 15.1668225, 'fee_close': 0.0025, 'fee_close_cost': None, 'fee_close_currency': None, 'fee_open': 0.0025, 'fee_open_cost': None, 'fee_open_currency': None, 'close_rate': None, 'close_rate_requested': None, 'amount': 123.0, 'amount_requested': 123.0, 'stake_amount': 0.001, 'trade_duration': None, 'trade_duration_s': None, 'close_profit': None, 'close_profit_pct': None, 'close_profit_abs': None, 'profit_ratio': None, 'profit_pct': None, 'profit_abs': None, 'sell_reason': None, 'sell_order_status': None, 'stop_loss_abs': None, 'stop_loss_ratio': None, 'stop_loss_pct': None, 'stoploss_order_id': None, 'stoploss_last_update': None, 'stoploss_last_update_timestamp': None, 'initial_stop_loss_abs': None, 'initial_stop_loss_pct': None, 'initial_stop_loss_ratio': None, 'min_rate': None, 'max_rate': None, 'strategy': None, 'timeframe': None, 'exchange': 'bittrex', } # Simulate dry_run entries trade = Trade( pair='XRP/BTC', stake_amount=0.001, amount=100.0, amount_requested=101.0, fee_open=fee.return_value, fee_close=fee.return_value, open_date=arrow.utcnow().shift(hours=-2).datetime, close_date=arrow.utcnow().shift(hours=-1).datetime, open_rate=0.123, close_rate=0.125, exchange='bittrex', ) result = trade.to_json() assert isinstance(result, dict) assert result == { 'trade_id': None, 'pair': 'XRP/BTC', 'open_date': trade.open_date.strftime("%Y-%m-%d %H:%M:%S"), 'open_timestamp': int(trade.open_date.timestamp() * 1000), 'close_date': trade.close_date.strftime("%Y-%m-%d %H:%M:%S"), 'close_timestamp': int(trade.close_date.timestamp() * 1000), 'open_rate': 0.123, 'close_rate': 0.125, 'amount': 100.0, 'amount_requested': 101.0, 'stake_amount': 0.001, 'trade_duration': 60, 'trade_duration_s': 3600, 'stop_loss_abs': None, 'stop_loss_pct': None, 'stop_loss_ratio': None, 'stoploss_order_id': None, 'stoploss_last_update': None, 'stoploss_last_update_timestamp': None, 'initial_stop_loss_abs': None, 'initial_stop_loss_pct': None, 'initial_stop_loss_ratio': None, 'close_profit': None, 'close_profit_pct': None, 'close_profit_abs': None, 'profit_ratio': None, 'profit_pct': None, 'profit_abs': None, 'close_rate_requested': None, 'fee_close': 0.0025, 'fee_close_cost': None, 'fee_close_currency': None, 'fee_open': 0.0025, 'fee_open_cost': None, 'fee_open_currency': None, 'is_open': None, 'max_rate': None, 'min_rate': None, 'open_order_id': None, 'open_rate_requested': None, 'open_trade_value': 12.33075, 'sell_reason': None, 'sell_order_status': None, 'strategy': None, 'timeframe': None, 'exchange': 'bittrex', }
def populate_trades(self, pair: str) -> dict: # Initialize the trades dict if it doesn't exist, persist it otherwise if not pair in self.custom_trade_info: self.custom_trade_info[pair] = {} # init the temp dicts and set the trade stuff to false trade_data = {} trade_data['active_trade'] = trade_data['other_trades'] = trade_data[ 'biggest_loser'] = False self.custom_trade_info['meta'] = {} # active trade stuff only works in live and dry, not backtest if self.config['runmode'].value in ('live', 'dry_run'): # find out if we have an open trade for this pair active_trade = Trade.get_trades([ Trade.pair == pair, Trade.is_open.is_(True), ]).all() # if so, get some information if active_trade: # get current price and update the min/max rate current_rate = self.get_current_price(pair, True) active_trade[0].adjust_min_max_rates(current_rate) # get how long the trade has been open in minutes and candles present = arrow.utcnow() trade_start = arrow.get(active_trade[0].open_date) open_minutes = (present - trade_start).total_seconds() // 60 # floor # set up the things we use in the strategy trade_data['active_trade'] = True trade_data['current_profit'] = active_trade[ 0].calc_profit_ratio(current_rate) trade_data['peak_profit'] = max( 0, active_trade[0].calc_profit_ratio( active_trade[0].max_rate)) trade_data['open_minutes']: int = open_minutes trade_data['open_candles']: int = ( open_minutes // active_trade[0].timeframe) # floor else: trade_data['current_profit'] = trade_data['peak_profit'] = 0.0 trade_data['open_minutes'] = trade_data['open_candles'] = 0 # if there are open trades not including the current pair, get some information # future reference, for *all* open trades: open_trades = Trade.get_open_trades() other_trades = Trade.get_trades([ Trade.pair != pair, Trade.is_open.is_(True), ]).all() if other_trades: trade_data['other_trades'] = True other_profit = tuple( trade.calc_profit_ratio( self.get_current_price(trade.pair, False)) for trade in other_trades) trade_data['avg_other_profit'] = mean(other_profit) # find which of our trades is the biggest loser if trade_data['current_profit'] < min(other_profit): trade_data['biggest_loser'] = True else: trade_data['avg_other_profit'] = 0 # get the number of free trade slots, storing in every pairs dict due to laziness open_trades = len(Trade.get_open_trades()) trade_data['free_slots'] = max( 0, self.config['max_open_trades'] - open_trades) return trade_data
def test_stoploss_reinitialization(default_conf, fee): init_db(default_conf['db_url']) trade = Trade( pair='ETH/BTC', stake_amount=0.001, fee_open=fee.return_value, open_date=arrow.utcnow().shift(hours=-2).datetime, amount=10, fee_close=fee.return_value, exchange='bittrex', open_rate=1, max_rate=1, ) trade.adjust_stop_loss(trade.open_rate, 0.05, True) assert trade.stop_loss == 0.95 assert trade.stop_loss_pct == -0.05 assert trade.initial_stop_loss == 0.95 assert trade.initial_stop_loss_pct == -0.05 Trade.query.session.add(trade) # Lower stoploss Trade.stoploss_reinitialization(0.06) trades = Trade.get_open_trades() assert len(trades) == 1 trade_adj = trades[0] assert trade_adj.stop_loss == 0.94 assert trade_adj.stop_loss_pct == -0.06 assert trade_adj.initial_stop_loss == 0.94 assert trade_adj.initial_stop_loss_pct == -0.06 # Raise stoploss Trade.stoploss_reinitialization(0.04) trades = Trade.get_open_trades() assert len(trades) == 1 trade_adj = trades[0] assert trade_adj.stop_loss == 0.96 assert trade_adj.stop_loss_pct == -0.04 assert trade_adj.initial_stop_loss == 0.96 assert trade_adj.initial_stop_loss_pct == -0.04 # Trailing stoploss (move stoplos up a bit) trade.adjust_stop_loss(1.02, 0.04) assert trade_adj.stop_loss == 0.9792 assert trade_adj.initial_stop_loss == 0.96 Trade.stoploss_reinitialization(0.04) trades = Trade.get_open_trades() assert len(trades) == 1 trade_adj = trades[0] # Stoploss should not change in this case. assert trade_adj.stop_loss == 0.9792 assert trade_adj.stop_loss_pct == -0.04 assert trade_adj.initial_stop_loss == 0.96 assert trade_adj.initial_stop_loss_pct == -0.04
def backtest(stake_amount: float, processed: Dict[str, DataFrame], max_open_trades: int = 0, realistic: bool = True, sell_profit_only: bool = False, stoploss: int = -1.00, use_sell_signal: bool = False) -> DataFrame: """ Implements backtesting functionality :param stake_amount: btc amount to use for each trade :param processed: a processed dictionary with format {pair, data} :param max_open_trades: maximum number of concurrent trades (default: 0, disabled) :param realistic: do we try to simulate realistic trades? (default: True) :return: DataFrame """ trades = [] trade_count_lock: dict = {} exchange._API = Bittrex({'key': '', 'secret': ''}) for pair, pair_data in processed.items(): pair_data['buy'], pair_data['sell'] = 0, 0 ticker = populate_sell_trend(populate_buy_trend(pair_data)) # for each buy point lock_pair_until = None buy_subset = ticker[ticker.buy == 1][[ 'buy', 'open', 'close', 'date', 'sell' ]] for row in buy_subset.itertuples(index=True): if realistic: if lock_pair_until is not None and row.Index <= lock_pair_until: continue if max_open_trades > 0: # Check if max_open_trades has already been reached for the given date if not trade_count_lock.get(row.date, 0) < max_open_trades: continue if max_open_trades > 0: # Increase lock trade_count_lock[row.date] = trade_count_lock.get(row.date, 0) + 1 trade = Trade(open_rate=row.close, open_date=row.date, stake_amount=stake_amount, amount=stake_amount / row.open, fee=exchange.get_fee()) # calculate win/lose forwards from buy point sell_subset = ticker[row.Index + 1:][['close', 'date', 'sell']] for row2 in sell_subset.itertuples(index=True): 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 current_profit_percent = trade.calc_profit_percent( rate=row2.close) if (sell_profit_only and current_profit_percent < 0): continue if min_roi_reached(trade, row2.close, row2.date) or \ (row2.sell == 1 and use_sell_signal) or \ current_profit_percent <= stoploss: current_profit_btc = trade.calc_profit(rate=row2.close) lock_pair_until = row2.Index trades.append( (pair, current_profit_percent, current_profit_btc, row2.Index - row.Index, current_profit_btc > 0, current_profit_btc < 0)) break labels = [ 'currency', 'profit_percent', 'profit_BTC', 'duration', 'profit', 'loss' ] return DataFrame.from_records(trades, columns=labels)
def test_update_fee(fee): trade = Trade( pair='ETH/BTC', stake_amount=0.001, fee_open=fee.return_value, open_date=arrow.utcnow().shift(hours=-2).datetime, amount=10, fee_close=fee.return_value, exchange='bittrex', open_rate=1, max_rate=1, ) fee_cost = 0.15 fee_currency = 'BTC' fee_rate = 0.0075 assert trade.fee_open_currency is None assert not trade.fee_updated('buy') assert not trade.fee_updated('sell') trade.update_fee(fee_cost, fee_currency, fee_rate, 'buy') assert trade.fee_updated('buy') assert not trade.fee_updated('sell') assert trade.fee_open_currency == fee_currency assert trade.fee_open_cost == fee_cost assert trade.fee_open == fee_rate # Setting buy rate should "guess" close rate assert trade.fee_close == fee_rate assert trade.fee_close_currency is None assert trade.fee_close_cost is None fee_rate = 0.0076 trade.update_fee(fee_cost, fee_currency, fee_rate, 'sell') assert trade.fee_updated('buy') assert trade.fee_updated('sell') assert trade.fee_close == 0.0076 assert trade.fee_close_cost == fee_cost assert trade.fee_close == fee_rate
def execute_buy(self, pair: str, stake_amount: float, price: Optional[float] = None) -> bool: """ Executes a limit buy for the given pair :param pair: pair for which we want to create a LIMIT_BUY :return: None """ pair_s = pair.replace('_', '/') pair_url = self.exchange.get_pair_detail_url(pair) stake_currency = self.config['stake_currency'] fiat_currency = self.config.get('fiat_display_currency', None) time_in_force = self.strategy.order_time_in_force['buy'] if price: buy_limit_requested = price else: # Calculate amount buy_limit_requested = self.get_target_bid( pair, self.exchange.get_ticker(pair)) min_stake_amount = self._get_min_pair_stake_amount( pair_s, buy_limit_requested) if min_stake_amount is not None and min_stake_amount > stake_amount: logger.warning( f'Can\'t open a new trade for {pair_s}: stake amount' f' is too small ({stake_amount} < {min_stake_amount})') return False amount = stake_amount / buy_limit_requested order = self.exchange.buy(pair=pair, ordertype=self.strategy.order_types['buy'], amount=amount, rate=buy_limit_requested, time_in_force=time_in_force) order_id = order['id'] order_status = order.get('status', None) # we assume the order is executed at the price requested buy_limit_filled_price = buy_limit_requested if order_status == 'expired' or order_status == 'rejected': order_type = self.strategy.order_types['buy'] order_tif = self.strategy.order_time_in_force['buy'] # return false if the order is not filled if float(order['filled']) == 0: logger.warning( 'Buy %s order with time in force %s for %s is %s by %s.' ' zero amount is fulfilled.', order_tif, order_type, pair_s, order_status, self.exchange.name) return False else: # the order is partially fulfilled # in case of IOC orders we can check immediately # if the order is fulfilled fully or partially logger.warning( 'Buy %s order with time in force %s for %s is %s by %s.' ' %s amount fulfilled out of %s (%s remaining which is canceled).', order_tif, order_type, pair_s, order_status, self.exchange.name, order['filled'], order['amount'], order['remaining']) stake_amount = order['cost'] amount = order['amount'] buy_limit_filled_price = order['price'] order_id = None # in case of FOK the order may be filled immediately and fully elif order_status == 'closed': stake_amount = order['cost'] amount = order['amount'] buy_limit_filled_price = order['price'] order_id = None self.rpc.send_msg({ 'type': RPCMessageType.BUY_NOTIFICATION, 'exchange': self.exchange.name.capitalize(), 'pair': pair_s, 'market_url': pair_url, 'limit': buy_limit_filled_price, 'stake_amount': stake_amount, 'stake_currency': stake_currency, 'fiat_currency': fiat_currency }) # Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker') trade = Trade(pair=pair, stake_amount=stake_amount, amount=amount, fee_open=fee, fee_close=fee, open_rate=buy_limit_filled_price, open_rate_requested=buy_limit_requested, open_date=datetime.utcnow(), exchange=self.exchange.id, open_order_id=order_id, strategy=self.strategy.get_strategy_name(), ticker_interval=constants.TICKER_INTERVAL_MINUTES[ self.config['ticker_interval']]) Trade.session.add(trade) Trade.session.flush() # Updating wallets self.wallets.update() return True
def handle_stoploss_on_exchange(self, trade: Trade) -> bool: """ Check if trade is fulfilled in which case the stoploss on exchange should be added immediately if stoploss on exchange is enabled. """ logger.debug('Handling stoploss on exchange %s ...', trade) stoploss_order = None try: # First we check if there is already a stoploss on exchange stoploss_order = self.exchange.get_order(trade.stoploss_order_id, trade.pair) \ if trade.stoploss_order_id else None except InvalidOrderException as exception: logger.warning('Unable to fetch stoploss order: %s', exception) # If buy order is fulfilled but there is no stoploss, we add a stoploss on exchange if (not trade.open_order_id and not stoploss_order): stoploss = self.edge.stoploss( pair=trade.pair) if self.edge else self.strategy.stoploss stop_price = trade.open_rate * (1 + stoploss) if self.create_stoploss_order(trade=trade, stop_price=stop_price, rate=stop_price): trade.stoploss_last_update = datetime.now() return False # If stoploss order is canceled for some reason we add it if stoploss_order and stoploss_order['status'] == 'canceled': if self.create_stoploss_order(trade=trade, stop_price=trade.stop_loss, rate=trade.stop_loss): return False else: trade.stoploss_order_id = None logger.warning( 'Stoploss order was cancelled, but unable to recreate one.' ) # We check if stoploss order is fulfilled if stoploss_order and stoploss_order['status'] == 'closed': trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value trade.update(stoploss_order) # Lock pair for one candle to prevent immediate rebuys self.strategy.lock_pair( trade.pair, timeframe_to_next_date(self.config['ticker_interval'])) self._notify_sell(trade, "stoploss") return True # Finally we check if stoploss on exchange should be moved up because of trailing. if stoploss_order and self.config.get('trailing_stop', False): # if trailing stoploss is enabled we check if stoploss value has changed # in which case we cancel stoploss order and put another one with new # value immediately self.handle_trailing_stoploss_on_exchange(trade, stoploss_order) return False