From fd875786fd1b02f4af164b1798224cdacb4f3652 Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Tue, 7 Dec 2021 11:16:11 +0200 Subject: [PATCH] Initial very crude DCA implementation attempt. Very alpha. No backtesting support. --- freqtrade/freqtradebot.py | 169 ++++++++++++++++++++++++++++++++ freqtrade/strategy/interface.py | 21 ++++ 2 files changed, 190 insertions(+) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 7d8e0ec2f..7de0ce79b 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -178,6 +178,9 @@ class FreqtradeBot(LoggingMixin): # First process current opened trades (positions) self.exit_positions(trades) + # Check if we need to adjust our current positions before attempting to buy new trades. + self.process_open_trade_positions() + # Then looking for buy opportunities if self.get_free_open_trades(): self.enter_positions() @@ -443,6 +446,172 @@ class FreqtradeBot(LoggingMixin): else: return False + +# +# BUY / increase / decrease positions / DCA logic and methods +# + def process_open_trade_positions(self) -> int: + """ + Tries to execute additional buy orders for open trades (positions) + """ + orders_created = 0 + + # Remove pairs for currently opened trades from the whitelist + for trade in Trade.get_open_trades(): + try: + orders_created += self.adjust_trade_position(trade) + except DependencyException as exception: + logger.warning('Unable to adjust position of trade for %s: %s', trade.pair, exception) + + if not orders_created: + logger.debug("Found no trades to modify. Trying again...") + + return orders_created + + + def adjust_trade_position(self, trade: Trade) -> int: + """ + Check the implemented trading strategy for adjustment command. + + If the pair triggers the adjustment a new buy-order gets issued towards the exchange. + Once that completes, the existing trade is modified to match new data. + :return: True if a order has been created. + """ + logger.debug(f"adjust_trade_position for pair {trade.pair}") + for order in trade.orders: + if order.ft_is_open: + logger.debug(f"Order {order} is still open.") + return 0 + + analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair, self.strategy.timeframe) + + sell_rate = self.exchange.get_rate(trade.pair, refresh=True, side="sell") + current_profit = trade.calc_profit_ratio(sell_rate) + amount_to_adjust = strategy_safe_wrapper(self.strategy.adjust_trade_position, default_retval=0.0)( + pair=trade.pair, trade=trade, current_time=datetime.now(timezone.utc), + current_rate=sell_rate, current_profit=current_profit) + + if amount_to_adjust > 0.0: + # We should increase our position + is_order_success = self.execute_trade_position_change(trade.pair, amount_to_adjust, trade) + if is_order_success: + return 1 + + if amount_to_adjust < 0.0: + # We should decrease our position + # TODO: Selling part of the trade not implemented yet. + return 0 + + return 0 + + + def execute_trade_position_change(self, pair: str, amount: float, trade: Trade) -> bool: + """ + Executes a limit buy for the given pair + :param pair: pair for which we want to create a LIMIT_BUY + :param stake_amount: amount of stake-currency for the pair + :return: True if a buy order is created, false if it fails. + """ + time_in_force = self.strategy.order_time_in_force['buy'] + + # Calculate price + proposed_enter_rate = self.exchange.get_rate(pair, refresh=True, side="buy") + enter_limit_requested = self.get_valid_price(proposed_enter_rate, proposed_enter_rate) + if not enter_limit_requested: + raise PricingError('Could not determine buy price.') + + min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, enter_limit_requested, + self.strategy.stoploss) + + stake_amount = amount * enter_limit_requested + + stake_amount = self.wallets.validate_stake_amount(pair, stake_amount, min_stake_amount) + + logger.error(f'Executing DCA buy: amount={amount}, stake={stake_amount}') + + if not stake_amount: + return False + + amount = self.exchange.amount_to_precision(pair, amount) + order = self.exchange.create_order(pair=pair, ordertype='market', side="buy", + amount=amount, rate=enter_limit_requested, + time_in_force=time_in_force) + order_obj = Order.parse_from_ccxt_object(order, pair, 'buy') + order_id = order['id'] + order_status = order.get('status', None) + # we assume the order is executed at the price requested + enter_limit_filled_price = enter_limit_requested + amount_requested = amount + + if order_status == 'expired' or order_status == 'rejected': + 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(adjustment) %s order with time in force %s for %s is %s by %s.' + ' zero amount is fulfilled.', + order_tif, order_type, pair, 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(adjustment) %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, order_status, self.exchange.name, + order['filled'], order['amount'], order['remaining'] + ) + stake_amount = order['cost'] + amount = safe_value_fallback(order, 'filled', 'amount') + enter_limit_filled_price = safe_value_fallback(order, 'average', 'price') + + # in case of FOK the order may be filled immediately and fully + elif order_status == 'closed': + stake_amount = order['cost'] + amount = safe_value_fallback(order, 'filled', 'amount') + enter_limit_filled_price = safe_value_fallback(order, 'average', 'price') + + # Fee is applied only once because we make a LIMIT_BUY but the final trade will apply the sell fee. + fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker') + + logger.info(f"Trade {pair} DCA order status {order_status}") + + total_amount = 0.0 + total_stake = 0.0 + for tempOrder in trade.orders: + if tempOrder.ft_is_open: + continue + if tempOrder.ft_order_side != 'buy': + # Skip not buy sides + continue + + if tempOrder.status == "closed": + tempOrderAmount = safe_value_fallback(order, 'filled', 'amount') + total_amount += tempOrderAmount + total_stake += tempOrder.average * tempOrderAmount + + total_amount += amount + total_stake += stake_amount + + trade.open_rate = total_stake / total_amount + + trade.fee_open += fee + trade.stake_amount = total_stake + trade.amount = total_amount + + trade.orders.append(order_obj) + trade.recalc_open_trade_value() + Trade.commit() + + # Updating wallets + self.wallets.update() + + self._notify_enter(trade, 'market') + + return True + + + def _check_depth_of_market_buy(self, pair: str, conf: Dict) -> bool: """ Checks depth of market before executing a buy diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 05469317b..e98cb5ff6 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -381,6 +381,27 @@ class IStrategy(ABC, HyperStrategyMixin): """ return proposed_stake + + def adjust_trade_position(self, pair: str, trade: Trade, + current_time: datetime, adjust_trade_position: float, + current_rate: float, current_profit: float, **kwargs) -> float: + """ + Custom trade adjustment logic, returning the amount that a trade shold be either increased or decreased. + + For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ + + When not implemented by a strategy, returns 0.0 + + :param pair: Pair that's currently analyzed + :param trade: trade object. + :param current_time: datetime object, containing the current datetime + :param current_rate: Rate, calculated based on pricing settings in ask_strategy. + :param current_profit: Current profit (as ratio), calculated based on current_rate. + :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. + :return float: Amount to adjust your trade (buy more or sell some) + """ + return 0.0 + def informative_pairs(self) -> ListPairsWithTimeframes: """ Define additional, informative pair/interval combinations to be cached from the exchange.