Merge branch 'develop' into pr/jpribyl/3210-1
This commit is contained in:
		| @@ -54,8 +54,11 @@ class FreqtradeBot: | ||||
|         # Init objects | ||||
|         self.config = config | ||||
|  | ||||
|         self._sell_rate_cache = TTLCache(maxsize=100, ttl=5) | ||||
|         self._buy_rate_cache = TTLCache(maxsize=100, ttl=5) | ||||
|         # Cache values for 1800 to avoid frequent polling of the exchange for prices | ||||
|         # Caching only applies to RPC methods, so prices for open trades are still | ||||
|         # refreshed once every iteration. | ||||
|         self._sell_rate_cache = TTLCache(maxsize=100, ttl=1800) | ||||
|         self._buy_rate_cache = TTLCache(maxsize=100, ttl=1800) | ||||
|  | ||||
|         self.strategy: IStrategy = StrategyResolver.load_strategy(self.config) | ||||
|  | ||||
| @@ -68,15 +71,15 @@ class FreqtradeBot: | ||||
|  | ||||
|         self.wallets = Wallets(self.config, self.exchange) | ||||
|  | ||||
|         self.dataprovider = DataProvider(self.config, self.exchange) | ||||
|         self.pairlists = PairListManager(self.exchange, self.config) | ||||
|  | ||||
|         self.dataprovider = DataProvider(self.config, self.exchange, self.pairlists) | ||||
|  | ||||
|         # Attach Dataprovider to Strategy baseclass | ||||
|         IStrategy.dp = self.dataprovider | ||||
|         # Attach Wallets to Strategy baseclass | ||||
|         IStrategy.wallets = self.wallets | ||||
|  | ||||
|         self.pairlists = PairListManager(self.exchange, self.config) | ||||
|  | ||||
|         # Initializing Edge only if enabled | ||||
|         self.edge = Edge(self.config, self.exchange, self.strategy) if \ | ||||
|             self.config.get('edge', {}).get('enabled', False) else None | ||||
| @@ -620,7 +623,7 @@ class FreqtradeBot: | ||||
|                     trades_closed += 1 | ||||
|                     continue | ||||
|                 # Check if we can sell our current pair | ||||
|                 if trade.open_order_id is None and self.handle_trade(trade): | ||||
|                 if trade.open_order_id is None and trade.is_open and self.handle_trade(trade): | ||||
|                     trades_closed += 1 | ||||
|  | ||||
|             except DependencyException as exception: | ||||
| @@ -762,7 +765,7 @@ class FreqtradeBot: | ||||
|         # 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.update_trade_state(trade, stoploss_order, sl_order=True) | ||||
|             # Lock pair for one candle to prevent immediate rebuys | ||||
|             self.strategy.lock_pair(trade.pair, | ||||
|                                     timeframe_to_next_date(self.config['ticker_interval'])) | ||||
| @@ -922,7 +925,8 @@ class FreqtradeBot: | ||||
|         """ | ||||
|         was_trade_fully_canceled = False | ||||
|  | ||||
|         if order['status'] != 'canceled': | ||||
|         # Cancelled orders may have the status of 'canceled' or 'closed' | ||||
|         if order['status'] not in ('canceled', 'closed'): | ||||
|             reason = constants.CANCEL_REASON['TIMEOUT'] | ||||
|             corder = self.exchange.cancel_order_with_result(trade.open_order_id, trade.pair, | ||||
|                                                             trade.amount) | ||||
| @@ -933,7 +937,10 @@ class FreqtradeBot: | ||||
|  | ||||
|         logger.info('Buy order %s for %s.', reason, trade) | ||||
|  | ||||
|         if safe_value_fallback(corder, order, 'remaining', 'remaining') == order['amount']: | ||||
|         # Using filled to determine the filled amount | ||||
|         filled_amount = safe_value_fallback(corder, order, 'filled', 'filled') | ||||
|  | ||||
|         if isclose(filled_amount, 0.0, abs_tol=constants.MATH_CLOSE_PREC): | ||||
|             logger.info('Buy order fully cancelled. Removing %s from database.', trade) | ||||
|             # if trade is not partially completed, just delete the trade | ||||
|             Trade.session.delete(trade) | ||||
| @@ -945,8 +952,7 @@ class FreqtradeBot: | ||||
|             # cancel_order may not contain the full order dict, so we need to fallback | ||||
|             # to the order dict aquired before cancelling. | ||||
|             # we need to fall back to the values from order if corder does not contain these keys. | ||||
|             trade.amount = order['amount'] - safe_value_fallback(corder, order, | ||||
|                                                                  'remaining', 'remaining') | ||||
|             trade.amount = filled_amount | ||||
|             trade.stake_amount = trade.amount * trade.open_rate | ||||
|             self.update_trade_state(trade, corder, trade.amount) | ||||
|  | ||||
| @@ -966,11 +972,15 @@ class FreqtradeBot: | ||||
|         Sell cancel - cancel order and update trade | ||||
|         :return: Reason for cancel | ||||
|         """ | ||||
|         # if trade is not partially completed, just cancel the trade | ||||
|         # if trade is not partially completed, just cancel the order | ||||
|         if order['remaining'] == order['amount'] or order.get('filled') == 0.0: | ||||
|             if not self.exchange.check_order_canceled_empty(order): | ||||
|                 # if trade is not partially completed, just delete the trade | ||||
|                 self.exchange.cancel_order(trade.open_order_id, trade.pair) | ||||
|                 try: | ||||
|                     # if trade is not partially completed, just delete the order | ||||
|                     self.exchange.cancel_order(trade.open_order_id, trade.pair) | ||||
|                 except InvalidOrderException: | ||||
|                     logger.exception(f"Could not cancel sell order {trade.open_order_id}") | ||||
|                     return 'error cancelling order' | ||||
|                 logger.info('Sell order %s for %s.', reason, trade) | ||||
|             else: | ||||
|                 reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE'] | ||||
| @@ -1014,7 +1024,7 @@ class FreqtradeBot: | ||||
|         if wallet_amount >= amount: | ||||
|             return amount | ||||
|         elif wallet_amount > amount * 0.98: | ||||
|             logger.info(f"{pair} - Falling back to wallet-amount.") | ||||
|             logger.info(f"{pair} - Falling back to wallet-amount {wallet_amount} -> {amount}.") | ||||
|             return wallet_amount | ||||
|         else: | ||||
|             raise DependencyException( | ||||
| @@ -1064,7 +1074,7 @@ class FreqtradeBot: | ||||
|         trade.sell_reason = sell_reason.value | ||||
|         # In case of market sell orders the order can be closed immediately | ||||
|         if order.get('status', 'unknown') == 'closed': | ||||
|             trade.update(order) | ||||
|             self.update_trade_state(trade, order) | ||||
|         Trade.session.flush() | ||||
|  | ||||
|         # Lock pair for one candle to prevent immediate rebuys | ||||
| @@ -1155,7 +1165,7 @@ class FreqtradeBot: | ||||
| # | ||||
|  | ||||
|     def update_trade_state(self, trade: Trade, action_order: dict = None, | ||||
|                            order_amount: float = None) -> bool: | ||||
|                            order_amount: float = None, sl_order: bool = False) -> bool: | ||||
|         """ | ||||
|         Checks trades with open orders and updates the amount if necessary | ||||
|         Handles closing both buy and sell orders. | ||||
| @@ -1163,84 +1173,125 @@ class FreqtradeBot: | ||||
|         """ | ||||
|         # Get order details for actual price per unit | ||||
|         if trade.open_order_id: | ||||
|             # Update trade with order values | ||||
|             logger.info('Found open order for %s', trade) | ||||
|             try: | ||||
|                 order = action_order or self.exchange.get_order(trade.open_order_id, trade.pair) | ||||
|             except InvalidOrderException as exception: | ||||
|                 logger.warning('Unable to fetch order %s: %s', trade.open_order_id, exception) | ||||
|                 return False | ||||
|             # Try update amount (binance-fix) | ||||
|             try: | ||||
|                 new_amount = self.get_real_amount(trade, order, order_amount) | ||||
|                 if not isclose(order['amount'], new_amount, abs_tol=constants.MATH_CLOSE_PREC): | ||||
|                     order['amount'] = new_amount | ||||
|                     order.pop('filled', None) | ||||
|                     # Fee was applied, so set to 0 | ||||
|                     trade.fee_open = 0 | ||||
|                     trade.recalc_open_trade_price() | ||||
|             except DependencyException as exception: | ||||
|                 logger.warning("Could not update trade amount: %s", exception) | ||||
|             order_id = trade.open_order_id | ||||
|         elif trade.stoploss_order_id and sl_order: | ||||
|             order_id = trade.stoploss_order_id | ||||
|         else: | ||||
|             return False | ||||
|         # Update trade with order values | ||||
|         logger.info('Found open order for %s', trade) | ||||
|         try: | ||||
|             order = action_order or self.exchange.get_order(order_id, trade.pair) | ||||
|         except InvalidOrderException as exception: | ||||
|             logger.warning('Unable to fetch order %s: %s', order_id, exception) | ||||
|             return False | ||||
|         # Try update amount (binance-fix) | ||||
|         try: | ||||
|             new_amount = self.get_real_amount(trade, order, order_amount) | ||||
|             if not isclose(order['amount'], new_amount, abs_tol=constants.MATH_CLOSE_PREC): | ||||
|                 order['amount'] = new_amount | ||||
|                 order.pop('filled', None) | ||||
|                 trade.recalc_open_trade_price() | ||||
|         except DependencyException as exception: | ||||
|             logger.warning("Could not update trade amount: %s", exception) | ||||
|  | ||||
|             if self.exchange.check_order_canceled_empty(order): | ||||
|                 # Trade has been cancelled on exchange | ||||
|                 # Handling of this will happen in check_handle_timeout. | ||||
|                 return True | ||||
|             trade.update(order) | ||||
|  | ||||
|             # Updating wallets when order is closed | ||||
|             if not trade.is_open: | ||||
|                 self.wallets.update() | ||||
|         if self.exchange.check_order_canceled_empty(order): | ||||
|             # Trade has been cancelled on exchange | ||||
|             # Handling of this will happen in check_handle_timeout. | ||||
|             return True | ||||
|         trade.update(order) | ||||
|  | ||||
|         # Updating wallets when order is closed | ||||
|         if not trade.is_open: | ||||
|             self.wallets.update() | ||||
|         return False | ||||
|  | ||||
|     def apply_fee_conditional(self, trade: Trade, trade_base_currency: str, | ||||
|                               amount: float, fee_abs: float) -> float: | ||||
|         """ | ||||
|         Applies the fee to amount (either from Order or from Trades). | ||||
|         Can eat into dust if more than the required asset is available. | ||||
|         """ | ||||
|         self.wallets.update() | ||||
|         if fee_abs != 0 and self.wallets.get_free(trade_base_currency) >= amount: | ||||
|             # Eat into dust if we own more than base currency | ||||
|             logger.info(f"Fee amount for {trade} was in base currency - " | ||||
|                         f"Eating Fee {fee_abs} into dust.") | ||||
|         elif fee_abs != 0: | ||||
|             real_amount = self.exchange.amount_to_precision(trade.pair, amount - fee_abs) | ||||
|             logger.info(f"Applying fee on amount for {trade} " | ||||
|                         f"(from {amount} to {real_amount}).") | ||||
|             return real_amount | ||||
|         return amount | ||||
|  | ||||
|     def get_real_amount(self, trade: Trade, order: Dict, order_amount: float = None) -> float: | ||||
|         """ | ||||
|         Get real amount for the trade | ||||
|         Detect and update trade fee. | ||||
|         Calls trade.update_fee() uppon correct detection. | ||||
|         Returns modified amount if the fee was taken from the destination currency. | ||||
|         Necessary for exchanges which charge fees in base currency (e.g. binance) | ||||
|         :return: identical (or new) amount for the trade | ||||
|         """ | ||||
|         # Init variables | ||||
|         if order_amount is None: | ||||
|             order_amount = order['amount'] | ||||
|         # Only run for closed orders | ||||
|         if trade.fee_open == 0 or order['status'] == 'open': | ||||
|         if trade.fee_updated(order.get('side', '')) or order['status'] == 'open': | ||||
|             return order_amount | ||||
|  | ||||
|         trade_base_currency = self.exchange.get_pair_base_currency(trade.pair) | ||||
|         # use fee from order-dict if possible | ||||
|         if ('fee' in order and order['fee'] is not None and | ||||
|                 (order['fee'].keys() >= {'currency', 'cost'})): | ||||
|             if (order['fee']['currency'] is not None and | ||||
|                     order['fee']['cost'] is not None and | ||||
|                     trade_base_currency == order['fee']['currency']): | ||||
|                 new_amount = order_amount - order['fee']['cost'] | ||||
|                 logger.info("Applying fee on amount for %s (from %s to %s) from Order", | ||||
|                             trade, order['amount'], new_amount) | ||||
|                 return new_amount | ||||
|         if self.exchange.order_has_fee(order): | ||||
|             fee_cost, fee_currency, fee_rate = self.exchange.extract_cost_curr_rate(order) | ||||
|             logger.info(f"Fee for Trade {trade} [{order.get('side')}]: " | ||||
|                         f"{fee_cost:.8g} {fee_currency} - rate: {fee_rate}") | ||||
|  | ||||
|         # Fallback to Trades | ||||
|             trade.update_fee(fee_cost, fee_currency, fee_rate, order.get('side', '')) | ||||
|             if trade_base_currency == fee_currency: | ||||
|                 # Apply fee to amount | ||||
|                 return self.apply_fee_conditional(trade, trade_base_currency, | ||||
|                                                   amount=order_amount, fee_abs=fee_cost) | ||||
|             return order_amount | ||||
|         return self.fee_detection_from_trades(trade, order, order_amount) | ||||
|  | ||||
|     def fee_detection_from_trades(self, trade: Trade, order: Dict, order_amount: float) -> float: | ||||
|         """ | ||||
|         fee-detection fallback to Trades. Parses result of fetch_my_trades to get correct fee. | ||||
|         """ | ||||
|         trades = self.exchange.get_trades_for_order(trade.open_order_id, trade.pair, | ||||
|                                                     trade.open_date) | ||||
|  | ||||
|         if len(trades) == 0: | ||||
|             logger.info("Applying fee on amount for %s failed: myTrade-Dict empty found", trade) | ||||
|             return order_amount | ||||
|         fee_currency = None | ||||
|         amount = 0 | ||||
|         fee_abs = 0 | ||||
|         fee_abs = 0.0 | ||||
|         fee_cost = 0.0 | ||||
|         trade_base_currency = self.exchange.get_pair_base_currency(trade.pair) | ||||
|         fee_rate_array: List[float] = [] | ||||
|         for exectrade in trades: | ||||
|             amount += exectrade['amount'] | ||||
|             if ("fee" in exectrade and exectrade['fee'] is not None and | ||||
|                     (exectrade['fee'].keys() >= {'currency', 'cost'})): | ||||
|             if self.exchange.order_has_fee(exectrade): | ||||
|                 fee_cost_, fee_currency, fee_rate_ = self.exchange.extract_cost_curr_rate(exectrade) | ||||
|                 fee_cost += fee_cost_ | ||||
|                 if fee_rate_ is not None: | ||||
|                     fee_rate_array.append(fee_rate_) | ||||
|                 # only applies if fee is in quote currency! | ||||
|                 if (exectrade['fee']['currency'] is not None and | ||||
|                         exectrade['fee']['cost'] is not None and | ||||
|                         trade_base_currency == exectrade['fee']['currency']): | ||||
|                     fee_abs += exectrade['fee']['cost'] | ||||
|                 if trade_base_currency == fee_currency: | ||||
|                     fee_abs += fee_cost_ | ||||
|         # Ensure at least one trade was found: | ||||
|         if fee_currency: | ||||
|             # fee_rate should use mean | ||||
|             fee_rate = sum(fee_rate_array) / float(len(fee_rate_array)) if fee_rate_array else None | ||||
|             trade.update_fee(fee_cost, fee_currency, fee_rate, order.get('side', '')) | ||||
|  | ||||
|         if not isclose(amount, order_amount, abs_tol=constants.MATH_CLOSE_PREC): | ||||
|             logger.warning(f"Amount {amount} does not match amount {trade.amount}") | ||||
|             raise DependencyException("Half bought? Amounts don't match") | ||||
|         real_amount = amount - fee_abs | ||||
|  | ||||
|         if fee_abs != 0: | ||||
|             logger.info(f"Applying fee on amount for {trade} " | ||||
|                         f"(from {order_amount} to {real_amount}) from Trades") | ||||
|         return real_amount | ||||
|             return self.apply_fee_conditional(trade, trade_base_currency, | ||||
|                                               amount=amount, fee_abs=fee_abs) | ||||
|         else: | ||||
|             return amount | ||||
|   | ||||
		Reference in New Issue
	
	Block a user