From fd875786fd1b02f4af164b1798224cdacb4f3652 Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Tue, 7 Dec 2021 11:16:11 +0200 Subject: [PATCH 01/90] 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. From fde67798730659ce3f022629853183a0023ddc79 Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Thu, 9 Dec 2021 14:47:44 +0200 Subject: [PATCH 02/90] Some code improvements. Still some bugs. --- freqtrade/freqtradebot.py | 104 ++++++++++---------------------- freqtrade/persistence/models.py | 20 ++++++ freqtrade/strategy/interface.py | 6 +- 3 files changed, 56 insertions(+), 74 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 7de0ce79b..b4503fe01 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -450,98 +450,84 @@ class FreqtradeBot(LoggingMixin): # # BUY / increase / decrease positions / DCA logic and methods # - def process_open_trade_positions(self) -> int: + def process_open_trade_positions(self): """ - Tries to execute additional buy orders for open trades (positions) + Tries to execute additional buy or sell orders for open trades (positions) """ - orders_created = 0 - - # Remove pairs for currently opened trades from the whitelist + # Walk through each pair and check if it needs changes for trade in Trade.get_open_trades(): try: - orders_created += self.adjust_trade_position(trade) + 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: + def adjust_trade_position(self, trade: Trade): """ Check the implemented trading strategy for adjustment command. - - If the pair triggers the adjustment a new buy-order gets issued towards the exchange. + If the strategy triggers the adjustment a new buy/sell-order gets issued. 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) + logger.debug(f"Order {order} is still open, skipping pair.") + return 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)( + amount_to_adjust = strategy_safe_wrapper(self.strategy.adjust_trade_position, default_retval=None)( 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: + if amount_to_adjust != None and 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 + self.execute_trade_position_change(trade.pair, amount_to_adjust, trade) - if amount_to_adjust < 0.0: + if amount_to_adjust != None and amount_to_adjust < 0.0: # We should decrease our position # TODO: Selling part of the trade not implemented yet. - return 0 + return - return 0 + return - def execute_trade_position_change(self, pair: str, amount: float, trade: Trade) -> bool: + def execute_trade_position_change(self, pair: str, amount: float, trade: Trade): """ - 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. + Executes a buy order for the given pair using specific amount + :param pair: pair for which we want to create a buy order + :param amount: amount of tradable pair to buy """ 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) + custom_entry_price = strategy_safe_wrapper(self.strategy.custom_entry_price, + default_retval=proposed_enter_rate)( + pair=pair, current_time=datetime.now(timezone.utc), + proposed_rate=proposed_enter_rate) + + enter_limit_requested = self.get_valid_price(custom_entry_price, 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}') + stake_amount = self.wallets.validate_stake_amount(pair, (amount * enter_limit_requested), min_stake_amount) if not stake_amount: + logger.info(f'Additional order failed to get stake amount for pair {pair}, amount={amount}, price={enter_limit_requested}') return False + logger.debug(f'Executing additional order: amount={amount}, stake={stake_amount}, price={enter_limit_requested}') + amount = self.exchange.amount_to_precision(pair, amount) - order = self.exchange.create_order(pair=pair, ordertype='market', side="buy", + 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'] @@ -563,44 +549,19 @@ class FreqtradeBot(LoggingMixin): ) 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 + logger.info(f"Trade {pair} adjustment order status {order_status}") trade.orders.append(order_obj) - trade.recalc_open_trade_value() + trade.recalc_trade_from_orders() Trade.commit() # Updating wallets @@ -1459,6 +1420,7 @@ class FreqtradeBot(LoggingMixin): logger.warning("Could not update trade amount: %s", exception) trade.update(order) + trade.recalc_trade_from_orders() Trade.commit() # Updating wallets when order is closed diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 2fcdd58bb..2c0923078 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -568,6 +568,26 @@ class LocalTrade(): profit_ratio = (close_trade_value / self.open_trade_value) - 1 return float(f"{profit_ratio:.8f}") + + def recalc_trade_from_orders(self): + total_amount = 0.0 + total_stake = 0.0 + for temp_order in self.orders: + if temp_order.ft_is_open == False and temp_order.status == "closed" and temp_order.ft_order_side == 'buy': + tmp_amount = temp_order.amount + if temp_order.filled is not None: + tmp_amount = temp_order.filled + total_amount += tmp_amount + total_stake += temp_order.average * tmp_amount + + if total_amount > 0: + self.open_rate = total_stake / total_amount + self.stake_amount = total_stake + self.amount = total_amount + self.fee_open_cost = self.fee_open * self.stake_amount + self.recalc_open_trade_value() + + def select_order(self, order_side: str, is_open: Optional[bool]) -> Optional[Order]: """ Finds latest order for this orderside and status diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index e98cb5ff6..fead93190 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -383,8 +383,8 @@ class IStrategy(ABC, HyperStrategyMixin): def adjust_trade_position(self, pair: str, trade: Trade, - current_time: datetime, adjust_trade_position: float, - current_rate: float, current_profit: float, **kwargs) -> float: + current_time: datetime, current_rate: float, current_profit: float, + **kwargs) -> Optional[float]: """ Custom trade adjustment logic, returning the amount that a trade shold be either increased or decreased. @@ -400,7 +400,7 @@ class IStrategy(ABC, HyperStrategyMixin): :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 + return None def informative_pairs(self) -> ListPairsWithTimeframes: """ From 28d0b5165acbd2cfa55ac9c76cb72cfe1a386059 Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Thu, 9 Dec 2021 19:47:24 +0200 Subject: [PATCH 03/90] Add unit-test --- tests/test_persistence.py | 164 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 164 insertions(+) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index d1d3ce382..4dea23663 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -1343,3 +1343,167 @@ def test_Trade_object_idem(): and item not in ('trades', 'trades_open', 'total_profit') and type(getattr(LocalTrade, item)) not in (property, FunctionType)): assert item in trade + + +def test_recalc_trade_from_orders(fee): + + o1_amount = 100 + o1_rate = 1 + o1_cost = o1_amount * o1_rate + o1_fee_cost = o1_cost * fee.return_value + o1_trade_val = o1_cost + o1_fee_cost + + trade = Trade( + pair='ADA/USDT', + stake_amount=o1_cost, + open_date=arrow.utcnow().shift(hours=-2).datetime, + amount=o1_amount, + fee_open=fee.return_value, + fee_close=fee.return_value, + exchange='binance', + open_rate=o1_rate, + max_rate=o1_rate, + ) + + assert fee.return_value == 0.0025 + assert trade._calc_open_trade_value() == o1_trade_val + assert trade.amount == o1_amount + assert trade.stake_amount == o1_cost + assert trade.open_rate == o1_rate + assert trade.open_trade_value == o1_trade_val + + # Calling without orders should not throw exceptions and change nothing + trade.recalc_trade_from_orders() + assert trade.amount == o1_amount + assert trade.stake_amount == o1_cost + assert trade.open_rate == o1_rate + assert trade.open_trade_value == o1_trade_val + + trade.update_fee(o1_fee_cost, 'BNB', fee.return_value, 'buy') + + assert len(trade.orders) == 0 + + # Check with 1 order + order1 = Order( + ft_order_side='buy', + ft_pair=trade.pair, + ft_is_open=False, + status="closed", + symbol=trade.pair, + order_type="market", + side="buy", + price=o1_rate, + average=o1_rate, + filled=o1_amount, + remaining=0, + cost=o1_amount, + order_date=trade.open_date, + order_filled_date=trade.open_date, + ) + trade.orders.append(order1) + trade.recalc_trade_from_orders() + + # Calling recalc with single initial order should not change anything + assert trade.amount == o1_amount + assert trade.stake_amount == o1_amount + assert trade.open_rate == o1_rate + assert trade.fee_open_cost == o1_fee_cost + assert trade.open_trade_value == o1_trade_val + + # One additional adjustment / DCA order + o2_amount = 125 + o2_rate = 0.9 + o2_cost = o2_amount * o2_rate + o2_fee_cost = o2_cost * fee.return_value + o2_trade_val = o2_cost + o2_fee_cost + + order2 = Order( + ft_order_side='buy', + ft_pair=trade.pair, + ft_is_open=False, + status="closed", + symbol=trade.pair, + order_type="market", + side="buy", + price=o2_rate, + average=o2_rate, + filled=o2_amount, + remaining=0, + cost=o2_cost, + order_date=arrow.utcnow().shift(hours=-1).datetime, + order_filled_date=arrow.utcnow().shift(hours=-1).datetime, + ) + trade.orders.append(order2) + trade.recalc_trade_from_orders() + + # Validate that the trade now has new averaged open price and total values + avg_price = (o1_cost + o2_cost) / (o1_amount + o2_amount) + assert trade.amount == o1_amount + o2_amount + assert trade.stake_amount == o1_amount + o2_cost + assert trade.open_rate == avg_price + assert trade.fee_open_cost == o1_fee_cost + o2_fee_cost + assert trade.open_trade_value == o1_trade_val + o2_trade_val + + # Let's try with multiple additional orders + o3_amount = 150 + o3_rate = 0.85 + o3_cost = o3_amount * o3_rate + o3_fee_cost = o3_cost * fee.return_value + o3_trade_val = o3_cost + o3_fee_cost + + order3 = Order( + ft_order_side='buy', + ft_pair=trade.pair, + ft_is_open=False, + status="closed", + symbol=trade.pair, + order_type="market", + side="buy", + price=o3_rate, + average=o3_rate, + filled=o3_amount, + remaining=0, + cost=o3_cost, + order_date=arrow.utcnow().shift(hours=-1).datetime, + order_filled_date=arrow.utcnow().shift(hours=-1).datetime, + ) + trade.orders.append(order3) + trade.recalc_trade_from_orders() + + # Validate that the sum is still correct and open rate is averaged + avg_price = (o1_cost + o2_cost + o3_cost) / (o1_amount + o2_amount + o3_amount) + assert trade.amount == o1_amount + o2_amount + o3_amount + assert trade.stake_amount == o1_cost + o2_cost + o3_cost + assert trade.open_rate == avg_price + assert round(trade.fee_open_cost, 8) == round(o1_fee_cost + o2_fee_cost + o3_fee_cost, 8) + assert round(trade.open_trade_value, 8) == round(o1_trade_val + o2_trade_val + o3_trade_val, 8) + + # Just to make sure sell orders are ignored, let's calculate one more time. + sell1 = Order( + ft_order_side='sell', + ft_pair=trade.pair, + ft_is_open=False, + status="closed", + symbol=trade.pair, + order_type="market", + side="sell", + price=avg_price + 0.95, + average=avg_price + 0.95, + filled=o1_amount + o2_amount + o3_amount, + remaining=0, + cost=o1_cost + o2_cost + o3_cost, + order_date=trade.open_date, + order_filled_date=trade.open_date, + ) + trade.orders.append(sell1) + trade.recalc_trade_from_orders() + + avg_price = (o1_cost + o2_cost + o3_cost) / (o1_amount + o2_amount + o3_amount) + + assert trade.amount == o1_amount + o2_amount + o3_amount + assert trade.stake_amount == o1_cost + o2_cost + o3_cost + assert trade.open_rate == avg_price + assert round(trade.fee_open_cost, 8) == round(o1_fee_cost + o2_fee_cost + o3_fee_cost, 8) + assert round(trade.open_trade_value, 8) == round(o1_trade_val + o2_trade_val + o3_trade_val, 8) + + From 00366c5c88a98a95196373a562babcd6d978840d Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Thu, 9 Dec 2021 20:03:41 +0200 Subject: [PATCH 04/90] Additional unit-tests --- freqtrade/persistence/models.py | 7 +- tests/test_persistence.py | 127 ++++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+), 3 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 2c0923078..74b4cf627 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -577,8 +577,9 @@ class LocalTrade(): tmp_amount = temp_order.amount if temp_order.filled is not None: tmp_amount = temp_order.filled - total_amount += tmp_amount - total_stake += temp_order.average * tmp_amount + if tmp_amount is not None and temp_order.average is not None: + total_amount += tmp_amount + total_stake += temp_order.average * tmp_amount if total_amount > 0: self.open_rate = total_stake / total_amount @@ -586,7 +587,7 @@ class LocalTrade(): self.amount = total_amount self.fee_open_cost = self.fee_open * self.stake_amount self.recalc_open_trade_value() - + def select_order(self, order_side: str, is_open: Optional[bool]) -> Optional[Order]: """ diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 4dea23663..a6a289007 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -1506,4 +1506,131 @@ def test_recalc_trade_from_orders(fee): assert round(trade.fee_open_cost, 8) == round(o1_fee_cost + o2_fee_cost + o3_fee_cost, 8) assert round(trade.open_trade_value, 8) == round(o1_trade_val + o2_trade_val + o3_trade_val, 8) +def test_recalc_trade_from_orders_ignores_bad_orders(fee): + + o1_amount = 100 + o1_rate = 1 + o1_cost = o1_amount * o1_rate + o1_fee_cost = o1_cost * fee.return_value + o1_trade_val = o1_cost + o1_fee_cost + + trade = Trade( + pair='ADA/USDT', + stake_amount=o1_cost, + open_date=arrow.utcnow().shift(hours=-2).datetime, + amount=o1_amount, + fee_open=fee.return_value, + fee_close=fee.return_value, + exchange='binance', + open_rate=o1_rate, + max_rate=o1_rate, + ) + trade.update_fee(o1_fee_cost, 'BNB', fee.return_value, 'buy') + # Check with 1 order + order1 = Order( + ft_order_side='buy', + ft_pair=trade.pair, + ft_is_open=False, + status="closed", + symbol=trade.pair, + order_type="market", + side="buy", + price=o1_rate, + average=o1_rate, + filled=o1_amount, + remaining=0, + cost=o1_amount, + order_date=trade.open_date, + order_filled_date=trade.open_date, + ) + trade.orders.append(order1) + trade.recalc_trade_from_orders() + + # Calling recalc with single initial order should not change anything + assert trade.amount == o1_amount + assert trade.stake_amount == o1_amount + assert trade.open_rate == o1_rate + assert trade.fee_open_cost == o1_fee_cost + assert trade.open_trade_value == o1_trade_val + + order2 = Order( + ft_order_side='buy', + ft_pair=trade.pair, + ft_is_open=True, + status="closed", + symbol=trade.pair, + order_type="market", + side="buy", + price=1, + average=2, + filled=3, + remaining=4, + cost=5, + order_date=arrow.utcnow().shift(hours=-1).datetime, + order_filled_date=arrow.utcnow().shift(hours=-1).datetime, + ) + trade.orders.append(order2) + trade.recalc_trade_from_orders() + + # Validate that the trade values have not been changed + assert trade.amount == o1_amount + assert trade.stake_amount == o1_amount + assert trade.open_rate == o1_rate + assert trade.fee_open_cost == o1_fee_cost + assert trade.open_trade_value == o1_trade_val + + # Let's try with some other orders + order3 = Order( + ft_order_side='buy', + ft_pair=trade.pair, + ft_is_open=False, + status="cancelled", + symbol=trade.pair, + order_type="market", + side="buy", + price=1, + average=2, + filled=3, + remaining=4, + cost=5, + order_date=arrow.utcnow().shift(hours=-1).datetime, + order_filled_date=arrow.utcnow().shift(hours=-1).datetime, + ) + trade.orders.append(order3) + trade.recalc_trade_from_orders() + + # Validate that the order values still are ignoring orders 2 and 3 + assert trade.amount == o1_amount + assert trade.stake_amount == o1_amount + assert trade.open_rate == o1_rate + assert trade.fee_open_cost == o1_fee_cost + assert trade.open_trade_value == o1_trade_val + + # Just to make sure sell orders are ignored, let's calculate one more time. + sell1 = Order( + ft_order_side='sell', + ft_pair=trade.pair, + ft_is_open=False, + status="closed", + symbol=trade.pair, + order_type="market", + side="sell", + price=4, + average=3, + filled=2, + remaining=1, + cost=5, + order_date=trade.open_date, + order_filled_date=trade.open_date, + ) + trade.orders.append(sell1) + trade.recalc_trade_from_orders() + + assert trade.amount == o1_amount + assert trade.stake_amount == o1_amount + assert trade.open_rate == o1_rate + assert trade.fee_open_cost == o1_fee_cost + assert trade.open_trade_value == o1_trade_val + + From b2c2852f8678371d64ffef899d8833138e8a913c Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Thu, 9 Dec 2021 23:21:35 +0200 Subject: [PATCH 05/90] Initial backtesting support. This does make it rather slow. --- freqtrade/optimize/backtesting.py | 73 ++++++++++++++++++++++++++++++- tests/test_persistence.py | 8 ++-- 2 files changed, 76 insertions(+), 5 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index d4b51d04d..b5277c216 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -24,7 +24,7 @@ from freqtrade.mixins import LoggingMixin from freqtrade.optimize.bt_progress import BTProgress from freqtrade.optimize.optimize_reports import (generate_backtest_stats, show_backtest_results, store_backtest_stats) -from freqtrade.persistence import LocalTrade, PairLocks, Trade +from freqtrade.persistence import LocalTrade, PairLocks, Trade, Order from freqtrade.plugins.pairlistmanager import PairListManager from freqtrade.plugins.protectionmanager import ProtectionManager from freqtrade.resolvers import ExchangeResolver, StrategyResolver @@ -350,8 +350,64 @@ class Backtesting: else: return sell_row[OPEN_IDX] + def _get_adjust_trade_entry_for_candle(self, trade: LocalTrade, sell_row: Tuple) -> Optional[LocalTrade]: + current_rate = sell_row[OPEN_IDX] + sell_candle_time = sell_row[DATE_IDX].to_pydatetime() + current_profit = trade.calc_profit_ratio(current_rate) + + amount_to_adjust = strategy_safe_wrapper(self.strategy.adjust_trade_position, default_retval=None)( + pair=trade.pair, trade=trade, current_time=sell_candle_time, + current_rate=current_rate, current_profit=current_profit) + + # Check if we should increase our position + if amount_to_adjust is not None and amount_to_adjust > 0.0: + return self._execute_trade_position_change(trade, sell_row, amount_to_adjust) + + return trade + + def _execute_trade_position_change(self, trade: LocalTrade, row: Tuple, + amount_to_adjust: float) -> Optional[LocalTrade]: + current_price = row[OPEN_IDX] + stake_amount = current_price * amount_to_adjust + propose_rate = min(max(current_price, row[LOW_IDX]), row[HIGH_IDX]) + available_amount = self.wallets.get_available_stake_amount() + + try: + min_stake_amount = self.exchange.get_min_pair_stake_amount(trade.pair, propose_rate, -0.05) or 0 + stake_amount = self.wallets.validate_stake_amount(trade.pair, stake_amount, min_stake_amount) + stake_amount = self.wallets._check_available_stake_amount(stake_amount, available_amount) + except DependencyException: + logger.debug(f"{trade.pair} adjustment failed, wallet is smaller than asked stake {stake_amount}") + return trade + + amount = stake_amount / current_price + if amount <= 0: + logger.debug(f"{trade.pair} adjustment failed, amount ended up being zero {amount}") + return trade + + order = Order( + ft_is_open=False, + ft_pair=trade.pair, + symbol=trade.pair, + ft_order_side="buy", + side="buy", + order_type="market", + status="closed", + price=propose_rate, + average=propose_rate, + amount=amount, + cost=stake_amount + ) + trade.orders.append(order) + trade.recalc_trade_from_orders() + self.wallets.update(); + return trade + def _get_sell_trade_entry_for_candle(self, trade: LocalTrade, sell_row: Tuple) -> Optional[LocalTrade]: + + trade = self._get_adjust_trade_entry_for_candle(trade, sell_row) + sell_candle_time = sell_row[DATE_IDX].to_pydatetime() sell = self.strategy.should_sell(trade, sell_row[OPEN_IDX], # type: ignore sell_candle_time, sell_row[BUY_IDX], @@ -476,6 +532,21 @@ class Backtesting: buy_tag=row[BUY_TAG_IDX] if has_buy_tag else None, exchange='backtesting', ) + order = Order( + ft_is_open=False, + ft_pair=trade.pair, + symbol=trade.pair, + ft_order_side="buy", + side="buy", + order_type="market", + status="closed", + price=trade.open_rate, + average=trade.open_rate, + amount=trade.amount, + cost=trade.stake_amount + trade.fee_open + ) + trade.orders = [] + trade.orders.append(order) return trade return None diff --git a/tests/test_persistence.py b/tests/test_persistence.py index a6a289007..174a93674 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -1475,8 +1475,8 @@ def test_recalc_trade_from_orders(fee): assert trade.amount == o1_amount + o2_amount + o3_amount assert trade.stake_amount == o1_cost + o2_cost + o3_cost assert trade.open_rate == avg_price - assert round(trade.fee_open_cost, 8) == round(o1_fee_cost + o2_fee_cost + o3_fee_cost, 8) - assert round(trade.open_trade_value, 8) == round(o1_trade_val + o2_trade_val + o3_trade_val, 8) + assert pytest.approx(trade.fee_open_cost) == o1_fee_cost + o2_fee_cost + o3_fee_cost + assert pytest.approx(trade.open_trade_value) == o1_trade_val + o2_trade_val + o3_trade_val # Just to make sure sell orders are ignored, let's calculate one more time. sell1 = Order( @@ -1503,8 +1503,8 @@ def test_recalc_trade_from_orders(fee): assert trade.amount == o1_amount + o2_amount + o3_amount assert trade.stake_amount == o1_cost + o2_cost + o3_cost assert trade.open_rate == avg_price - assert round(trade.fee_open_cost, 8) == round(o1_fee_cost + o2_fee_cost + o3_fee_cost, 8) - assert round(trade.open_trade_value, 8) == round(o1_trade_val + o2_trade_val + o3_trade_val, 8) + assert pytest.approx(trade.fee_open_cost) == o1_fee_cost + o2_fee_cost + o3_fee_cost + assert pytest.approx(trade.open_trade_value) == o1_trade_val + o2_trade_val + o3_trade_val def test_recalc_trade_from_orders_ignores_bad_orders(fee): From c179951cca4f87bf0aae5ce6a1f7afa492ee3902 Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Fri, 10 Dec 2021 20:42:24 +0200 Subject: [PATCH 06/90] Expect stake_amount, not actual amount of pair from strategy for DCA. --- freqtrade/freqtradebot.py | 22 ++++++++++++---------- freqtrade/optimize/backtesting.py | 20 ++++++++++---------- freqtrade/persistence/models.py | 1 + freqtrade/strategy/interface.py | 4 ++-- 4 files changed, 25 insertions(+), 22 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index b4503fe01..7bce1d084 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -476,15 +476,15 @@ class FreqtradeBot(LoggingMixin): 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=None)( + stake_to_adjust = strategy_safe_wrapper(self.strategy.adjust_trade_position, default_retval=None)( pair=trade.pair, trade=trade, current_time=datetime.now(timezone.utc), current_rate=sell_rate, current_profit=current_profit) - if amount_to_adjust != None and amount_to_adjust > 0.0: + if stake_to_adjust != None and stake_to_adjust > 0.0: # We should increase our position - self.execute_trade_position_change(trade.pair, amount_to_adjust, trade) + self.execute_trade_position_change(trade.pair, stake_to_adjust, trade) - if amount_to_adjust != None and amount_to_adjust < 0.0: + if stake_to_adjust != None and stake_to_adjust < 0.0: # We should decrease our position # TODO: Selling part of the trade not implemented yet. return @@ -492,7 +492,7 @@ class FreqtradeBot(LoggingMixin): return - def execute_trade_position_change(self, pair: str, amount: float, trade: Trade): + def execute_trade_position_change(self, pair: str, stake_amount: float, trade: Trade): """ Executes a buy order for the given pair using specific amount :param pair: pair for which we want to create a buy order @@ -514,16 +514,18 @@ class FreqtradeBot(LoggingMixin): min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, enter_limit_requested, self.strategy.stoploss) - stake_amount = self.wallets.validate_stake_amount(pair, (amount * enter_limit_requested), min_stake_amount) + stake_amount = self.wallets.validate_stake_amount(pair, stake_amount, min_stake_amount) + + amount = self.exchange.amount_to_precision(pair, stake_amount / enter_limit_requested) if not stake_amount: logger.info(f'Additional order failed to get stake amount for pair {pair}, amount={amount}, price={enter_limit_requested}') return False - + logger.debug(f'Executing additional order: amount={amount}, stake={stake_amount}, price={enter_limit_requested}') - amount = self.exchange.amount_to_precision(pair, amount) - order = self.exchange.create_order(pair=pair, ordertype="market", side="buy", + order_type = 'market' + order = self.exchange.create_order(pair=pair, ordertype=order_type, side="buy", amount=amount, rate=enter_limit_requested, time_in_force=time_in_force) order_obj = Order.parse_from_ccxt_object(order, pair, 'buy') @@ -567,7 +569,7 @@ class FreqtradeBot(LoggingMixin): # Updating wallets self.wallets.update() - self._notify_enter(trade, 'market') + self._notify_enter(trade, order_type) return True diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index b5277c216..2b6314ad6 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -350,25 +350,24 @@ class Backtesting: else: return sell_row[OPEN_IDX] - def _get_adjust_trade_entry_for_candle(self, trade: LocalTrade, sell_row: Tuple) -> Optional[LocalTrade]: - current_rate = sell_row[OPEN_IDX] - sell_candle_time = sell_row[DATE_IDX].to_pydatetime() + def _get_adjust_trade_entry_for_candle(self, trade: LocalTrade, row: Tuple) -> Optional[LocalTrade]: + current_rate = row[OPEN_IDX] + sell_candle_time = row[DATE_IDX].to_pydatetime() current_profit = trade.calc_profit_ratio(current_rate) - amount_to_adjust = strategy_safe_wrapper(self.strategy.adjust_trade_position, default_retval=None)( + stake_amount = strategy_safe_wrapper(self.strategy.adjust_trade_position, default_retval=None)( pair=trade.pair, trade=trade, current_time=sell_candle_time, current_rate=current_rate, current_profit=current_profit) # Check if we should increase our position - if amount_to_adjust is not None and amount_to_adjust > 0.0: - return self._execute_trade_position_change(trade, sell_row, amount_to_adjust) + if stake_amount is not None and stake_amount > 0.0: + return self._execute_trade_position_change(trade, row, stake_amount) return trade def _execute_trade_position_change(self, trade: LocalTrade, row: Tuple, - amount_to_adjust: float) -> Optional[LocalTrade]: + stake_amount: float) -> Optional[LocalTrade]: current_price = row[OPEN_IDX] - stake_amount = current_price * amount_to_adjust propose_rate = min(max(current_price, row[LOW_IDX]), row[HIGH_IDX]) available_amount = self.wallets.get_available_stake_amount() @@ -385,7 +384,7 @@ class Backtesting: logger.debug(f"{trade.pair} adjustment failed, amount ended up being zero {amount}") return trade - order = Order( + buy_order = Order( ft_is_open=False, ft_pair=trade.pair, symbol=trade.pair, @@ -398,8 +397,9 @@ class Backtesting: amount=amount, cost=stake_amount ) - trade.orders.append(order) + trade.orders.append(buy_order) trade.recalc_trade_from_orders() + self.wallets.update(); return trade diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 74b4cf627..3c9b69ac2 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -587,6 +587,7 @@ class LocalTrade(): self.amount = total_amount self.fee_open_cost = self.fee_open * self.stake_amount self.recalc_open_trade_value() + self.adjust_stop_loss(self.open_rate, self.stop_loss_pct) def select_order(self, order_side: str, is_open: Optional[bool]) -> Optional[Order]: diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index fead93190..57ccdef31 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -386,7 +386,7 @@ class IStrategy(ABC, HyperStrategyMixin): current_time: datetime, current_rate: float, current_profit: float, **kwargs) -> Optional[float]: """ - Custom trade adjustment logic, returning the amount that a trade shold be either increased or decreased. + Custom trade adjustment logic, returning the stake amount that a trade shold be either increased or decreased. For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ @@ -398,7 +398,7 @@ class IStrategy(ABC, HyperStrategyMixin): :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 float: Stake amount to adjust your trade """ return None From 1e3fc5e984724ad64fb9dadc1fb8be8a8a142b96 Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Fri, 10 Dec 2021 22:48:00 +0200 Subject: [PATCH 07/90] Slight code touchup --- freqtrade/optimize/backtesting.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 2b6314ad6..9c9f99030 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -351,13 +351,12 @@ class Backtesting: return sell_row[OPEN_IDX] def _get_adjust_trade_entry_for_candle(self, trade: LocalTrade, row: Tuple) -> Optional[LocalTrade]: - current_rate = row[OPEN_IDX] - sell_candle_time = row[DATE_IDX].to_pydatetime() - current_profit = trade.calc_profit_ratio(current_rate) + current_profit = trade.calc_profit_ratio(row[OPEN_IDX]) + stake_amount = strategy_safe_wrapper(self.strategy.adjust_trade_position, default_retval=None)( - pair=trade.pair, trade=trade, current_time=sell_candle_time, - current_rate=current_rate, current_profit=current_profit) + pair=trade.pair, trade=trade, current_time=row[DATE_IDX].to_pydatetime(), + current_rate=row[OPEN_IDX], current_profit=current_profit) # Check if we should increase our position if stake_amount is not None and stake_amount > 0.0: @@ -399,7 +398,6 @@ class Backtesting: ) trade.orders.append(buy_order) trade.recalc_trade_from_orders() - self.wallets.update(); return trade From b7bf3247b80d85c1eedabf43128a2a84233fa437 Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Fri, 10 Dec 2021 23:17:12 +0200 Subject: [PATCH 08/90] Only adjust stoploss if it's set. --- freqtrade/persistence/models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 3c9b69ac2..4135ed479 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -587,7 +587,8 @@ class LocalTrade(): self.amount = total_amount self.fee_open_cost = self.fee_open * self.stake_amount self.recalc_open_trade_value() - self.adjust_stop_loss(self.open_rate, self.stop_loss_pct) + if self.stop_loss_pct is not None and self.open_rate is not None: + self.adjust_stop_loss(self.open_rate, self.stop_loss_pct) def select_order(self, order_side: str, is_open: Optional[bool]) -> Optional[Order]: From f97662e816377359b209bea908d61d5c87c00859 Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Sat, 11 Dec 2021 00:28:12 +0200 Subject: [PATCH 09/90] Add position_adjustment_enable config keyword to enable it. --- freqtrade/configuration/configuration.py | 3 + freqtrade/freqtradebot.py | 5 +- freqtrade/optimize/backtesting.py | 6 +- .../test_backtesting_adjust_position.py | 95 ++++++++++ .../strats/strategy_test_position_adjust.py | 165 ++++++++++++++++++ 5 files changed, 270 insertions(+), 4 deletions(-) create mode 100644 tests/optimize/test_backtesting_adjust_position.py create mode 100644 tests/strategy/strats/strategy_test_position_adjust.py diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index f5a674878..75c973b79 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -173,6 +173,9 @@ class Configuration: if 'sd_notify' in self.args and self.args['sd_notify']: config['internals'].update({'sd_notify': True}) + if config.get('position_adjustment_enable', False): + logger.warning('`position_adjustment` has been enabled for strategy.') + def _process_datadir_options(self, config: Dict[str, Any]) -> None: """ Extract information for sys.argv and load directory configurations diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 7bce1d084..bded7a3f2 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -179,7 +179,8 @@ class FreqtradeBot(LoggingMixin): self.exit_positions(trades) # Check if we need to adjust our current positions before attempting to buy new trades. - self.process_open_trade_positions() + if self.config.get('position_adjustment_enable', False): + self.process_open_trade_positions() # Then looking for buy opportunities if self.get_free_open_trades(): @@ -521,7 +522,7 @@ class FreqtradeBot(LoggingMixin): if not stake_amount: logger.info(f'Additional order failed to get stake amount for pair {pair}, amount={amount}, price={enter_limit_requested}') return False - + logger.debug(f'Executing additional order: amount={amount}, stake={stake_amount}, price={enter_limit_requested}') order_type = 'market' diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 9c9f99030..29633b4ed 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -403,8 +403,10 @@ class Backtesting: def _get_sell_trade_entry_for_candle(self, trade: LocalTrade, sell_row: Tuple) -> Optional[LocalTrade]: - - trade = self._get_adjust_trade_entry_for_candle(trade, sell_row) + + # Check if we need to adjust our current positions + if self.config.get('position_adjustment_enable', False): + trade = self._get_adjust_trade_entry_for_candle(trade, sell_row) sell_candle_time = sell_row[DATE_IDX].to_pydatetime() sell = self.strategy.should_sell(trade, sell_row[OPEN_IDX], # type: ignore diff --git a/tests/optimize/test_backtesting_adjust_position.py b/tests/optimize/test_backtesting_adjust_position.py new file mode 100644 index 000000000..78bc4758e --- /dev/null +++ b/tests/optimize/test_backtesting_adjust_position.py @@ -0,0 +1,95 @@ +# pragma pylint: disable=missing-docstring, W0212, line-too-long, C0103, unused-argument + +import random +from datetime import datetime, timedelta, timezone +from pathlib import Path +from unittest.mock import MagicMock, PropertyMock +import logging + +import numpy as np +import pandas as pd +import pytest +from arrow import Arrow + +from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_backtesting +from freqtrade.configuration import TimeRange +from freqtrade.data import history +from freqtrade.data.btanalysis import BT_DATA_COLUMNS, evaluate_result_multi +from freqtrade.data.converter import clean_ohlcv_dataframe +from freqtrade.data.dataprovider import DataProvider +from freqtrade.data.history import get_timerange +from freqtrade.enums import RunMode, SellType +from freqtrade.exceptions import DependencyException, OperationalException +from freqtrade.optimize.backtesting import Backtesting +from freqtrade.persistence import LocalTrade +from freqtrade.resolvers import StrategyResolver +from tests.conftest import (get_args, log_has, log_has_re, patch_exchange, + patched_configuration_load_config_file) + +def test_backtest_position_adjustment(default_conf, fee, mocker, testdatadir) -> None: + default_conf['use_sell_signal'] = False + mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) + mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) + patch_exchange(mocker) + default_conf.update({ + "position_adjustment_enable": True, + "stake_amount": 100.0, + "dry_run_wallet": 1000.0, + "strategy": "StrategyTestPositionAdjust" + }) + backtesting = Backtesting(default_conf) + backtesting._set_strategy(backtesting.strategylist[0]) + pair = 'UNITTEST/BTC' + timerange = TimeRange('date', None, 1517227800, 0) + data = history.load_data(datadir=testdatadir, timeframe='5m', pairs=['UNITTEST/BTC'], + timerange=timerange) + processed = backtesting.strategy.advise_all_indicators(data) + min_date, max_date = get_timerange(processed) + result = backtesting.backtest( + processed=processed, + start_date=min_date, + end_date=max_date, + max_open_trades=10, + position_stacking=False, + ) + results = result['results'] + assert not results.empty + assert len(results) == 2 + + expected = pd.DataFrame( + {'pair': [pair, pair], + 'stake_amount': [500.0, 100.0], + 'amount': [4806.87657523, 970.63960782], + 'open_date': pd.to_datetime([Arrow(2018, 1, 29, 18, 40, 0).datetime, + Arrow(2018, 1, 30, 3, 30, 0).datetime], utc=True + ), + 'close_date': pd.to_datetime([Arrow(2018, 1, 29, 22, 00, 0).datetime, + Arrow(2018, 1, 30, 4, 10, 0).datetime], utc=True), + 'open_rate': [0.10401764894444211, 0.10302485], + 'close_rate': [0.10453904066847439, 0.103541], + 'fee_open': [0.0025, 0.0025], + 'fee_close': [0.0025, 0.0025], + 'trade_duration': [200, 40], + 'profit_ratio': [0.0, 0.0], + 'profit_abs': [0.0, 0.0], + 'sell_reason': [SellType.ROI.value, SellType.ROI.value], + 'initial_stop_loss_abs': [0.0940005, 0.09272236], + 'initial_stop_loss_ratio': [-0.1, -0.1], + 'stop_loss_abs': [0.0940005, 0.09272236], + 'stop_loss_ratio': [-0.1, -0.1], + 'min_rate': [0.10370188, 0.10300000000000001], + 'max_rate': [0.10481985, 0.1038888], + 'is_open': [False, False], + 'buy_tag': [None, None], + }) + pd.testing.assert_frame_equal(results, expected) + data_pair = processed[pair] + for _, t in results.iterrows(): + ln = data_pair.loc[data_pair["date"] == t["open_date"]] + # Check open trade rate alignes to open rate + assert ln is not None + # check close trade rate alignes to close rate or is between high and low + ln = data_pair.loc[data_pair["date"] == t["close_date"]] + assert (round(ln.iloc[0]["open"], 6) == round(t["close_rate"], 6) or + round(ln.iloc[0]["low"], 6) < round( + t["close_rate"], 6) < round(ln.iloc[0]["high"], 6)) diff --git a/tests/strategy/strats/strategy_test_position_adjust.py b/tests/strategy/strats/strategy_test_position_adjust.py new file mode 100644 index 000000000..cd786ed54 --- /dev/null +++ b/tests/strategy/strats/strategy_test_position_adjust.py @@ -0,0 +1,165 @@ +# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement + +import talib.abstract as ta +from pandas import DataFrame + +import freqtrade.vendor.qtpylib.indicators as qtpylib +from freqtrade.strategy.interface import IStrategy +from freqtrade.persistence import Trade +from datetime import datetime + +class StrategyTestPositionAdjust(IStrategy): + """ + Strategy used by tests freqtrade bot. + Please do not modify this strategy, it's intended for internal use only. + Please look at the SampleStrategy in the user_data/strategy directory + or strategy repository https://github.com/freqtrade/freqtrade-strategies + for samples and inspiration. + """ + INTERFACE_VERSION = 2 + + # Minimal ROI designed for the strategy + minimal_roi = { + "40": 0.0, + "30": 0.01, + "20": 0.02, + "0": 0.04 + } + + # Optimal stoploss designed for the strategy + stoploss = -0.10 + + # Optimal timeframe for the strategy + timeframe = '5m' + + # Optional order type mapping + order_types = { + 'buy': 'limit', + 'sell': 'limit', + 'stoploss': 'limit', + 'stoploss_on_exchange': False + } + + # Number of candles the strategy requires before producing valid signals + startup_candle_count: int = 20 + + # Optional time in force for orders + order_time_in_force = { + 'buy': 'gtc', + 'sell': 'gtc', + } + + def informative_pairs(self): + """ + Define additional, informative pair/interval combinations to be cached from the exchange. + These pair/interval combinations are non-tradeable, unless they are part + of the whitelist as well. + For more information, please consult the documentation + :return: List of tuples in the format (pair, interval) + Sample: return [("ETH/USDT", "5m"), + ("BTC/USDT", "15m"), + ] + """ + return [] + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Adds several different TA indicators to the given DataFrame + + Performance Note: For the best performance be frugal on the number of indicators + you are using. Let uncomment only the indicator you are using in your strategies + or your hyperopt configuration, otherwise you will waste your memory and CPU usage. + :param dataframe: Dataframe with data from the exchange + :param metadata: Additional information, like the currently traded pair + :return: a Dataframe with all mandatory indicators for the strategies + """ + + # Momentum Indicator + # ------------------------------------ + + # ADX + dataframe['adx'] = ta.ADX(dataframe) + + # MACD + macd = ta.MACD(dataframe) + dataframe['macd'] = macd['macd'] + dataframe['macdsignal'] = macd['macdsignal'] + dataframe['macdhist'] = macd['macdhist'] + + # Minus Directional Indicator / Movement + dataframe['minus_di'] = ta.MINUS_DI(dataframe) + + # Plus Directional Indicator / Movement + dataframe['plus_di'] = ta.PLUS_DI(dataframe) + + # RSI + dataframe['rsi'] = ta.RSI(dataframe) + + # Stoch fast + stoch_fast = ta.STOCHF(dataframe) + dataframe['fastd'] = stoch_fast['fastd'] + dataframe['fastk'] = stoch_fast['fastk'] + + # Bollinger bands + bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2) + dataframe['bb_lowerband'] = bollinger['lower'] + dataframe['bb_middleband'] = bollinger['mid'] + dataframe['bb_upperband'] = bollinger['upper'] + + # EMA - Exponential Moving Average + dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10) + + return dataframe + + def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Based on TA indicators, populates the buy signal for the given dataframe + :param dataframe: DataFrame + :param metadata: Additional information, like the currently traded pair + :return: DataFrame with buy column + """ + dataframe.loc[ + ( + (dataframe['rsi'] < 35) & + (dataframe['fastd'] < 35) & + (dataframe['adx'] > 30) & + (dataframe['plus_di'] > 0.5) + ) | + ( + (dataframe['adx'] > 65) & + (dataframe['plus_di'] > 0.5) + ), + 'buy'] = 1 + + return dataframe + + def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Based on TA indicators, populates the sell signal for the given dataframe + :param dataframe: DataFrame + :param metadata: Additional information, like the currently traded pair + :return: DataFrame with buy column + """ + dataframe.loc[ + ( + ( + (qtpylib.crossed_above(dataframe['rsi'], 70)) | + (qtpylib.crossed_above(dataframe['fastd'], 70)) + ) & + (dataframe['adx'] > 10) & + (dataframe['minus_di'] > 0) + ) | + ( + (dataframe['adx'] > 70) & + (dataframe['minus_di'] > 0.5) + ), + 'sell'] = 1 + return dataframe + + def adjust_trade_position(self, pair: str, trade: Trade, current_time: datetime, + current_rate: float, current_profit: float, **kwargs): + + if current_profit < -0.0075: + return self.wallets.get_trade_stake_amount(pair, None) + + return None From f11a40f1445e70b38f86537219b47da9ca45ebfb Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Sat, 11 Dec 2021 17:14:04 +0200 Subject: [PATCH 10/90] Improve documentation on adjust_trade_position and position_adjustment_enable --- docs/bot-basics.md | 5 ++- docs/configuration.md | 10 +++++ docs/plotting.md | 3 ++ docs/strategy-advanced.md | 74 +++++++++++++++++++++++++++++++ docs/strategy-callbacks.md | 47 ++++++++++++++++++++ freqtrade/freqtradebot.py | 5 ++- freqtrade/optimize/backtesting.py | 7 +-- freqtrade/strategy/interface.py | 5 ++- 8 files changed, 149 insertions(+), 7 deletions(-) diff --git a/docs/bot-basics.md b/docs/bot-basics.md index 0b9f7b67c..92ecfc901 100644 --- a/docs/bot-basics.md +++ b/docs/bot-basics.md @@ -38,6 +38,8 @@ By default, loop runs every few seconds (`internals.process_throttle_secs`) and * Considers stoploss, ROI and sell-signal, `custom_sell()` and `custom_stoploss()`. * Determine sell-price based on `ask_strategy` configuration setting or by using the `custom_exit_price()` callback. * Before a sell order is placed, `confirm_trade_exit()` strategy callback is called. +* Check position adjustments for open trades if enabled. + * Call `adjust_trade_position()` strategy callback and place additional order if required. * Check if trade-slots are still available (if `max_open_trades` is reached). * Verifies buy signal trying to enter new positions. * Determine buy-price based on `bid_strategy` configuration setting, or by using the `custom_entry_price()` callback. @@ -60,7 +62,8 @@ This loop will be repeated again and again until the bot is stopped. * Determine stake size by calling the `custom_stake_amount()` callback. * Call `custom_stoploss()` and `custom_sell()` to find custom exit points. * For sells based on sell-signal and custom-sell: Call `custom_exit_price()` to determine exit price (Prices are moved to be within the closing candle). - + * Check position adjustments for open trades if enabled and call `adjust_trade_position()` determine additional order is required. + * Generate backtest report output !!! Note diff --git a/docs/configuration.md b/docs/configuration.md index 00ab66ceb..3a9c19161 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -171,6 +171,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `user_data_dir` | Directory containing user data.
*Defaults to `./user_data/`*.
**Datatype:** String | `dataformat_ohlcv` | Data format to use to store historical candle (OHLCV) data.
*Defaults to `json`*.
**Datatype:** String | `dataformat_trades` | Data format to use to store historical trades data.
*Defaults to `jsongz`*.
**Datatype:** String +| `position_adjustment_enable` | Enables the strategy to use position adjustments (additional buys or sells). More information below.
**Datatype:** Boolean ### Parameters in the strategy @@ -583,6 +584,15 @@ export HTTPS_PROXY="http://addr:port" freqtrade ``` +### Understand position_adjustment_enable + +The `position_adjustment_enable` configuration parameter enables the usage of `adjust_trade_position()` callback in strategy. +For performance reasons, it's disabled by default, and freqtrade will show a warning message on startup if enabled. +This can be dangerous with some strategies, so use with care. + +See [the strategy callbacks](strategy-callbacks.md) for details on usage. + + ## Next step Now you have configured your config.json, the next step is to [start your bot](bot-usage.md). diff --git a/docs/plotting.md b/docs/plotting.md index b2d7654f6..b81d57b7e 100644 --- a/docs/plotting.md +++ b/docs/plotting.md @@ -273,6 +273,9 @@ def plot_config(self): !!! Warning `plotly` arguments are only supported with plotly library and will not work with freq-ui. +!!! Note + If `position_adjustment_enable` / `adjust_trade_position()` is used, the trade initial buy price is averaged over multiple orders and the trade start price will most likely appear outside the candle range. + ## Plot profit ![plot-profit](assets/plot-profit.png) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 4cc607883..c3527ec54 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -229,3 +229,77 @@ for val in self.buy_ema_short.range: # Append columns to existing dataframe merged_frame = pd.concat(frames, axis=1) ``` + +### Adjust trade position + +`adjust_trade_position()` can be used to perform additional orders to manage risk with DCA (Dollar Cost Averaging) for example. + +!!! Tip: The `position_adjustment_enable` configuration parameter must be enabled to use adjust_trade_position callback in strategy. + +!!! Warning: Additional orders also mean additional fees. + +!!! Warning: Stoploss is still calculated from the initial opening price, not averaged price. + +``` python +from freqtrade.persistence import Trade + + +class DigDeeperStrategy(IStrategy): + + # Attempts to handle large drops with DCA. High stoploss is required. + stoploss = -0.30 + + # ... populate_* methods + + def adjust_trade_position(self, pair: str, trade: Trade, + current_time: datetime, current_rate: float, current_profit: float, + **kwargs) -> Optional[float]: + """ + Custom trade adjustment logic, returning the stake amount that a trade should be increased. + This means extra buy orders with additional fees. + + :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: Stake amount to adjust your trade + """ + + if current_profit > -0.05: + return None + + # Obtain pair dataframe. + dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) + + # Only buy when not actively falling price. + if dataframe['close'] < dataframe['close'].shift(1): + return None + + count_of_buys = 0 + for order in trade.orders: + # Instantly stop when there's an open order + if order.ft_is_open: + return None + if order.ft_order_side == 'buy' and order.status == "closed": + count_of_buys += 1 + + # Allow up to 3 additional increasingly larger buys (4 in total) + # Initial buy is 1x + # If that falls to -5% profit, we buy 1.25x more, average profit should increase to roughly -2.2% + # If that falles down to -5% again, we buy 1.5x more + # If that falles once again down to -5%, we buy 1.75x more + # Total stake for this trade would be 1 + 1.25 + 1.5 + 1.75 = 5.5x of the initial allowed stake. + # Hope you have a deep wallet! + if 0 < count_of_buys <= 3: + try: + stake_amount = self.wallets.get_trade_stake_amount(pair, None) + stake_amount = stake_amount * (1 + (count_of_buys * 0.25)) + return stake_amount + except Exception as exception: + return None + + return None + +``` diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index 11032433d..fcf861145 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -15,6 +15,7 @@ Currently available callbacks: * [`check_buy_timeout()` and `check_sell_timeout()](#custom-order-timeout-rules) * [`confirm_trade_entry()`](#trade-entry-buy-order-confirmation) * [`confirm_trade_exit()`](#trade-exit-sell-order-confirmation) +* [`adjust_trade_position()`](#adjust-trade-position) !!! Tip "Callback calling sequence" You can find the callback calling sequence in [bot-basics](bot-basics.md#bot-execution-logic) @@ -568,3 +569,49 @@ class AwesomeStrategy(IStrategy): return True ``` + +### Adjust trade position + +`adjust_trade_position()` can be used to perform additional orders to manage risk with DCA (Dollar Cost Averaging). + +!!! Tip: The `position_adjustment_enable` configuration parameter must be enabled to use adjust_trade_position callback in strategy. + +!!! Warning: Additional orders also mean additional fees. + +!!! Warning: Stoploss is still calculated from the initial opening price, not averaged price. + +``` python +from freqtrade.persistence import Trade + + +class AwesomeStrategy(IStrategy): + + # ... populate_* methods + + def adjust_trade_position(self, pair: str, trade: Trade, + current_time: datetime, current_rate: float, current_profit: float, + **kwargs) -> Optional[float]: + """ + Custom trade adjustment logic, returning the stake amount that a trade should be increased. + This means extra buy orders with additional fees. + + For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ + + When not implemented by a strategy, returns None + + :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: Stake amount to adjust your trade + """ + + # Example: If 10% loss / -10% profit then buy more the same amount we had before. + if current_profit < -0.10: + return trade.stake_amount + + return None + +``` diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index bded7a3f2..b29eca7f3 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -102,6 +102,9 @@ class FreqtradeBot(LoggingMixin): self._exit_lock = Lock() LoggingMixin.__init__(self, logger, timeframe_to_seconds(self.strategy.timeframe)) + # Is Position Adjustment enabled? + self.position_adjustment = bool(self.config.get('position_adjustment_enable', False)) + def notify_status(self, msg: str) -> None: """ Public method for users of this class (worker, etc.) to send notifications @@ -179,7 +182,7 @@ class FreqtradeBot(LoggingMixin): self.exit_positions(trades) # Check if we need to adjust our current positions before attempting to buy new trades. - if self.config.get('position_adjustment_enable', False): + if self.position_adjustment: self.process_open_trade_positions() # Then looking for buy opportunities diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 29633b4ed..db7896983 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -118,6 +118,7 @@ class Backtesting: # Add maximum startup candle count to configuration for informative pairs support self.config['startup_candle_count'] = self.required_startup self.exchange.validate_required_startup_candles(self.required_startup, self.timeframe) + self.position_adjustment = bool(self.config.get('position_adjustment_enable', False)) self.init_backtest() def __del__(self): @@ -353,7 +354,7 @@ class Backtesting: def _get_adjust_trade_entry_for_candle(self, trade: LocalTrade, row: Tuple) -> Optional[LocalTrade]: current_profit = trade.calc_profit_ratio(row[OPEN_IDX]) - + stake_amount = strategy_safe_wrapper(self.strategy.adjust_trade_position, default_retval=None)( pair=trade.pair, trade=trade, current_time=row[DATE_IDX].to_pydatetime(), current_rate=row[OPEN_IDX], current_profit=current_profit) @@ -403,9 +404,9 @@ class Backtesting: def _get_sell_trade_entry_for_candle(self, trade: LocalTrade, sell_row: Tuple) -> Optional[LocalTrade]: - + # Check if we need to adjust our current positions - if self.config.get('position_adjustment_enable', False): + if self.position_adjustment: trade = self._get_adjust_trade_entry_for_candle(trade, sell_row) sell_candle_time = sell_row[DATE_IDX].to_pydatetime() diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 57ccdef31..81909e773 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -386,11 +386,12 @@ class IStrategy(ABC, HyperStrategyMixin): current_time: datetime, current_rate: float, current_profit: float, **kwargs) -> Optional[float]: """ - Custom trade adjustment logic, returning the stake amount that a trade shold be either increased or decreased. + Custom trade adjustment logic, returning the stake amount that a trade should be increased. + This means extra buy orders with additional fees. For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ - When not implemented by a strategy, returns 0.0 + When not implemented by a strategy, returns None :param pair: Pair that's currently analyzed :param trade: trade object. From 71147d2899203557bec230dfd06085140b30cac0 Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Sat, 11 Dec 2021 18:25:05 +0200 Subject: [PATCH 11/90] Attempt to support limit orders for position adjustment. --- freqtrade/freqtradebot.py | 35 ++++++++++++++++++++++++++++----- freqtrade/persistence/models.py | 8 ++++++-- 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index b29eca7f3..dce708711 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -469,15 +469,14 @@ class FreqtradeBot(LoggingMixin): def adjust_trade_position(self, trade: Trade): """ Check the implemented trading strategy for adjustment command. - If the strategy triggers the adjustment a new buy/sell-order gets issued. + If the strategy triggers the adjustment, a new order gets issued. Once that completes, the existing trade is modified to match new data. """ - 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, skipping pair.") return + logger.debug(f"adjust_trade_position for pair {trade.pair}") sell_rate = self.exchange.get_rate(trade.pair, refresh=True, side="sell") current_profit = trade.calc_profit_ratio(sell_rate) stake_to_adjust = strategy_safe_wrapper(self.strategy.adjust_trade_position, default_retval=None)( @@ -491,6 +490,7 @@ class FreqtradeBot(LoggingMixin): if stake_to_adjust != None and stake_to_adjust < 0.0: # We should decrease our position # TODO: Selling part of the trade not implemented yet. + logger.error(f"Unable to decrease trade position / sell partially for pair {trade.pair}, feature not implemented.") return return @@ -528,7 +528,7 @@ class FreqtradeBot(LoggingMixin): logger.debug(f'Executing additional order: amount={amount}, stake={stake_amount}, price={enter_limit_requested}') - order_type = 'market' + order_type = self.strategy.order_types['buy'] order = self.exchange.create_order(pair=pair, ordertype=order_type, side="buy", amount=amount, rate=enter_limit_requested, time_in_force=time_in_force) @@ -573,7 +573,7 @@ class FreqtradeBot(LoggingMixin): # Updating wallets self.wallets.update() - self._notify_enter(trade, order_type) + self._notify_additional_buy(trade, order_obj, order_type) return True @@ -729,6 +729,31 @@ class FreqtradeBot(LoggingMixin): return True + def _notify_additional_buy(self, trade: Trade, order: Order, order_type: Optional[str] = None, + fill: bool = False) -> None: + """ + Sends rpc notification when a buy occurred. + """ + msg = { + 'trade_id': trade.id, + 'type': RPCMessageType.BUY_FILL if fill else RPCMessageType.BUY, + 'buy_tag': "adjust_trade_position", + 'exchange': self.exchange.name.capitalize(), + 'pair': trade.pair, + 'limit': order.price, # Deprecated (?) + 'open_rate': order.price, + 'order_type': order_type, + 'stake_amount': order.cost, + 'stake_currency': self.config['stake_currency'], + 'fiat_currency': self.config.get('fiat_display_currency', None), + 'amount': order.amount, + 'open_date': order.order_filled_date or datetime.utcnow(), + 'current_rate': order.price, + } + + # Send the message + self.rpc.send_msg(msg) + def _notify_enter(self, trade: Trade, order_type: Optional[str] = None, fill: bool = False) -> None: """ diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 4135ed479..af8a4742f 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -568,8 +568,13 @@ class LocalTrade(): profit_ratio = (close_trade_value / self.open_trade_value) - 1 return float(f"{profit_ratio:.8f}") - def recalc_trade_from_orders(self): + # We need at least 2 orders for averaging amounts and rates. + if len(self.orders) < 2: + # Just in case, still recalc open trade value + self.recalc_open_trade_value() + return + total_amount = 0.0 total_stake = 0.0 for temp_order in self.orders: @@ -590,7 +595,6 @@ class LocalTrade(): if self.stop_loss_pct is not None and self.open_rate is not None: self.adjust_stop_loss(self.open_rate, self.stop_loss_pct) - def select_order(self, order_side: str, is_open: Optional[bool]) -> Optional[Order]: """ Finds latest order for this orderside and status From 64558e60d38b826ec715e544c629120d279206e5 Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Sat, 11 Dec 2021 19:45:30 +0200 Subject: [PATCH 12/90] Fix bug in example. --- docs/strategy-advanced.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index c3527ec54..523d0accd 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -274,7 +274,9 @@ class DigDeeperStrategy(IStrategy): dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) # Only buy when not actively falling price. - if dataframe['close'] < dataframe['close'].shift(1): + last_candle = dataframe.iloc[-1].squeeze() + previous_candle = dataframe.iloc[-2].squeeze() + if last_candle.close < previous_candle.close: return None count_of_buys = 0 From c6256aba35ed03195f24ebc8cf053a0e84577664 Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Sun, 12 Dec 2021 08:32:15 +0200 Subject: [PATCH 13/90] Improve documentation. --- docs/configuration.md | 2 +- docs/strategy-callbacks.md | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index 1d695db3e..c982a1d7f 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -589,7 +589,7 @@ freqtrade The `position_adjustment_enable` configuration parameter enables the usage of `adjust_trade_position()` callback in strategy. For performance reasons, it's disabled by default, and freqtrade will show a warning message on startup if enabled. -This can be dangerous with some strategies, so use with care. +Enabling this does nothing unless the strategy also implements `adjust_trade_position()` callback. See [the strategy callbacks](strategy-callbacks.md) for details on usage. diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index fcf861145..886fdbc59 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -573,12 +573,17 @@ class AwesomeStrategy(IStrategy): ### Adjust trade position `adjust_trade_position()` can be used to perform additional orders to manage risk with DCA (Dollar Cost Averaging). +The strategy is expected to return a stake_amount if and when an additional buy order should be made (position is increased). +If there is not enough funds in the wallet then nothing will happen. + +Note: Current implementation does not support decreasing position size with partial sales! !!! Tip: The `position_adjustment_enable` configuration parameter must be enabled to use adjust_trade_position callback in strategy. !!! Warning: Additional orders also mean additional fees. !!! Warning: Stoploss is still calculated from the initial opening price, not averaged price. + So if you do 3 additional buys at -7% and have a stoploss at -10% then you will most likely trigger stoploss while the UI will be showing you an average profit of -3%. ``` python from freqtrade.persistence import Trade From 1017b68af9b09d60db9659763134bf9ffd78ceea Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Mon, 13 Dec 2021 02:27:09 +0200 Subject: [PATCH 14/90] Fix some unit-tests. Use common trade entry code. --- freqtrade/freqtradebot.py | 182 ++++-------------- .../test_backtesting_adjust_position.py | 2 +- .../strats/strategy_test_position_adjust.py | 165 ---------------- tests/strategy/strats/strategy_test_v2.py | 11 +- 4 files changed, 51 insertions(+), 309 deletions(-) delete mode 100644 tests/strategy/strats/strategy_test_position_adjust.py diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index dce708711..84e938e02 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -479,15 +479,15 @@ class FreqtradeBot(LoggingMixin): logger.debug(f"adjust_trade_position for pair {trade.pair}") sell_rate = self.exchange.get_rate(trade.pair, refresh=True, side="sell") current_profit = trade.calc_profit_ratio(sell_rate) - stake_to_adjust = strategy_safe_wrapper(self.strategy.adjust_trade_position, default_retval=None)( + stake_amount = strategy_safe_wrapper(self.strategy.adjust_trade_position, default_retval=None)( pair=trade.pair, trade=trade, current_time=datetime.now(timezone.utc), current_rate=sell_rate, current_profit=current_profit) - if stake_to_adjust != None and stake_to_adjust > 0.0: + if stake_amount != None and stake_amount > 0.0: # We should increase our position - self.execute_trade_position_change(trade.pair, stake_to_adjust, trade) + self.execute_entry(trade.pair, stake_amount, trade=trade) - if stake_to_adjust != None and stake_to_adjust < 0.0: + if stake_amount != None and stake_amount < 0.0: # We should decrease our position # TODO: Selling part of the trade not implemented yet. logger.error(f"Unable to decrease trade position / sell partially for pair {trade.pair}, feature not implemented.") @@ -495,90 +495,6 @@ class FreqtradeBot(LoggingMixin): return - - def execute_trade_position_change(self, pair: str, stake_amount: float, trade: Trade): - """ - Executes a buy order for the given pair using specific amount - :param pair: pair for which we want to create a buy order - :param amount: amount of tradable pair to buy - """ - time_in_force = self.strategy.order_time_in_force['buy'] - - # Calculate price - proposed_enter_rate = self.exchange.get_rate(pair, refresh=True, side="buy") - custom_entry_price = strategy_safe_wrapper(self.strategy.custom_entry_price, - default_retval=proposed_enter_rate)( - pair=pair, current_time=datetime.now(timezone.utc), - proposed_rate=proposed_enter_rate) - - enter_limit_requested = self.get_valid_price(custom_entry_price, 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 = self.wallets.validate_stake_amount(pair, stake_amount, min_stake_amount) - - amount = self.exchange.amount_to_precision(pair, stake_amount / enter_limit_requested) - - if not stake_amount: - logger.info(f'Additional order failed to get stake amount for pair {pair}, amount={amount}, price={enter_limit_requested}') - return False - - logger.debug(f'Executing additional order: amount={amount}, stake={stake_amount}, price={enter_limit_requested}') - - order_type = self.strategy.order_types['buy'] - order = self.exchange.create_order(pair=pair, ordertype=order_type, 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_status = order.get('status', None) - - 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') - - # 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') - - # 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} adjustment order status {order_status}") - - trade.orders.append(order_obj) - trade.recalc_trade_from_orders() - Trade.commit() - - # Updating wallets - self.wallets.update() - - self._notify_additional_buy(trade, order_obj, order_type) - - return True - - - def _check_depth_of_market_buy(self, pair: str, conf: Dict) -> bool: """ Checks depth of market before executing a buy @@ -604,7 +520,8 @@ class FreqtradeBot(LoggingMixin): return False def execute_entry(self, pair: str, stake_amount: float, price: Optional[float] = None, *, - ordertype: Optional[str] = None, buy_tag: Optional[str] = None) -> bool: + ordertype: Optional[str] = None, buy_tag: Optional[str] = None, + trade: Optional[Trade] = None) -> bool: """ Executes a limit buy for the given pair :param pair: pair for which we want to create a LIMIT_BUY @@ -631,7 +548,7 @@ class FreqtradeBot(LoggingMixin): min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, enter_limit_requested, self.strategy.stoploss) - if not self.edge: + if not self.edge and trade is None: max_stake_amount = self.wallets.get_available_stake_amount() stake_amount = strategy_safe_wrapper(self.strategy.custom_stake_amount, default_retval=stake_amount)( @@ -641,15 +558,20 @@ class FreqtradeBot(LoggingMixin): stake_amount = self.wallets.validate_stake_amount(pair, stake_amount, min_stake_amount) if not stake_amount: + logger.error(f"No stake amount? {pair} {stake_amount} {max_stake_amount}") return False - logger.info(f"Buy signal found: about create a new trade for {pair} with stake_amount: " - f"{stake_amount} ...") + if trade is None: + logger.info(f"Buy signal found: about create a new trade for {pair} with stake_amount: " + f"{stake_amount} ...") + else: + logger.info(f"Position adjust: about create a new order for {pair} with stake_amount: " + f"{stake_amount} ...") amount = stake_amount / enter_limit_requested order_type = ordertype or self.strategy.order_types['buy'] - if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)( + if trade is None and not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)( pair=pair, order_type=order_type, amount=amount, rate=enter_limit_requested, time_in_force=time_in_force, current_time=datetime.now(timezone.utc)): logger.info(f"User requested abortion of buying {pair}") @@ -696,32 +618,33 @@ class FreqtradeBot(LoggingMixin): # 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, - is_open=True, - amount_requested=amount_requested, - fee_open=fee, - fee_close=fee, - open_rate=enter_limit_filled_price, - open_rate_requested=enter_limit_requested, - open_date=datetime.utcnow(), - exchange=self.exchange.id, - open_order_id=order_id, - strategy=self.strategy.get_strategy_name(), - buy_tag=buy_tag, - timeframe=timeframe_to_minutes(self.config['timeframe']) - ) + if trade is None: + trade = Trade( + pair=pair, + stake_amount=stake_amount, + amount=amount, + is_open=True, + amount_requested=amount_requested, + fee_open=fee, + fee_close=fee, + open_rate=enter_limit_filled_price, + open_rate_requested=enter_limit_requested, + open_date=datetime.utcnow(), + exchange=self.exchange.id, + open_order_id=order_id, + strategy=self.strategy.get_strategy_name(), + buy_tag=buy_tag, + timeframe=timeframe_to_minutes(self.config['timeframe']) + ) trade.orders.append(order_obj) - + trade.recalc_trade_from_orders() Trade.query.session.add(trade) Trade.commit() # Updating wallets self.wallets.update() - self._notify_enter(trade, order_type) + self._notify_enter(trade, order, order_type) # Update fees if order is closed if order_status == 'closed': @@ -729,32 +652,7 @@ class FreqtradeBot(LoggingMixin): return True - def _notify_additional_buy(self, trade: Trade, order: Order, order_type: Optional[str] = None, - fill: bool = False) -> None: - """ - Sends rpc notification when a buy occurred. - """ - msg = { - 'trade_id': trade.id, - 'type': RPCMessageType.BUY_FILL if fill else RPCMessageType.BUY, - 'buy_tag': "adjust_trade_position", - 'exchange': self.exchange.name.capitalize(), - 'pair': trade.pair, - 'limit': order.price, # Deprecated (?) - 'open_rate': order.price, - 'order_type': order_type, - 'stake_amount': order.cost, - 'stake_currency': self.config['stake_currency'], - 'fiat_currency': self.config.get('fiat_display_currency', None), - 'amount': order.amount, - 'open_date': order.order_filled_date or datetime.utcnow(), - 'current_rate': order.price, - } - - # Send the message - self.rpc.send_msg(msg) - - def _notify_enter(self, trade: Trade, order_type: Optional[str] = None, + def _notify_enter(self, trade: Trade, order: Dict, order_type: Optional[str] = None, fill: bool = False) -> None: """ Sends rpc notification when a buy occurred. @@ -765,13 +663,13 @@ class FreqtradeBot(LoggingMixin): 'buy_tag': trade.buy_tag, 'exchange': self.exchange.name.capitalize(), 'pair': trade.pair, - 'limit': trade.open_rate, # Deprecated (?) - 'open_rate': trade.open_rate, + 'limit': safe_value_fallback(order, 'average', 'price'), # Deprecated (?) + 'open_rate': safe_value_fallback(order, 'average', 'price'), 'order_type': order_type, 'stake_amount': trade.stake_amount, 'stake_currency': self.config['stake_currency'], 'fiat_currency': self.config.get('fiat_display_currency', None), - 'amount': trade.amount, + 'amount': safe_value_fallback(order, 'filled', 'amount') or trade.amount, 'open_date': trade.open_date or datetime.utcnow(), 'current_rate': trade.open_rate_requested, } @@ -1462,7 +1360,7 @@ class FreqtradeBot(LoggingMixin): self.wallets.update() elif send_msg and not trade.open_order_id: # Buy fill - self._notify_enter(trade, fill=True) + self._notify_enter(trade, order, fill=True) return False diff --git a/tests/optimize/test_backtesting_adjust_position.py b/tests/optimize/test_backtesting_adjust_position.py index 78bc4758e..e32b9f6c6 100644 --- a/tests/optimize/test_backtesting_adjust_position.py +++ b/tests/optimize/test_backtesting_adjust_position.py @@ -35,7 +35,7 @@ def test_backtest_position_adjustment(default_conf, fee, mocker, testdatadir) -> "position_adjustment_enable": True, "stake_amount": 100.0, "dry_run_wallet": 1000.0, - "strategy": "StrategyTestPositionAdjust" + "strategy": "StrategyTestV2" }) backtesting = Backtesting(default_conf) backtesting._set_strategy(backtesting.strategylist[0]) diff --git a/tests/strategy/strats/strategy_test_position_adjust.py b/tests/strategy/strats/strategy_test_position_adjust.py deleted file mode 100644 index cd786ed54..000000000 --- a/tests/strategy/strats/strategy_test_position_adjust.py +++ /dev/null @@ -1,165 +0,0 @@ -# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement - -import talib.abstract as ta -from pandas import DataFrame - -import freqtrade.vendor.qtpylib.indicators as qtpylib -from freqtrade.strategy.interface import IStrategy -from freqtrade.persistence import Trade -from datetime import datetime - -class StrategyTestPositionAdjust(IStrategy): - """ - Strategy used by tests freqtrade bot. - Please do not modify this strategy, it's intended for internal use only. - Please look at the SampleStrategy in the user_data/strategy directory - or strategy repository https://github.com/freqtrade/freqtrade-strategies - for samples and inspiration. - """ - INTERFACE_VERSION = 2 - - # Minimal ROI designed for the strategy - minimal_roi = { - "40": 0.0, - "30": 0.01, - "20": 0.02, - "0": 0.04 - } - - # Optimal stoploss designed for the strategy - stoploss = -0.10 - - # Optimal timeframe for the strategy - timeframe = '5m' - - # Optional order type mapping - order_types = { - 'buy': 'limit', - 'sell': 'limit', - 'stoploss': 'limit', - 'stoploss_on_exchange': False - } - - # Number of candles the strategy requires before producing valid signals - startup_candle_count: int = 20 - - # Optional time in force for orders - order_time_in_force = { - 'buy': 'gtc', - 'sell': 'gtc', - } - - def informative_pairs(self): - """ - Define additional, informative pair/interval combinations to be cached from the exchange. - These pair/interval combinations are non-tradeable, unless they are part - of the whitelist as well. - For more information, please consult the documentation - :return: List of tuples in the format (pair, interval) - Sample: return [("ETH/USDT", "5m"), - ("BTC/USDT", "15m"), - ] - """ - return [] - - def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - """ - Adds several different TA indicators to the given DataFrame - - Performance Note: For the best performance be frugal on the number of indicators - you are using. Let uncomment only the indicator you are using in your strategies - or your hyperopt configuration, otherwise you will waste your memory and CPU usage. - :param dataframe: Dataframe with data from the exchange - :param metadata: Additional information, like the currently traded pair - :return: a Dataframe with all mandatory indicators for the strategies - """ - - # Momentum Indicator - # ------------------------------------ - - # ADX - dataframe['adx'] = ta.ADX(dataframe) - - # MACD - macd = ta.MACD(dataframe) - dataframe['macd'] = macd['macd'] - dataframe['macdsignal'] = macd['macdsignal'] - dataframe['macdhist'] = macd['macdhist'] - - # Minus Directional Indicator / Movement - dataframe['minus_di'] = ta.MINUS_DI(dataframe) - - # Plus Directional Indicator / Movement - dataframe['plus_di'] = ta.PLUS_DI(dataframe) - - # RSI - dataframe['rsi'] = ta.RSI(dataframe) - - # Stoch fast - stoch_fast = ta.STOCHF(dataframe) - dataframe['fastd'] = stoch_fast['fastd'] - dataframe['fastk'] = stoch_fast['fastk'] - - # Bollinger bands - bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2) - dataframe['bb_lowerband'] = bollinger['lower'] - dataframe['bb_middleband'] = bollinger['mid'] - dataframe['bb_upperband'] = bollinger['upper'] - - # EMA - Exponential Moving Average - dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10) - - return dataframe - - def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - """ - Based on TA indicators, populates the buy signal for the given dataframe - :param dataframe: DataFrame - :param metadata: Additional information, like the currently traded pair - :return: DataFrame with buy column - """ - dataframe.loc[ - ( - (dataframe['rsi'] < 35) & - (dataframe['fastd'] < 35) & - (dataframe['adx'] > 30) & - (dataframe['plus_di'] > 0.5) - ) | - ( - (dataframe['adx'] > 65) & - (dataframe['plus_di'] > 0.5) - ), - 'buy'] = 1 - - return dataframe - - def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - """ - Based on TA indicators, populates the sell signal for the given dataframe - :param dataframe: DataFrame - :param metadata: Additional information, like the currently traded pair - :return: DataFrame with buy column - """ - dataframe.loc[ - ( - ( - (qtpylib.crossed_above(dataframe['rsi'], 70)) | - (qtpylib.crossed_above(dataframe['fastd'], 70)) - ) & - (dataframe['adx'] > 10) & - (dataframe['minus_di'] > 0) - ) | - ( - (dataframe['adx'] > 70) & - (dataframe['minus_di'] > 0.5) - ), - 'sell'] = 1 - return dataframe - - def adjust_trade_position(self, pair: str, trade: Trade, current_time: datetime, - current_rate: float, current_profit: float, **kwargs): - - if current_profit < -0.0075: - return self.wallets.get_trade_stake_amount(pair, None) - - return None diff --git a/tests/strategy/strats/strategy_test_v2.py b/tests/strategy/strats/strategy_test_v2.py index 53e39526f..ac02b4436 100644 --- a/tests/strategy/strats/strategy_test_v2.py +++ b/tests/strategy/strats/strategy_test_v2.py @@ -5,7 +5,8 @@ from pandas import DataFrame import freqtrade.vendor.qtpylib.indicators as qtpylib from freqtrade.strategy.interface import IStrategy - +from freqtrade.persistence import Trade +from datetime import datetime class StrategyTestV2(IStrategy): """ @@ -154,3 +155,11 @@ class StrategyTestV2(IStrategy): ), 'sell'] = 1 return dataframe + + def adjust_trade_position(self, pair: str, trade: Trade, current_time: datetime, + current_rate: float, current_profit: float, **kwargs): + + if current_profit < -0.0075: + return self.wallets.get_trade_stake_amount(pair, None) + + return None From 2c3e5fa08022f3a54ca2b1674d092ba1e04c9d7a Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Mon, 13 Dec 2021 02:30:29 +0200 Subject: [PATCH 15/90] Remove extra logging. --- freqtrade/freqtradebot.py | 1 - 1 file changed, 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 84e938e02..4885a3a8a 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -558,7 +558,6 @@ class FreqtradeBot(LoggingMixin): stake_amount = self.wallets.validate_stake_amount(pair, stake_amount, min_stake_amount) if not stake_amount: - logger.error(f"No stake amount? {pair} {stake_amount} {max_stake_amount}") return False if trade is None: From 1362bd962666b407fa07ef3637f0757c48309f30 Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Mon, 13 Dec 2021 02:46:37 +0200 Subject: [PATCH 16/90] Fix potential problem. --- freqtrade/freqtradebot.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 4885a3a8a..18a02c3bc 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -656,6 +656,7 @@ class FreqtradeBot(LoggingMixin): """ Sends rpc notification when a buy occurred. """ + open_rate = safe_value_fallback(order, 'average', 'price') msg = { 'trade_id': trade.id, 'type': RPCMessageType.BUY_FILL if fill else RPCMessageType.BUY, @@ -663,7 +664,7 @@ class FreqtradeBot(LoggingMixin): 'exchange': self.exchange.name.capitalize(), 'pair': trade.pair, 'limit': safe_value_fallback(order, 'average', 'price'), # Deprecated (?) - 'open_rate': safe_value_fallback(order, 'average', 'price'), + 'open_rate': open_rate or trade.open_rate, 'order_type': order_type, 'stake_amount': trade.stake_amount, 'stake_currency': self.config['stake_currency'], From 6f6e7467f5fc2c3f798c8e2e64551f15cd58e854 Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Mon, 13 Dec 2021 11:17:24 +0200 Subject: [PATCH 17/90] Fix potential problem. --- freqtrade/freqtradebot.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 18a02c3bc..69dfe39a1 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -657,6 +657,9 @@ class FreqtradeBot(LoggingMixin): Sends rpc notification when a buy occurred. """ open_rate = safe_value_fallback(order, 'average', 'price') + if open_rate is None: + open_rate = trade.open_rate + msg = { 'trade_id': trade.id, 'type': RPCMessageType.BUY_FILL if fill else RPCMessageType.BUY, @@ -664,7 +667,7 @@ class FreqtradeBot(LoggingMixin): 'exchange': self.exchange.name.capitalize(), 'pair': trade.pair, 'limit': safe_value_fallback(order, 'average', 'price'), # Deprecated (?) - 'open_rate': open_rate or trade.open_rate, + 'open_rate': open_rate, 'order_type': order_type, 'stake_amount': trade.stake_amount, 'stake_currency': self.config['stake_currency'], From d4b31263ca3499393f07cf86a645d44bb63ad5eb Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Mon, 13 Dec 2021 13:54:01 +0200 Subject: [PATCH 18/90] Fix open rate being None formatting error. --- freqtrade/freqtradebot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 69dfe39a1..6efe08a5d 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -666,7 +666,7 @@ class FreqtradeBot(LoggingMixin): 'buy_tag': trade.buy_tag, 'exchange': self.exchange.name.capitalize(), 'pair': trade.pair, - 'limit': safe_value_fallback(order, 'average', 'price'), # Deprecated (?) + 'limit': open_rate, # Deprecated (?) 'open_rate': open_rate, 'order_type': order_type, 'stake_amount': trade.stake_amount, From 468076cf54aa3cf8a52097912815aa50a834ab7a Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Mon, 13 Dec 2021 20:32:13 +0200 Subject: [PATCH 19/90] This has to be reset since otherwise it will not handle live limit orders after first buy. --- freqtrade/freqtradebot.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 6efe08a5d..c6a2d667f 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -635,6 +635,7 @@ class FreqtradeBot(LoggingMixin): buy_tag=buy_tag, timeframe=timeframe_to_minutes(self.config['timeframe']) ) + trade.fee_open_currency = None trade.orders.append(order_obj) trade.recalc_trade_from_orders() Trade.query.session.add(trade) From 9be29c6e9248364a6c6d2fd72202b88512cef8cc Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Mon, 13 Dec 2021 20:44:18 +0200 Subject: [PATCH 20/90] Theoretically fix second order timeout/canceling deleting the whole order. --- freqtrade/freqtradebot.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index c6a2d667f..4cd8b51a4 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1034,10 +1034,16 @@ class FreqtradeBot(LoggingMixin): filled_amount = safe_value_fallback2(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.delete() - was_trade_fully_canceled = True - reason += f", {constants.CANCEL_REASON['FULLY_CANCELLED']}" + # if trade is not partially completed and it's the only order, just delete the trade + if len(trade.orders) <= 1: + trade.delete() + was_trade_fully_canceled = True + reason += f", {constants.CANCEL_REASON['FULLY_CANCELLED']}" + else: + # FIXME TODO: This could possibly reworked to not duplicate the code 15 lines below. + self.update_trade_state(trade, trade.open_order_id, corder) + trade.open_order_id = None + logger.info('Partial buy order timeout for %s.', trade) else: # if trade is partially complete, edit the stake details for the trade # and close the order From 462270bc5a56ba4ac0799280fb884c6a970052d8 Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Thu, 16 Dec 2021 22:57:56 +0200 Subject: [PATCH 21/90] Fix a case where the amount was not recalculated. Added additional temporary logging. --- freqtrade/freqtradebot.py | 24 +++++++++++++++++++++-- freqtrade/persistence/models.py | 19 +++++++++++------- tests/strategy/strats/strategy_test_v2.py | 4 ++++ tests/test_persistence.py | 4 ++-- 4 files changed, 40 insertions(+), 11 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 137a47eaa..c6cc0a14c 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -564,8 +564,8 @@ class FreqtradeBot(LoggingMixin): logger.info(f"Buy signal found: about create a new trade for {pair} with stake_amount: " f"{stake_amount} ...") else: - logger.info(f"Position adjust: about create a new order for {pair} with stake_amount: " - f"{stake_amount} ...") + logger.info(f"Position adjust: about to create a new order for {pair} with stake_amount: " + f"{stake_amount} for {trade}") amount = stake_amount / enter_limit_requested order_type = ordertype or self.strategy.order_types['buy'] @@ -582,6 +582,7 @@ class FreqtradeBot(LoggingMixin): order_obj = Order.parse_from_ccxt_object(order, pair, 'buy') order_id = order['id'] order_status = order.get('status', None) + logger.info(f"Order #{order_id} was created for {pair} and status is {order_status}.") # we assume the order is executed at the price requested enter_limit_filled_price = enter_limit_requested @@ -641,6 +642,22 @@ class FreqtradeBot(LoggingMixin): Trade.query.session.add(trade) Trade.commit() + if trade is not None: + if order_status == 'closed': + logger.info(f"DCA order closed, trade should be up to date: {trade}") + # First cancelling stoploss on exchange ... + if self.strategy.order_types.get('stoploss_on_exchange') and trade.stoploss_order_id: + try: + logger.info(f"Canceling stoploss on exchange, recreating with new amount for {trade}") + co = self.exchange.cancel_stoploss_order_with_result(trade.stoploss_order_id, + trade.pair, trade.amount) + trade.update_order(co) + except InvalidOrderException: + logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}") + + else: + logger.info(f"DCA order {order_status}, will wait for resolution: {trade}") + # Updating wallets self.wallets.update() @@ -1344,7 +1361,9 @@ class FreqtradeBot(LoggingMixin): logger.warning('Unable to fetch order %s: %s', order_id, exception) return False + logger.info(f"Updating order from exchange: {order}") trade.update_order(order) + trade.recalc_trade_from_orders() if self.exchange.check_order_canceled_empty(order): # Trade has been cancelled on exchange @@ -1365,6 +1384,7 @@ class FreqtradeBot(LoggingMixin): trade.update(order) trade.recalc_trade_from_orders() Trade.commit() + logger.info(f"Trade has been updated: {trade}") # Updating wallets when order is closed if not trade.is_open: diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index af8a4742f..e1f16a8dc 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -428,6 +428,7 @@ class LocalTrade(): if self.is_open: logger.info(f'{order_type.upper()}_BUY has been fulfilled for {self}.') self.open_order_id = None + self.recalc_trade_from_orders() elif order_type in ('market', 'limit') and order['side'] == 'sell': if self.is_open: logger.info(f'{order_type.upper()}_SELL has been fulfilled for {self}.') @@ -578,13 +579,17 @@ class LocalTrade(): total_amount = 0.0 total_stake = 0.0 for temp_order in self.orders: - if temp_order.ft_is_open == False and temp_order.status == "closed" and temp_order.ft_order_side == 'buy': - tmp_amount = temp_order.amount - if temp_order.filled is not None: - tmp_amount = temp_order.filled - if tmp_amount is not None and temp_order.average is not None: - total_amount += tmp_amount - total_stake += temp_order.average * tmp_amount + if temp_order.ft_is_open or \ + temp_order.ft_order_side != 'buy' or \ + temp_order.status not in NON_OPEN_EXCHANGE_STATES: + continue + + tmp_amount = temp_order.amount + if temp_order.filled is not None: + tmp_amount = temp_order.filled + if tmp_amount > 0.0 and temp_order.average is not None: + total_amount += tmp_amount + total_stake += temp_order.average * tmp_amount if total_amount > 0: self.open_rate = total_stake / total_amount diff --git a/tests/strategy/strats/strategy_test_v2.py b/tests/strategy/strats/strategy_test_v2.py index ac02b4436..5e7403a78 100644 --- a/tests/strategy/strats/strategy_test_v2.py +++ b/tests/strategy/strats/strategy_test_v2.py @@ -160,6 +160,10 @@ class StrategyTestV2(IStrategy): current_rate: float, current_profit: float, **kwargs): if current_profit < -0.0075: + for order in trade.orders: + if order.ft_is_open: + return None + return self.wallets.get_trade_stake_amount(pair, None) return None diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 174a93674..6aa7fb741 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -1563,7 +1563,7 @@ def test_recalc_trade_from_orders_ignores_bad_orders(fee): side="buy", price=1, average=2, - filled=3, + filled=0, remaining=4, cost=5, order_date=arrow.utcnow().shift(hours=-1).datetime, @@ -1590,7 +1590,7 @@ def test_recalc_trade_from_orders_ignores_bad_orders(fee): side="buy", price=1, average=2, - filled=3, + filled=0, remaining=4, cost=5, order_date=arrow.utcnow().shift(hours=-1).datetime, From d10fb95fcec254a71917f6078a4e0e342c5f9e75 Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Fri, 17 Dec 2021 22:25:02 +0200 Subject: [PATCH 22/90] Fix typo --- freqtrade/optimize/bt_progress.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/bt_progress.py b/freqtrade/optimize/bt_progress.py index d295956c7..c3b105915 100644 --- a/freqtrade/optimize/bt_progress.py +++ b/freqtrade/optimize/bt_progress.py @@ -12,7 +12,7 @@ class BTProgress: def init_step(self, action: BacktestState, max_steps: float): self._action = action self._max_steps = max_steps - self._proress = 0 + self._progress = 0 def set_new_value(self, new_value: float): self._progress = new_value From cc28f73d7f8025554a198fc0dfeec27dd3cf69d8 Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Fri, 17 Dec 2021 22:29:41 +0200 Subject: [PATCH 23/90] Hopefully fix orders being left lingering and trade not updating once they are complete --- freqtrade/freqtradebot.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index c6cc0a14c..e401349d9 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -637,6 +637,7 @@ class FreqtradeBot(LoggingMixin): timeframe=timeframe_to_minutes(self.config['timeframe']) ) trade.fee_open_currency = None + trade.fee_open_currency = order_id trade.orders.append(order_obj) trade.recalc_trade_from_orders() Trade.query.session.add(trade) From 30673f84f952162187fd54e07d00a204e04ed189 Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Sat, 18 Dec 2021 11:00:25 +0200 Subject: [PATCH 24/90] Flake8 compatibility --- freqtrade/optimize/backtesting.py | 20 +++++++++++------- freqtrade/strategy/interface.py | 7 +++---- .../test_backtesting_adjust_position.py | 21 +++---------------- tests/strategy/strats/strategy_test_v2.py | 3 ++- tests/test_persistence.py | 4 +--- 5 files changed, 22 insertions(+), 33 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index db7896983..2c6d509f7 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -351,11 +351,13 @@ class Backtesting: else: return sell_row[OPEN_IDX] - def _get_adjust_trade_entry_for_candle(self, trade: LocalTrade, row: Tuple) -> Optional[LocalTrade]: + def _get_adjust_trade_entry_for_candle(self, trade: LocalTrade, row: Tuple + ) -> Optional[LocalTrade]: current_profit = trade.calc_profit_ratio(row[OPEN_IDX]) - stake_amount = strategy_safe_wrapper(self.strategy.adjust_trade_position, default_retval=None)( + stake_amount = strategy_safe_wrapper(self.strategy.adjust_trade_position, + default_retval=None)( pair=trade.pair, trade=trade, current_time=row[DATE_IDX].to_pydatetime(), current_rate=row[OPEN_IDX], current_profit=current_profit) @@ -372,11 +374,15 @@ class Backtesting: available_amount = self.wallets.get_available_stake_amount() try: - min_stake_amount = self.exchange.get_min_pair_stake_amount(trade.pair, propose_rate, -0.05) or 0 - stake_amount = self.wallets.validate_stake_amount(trade.pair, stake_amount, min_stake_amount) - stake_amount = self.wallets._check_available_stake_amount(stake_amount, available_amount) + min_stake_amount = self.exchange.get_min_pair_stake_amount( + trade.pair, propose_rate, -0.05) or 0 + stake_amount = self.wallets.validate_stake_amount(trade.pair, + stake_amount, min_stake_amount) + stake_amount = self.wallets._check_available_stake_amount(stake_amount, + available_amount) except DependencyException: - logger.debug(f"{trade.pair} adjustment failed, wallet is smaller than asked stake {stake_amount}") + logger.debug(f"{trade.pair} adjustment failed, " + f"wallet is smaller than asked stake {stake_amount}") return trade amount = stake_amount / current_price @@ -399,7 +405,7 @@ class Backtesting: ) trade.orders.append(buy_order) trade.recalc_trade_from_orders() - self.wallets.update(); + self.wallets.update() return trade def _get_sell_trade_entry_for_candle(self, trade: LocalTrade, diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 81909e773..1adcf42ca 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -381,10 +381,9 @@ class IStrategy(ABC, HyperStrategyMixin): """ return proposed_stake - - def adjust_trade_position(self, pair: str, trade: Trade, - current_time: datetime, current_rate: float, current_profit: float, - **kwargs) -> Optional[float]: + def adjust_trade_position(self, pair: str, trade: Trade, current_time: datetime, + current_rate: float, current_profit: float, **kwargs + ) -> Optional[float]: """ Custom trade adjustment logic, returning the stake amount that a trade should be increased. This means extra buy orders with additional fees. diff --git a/tests/optimize/test_backtesting_adjust_position.py b/tests/optimize/test_backtesting_adjust_position.py index e32b9f6c6..4d87da503 100644 --- a/tests/optimize/test_backtesting_adjust_position.py +++ b/tests/optimize/test_backtesting_adjust_position.py @@ -1,30 +1,15 @@ # pragma pylint: disable=missing-docstring, W0212, line-too-long, C0103, unused-argument -import random -from datetime import datetime, timedelta, timezone -from pathlib import Path -from unittest.mock import MagicMock, PropertyMock -import logging - -import numpy as np import pandas as pd -import pytest from arrow import Arrow -from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_backtesting from freqtrade.configuration import TimeRange from freqtrade.data import history -from freqtrade.data.btanalysis import BT_DATA_COLUMNS, evaluate_result_multi -from freqtrade.data.converter import clean_ohlcv_dataframe -from freqtrade.data.dataprovider import DataProvider from freqtrade.data.history import get_timerange -from freqtrade.enums import RunMode, SellType -from freqtrade.exceptions import DependencyException, OperationalException +from freqtrade.enums import SellType from freqtrade.optimize.backtesting import Backtesting -from freqtrade.persistence import LocalTrade -from freqtrade.resolvers import StrategyResolver -from tests.conftest import (get_args, log_has, log_has_re, patch_exchange, - patched_configuration_load_config_file) +from tests.conftest import (patch_exchange) + def test_backtest_position_adjustment(default_conf, fee, mocker, testdatadir) -> None: default_conf['use_sell_signal'] = False diff --git a/tests/strategy/strats/strategy_test_v2.py b/tests/strategy/strats/strategy_test_v2.py index 5e7403a78..6dce14f42 100644 --- a/tests/strategy/strats/strategy_test_v2.py +++ b/tests/strategy/strats/strategy_test_v2.py @@ -8,6 +8,7 @@ from freqtrade.strategy.interface import IStrategy from freqtrade.persistence import Trade from datetime import datetime + class StrategyTestV2(IStrategy): """ Strategy used by tests freqtrade bot. @@ -163,7 +164,7 @@ class StrategyTestV2(IStrategy): for order in trade.orders: if order.ft_is_open: return None - + return self.wallets.get_trade_stake_amount(pair, None) return None diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 6aa7fb741..1a0c3c408 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -1506,6 +1506,7 @@ def test_recalc_trade_from_orders(fee): assert pytest.approx(trade.fee_open_cost) == o1_fee_cost + o2_fee_cost + o3_fee_cost assert pytest.approx(trade.open_trade_value) == o1_trade_val + o2_trade_val + o3_trade_val + def test_recalc_trade_from_orders_ignores_bad_orders(fee): o1_amount = 100 @@ -1631,6 +1632,3 @@ def test_recalc_trade_from_orders_ignores_bad_orders(fee): assert trade.open_rate == o1_rate assert trade.fee_open_cost == o1_fee_cost assert trade.open_trade_value == o1_trade_val - - - From b094430c26a5e1d072d0b57717ce9d911f5aba39 Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Sat, 18 Dec 2021 11:01:06 +0200 Subject: [PATCH 25/90] Restructure for less complexity. Flake8 --- freqtrade/freqtradebot.py | 129 +++++++++++++++++++------------------- 1 file changed, 66 insertions(+), 63 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index e401349d9..5a94dcb1f 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -450,9 +450,8 @@ class FreqtradeBot(LoggingMixin): else: return False - # -# BUY / increase / decrease positions / DCA logic and methods +# BUY / increase positions / DCA logic and methods # def process_open_trade_positions(self): """ @@ -463,8 +462,8 @@ class FreqtradeBot(LoggingMixin): try: self.adjust_trade_position(trade) except DependencyException as exception: - logger.warning('Unable to adjust position of trade for %s: %s', trade.pair, exception) - + logger.warning('Unable to adjust position of trade for %s: %s', + trade.pair, exception) def adjust_trade_position(self, trade: Trade): """ @@ -479,18 +478,20 @@ class FreqtradeBot(LoggingMixin): logger.debug(f"adjust_trade_position for pair {trade.pair}") sell_rate = self.exchange.get_rate(trade.pair, refresh=True, side="sell") current_profit = trade.calc_profit_ratio(sell_rate) - stake_amount = strategy_safe_wrapper(self.strategy.adjust_trade_position, default_retval=None)( + stake_amount = strategy_safe_wrapper(self.strategy.adjust_trade_position, + default_retval=None)( pair=trade.pair, trade=trade, current_time=datetime.now(timezone.utc), current_rate=sell_rate, current_profit=current_profit) - if stake_amount != None and stake_amount > 0.0: + if stake_amount is not None and stake_amount > 0.0: # We should increase our position self.execute_entry(trade.pair, stake_amount, trade=trade) - if stake_amount != None and stake_amount < 0.0: + if stake_amount is not None and stake_amount < 0.0: # We should decrease our position # TODO: Selling part of the trade not implemented yet. - logger.error(f"Unable to decrease trade position / sell partially for pair {trade.pair}, feature not implemented.") + logger.error(f"Unable to decrease trade position / sell partially" + f" for pair {trade.pair}, feature not implemented.") return return @@ -528,49 +529,28 @@ class FreqtradeBot(LoggingMixin): :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'] - if price: - enter_limit_requested = price - else: - # Calculate price - proposed_enter_rate = self.exchange.get_rate(pair, refresh=True, side="buy") - custom_entry_price = strategy_safe_wrapper(self.strategy.custom_entry_price, - default_retval=proposed_enter_rate)( - pair=pair, current_time=datetime.now(timezone.utc), - proposed_rate=proposed_enter_rate) + pos_adjust = trade is not None - enter_limit_requested = self.get_valid_price(custom_entry_price, 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) - - if not self.edge and trade is None: - max_stake_amount = self.wallets.get_available_stake_amount() - stake_amount = strategy_safe_wrapper(self.strategy.custom_stake_amount, - default_retval=stake_amount)( - pair=pair, current_time=datetime.now(timezone.utc), - current_rate=enter_limit_requested, proposed_stake=stake_amount, - min_stake=min_stake_amount, max_stake=max_stake_amount) - stake_amount = self.wallets.validate_stake_amount(pair, stake_amount, min_stake_amount) + enter_limit_requested, stake_amount = self.get_valid_enter_price_and_stake( + pair, price, stake_amount, trade) if not stake_amount: return False - if trade is None: + if pos_adjust: + logger.info(f"Position adjust: about to create a new order for {pair} with stake: " + f"{stake_amount} for {trade}") + else: logger.info(f"Buy signal found: about create a new trade for {pair} with stake_amount: " f"{stake_amount} ...") - else: - logger.info(f"Position adjust: about to create a new order for {pair} with stake_amount: " - f"{stake_amount} for {trade}") amount = stake_amount / enter_limit_requested order_type = ordertype or self.strategy.order_types['buy'] + time_in_force = self.strategy.order_time_in_force['buy'] - if trade is None and not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)( + if not pos_adjust and not strategy_safe_wrapper( + self.strategy.confirm_trade_entry, default_retval=True)( pair=pair, order_type=order_type, amount=amount, rate=enter_limit_requested, time_in_force=time_in_force, current_time=datetime.now(timezone.utc)): logger.info(f"User requested abortion of buying {pair}") @@ -618,7 +598,7 @@ class FreqtradeBot(LoggingMixin): # Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker') - if trade is None: + if not pos_adjust: trade = Trade( pair=pair, stake_amount=stake_amount, @@ -643,33 +623,62 @@ class FreqtradeBot(LoggingMixin): Trade.query.session.add(trade) Trade.commit() - if trade is not None: - if order_status == 'closed': - logger.info(f"DCA order closed, trade should be up to date: {trade}") - # First cancelling stoploss on exchange ... - if self.strategy.order_types.get('stoploss_on_exchange') and trade.stoploss_order_id: - try: - logger.info(f"Canceling stoploss on exchange, recreating with new amount for {trade}") - co = self.exchange.cancel_stoploss_order_with_result(trade.stoploss_order_id, - trade.pair, trade.amount) - trade.update_order(co) - except InvalidOrderException: - logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}") - - else: - logger.info(f"DCA order {order_status}, will wait for resolution: {trade}") - # Updating wallets self.wallets.update() self._notify_enter(trade, order, order_type) + if pos_adjust: + if order_status == 'closed': + logger.info(f"DCA order closed, trade should be up to date: {trade}") + trade = self.cancel_stoploss_on_exchange(trade) + else: + logger.info(f"DCA order {order_status}, will wait for resolution: {trade}") + # Update fees if order is closed if order_status == 'closed': self.update_trade_state(trade, order_id, order) return True + def cancel_stoploss_on_exchange(self, trade: Trade) -> Trade: + # First cancelling stoploss on exchange ... + if self.strategy.order_types.get('stoploss_on_exchange') and trade.stoploss_order_id: + try: + logger.info(f"Canceling stoploss on exchange for {trade}") + co = self.exchange.cancel_stoploss_order_with_result( + trade.stoploss_order_id, trade.pair, trade.amount) + trade.update_order(co) + except InvalidOrderException: + logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}") + return trade + + def get_valid_enter_price_and_stake(self, pair, price, stake_amount, trade): + if price: + enter_limit_requested = price + else: + # Calculate price + proposed_enter_rate = self.exchange.get_rate(pair, refresh=True, side="buy") + custom_entry_price = strategy_safe_wrapper(self.strategy.custom_entry_price, + default_retval=proposed_enter_rate)( + pair=pair, current_time=datetime.now(timezone.utc), + proposed_rate=proposed_enter_rate) + + enter_limit_requested = self.get_valid_price(custom_entry_price, 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) + if not self.edge and trade is None: + max_stake_amount = self.wallets.get_available_stake_amount() + stake_amount = strategy_safe_wrapper(self.strategy.custom_stake_amount, + default_retval=stake_amount)( + pair=pair, current_time=datetime.now(timezone.utc), + current_rate=enter_limit_requested, proposed_stake=stake_amount, + min_stake=min_stake_amount, max_stake=max_stake_amount) + stake_amount = self.wallets.validate_stake_amount(pair, stake_amount, min_stake_amount) + return enter_limit_requested, stake_amount + def _notify_enter(self, trade: Trade, order: Dict, order_type: Optional[str] = None, fill: bool = False) -> None: """ @@ -1189,13 +1198,7 @@ class FreqtradeBot(LoggingMixin): limit = self.get_valid_price(custom_exit_price, proposed_limit_rate) # First cancelling stoploss on exchange ... - if self.strategy.order_types.get('stoploss_on_exchange') and trade.stoploss_order_id: - try: - co = self.exchange.cancel_stoploss_order_with_result(trade.stoploss_order_id, - trade.pair, trade.amount) - trade.update_order(co) - except InvalidOrderException: - logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}") + trade = self.cancel_stoploss_on_exchange(trade) order_type = ordertype or self.strategy.order_types[sell_type] if sell_reason.sell_type == SellType.EMERGENCY_SELL: From db2f0660fa1185be32b57ea92f97ec701222cf71 Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Sat, 18 Dec 2021 11:15:59 +0200 Subject: [PATCH 26/90] Some more compatibility fixes. --- freqtrade/freqtradebot.py | 3 ++- freqtrade/optimize/backtesting.py | 6 +++--- tests/optimize/test_backtesting_adjust_position.py | 2 +- tests/strategy/strats/strategy_test_v2.py | 5 +++-- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 5a94dcb1f..8aca76a5f 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -598,7 +598,7 @@ class FreqtradeBot(LoggingMixin): # Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker') - if not pos_adjust: + if trade is None: trade = Trade( pair=pair, stake_amount=stake_amount, @@ -616,6 +616,7 @@ class FreqtradeBot(LoggingMixin): buy_tag=buy_tag, timeframe=timeframe_to_minutes(self.config['timeframe']) ) + trade.fee_open_currency = None trade.fee_open_currency = order_id trade.orders.append(order_obj) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 2c6d509f7..e5585463c 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -24,7 +24,7 @@ from freqtrade.mixins import LoggingMixin from freqtrade.optimize.bt_progress import BTProgress from freqtrade.optimize.optimize_reports import (generate_backtest_stats, show_backtest_results, store_backtest_stats) -from freqtrade.persistence import LocalTrade, PairLocks, Trade, Order +from freqtrade.persistence import LocalTrade, Order, PairLocks, Trade from freqtrade.plugins.pairlistmanager import PairListManager from freqtrade.plugins.protectionmanager import ProtectionManager from freqtrade.resolvers import ExchangeResolver, StrategyResolver @@ -352,7 +352,7 @@ class Backtesting: return sell_row[OPEN_IDX] def _get_adjust_trade_entry_for_candle(self, trade: LocalTrade, row: Tuple - ) -> Optional[LocalTrade]: + ) -> LocalTrade: current_profit = trade.calc_profit_ratio(row[OPEN_IDX]) @@ -368,7 +368,7 @@ class Backtesting: return trade def _execute_trade_position_change(self, trade: LocalTrade, row: Tuple, - stake_amount: float) -> Optional[LocalTrade]: + stake_amount: float) -> LocalTrade: current_price = row[OPEN_IDX] propose_rate = min(max(current_price, row[LOW_IDX]), row[HIGH_IDX]) available_amount = self.wallets.get_available_stake_amount() diff --git a/tests/optimize/test_backtesting_adjust_position.py b/tests/optimize/test_backtesting_adjust_position.py index 4d87da503..03a3339d4 100644 --- a/tests/optimize/test_backtesting_adjust_position.py +++ b/tests/optimize/test_backtesting_adjust_position.py @@ -8,7 +8,7 @@ from freqtrade.data import history from freqtrade.data.history import get_timerange from freqtrade.enums import SellType from freqtrade.optimize.backtesting import Backtesting -from tests.conftest import (patch_exchange) +from tests.conftest import patch_exchange def test_backtest_position_adjustment(default_conf, fee, mocker, testdatadir) -> None: diff --git a/tests/strategy/strats/strategy_test_v2.py b/tests/strategy/strats/strategy_test_v2.py index 6dce14f42..6371c1029 100644 --- a/tests/strategy/strats/strategy_test_v2.py +++ b/tests/strategy/strats/strategy_test_v2.py @@ -1,12 +1,13 @@ # pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement +from datetime import datetime + import talib.abstract as ta from pandas import DataFrame import freqtrade.vendor.qtpylib.indicators as qtpylib -from freqtrade.strategy.interface import IStrategy from freqtrade.persistence import Trade -from datetime import datetime +from freqtrade.strategy.interface import IStrategy class StrategyTestV2(IStrategy): From 1eb83f9a628a3554f29cabba20b390a0255e3fcc Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Sat, 18 Dec 2021 18:55:12 +0200 Subject: [PATCH 27/90] Fix documentation formatting. --- docs/strategy-advanced.md | 13 ++++++++----- docs/strategy-callbacks.md | 14 +++++++++----- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 523d0accd..6d55084b7 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -230,15 +230,18 @@ for val in self.buy_ema_short.range: merged_frame = pd.concat(frames, axis=1) ``` -### Adjust trade position +## Adjust trade position `adjust_trade_position()` can be used to perform additional orders to manage risk with DCA (Dollar Cost Averaging) for example. -!!! Tip: The `position_adjustment_enable` configuration parameter must be enabled to use adjust_trade_position callback in strategy. +!!! Note + The `position_adjustment_enable` configuration parameter must be enabled to use adjust_trade_position callback in strategy. -!!! Warning: Additional orders also mean additional fees. +!!! Warning + Additional orders also mean additional fees. -!!! Warning: Stoploss is still calculated from the initial opening price, not averaged price. +!!! Warning + Stoploss is still calculated from the initial opening price, not averaged price. ``` python from freqtrade.persistence import Trade @@ -276,7 +279,7 @@ class DigDeeperStrategy(IStrategy): # Only buy when not actively falling price. last_candle = dataframe.iloc[-1].squeeze() previous_candle = dataframe.iloc[-2].squeeze() - if last_candle.close < previous_candle.close: + if last_candle['close'] < previous_candle['close']: return None count_of_buys = 0 diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index 886fdbc59..0026b9561 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -570,19 +570,23 @@ class AwesomeStrategy(IStrategy): ``` -### Adjust trade position +## Adjust trade position `adjust_trade_position()` can be used to perform additional orders to manage risk with DCA (Dollar Cost Averaging). The strategy is expected to return a stake_amount if and when an additional buy order should be made (position is increased). If there is not enough funds in the wallet then nothing will happen. -Note: Current implementation does not support decreasing position size with partial sales! +!!! Note + Current implementation does not support decreasing position size with partial sales! -!!! Tip: The `position_adjustment_enable` configuration parameter must be enabled to use adjust_trade_position callback in strategy. +!!! Tip + The `position_adjustment_enable` configuration parameter must be enabled to use adjust_trade_position callback in strategy. -!!! Warning: Additional orders also mean additional fees. +!!! Warning + Additional orders also mean additional fees. -!!! Warning: Stoploss is still calculated from the initial opening price, not averaged price. +!!! Warning + Stoploss is still calculated from the initial opening price, not averaged price. So if you do 3 additional buys at -7% and have a stoploss at -10% then you will most likely trigger stoploss while the UI will be showing you an average profit of -3%. ``` python From 3aca3a71337685b9ca07734fab5c1bba4043f47c Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Sat, 18 Dec 2021 18:55:47 +0200 Subject: [PATCH 28/90] Use parentheses instead of backslash --- freqtrade/persistence/models.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index e1f16a8dc..d169c4d27 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -579,9 +579,9 @@ class LocalTrade(): total_amount = 0.0 total_stake = 0.0 for temp_order in self.orders: - if temp_order.ft_is_open or \ - temp_order.ft_order_side != 'buy' or \ - temp_order.status not in NON_OPEN_EXCHANGE_STATES: + if (temp_order.ft_is_open or + (temp_order.ft_order_side != 'buy') or + (temp_order.status not in NON_OPEN_EXCHANGE_STATES)): continue tmp_amount = temp_order.amount From 5da38f3613167094e66557273cc4472d9ce75fe5 Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Sun, 19 Dec 2021 10:36:47 +0200 Subject: [PATCH 29/90] Fix typo. Make sure trade is market open. --- freqtrade/freqtradebot.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 8aca76a5f..9e9387b70 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -617,8 +617,9 @@ class FreqtradeBot(LoggingMixin): timeframe=timeframe_to_minutes(self.config['timeframe']) ) + trade.is_open = True trade.fee_open_currency = None - trade.fee_open_currency = order_id + trade.open_order_id = order_id trade.orders.append(order_obj) trade.recalc_trade_from_orders() Trade.query.session.add(trade) From f28d95ffb5ae92114c8b67bd0ed75bb3698a258b Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Sun, 19 Dec 2021 12:27:17 +0200 Subject: [PATCH 30/90] Add test for position adjust --- tests/test_freqtradebot.py | 153 +++++++++++++++++++++++++++++++++++++ 1 file changed, 153 insertions(+) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 2d2664055..c0634588d 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -26,6 +26,7 @@ from tests.conftest import (create_mock_trades, get_patched_freqtradebot, get_pa from tests.conftest_trades import (MOCK_TRADE_COUNT, mock_order_1, mock_order_2, mock_order_2_sell, mock_order_3, mock_order_3_sell, mock_order_4, mock_order_5_stoploss, mock_order_6_sell) +from typing import List def patch_RPCManager(mocker) -> MagicMock: @@ -4308,3 +4309,155 @@ def test_get_valid_price(mocker, default_conf_usdt) -> None: assert valid_price_at_min_alwd > custom_price_under_min_alwd assert valid_price_at_min_alwd < proposed_price + +def test_position_adjust(mocker, default_conf_usdt, fee, limit_buy_order_usdt, + limit_buy_order_usdt_open) -> None: + patch_RPCManager(mocker) + patch_exchange(mocker) + freqtrade = FreqtradeBot(default_conf_usdt) + freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=True) + bid = 11 + stake_amount = 10 + buy_rate_mock = MagicMock(return_value=bid) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_rate=buy_rate_mock, + fetch_ticker=MagicMock(return_value={ + 'bid': 10, + 'ask': 12, + 'last': 11 + }), + get_min_pair_stake_amount=MagicMock(return_value=1), + get_fee=fee, + ) + pair = 'ETH/USDT' + + # Initial buy + limit_buy_order_usdt['status'] = 'closed' + limit_buy_order_usdt['pair'] = pair + limit_buy_order_usdt['ft_pair'] = pair + limit_buy_order_usdt['ft_order_side'] = 'buy' + limit_buy_order_usdt['side'] = 'buy' + limit_buy_order_usdt['price'] = bid + limit_buy_order_usdt['average'] = bid + limit_buy_order_usdt['cost'] = bid * stake_amount + limit_buy_order_usdt['amount'] = stake_amount + limit_buy_order_usdt['filled'] = stake_amount + limit_buy_order_usdt['ft_is_open'] = False + limit_buy_order_usdt['id'] = '650' + limit_buy_order_usdt['order_id'] = '650' + mocker.patch('freqtrade.exchange.Exchange.create_order', + MagicMock(return_value=limit_buy_order_usdt)) + mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order', + MagicMock(return_value=limit_buy_order_usdt)) + assert freqtrade.execute_entry(pair, stake_amount) + # Should create an closed trade with an no open order id + # Order is filled and trade is open + orders = Order.query.all() + assert orders + assert len(orders) == 1 + trade = Trade.query.first() + assert trade + assert trade.is_open is True + assert trade.open_order_id is None + assert trade.open_rate == 11 + assert trade.stake_amount == 110 + + # Assume it does nothing since trade is still open and no new orders + freqtrade.update_closed_trades_without_assigned_fees() + + trade = Trade.query.first() + assert trade + assert trade.is_open is True + assert trade.open_order_id is None + assert trade.open_rate == 11 + assert trade.stake_amount == 110 + + # First position adjustment buy. + limit_buy_order_usdt_open['ft_pair'] = pair + limit_buy_order_usdt_open['ft_order_side'] = 'buy' + limit_buy_order_usdt_open['side'] = 'buy' + limit_buy_order_usdt_open['status'] = None + limit_buy_order_usdt_open['price'] = 9 + limit_buy_order_usdt_open['amount'] = 12 + limit_buy_order_usdt_open['cost'] = 100 + limit_buy_order_usdt_open['ft_is_open'] = True + limit_buy_order_usdt_open['id'] = 651 + limit_buy_order_usdt_open['order_id'] = 651 + mocker.patch('freqtrade.exchange.Exchange.create_order', + MagicMock(return_value=limit_buy_order_usdt_open)) + mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order', + MagicMock(return_value=limit_buy_order_usdt_open)) + assert freqtrade.execute_entry(pair, stake_amount, trade=trade) + + orders = Order.query.all() + assert orders + assert len(orders) == 2 + trade = Trade.query.first() + assert trade + assert trade.open_order_id == '651' + assert trade.open_rate == 11 + assert trade.amount == 10 + assert trade.stake_amount == 110 + + # Assume it does nothing since trade is still open + mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order', + MagicMock(return_value=limit_buy_order_usdt_open)) + freqtrade.update_closed_trades_without_assigned_fees() + + orders = Order.query.all() + assert orders + assert len(orders) == 2 + # Assert that the trade is found as open and without fees + trades: List[Trade] = Trade.get_open_trades_without_assigned_fees() + assert len(trades) == 1 + # Assert trade is as expected + trade = Trade.query.first() + assert trade + assert trade.open_order_id == '651' + assert trade.open_rate == 11 + assert trade.amount == 10 + assert trade.stake_amount == 110 + assert trade.fee_updated('buy') is False + + # Make sure the closed order is found as the first order. + order = trade.select_order('buy', False) + assert order.order_id == '650' + + # Now close the order so it should update. + limit_buy_order_usdt_open['ft_pair'] = pair + limit_buy_order_usdt_open['status'] = 'closed' + limit_buy_order_usdt_open['ft_order_side'] = 'buy' + limit_buy_order_usdt_open['side'] = 'buy' + limit_buy_order_usdt_open['price'] = 9 + limit_buy_order_usdt_open['average'] = 9 + limit_buy_order_usdt_open['amount'] = 12 + limit_buy_order_usdt_open['filled'] = 12 + limit_buy_order_usdt_open['cost'] = 108 + limit_buy_order_usdt_open['ft_is_open'] = False + limit_buy_order_usdt_open['id'] = '651' + limit_buy_order_usdt_open['order_id'] = '651' + mocker.patch('freqtrade.exchange.Exchange.fetch_order', + MagicMock(return_value=limit_buy_order_usdt_open)) + freqtrade.check_handle_timedout() + + # Assert trade is as expected (averaged dca) + trade = Trade.query.first() + assert trade + assert trade.open_order_id is None + assert pytest.approx(trade.open_rate) == 9.90909090909 + assert trade.amount == 22 + assert trade.stake_amount == 218 + + orders = Order.query.all() + assert orders + assert len(orders) == 2 + + # Make sure the closed order is found as the second order. + order = trade.select_order('buy', False) + assert order.order_id == '651' + + # Assert that the trade is not found as open and without fees + trades: List[Trade] = Trade.get_open_trades_without_assigned_fees() + assert len(trades) == 1 + From f6d36ce56b258a508198d9252dc9b57e53acde31 Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Mon, 20 Dec 2021 22:07:42 +0200 Subject: [PATCH 31/90] Fix the dca order not being counted bug. --- freqtrade/freqtradebot.py | 4 +- tests/test_freqtradebot.py | 183 +++++++++++++++++++++++++++---------- 2 files changed, 138 insertions(+), 49 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 9e9387b70..b7318279c 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -292,8 +292,8 @@ class FreqtradeBot(LoggingMixin): for trade in trades: if trade.is_open and not trade.fee_updated('buy'): order = trade.select_order('buy', False) - if order: - logger.info(f"Updating buy-fee on trade {trade} for order {order.order_id}.") + open_order = trade.select_order('buy', True) + if order and open_order is None: self.update_trade_state(trade, order.order_id, send_msg=False) def handle_insufficient_funds(self, trade: Trade): diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index c0634588d..31d9cd4c6 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -4310,10 +4310,17 @@ def test_get_valid_price(mocker, default_conf_usdt) -> None: assert valid_price_at_min_alwd > custom_price_under_min_alwd assert valid_price_at_min_alwd < proposed_price -def test_position_adjust(mocker, default_conf_usdt, fee, limit_buy_order_usdt, - limit_buy_order_usdt_open) -> None: + +def test_position_adjust(mocker, default_conf_usdt, fee) -> None: patch_RPCManager(mocker) patch_exchange(mocker) + patch_wallet(mocker, free=10000) + default_conf_usdt.update({ + "position_adjustment_enable": True, + "dry_run": False, + "stake_amount": 10.0, + "dry_run_wallet": 1000.0, + }) freqtrade = FreqtradeBot(default_conf_usdt) freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=True) bid = 11 @@ -4333,23 +4340,26 @@ def test_position_adjust(mocker, default_conf_usdt, fee, limit_buy_order_usdt, pair = 'ETH/USDT' # Initial buy - limit_buy_order_usdt['status'] = 'closed' - limit_buy_order_usdt['pair'] = pair - limit_buy_order_usdt['ft_pair'] = pair - limit_buy_order_usdt['ft_order_side'] = 'buy' - limit_buy_order_usdt['side'] = 'buy' - limit_buy_order_usdt['price'] = bid - limit_buy_order_usdt['average'] = bid - limit_buy_order_usdt['cost'] = bid * stake_amount - limit_buy_order_usdt['amount'] = stake_amount - limit_buy_order_usdt['filled'] = stake_amount - limit_buy_order_usdt['ft_is_open'] = False - limit_buy_order_usdt['id'] = '650' - limit_buy_order_usdt['order_id'] = '650' + closed_successful_buy_order = { + 'pair': pair, + 'ft_pair': pair, + 'ft_order_side': 'buy', + 'side': 'buy', + 'type': 'limit', + 'status': 'closed', + 'price': bid, + 'average': bid, + 'cost': bid * stake_amount, + 'amount': stake_amount, + 'filled': stake_amount, + 'ft_is_open': False, + 'id': '650', + 'order_id': '650' + } mocker.patch('freqtrade.exchange.Exchange.create_order', - MagicMock(return_value=limit_buy_order_usdt)) + MagicMock(return_value=closed_successful_buy_order)) mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order', - MagicMock(return_value=limit_buy_order_usdt)) + MagicMock(return_value=closed_successful_buy_order)) assert freqtrade.execute_entry(pair, stake_amount) # Should create an closed trade with an no open order id # Order is filled and trade is open @@ -4363,7 +4373,7 @@ def test_position_adjust(mocker, default_conf_usdt, fee, limit_buy_order_usdt, assert trade.open_rate == 11 assert trade.stake_amount == 110 - # Assume it does nothing since trade is still open and no new orders + # Assume it does nothing since order is closed and trade is open freqtrade.update_closed_trades_without_assigned_fees() trade = Trade.query.first() @@ -4372,22 +4382,36 @@ def test_position_adjust(mocker, default_conf_usdt, fee, limit_buy_order_usdt, assert trade.open_order_id is None assert trade.open_rate == 11 assert trade.stake_amount == 110 + assert not trade.fee_updated('buy') + + freqtrade.check_handle_timedout() + + trade = Trade.query.first() + assert trade + assert trade.is_open is True + assert trade.open_order_id is None + assert trade.open_rate == 11 + assert trade.stake_amount == 110 + assert not trade.fee_updated('buy') # First position adjustment buy. - limit_buy_order_usdt_open['ft_pair'] = pair - limit_buy_order_usdt_open['ft_order_side'] = 'buy' - limit_buy_order_usdt_open['side'] = 'buy' - limit_buy_order_usdt_open['status'] = None - limit_buy_order_usdt_open['price'] = 9 - limit_buy_order_usdt_open['amount'] = 12 - limit_buy_order_usdt_open['cost'] = 100 - limit_buy_order_usdt_open['ft_is_open'] = True - limit_buy_order_usdt_open['id'] = 651 - limit_buy_order_usdt_open['order_id'] = 651 + open_dca_order_1 = { + 'ft_pair': pair, + 'ft_order_side': 'buy', + 'side': 'buy', + 'type': 'limit', + 'status': None, + 'price': 9, + 'amount': 12, + 'cost': 100, + 'ft_is_open': True, + 'id': '651', + 'order_id': '651' + } mocker.patch('freqtrade.exchange.Exchange.create_order', - MagicMock(return_value=limit_buy_order_usdt_open)) + MagicMock(return_value=open_dca_order_1)) mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order', - MagicMock(return_value=limit_buy_order_usdt_open)) + MagicMock(return_value=open_dca_order_1)) assert freqtrade.execute_entry(pair, stake_amount, trade=trade) orders = Order.query.all() @@ -4399,10 +4423,28 @@ def test_position_adjust(mocker, default_conf_usdt, fee, limit_buy_order_usdt, assert trade.open_rate == 11 assert trade.amount == 10 assert trade.stake_amount == 110 + assert not trade.fee_updated('buy') + trades: List[Trade] = Trade.get_open_trades_without_assigned_fees() + assert len(trades) == 1 + assert trade.is_open + assert not trade.fee_updated('buy') + order = trade.select_order('buy', False) + assert order + assert order.order_id == '650' - # Assume it does nothing since trade is still open - mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order', - MagicMock(return_value=limit_buy_order_usdt_open)) + def make_sure_its_651(*apos, **kwargs): + + if apos[0] == '650': + return closed_successful_buy_order + if apos[0] == '651': + return open_dca_order_1 + return None + + # Assume it does nothing since order is still open + fetch_order_mm = MagicMock(side_effect=make_sure_its_651) + mocker.patch('freqtrade.exchange.Exchange.create_order', fetch_order_mm) + mocker.patch('freqtrade.exchange.Exchange.fetch_order', fetch_order_mm) + mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order', fetch_order_mm) freqtrade.update_closed_trades_without_assigned_fees() orders = Order.query.all() @@ -4418,27 +4460,35 @@ def test_position_adjust(mocker, default_conf_usdt, fee, limit_buy_order_usdt, assert trade.open_rate == 11 assert trade.amount == 10 assert trade.stake_amount == 110 - assert trade.fee_updated('buy') is False + assert not trade.fee_updated('buy') # Make sure the closed order is found as the first order. order = trade.select_order('buy', False) assert order.order_id == '650' # Now close the order so it should update. - limit_buy_order_usdt_open['ft_pair'] = pair - limit_buy_order_usdt_open['status'] = 'closed' - limit_buy_order_usdt_open['ft_order_side'] = 'buy' - limit_buy_order_usdt_open['side'] = 'buy' - limit_buy_order_usdt_open['price'] = 9 - limit_buy_order_usdt_open['average'] = 9 - limit_buy_order_usdt_open['amount'] = 12 - limit_buy_order_usdt_open['filled'] = 12 - limit_buy_order_usdt_open['cost'] = 108 - limit_buy_order_usdt_open['ft_is_open'] = False - limit_buy_order_usdt_open['id'] = '651' - limit_buy_order_usdt_open['order_id'] = '651' + closed_dca_order_1 = { + 'ft_pair': pair, + 'ft_order_side': 'buy', + 'side': 'buy', + 'type': 'limit', + 'status': 'closed', + 'price': 9, + 'average': 9, + 'amount': 12, + 'filled': 12, + 'cost': 108, + 'ft_is_open': False, + 'id': '651', + 'order_id': '651' + } + + mocker.patch('freqtrade.exchange.Exchange.create_order', + MagicMock(return_value=closed_dca_order_1)) mocker.patch('freqtrade.exchange.Exchange.fetch_order', - MagicMock(return_value=limit_buy_order_usdt_open)) + MagicMock(return_value=closed_dca_order_1)) + mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order', + MagicMock(return_value=closed_dca_order_1)) freqtrade.check_handle_timedout() # Assert trade is as expected (averaged dca) @@ -4461,3 +4511,42 @@ def test_position_adjust(mocker, default_conf_usdt, fee, limit_buy_order_usdt, trades: List[Trade] = Trade.get_open_trades_without_assigned_fees() assert len(trades) == 1 + # Add a second DCA + closed_dca_order_2 = { + 'ft_pair': pair, + 'status': 'closed', + 'ft_order_side': 'buy', + 'side': 'buy', + 'type': 'limit', + 'price': 7, + 'average': 7, + 'amount': 15, + 'filled': 15, + 'cost': 105, + 'ft_is_open': False, + 'id': '652', + 'order_id': '652' + } + mocker.patch('freqtrade.exchange.Exchange.create_order', + MagicMock(return_value=closed_dca_order_2)) + mocker.patch('freqtrade.exchange.Exchange.fetch_order', + MagicMock(return_value=closed_dca_order_2)) + mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order', + MagicMock(return_value=closed_dca_order_2)) + assert freqtrade.execute_entry(pair, stake_amount, trade=trade) + + # Assert trade is as expected (averaged dca) + trade = Trade.query.first() + assert trade + assert trade.open_order_id is None + assert pytest.approx(trade.open_rate) == 8.729729729729 + assert trade.amount == 37 + assert trade.stake_amount == 323 + + orders = Order.query.all() + assert orders + assert len(orders) == 3 + + # Make sure the closed order is found as the second order. + order = trade.select_order('buy', False) + assert order.order_id == '652' From c9243fb4f61019d3c872631f021cbebea4460fbd Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Mon, 20 Dec 2021 22:45:46 +0200 Subject: [PATCH 32/90] Use buy side for price since mostly used for DCA. --- freqtrade/freqtradebot.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index b7318279c..7c5f1b687 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -471,17 +471,18 @@ class FreqtradeBot(LoggingMixin): If the strategy triggers the adjustment, a new order gets issued. Once that completes, the existing trade is modified to match new data. """ + # If there is any open orders, wait for them to finish. for order in trade.orders: if order.ft_is_open: return logger.debug(f"adjust_trade_position for pair {trade.pair}") - sell_rate = self.exchange.get_rate(trade.pair, refresh=True, side="sell") - current_profit = trade.calc_profit_ratio(sell_rate) + current_rate = self.exchange.get_rate(trade.pair, refresh=True, side="buy") + current_profit = trade.calc_profit_ratio(current_rate) stake_amount = strategy_safe_wrapper(self.strategy.adjust_trade_position, default_retval=None)( pair=trade.pair, trade=trade, current_time=datetime.now(timezone.utc), - current_rate=sell_rate, current_profit=current_profit) + current_rate=current_rate, current_profit=current_profit) if stake_amount is not None and stake_amount > 0.0: # We should increase our position From 4862cdb296f0199d26497d594155b9e9b545c5a1 Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Tue, 21 Dec 2021 00:11:01 +0200 Subject: [PATCH 33/90] Improve documentation. --- docs/strategy-advanced.md | 7 ++++--- docs/strategy-callbacks.md | 5 ++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 6d55084b7..99b4a5a70 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -233,13 +233,14 @@ merged_frame = pd.concat(frames, axis=1) ## Adjust trade position `adjust_trade_position()` can be used to perform additional orders to manage risk with DCA (Dollar Cost Averaging) for example. +The strategy is expected to return a stake_amount if and when an additional buy order should be made (position is increased). +If there is not enough funds in the wallet then nothing will happen. +Additional orders also mean additional fees and those orders don't count towards `max_open_trades`. +Unlimited stake amount with trade position increasing is highly not recommended as your DCA orders would compete with your normal trade open orders. !!! Note The `position_adjustment_enable` configuration parameter must be enabled to use adjust_trade_position callback in strategy. -!!! Warning - Additional orders also mean additional fees. - !!! Warning Stoploss is still calculated from the initial opening price, not averaged price. diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index 0026b9561..0a6f5896b 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -575,6 +575,8 @@ class AwesomeStrategy(IStrategy): `adjust_trade_position()` can be used to perform additional orders to manage risk with DCA (Dollar Cost Averaging). The strategy is expected to return a stake_amount if and when an additional buy order should be made (position is increased). If there is not enough funds in the wallet then nothing will happen. +Additional orders also mean additional fees and those orders don't count towards `max_open_trades`. +Unlimited stake amount with trade position increasing is highly not recommended as your DCA orders would compete with your normal trade open orders. !!! Note Current implementation does not support decreasing position size with partial sales! @@ -582,9 +584,6 @@ If there is not enough funds in the wallet then nothing will happen. !!! Tip The `position_adjustment_enable` configuration parameter must be enabled to use adjust_trade_position callback in strategy. -!!! Warning - Additional orders also mean additional fees. - !!! Warning Stoploss is still calculated from the initial opening price, not averaged price. So if you do 3 additional buys at -7% and have a stoploss at -10% then you will most likely trigger stoploss while the UI will be showing you an average profit of -3%. From fa01cbf5469cb2e5b1773a1203ba0f6ffcd4291d Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Tue, 21 Dec 2021 22:23:01 +0200 Subject: [PATCH 34/90] iSort --- tests/test_freqtradebot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 31d9cd4c6..18f720782 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -5,6 +5,7 @@ import logging import time from copy import deepcopy from math import isclose +from typing import List from unittest.mock import ANY, MagicMock, PropertyMock import arrow @@ -26,7 +27,6 @@ from tests.conftest import (create_mock_trades, get_patched_freqtradebot, get_pa from tests.conftest_trades import (MOCK_TRADE_COUNT, mock_order_1, mock_order_2, mock_order_2_sell, mock_order_3, mock_order_3_sell, mock_order_4, mock_order_5_stoploss, mock_order_6_sell) -from typing import List def patch_RPCManager(mocker) -> MagicMock: From 7df3e7ada46e3bf551e4f0870067c3c457be507c Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Wed, 22 Dec 2021 02:19:11 +0200 Subject: [PATCH 35/90] Add base_stake_amount_ratio config param to support unlimited stakes. --- docs/configuration.md | 9 +++++++++ freqtrade/wallets.py | 11 +++++++++++ 2 files changed, 20 insertions(+) diff --git a/docs/configuration.md b/docs/configuration.md index c982a1d7f..c531c59ab 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -83,6 +83,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `stake_amount` | **Required.** Amount of crypto-currency your bot will use for each trade. Set it to `"unlimited"` to allow the bot to use all available balance. [More information below](#configuring-amount-per-trade).
**Datatype:** Positive float or `"unlimited"`. | `tradable_balance_ratio` | Ratio of the total account balance the bot is allowed to trade. [More information below](#configuring-amount-per-trade).
*Defaults to `0.99` 99%).*
**Datatype:** Positive float between `0.1` and `1.0`. | `available_capital` | Available starting capital for the bot. Useful when running multiple bots on the same exchange account.[More information below](#configuring-amount-per-trade).
**Datatype:** Positive float. +| `base_stake_amount_ratio` | When using position adjustment with unlimited stakes, the strategy often requires that some funds are left for additional buy orders. You can define the ratio that the initial buy order can use from the calculated unlimited stake amount. [More information below](#configuring-amount-per-trade).
*Defaults to `1.0`.*
**Datatype:** Float (as ratio) | `amend_last_stake_amount` | Use reduced last stake amount if necessary. [More information below](#configuring-amount-per-trade).
*Defaults to `false`.*
**Datatype:** Boolean | `last_stake_amount_min_ratio` | Defines minimum stake amount that has to be left and executed. Applies only to the last stake amount when it's amended to a reduced value (i.e. if `amend_last_stake_amount` is set to `true`). [More information below](#configuring-amount-per-trade).
*Defaults to `0.5`.*
**Datatype:** Float (as ratio) | `amount_reserve_percent` | Reserve some amount in min pair stake amount. The bot will reserve `amount_reserve_percent` + stoploss value when calculating min pair stake amount in order to avoid possible trade refusals.
*Defaults to `0.05` (5%).*
**Datatype:** Positive Float as ratio. @@ -303,6 +304,14 @@ To allow the bot to trade all the available `stake_currency` in your account (mi When using `"stake_amount" : "unlimited",` in combination with Dry-Run, Backtesting or Hyperopt, the balance will be simulated starting with a stake of `dry_run_wallet` which will evolve. It is therefore important to set `dry_run_wallet` to a sensible value (like 0.05 or 0.01 for BTC and 1000 or 100 for USDT, for example), otherwise, it may simulate trades with 100 BTC (or more) or 0.05 USDT (or less) at once - which may not correspond to your real available balance or is less than the exchange minimal limit for the order amount for the stake currency. +#### Dynamic stake amount with position adjustment +When you want to use position adjustment with unlimited stakes, you must also set `base_stake_amount_ratio` to a reasonable value depending on your strategy. +Typical value would be in the range of 0.1 - 0.5, but depends highly on your strategy and how much you wish to leave into the wallet as position adjustment buffer. +
+For example if your position adjustment assumes it can do 2 additional buys with the same stake amounts then your `base_stake_amount_ratio` should be 0.333333 (33.3333%) of your calculated unlimited stake amount. +
+Or another example if your position adjustment assumes it can do 1 additional buy with 3x the original stake amount then your `base_stake_amount_ratio` should be 0.25 (25%) of your calculated unlimited stake amount. + --8<-- "includes/pricing.md" ### Understand minimal_roi diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index 98a39ea2d..02c3cde18 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -184,6 +184,17 @@ class Wallets: return 0 possible_stake = (available_amount + val_tied_up) / self._config['max_open_trades'] + + # Position Adjustment dynamic base order size + try: + if self._config.get('position_adjustment_enable', False): + base_stake_amount_ratio = self._config.get('base_stake_amount_ratio', 1.0) + if base_stake_amount_ratio > 0.0: + possible_stake = possible_stake * base_stake_amount_ratio + except Exception as e: + logger.warning("Invalid base_stake_amount_ratio", e) + return 0 + # Theoretical amount can be above available amount - therefore limit to available amount! return min(possible_stake, available_amount) From da2e07b7fea05d63ef7aba6c8f616c911e54355e Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Wed, 22 Dec 2021 02:42:44 +0200 Subject: [PATCH 36/90] Unittest base_stake_amount_ratio --- tests/test_wallets.py | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/tests/test_wallets.py b/tests/test_wallets.py index 3e02cdb09..783b0eded 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -121,19 +121,19 @@ def test_get_trade_stake_amount_no_stake_amount(default_conf, mocker) -> None: freqtrade.wallets.get_trade_stake_amount('ETH/BTC') -@pytest.mark.parametrize("balance_ratio,capital,result1,result2", [ - (1, None, 50, 66.66666), - (0.99, None, 49.5, 66.0), - (0.50, None, 25, 33.3333), +@pytest.mark.parametrize("balance_ratio,capital,result1,result2,result3", [ + (1, None, 50, 66.66666, 250), + (0.99, None, 49.5, 66.0, 247.5), + (0.50, None, 25, 33.3333, 125), # Tests with capital ignore balance_ratio - (1, 100, 50, 0.0), - (0.99, 200, 50, 66.66666), - (0.99, 150, 50, 50), - (0.50, 50, 25, 0.0), - (0.50, 10, 5, 0.0), + (1, 100, 50, 0.0, 0.0), + (0.99, 200, 50, 66.66666, 50), + (0.99, 150, 50, 50, 37.5), + (0.50, 50, 25, 0.0, 0.0), + (0.50, 10, 5, 0.0, 0.0), ]) def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, balance_ratio, capital, - result1, result2, limit_buy_order_open, + result1, result2, result3, limit_buy_order_open, fee, mocker) -> None: mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -179,6 +179,14 @@ def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, balance_r result = freqtrade.wallets.get_trade_stake_amount('NEO/USDT') assert result == 0 + freqtrade.config['max_open_trades'] = 2 + freqtrade.config['dry_run_wallet'] = 1000 + freqtrade.wallets.start_cap = 1000 + freqtrade.config['position_adjustment_enable'] = True + freqtrade.config['base_stake_amount_ratio'] = 0.5 + result = freqtrade.wallets.get_trade_stake_amount('LTC/USDT') + assert result == result3 + @pytest.mark.parametrize('stake_amount,min_stake_amount,max_stake_amount,expected', [ (22, 11, 50, 22), From e439ae1fea44140eea78b688321762dfd06f0854 Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Wed, 22 Dec 2021 11:20:03 +0200 Subject: [PATCH 37/90] Update wallet balance on every order close, not only trade close --- freqtrade/freqtradebot.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 7c5f1b687..ef8f7c9dc 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1394,11 +1394,13 @@ class FreqtradeBot(LoggingMixin): logger.info(f"Trade has been updated: {trade}") # Updating wallets when order is closed + if order['status'] in constants.NON_OPEN_EXCHANGE_STATES: + self.wallets.update() + if not trade.is_open: if send_msg and not stoploss_order and not trade.open_order_id: self._notify_exit(trade, '', True) self.handle_protections(trade.pair) - self.wallets.update() elif send_msg and not trade.open_order_id: # Buy fill self._notify_enter(trade, order, fill=True) From d70ddeef9ae51e0c17ba9a3a5227d67a80b717b3 Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Wed, 22 Dec 2021 11:43:48 +0200 Subject: [PATCH 38/90] Remove whitespace. Darn IntelliJ. --- freqtrade/freqtradebot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index ef8f7c9dc..70d73e75c 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1396,7 +1396,7 @@ class FreqtradeBot(LoggingMixin): # Updating wallets when order is closed if order['status'] in constants.NON_OPEN_EXCHANGE_STATES: self.wallets.update() - + if not trade.is_open: if send_msg and not stoploss_order and not trade.open_order_id: self._notify_exit(trade, '', True) From 2e23e88fc10fbefc0316f776b464a829b08ed1da Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Wed, 22 Dec 2021 11:49:43 +0200 Subject: [PATCH 39/90] Re-add back the log i accidentally removed. --- freqtrade/freqtradebot.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 70d73e75c..40ad331d6 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -294,6 +294,7 @@ class FreqtradeBot(LoggingMixin): order = trade.select_order('buy', False) open_order = trade.select_order('buy', True) if order and open_order is None: + logger.info(f"Updating buy-fee on trade {trade} for order {order.order_id}.") self.update_trade_state(trade, order.order_id, send_msg=False) def handle_insufficient_funds(self, trade: Trade): From ace0a83c0c9fcfd2d5cb60764f428ecdc7c7823f Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Thu, 23 Dec 2021 11:57:53 +0200 Subject: [PATCH 40/90] Allow forcebuy to also buy more when trade is already open. --- freqtrade/rpc/rpc.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 3328af30b..ab7e99797 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -721,7 +721,8 @@ class RPC: # check if pair already has an open pair trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first() if trade: - raise RPCException(f'position for {pair} already open - id: {trade.id}') + if not self._config.get('position_adjustment_enable', False): + raise RPCException(f'position for {pair} already open - id: {trade.id}') # gen stake amount stakeamount = self._freqtrade.wallets.get_trade_stake_amount(pair) @@ -730,7 +731,8 @@ class RPC: if not order_type: order_type = self._freqtrade.strategy.order_types.get( 'forcebuy', self._freqtrade.strategy.order_types['buy']) - if self._freqtrade.execute_entry(pair, stakeamount, price, ordertype=order_type): + if self._freqtrade.execute_entry(pair, stakeamount, price, + ordertype=order_type, trade=trade): Trade.commit() trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first() return trade From 8bf1001b33a2c5f6468ee5e83f0f0e7f528a0222 Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Thu, 23 Dec 2021 12:41:37 +0200 Subject: [PATCH 41/90] Fix test failing when user_data already contains data... --- tests/optimize/test_hyperopt.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index a43c62376..b8c715e6c 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -3,6 +3,7 @@ from datetime import datetime from pathlib import Path from unittest.mock import ANY, MagicMock +import os import pandas as pd import pytest from arrow import Arrow @@ -169,6 +170,7 @@ def test_start_no_hyperopt_allowed(mocker, hyperopt_conf, caplog) -> None: def test_start_no_data(mocker, hyperopt_conf) -> None: + hyperopt_conf['user_data_dir'] = Path("tests") patched_configuration_load_config_file(mocker, hyperopt_conf) mocker.patch('freqtrade.data.history.load_pair_history', MagicMock(return_value=pd.DataFrame)) mocker.patch( @@ -189,6 +191,7 @@ def test_start_no_data(mocker, hyperopt_conf) -> None: with pytest.raises(OperationalException, match='No data found. Terminating.'): start_hyperopt(pargs) + os.unlink(Hyperopt.get_lock_filename(hyperopt_conf)) def test_start_filelock(mocker, hyperopt_conf, caplog) -> None: hyperopt_mock = MagicMock(side_effect=Timeout(Hyperopt.get_lock_filename(hyperopt_conf))) From 8393c99b6296ad053706fef5b879b5b608556189 Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Thu, 23 Dec 2021 16:25:27 +0200 Subject: [PATCH 42/90] Whoops, missing a line. --- tests/optimize/test_hyperopt.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index b8c715e6c..1a6d7f06b 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -193,6 +193,7 @@ def test_start_no_data(mocker, hyperopt_conf) -> None: os.unlink(Hyperopt.get_lock_filename(hyperopt_conf)) + def test_start_filelock(mocker, hyperopt_conf, caplog) -> None: hyperopt_mock = MagicMock(side_effect=Timeout(Hyperopt.get_lock_filename(hyperopt_conf))) patched_configuration_load_config_file(mocker, hyperopt_conf) From bc60139ae346ae26cf2a9b3b27afa14c523ca172 Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Thu, 23 Dec 2021 16:40:47 +0200 Subject: [PATCH 43/90] I really should make this flake8 / isort check automatic before commit. --- tests/optimize/test_hyperopt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 1a6d7f06b..04fd5d458 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -1,9 +1,9 @@ # pragma pylint: disable=missing-docstring,W0212,C0103 +import os from datetime import datetime from pathlib import Path from unittest.mock import ANY, MagicMock -import os import pandas as pd import pytest from arrow import Arrow From 0c4664e8f412a53bce167064b885a14821d4382f Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Thu, 23 Dec 2021 17:39:43 +0200 Subject: [PATCH 44/90] Lock file is not always left behind so handle it. --- tests/optimize/test_hyperopt.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 04fd5d458..5e8d899c8 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -191,7 +191,11 @@ def test_start_no_data(mocker, hyperopt_conf) -> None: with pytest.raises(OperationalException, match='No data found. Terminating.'): start_hyperopt(pargs) - os.unlink(Hyperopt.get_lock_filename(hyperopt_conf)) + # Cleanup since that failed hyperopt start leaves a lockfile. + try: + os.unlink(Hyperopt.get_lock_filename(hyperopt_conf)) + except Exception: + pass def test_start_filelock(mocker, hyperopt_conf, caplog) -> None: From ac690e92156450b1b641632fa9537d18992679b1 Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Thu, 23 Dec 2021 18:49:11 +0200 Subject: [PATCH 45/90] Remove unnecessary returns. --- freqtrade/freqtradebot.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 40ad331d6..9daeb5139 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -494,9 +494,6 @@ class FreqtradeBot(LoggingMixin): # TODO: Selling part of the trade not implemented yet. logger.error(f"Unable to decrease trade position / sell partially" f" for pair {trade.pair}, feature not implemented.") - return - - return def _check_depth_of_market_buy(self, pair: str, conf: Dict) -> bool: """ From de79d25cafd8af46eb93924a5fb436dc452f9c38 Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Fri, 24 Dec 2021 12:38:43 +0200 Subject: [PATCH 46/90] Refactoring to use strategy based configuration --- docs/configuration.md | 13 +- docs/strategy-advanced.md | 27 ++-- docs/strategy-callbacks.md | 54 +------ freqtrade/configuration/configuration.py | 3 - freqtrade/freqtradebot.py | 20 +-- freqtrade/optimize/backtesting.py | 139 ++++++++---------- freqtrade/resolvers/strategy_resolver.py | 3 +- freqtrade/rpc/rpc.py | 2 +- freqtrade/strategy/interface.py | 3 + freqtrade/wallets.py | 10 -- .../test_backtesting_adjust_position.py | 2 +- tests/strategy/strats/strategy_test_v2.py | 13 +- tests/test_wallets.py | 28 ++-- 13 files changed, 117 insertions(+), 200 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index c531c59ab..3bea83ad3 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -83,7 +83,6 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `stake_amount` | **Required.** Amount of crypto-currency your bot will use for each trade. Set it to `"unlimited"` to allow the bot to use all available balance. [More information below](#configuring-amount-per-trade).
**Datatype:** Positive float or `"unlimited"`. | `tradable_balance_ratio` | Ratio of the total account balance the bot is allowed to trade. [More information below](#configuring-amount-per-trade).
*Defaults to `0.99` 99%).*
**Datatype:** Positive float between `0.1` and `1.0`. | `available_capital` | Available starting capital for the bot. Useful when running multiple bots on the same exchange account.[More information below](#configuring-amount-per-trade).
**Datatype:** Positive float. -| `base_stake_amount_ratio` | When using position adjustment with unlimited stakes, the strategy often requires that some funds are left for additional buy orders. You can define the ratio that the initial buy order can use from the calculated unlimited stake amount. [More information below](#configuring-amount-per-trade).
*Defaults to `1.0`.*
**Datatype:** Float (as ratio) | `amend_last_stake_amount` | Use reduced last stake amount if necessary. [More information below](#configuring-amount-per-trade).
*Defaults to `false`.*
**Datatype:** Boolean | `last_stake_amount_min_ratio` | Defines minimum stake amount that has to be left and executed. Applies only to the last stake amount when it's amended to a reduced value (i.e. if `amend_last_stake_amount` is set to `true`). [More information below](#configuring-amount-per-trade).
*Defaults to `0.5`.*
**Datatype:** Float (as ratio) | `amount_reserve_percent` | Reserve some amount in min pair stake amount. The bot will reserve `amount_reserve_percent` + stoploss value when calculating min pair stake amount in order to avoid possible trade refusals.
*Defaults to `0.05` (5%).*
**Datatype:** Positive Float as ratio. @@ -173,7 +172,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `user_data_dir` | Directory containing user data.
*Defaults to `./user_data/`*.
**Datatype:** String | `dataformat_ohlcv` | Data format to use to store historical candle (OHLCV) data.
*Defaults to `json`*.
**Datatype:** String | `dataformat_trades` | Data format to use to store historical trades data.
*Defaults to `jsongz`*.
**Datatype:** String -| `position_adjustment_enable` | Enables the strategy to use position adjustments (additional buys or sells). More information below.
**Datatype:** Boolean +| `position_adjustment_enable` | Enables the strategy to use position adjustments (additional buys or sells). [More information here](strategy-callbacks.md#adjust-trade-position).
[Strategy Override](#parameters-in-the-strategy).
*Defaults to `true`.*
**Datatype:** Boolean ### Parameters in the strategy @@ -198,6 +197,7 @@ Values set in the configuration file always overwrite values set in the strategy * `sell_profit_offset` * `ignore_roi_if_buy_signal` * `ignore_buying_expired_candle_after` +* `position_adjustment_enable` ### Configuring amount per trade @@ -594,15 +594,6 @@ export HTTPS_PROXY="http://addr:port" freqtrade ``` -### Understand position_adjustment_enable - -The `position_adjustment_enable` configuration parameter enables the usage of `adjust_trade_position()` callback in strategy. -For performance reasons, it's disabled by default, and freqtrade will show a warning message on startup if enabled. -Enabling this does nothing unless the strategy also implements `adjust_trade_position()` callback. - -See [the strategy callbacks](strategy-callbacks.md) for details on usage. - - ## Next step Now you have configured your config.json, the next step is to [start your bot](bot-usage.md). diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 99b4a5a70..b82026817 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -232,14 +232,13 @@ merged_frame = pd.concat(frames, axis=1) ## Adjust trade position +The `position_adjustment_enable` strategy property enables the usage of `adjust_trade_position()` callback in strategy. +For performance reasons, it's disabled by default and freqtrade will show a warning message on startup if enabled. `adjust_trade_position()` can be used to perform additional orders to manage risk with DCA (Dollar Cost Averaging) for example. The strategy is expected to return a stake_amount if and when an additional buy order should be made (position is increased). If there is not enough funds in the wallet then nothing will happen. Additional orders also mean additional fees and those orders don't count towards `max_open_trades`. -Unlimited stake amount with trade position increasing is highly not recommended as your DCA orders would compete with your normal trade open orders. - -!!! Note - The `position_adjustment_enable` configuration parameter must be enabled to use adjust_trade_position callback in strategy. +Using unlimited stake amount with DCA orders requires you to also implement `custom_stake_amount` callback to avoid allocating all funcds to initial order. !!! Warning Stoploss is still calculated from the initial opening price, not averaged price. @@ -253,8 +252,21 @@ class DigDeeperStrategy(IStrategy): # Attempts to handle large drops with DCA. High stoploss is required. stoploss = -0.30 + max_dca_orders = 3 + # ... populate_* methods + + # Let unlimited stakes leave funds open for DCA orders + def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float, + proposed_stake: float, min_stake: float, max_stake: float, + **kwargs) -> float: + + if self.config['stake_amount'] == 'unlimited': + return proposed_stake / 5.5 + # Use default stake amount. + return proposed_stake + def adjust_trade_position(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, **kwargs) -> Optional[float]: @@ -270,7 +282,7 @@ class DigDeeperStrategy(IStrategy): :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. :return float: Stake amount to adjust your trade """ - + if current_profit > -0.05: return None @@ -285,9 +297,6 @@ class DigDeeperStrategy(IStrategy): count_of_buys = 0 for order in trade.orders: - # Instantly stop when there's an open order - if order.ft_is_open: - return None if order.ft_order_side == 'buy' and order.status == "closed": count_of_buys += 1 @@ -298,7 +307,7 @@ class DigDeeperStrategy(IStrategy): # If that falles once again down to -5%, we buy 1.75x more # Total stake for this trade would be 1 + 1.25 + 1.5 + 1.75 = 5.5x of the initial allowed stake. # Hope you have a deep wallet! - if 0 < count_of_buys <= 3: + if 0 < count_of_buys <= self.max_dca_orders: try: stake_amount = self.wallets.get_trade_stake_amount(pair, None) stake_amount = stake_amount * (1 + (count_of_buys * 0.25)) diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index 0a6f5896b..42a63bae7 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -572,54 +572,10 @@ class AwesomeStrategy(IStrategy): ## Adjust trade position -`adjust_trade_position()` can be used to perform additional orders to manage risk with DCA (Dollar Cost Averaging). +The `position_adjustment_enable` strategy property enables the usage of `adjust_trade_position()` callback in strategy. +For performance reasons, it's disabled by default and freqtrade will show a warning message on startup if enabled. +Enabling this does nothing unless the strategy also implements `adjust_trade_position()` callback. +`adjust_trade_position()` can be used to perform additional orders, for example to manage risk with DCA (Dollar Cost Averaging). The strategy is expected to return a stake_amount if and when an additional buy order should be made (position is increased). -If there is not enough funds in the wallet then nothing will happen. -Additional orders also mean additional fees and those orders don't count towards `max_open_trades`. -Unlimited stake amount with trade position increasing is highly not recommended as your DCA orders would compete with your normal trade open orders. -!!! Note - Current implementation does not support decreasing position size with partial sales! - -!!! Tip - The `position_adjustment_enable` configuration parameter must be enabled to use adjust_trade_position callback in strategy. - -!!! Warning - Stoploss is still calculated from the initial opening price, not averaged price. - So if you do 3 additional buys at -7% and have a stoploss at -10% then you will most likely trigger stoploss while the UI will be showing you an average profit of -3%. - -``` python -from freqtrade.persistence import Trade - - -class AwesomeStrategy(IStrategy): - - # ... populate_* methods - - def adjust_trade_position(self, pair: str, trade: Trade, - current_time: datetime, current_rate: float, current_profit: float, - **kwargs) -> Optional[float]: - """ - Custom trade adjustment logic, returning the stake amount that a trade should be increased. - This means extra buy orders with additional fees. - - For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ - - When not implemented by a strategy, returns None - - :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: Stake amount to adjust your trade - """ - - # Example: If 10% loss / -10% profit then buy more the same amount we had before. - if current_profit < -0.10: - return trade.stake_amount - - return None - -``` +[See Advanced Strategies for an example](strategy-advanced.md#adjust-trade-position) diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 75c973b79..f5a674878 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -173,9 +173,6 @@ class Configuration: if 'sd_notify' in self.args and self.args['sd_notify']: config['internals'].update({'sd_notify': True}) - if config.get('position_adjustment_enable', False): - logger.warning('`position_adjustment` has been enabled for strategy.') - def _process_datadir_options(self, config: Dict[str, Any]) -> None: """ Extract information for sys.argv and load directory configurations diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 9daeb5139..22fcc4e9a 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -102,9 +102,6 @@ class FreqtradeBot(LoggingMixin): self._exit_lock = Lock() LoggingMixin.__init__(self, logger, timeframe_to_seconds(self.strategy.timeframe)) - # Is Position Adjustment enabled? - self.position_adjustment = bool(self.config.get('position_adjustment_enable', False)) - def notify_status(self, msg: str) -> None: """ Public method for users of this class (worker, etc.) to send notifications @@ -182,7 +179,7 @@ class FreqtradeBot(LoggingMixin): self.exit_positions(trades) # Check if we need to adjust our current positions before attempting to buy new trades. - if self.position_adjustment: + if self.strategy.position_adjustment_enable: self.process_open_trade_positions() # Then looking for buy opportunities @@ -460,26 +457,25 @@ class FreqtradeBot(LoggingMixin): """ # Walk through each pair and check if it needs changes for trade in Trade.get_open_trades(): + # If there is any open orders, wait for them to finish. + for order in trade.orders: + if order.ft_is_open: + break try: - self.adjust_trade_position(trade) + self.check_and_call_adjust_trade_position(trade) except DependencyException as exception: logger.warning('Unable to adjust position of trade for %s: %s', trade.pair, exception) - def adjust_trade_position(self, trade: Trade): + def check_and_call_adjust_trade_position(self, trade: Trade): """ Check the implemented trading strategy for adjustment command. If the strategy triggers the adjustment, a new order gets issued. Once that completes, the existing trade is modified to match new data. """ - # If there is any open orders, wait for them to finish. - for order in trade.orders: - if order.ft_is_open: - return - - logger.debug(f"adjust_trade_position for pair {trade.pair}") current_rate = self.exchange.get_rate(trade.pair, refresh=True, side="buy") current_profit = trade.calc_profit_ratio(current_rate) + logger.debug(f"Calling adjust_trade_position for pair {trade.pair}") stake_amount = strategy_safe_wrapper(self.strategy.adjust_trade_position, default_retval=None)( pair=trade.pair, trade=trade, current_time=datetime.now(timezone.utc), diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index e5585463c..b40f06a1e 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -118,7 +118,6 @@ class Backtesting: # Add maximum startup candle count to configuration for informative pairs support self.config['startup_candle_count'] = self.required_startup self.exchange.validate_required_startup_candles(self.required_startup, self.timeframe) - self.position_adjustment = bool(self.config.get('position_adjustment_enable', False)) self.init_backtest() def __del__(self): @@ -354,8 +353,12 @@ class Backtesting: def _get_adjust_trade_entry_for_candle(self, trade: LocalTrade, row: Tuple ) -> LocalTrade: - current_profit = trade.calc_profit_ratio(row[OPEN_IDX]) + # If there is any open orders, wait for them to finish. + for order in trade.orders: + if order.ft_is_open: + return trade + current_profit = trade.calc_profit_ratio(row[OPEN_IDX]) stake_amount = strategy_safe_wrapper(self.strategy.adjust_trade_position, default_retval=None)( pair=trade.pair, trade=trade, current_time=row[DATE_IDX].to_pydatetime(), @@ -363,56 +366,17 @@ class Backtesting: # Check if we should increase our position if stake_amount is not None and stake_amount > 0.0: - return self._execute_trade_position_change(trade, row, stake_amount) + pos_trade = self._enter_trade(trade.pair, row, stake_amount, trade) + if pos_trade is not None: + return pos_trade return trade - def _execute_trade_position_change(self, trade: LocalTrade, row: Tuple, - stake_amount: float) -> LocalTrade: - current_price = row[OPEN_IDX] - propose_rate = min(max(current_price, row[LOW_IDX]), row[HIGH_IDX]) - available_amount = self.wallets.get_available_stake_amount() - - try: - min_stake_amount = self.exchange.get_min_pair_stake_amount( - trade.pair, propose_rate, -0.05) or 0 - stake_amount = self.wallets.validate_stake_amount(trade.pair, - stake_amount, min_stake_amount) - stake_amount = self.wallets._check_available_stake_amount(stake_amount, - available_amount) - except DependencyException: - logger.debug(f"{trade.pair} adjustment failed, " - f"wallet is smaller than asked stake {stake_amount}") - return trade - - amount = stake_amount / current_price - if amount <= 0: - logger.debug(f"{trade.pair} adjustment failed, amount ended up being zero {amount}") - return trade - - buy_order = Order( - ft_is_open=False, - ft_pair=trade.pair, - symbol=trade.pair, - ft_order_side="buy", - side="buy", - order_type="market", - status="closed", - price=propose_rate, - average=propose_rate, - amount=amount, - cost=stake_amount - ) - trade.orders.append(buy_order) - trade.recalc_trade_from_orders() - self.wallets.update() - return trade - def _get_sell_trade_entry_for_candle(self, trade: LocalTrade, sell_row: Tuple) -> Optional[LocalTrade]: # Check if we need to adjust our current positions - if self.position_adjustment: + if self.strategy.position_adjustment_enable: trade = self._get_adjust_trade_entry_for_candle(trade, sell_row) sell_candle_time = sell_row[DATE_IDX].to_pydatetime() @@ -490,11 +454,16 @@ class Backtesting: else: return self._get_sell_trade_entry_for_candle(trade, sell_row) - def _enter_trade(self, pair: str, row: List) -> Optional[LocalTrade]: - try: - stake_amount = self.wallets.get_trade_stake_amount(pair, None) - except DependencyException: - return None + def _enter_trade(self, pair: str, row: Tuple, stake_amount: Optional[float] = None, + trade: Optional[LocalTrade] = None) -> Optional[LocalTrade]: + + pos_adjust = trade is not None + if stake_amount is None: + try: + stake_amount = self.wallets.get_trade_stake_amount(pair, None) + except DependencyException: + return trade + # let's call the custom entry price, using the open price as default price propose_rate = strategy_safe_wrapper(self.strategy.custom_entry_price, default_retval=row[OPEN_IDX])( @@ -507,38 +476,47 @@ class Backtesting: min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, propose_rate, -0.05) or 0 max_stake_amount = self.wallets.get_available_stake_amount() - stake_amount = strategy_safe_wrapper(self.strategy.custom_stake_amount, - default_retval=stake_amount)( - pair=pair, current_time=row[DATE_IDX].to_pydatetime(), current_rate=propose_rate, - proposed_stake=stake_amount, min_stake=min_stake_amount, max_stake=max_stake_amount) + if not pos_adjust: + stake_amount = strategy_safe_wrapper(self.strategy.custom_stake_amount, + default_retval=stake_amount)( + pair=pair, current_time=row[DATE_IDX].to_pydatetime(), current_rate=propose_rate, + proposed_stake=stake_amount, min_stake=min_stake_amount, max_stake=max_stake_amount) + stake_amount = self.wallets.validate_stake_amount(pair, stake_amount, min_stake_amount) if not stake_amount: - return None + # In case of pos adjust, still return the original trade + # If not pos adjust, trade is None + return trade order_type = self.strategy.order_types['buy'] time_in_force = self.strategy.order_time_in_force['sell'] # Confirm trade entry: - if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)( - pair=pair, order_type=order_type, amount=stake_amount, rate=propose_rate, - time_in_force=time_in_force, current_time=row[DATE_IDX].to_pydatetime()): - return None + if not pos_adjust: + if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)( + pair=pair, order_type=order_type, amount=stake_amount, rate=propose_rate, + time_in_force=time_in_force, current_time=row[DATE_IDX].to_pydatetime()): + return None if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount): - # Enter trade - has_buy_tag = len(row) >= BUY_TAG_IDX + 1 - trade = LocalTrade( - pair=pair, - open_rate=propose_rate, - open_date=row[DATE_IDX].to_pydatetime(), - stake_amount=stake_amount, - amount=round(stake_amount / propose_rate, 8), - fee_open=self.fee, - fee_close=self.fee, - is_open=True, - buy_tag=row[BUY_TAG_IDX] if has_buy_tag else None, - exchange='backtesting', - ) + amount = round(stake_amount / propose_rate, 8) + if trade is None: + # Enter trade + has_buy_tag = len(row) >= BUY_TAG_IDX + 1 + trade = LocalTrade( + pair=pair, + open_rate=propose_rate, + open_date=row[DATE_IDX].to_pydatetime(), + stake_amount=stake_amount, + amount=amount, + fee_open=self.fee, + fee_close=self.fee, + is_open=True, + buy_tag=row[BUY_TAG_IDX] if has_buy_tag else None, + exchange='backtesting', + orders=[] + ) + order = Order( ft_is_open=False, ft_pair=trade.pair, @@ -547,15 +525,16 @@ class Backtesting: side="buy", order_type="market", status="closed", - price=trade.open_rate, - average=trade.open_rate, - amount=trade.amount, - cost=trade.stake_amount + trade.fee_open + price=propose_rate, + average=propose_rate, + amount=amount, + cost=stake_amount + trade.fee_open ) - trade.orders = [] trade.orders.append(order) - return trade - return None + if pos_adjust: + trade.recalc_trade_from_orders() + + return trade def handle_left_open(self, open_trades: Dict[str, List[LocalTrade]], data: Dict[str, List[Tuple]]) -> List[LocalTrade]: diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index a7b95a3c5..95177c000 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -96,7 +96,8 @@ class StrategyResolver(IResolver): ("ignore_roi_if_buy_signal", False), ("sell_profit_offset", 0.0), ("disable_dataframe_checks", False), - ("ignore_buying_expired_candle_after", 0) + ("ignore_buying_expired_candle_after", 0), + ("position_adjustment_enable", False) ] for attribute, default in attributes: StrategyResolver._override_attribute_helper(strategy, config, diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index ab7e99797..f53bc7d94 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -721,7 +721,7 @@ class RPC: # check if pair already has an open pair trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first() if trade: - if not self._config.get('position_adjustment_enable', False): + if not self._freqtrade.strategy.position_adjustment_enable: raise RPCException(f'position for {pair} already open - id: {trade.id}') # gen stake amount diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 1adcf42ca..92a79488c 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -106,6 +106,9 @@ class IStrategy(ABC, HyperStrategyMixin): sell_profit_offset: float ignore_roi_if_buy_signal: bool + # Position adjustment is disabled by default + position_adjustment_enable: bool = False + # Number of seconds after which the candle will no longer result in a buy on expired candles ignore_buying_expired_candle_after: int = 0 diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index 02c3cde18..90672c315 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -185,16 +185,6 @@ class Wallets: possible_stake = (available_amount + val_tied_up) / self._config['max_open_trades'] - # Position Adjustment dynamic base order size - try: - if self._config.get('position_adjustment_enable', False): - base_stake_amount_ratio = self._config.get('base_stake_amount_ratio', 1.0) - if base_stake_amount_ratio > 0.0: - possible_stake = possible_stake * base_stake_amount_ratio - except Exception as e: - logger.warning("Invalid base_stake_amount_ratio", e) - return 0 - # Theoretical amount can be above available amount - therefore limit to available amount! return min(possible_stake, available_amount) diff --git a/tests/optimize/test_backtesting_adjust_position.py b/tests/optimize/test_backtesting_adjust_position.py index 03a3339d4..38d0a27d2 100644 --- a/tests/optimize/test_backtesting_adjust_position.py +++ b/tests/optimize/test_backtesting_adjust_position.py @@ -17,7 +17,6 @@ def test_backtest_position_adjustment(default_conf, fee, mocker, testdatadir) -> mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) patch_exchange(mocker) default_conf.update({ - "position_adjustment_enable": True, "stake_amount": 100.0, "dry_run_wallet": 1000.0, "strategy": "StrategyTestV2" @@ -28,6 +27,7 @@ def test_backtest_position_adjustment(default_conf, fee, mocker, testdatadir) -> timerange = TimeRange('date', None, 1517227800, 0) data = history.load_data(datadir=testdatadir, timeframe='5m', pairs=['UNITTEST/BTC'], timerange=timerange) + backtesting.strategy.position_adjustment_enable = True processed = backtesting.strategy.advise_all_indicators(data) min_date, max_date = get_timerange(processed) result = backtesting.backtest( diff --git a/tests/strategy/strats/strategy_test_v2.py b/tests/strategy/strats/strategy_test_v2.py index 6371c1029..0b0e533c3 100644 --- a/tests/strategy/strats/strategy_test_v2.py +++ b/tests/strategy/strats/strategy_test_v2.py @@ -6,6 +6,7 @@ import talib.abstract as ta from pandas import DataFrame import freqtrade.vendor.qtpylib.indicators as qtpylib +from freqtrade.exceptions import DependencyException from freqtrade.persistence import Trade from freqtrade.strategy.interface import IStrategy @@ -51,6 +52,9 @@ class StrategyTestV2(IStrategy): 'sell': 'gtc', } + # By default this strategy does not use Position Adjustments + position_adjustment_enable = False + def informative_pairs(self): """ Define additional, informative pair/interval combinations to be cached from the exchange. @@ -162,10 +166,9 @@ class StrategyTestV2(IStrategy): current_rate: float, current_profit: float, **kwargs): if current_profit < -0.0075: - for order in trade.orders: - if order.ft_is_open: - return None - - return self.wallets.get_trade_stake_amount(pair, None) + try: + return self.wallets.get_trade_stake_amount(pair, None) + except DependencyException: + pass return None diff --git a/tests/test_wallets.py b/tests/test_wallets.py index 783b0eded..3e02cdb09 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -121,19 +121,19 @@ def test_get_trade_stake_amount_no_stake_amount(default_conf, mocker) -> None: freqtrade.wallets.get_trade_stake_amount('ETH/BTC') -@pytest.mark.parametrize("balance_ratio,capital,result1,result2,result3", [ - (1, None, 50, 66.66666, 250), - (0.99, None, 49.5, 66.0, 247.5), - (0.50, None, 25, 33.3333, 125), +@pytest.mark.parametrize("balance_ratio,capital,result1,result2", [ + (1, None, 50, 66.66666), + (0.99, None, 49.5, 66.0), + (0.50, None, 25, 33.3333), # Tests with capital ignore balance_ratio - (1, 100, 50, 0.0, 0.0), - (0.99, 200, 50, 66.66666, 50), - (0.99, 150, 50, 50, 37.5), - (0.50, 50, 25, 0.0, 0.0), - (0.50, 10, 5, 0.0, 0.0), + (1, 100, 50, 0.0), + (0.99, 200, 50, 66.66666), + (0.99, 150, 50, 50), + (0.50, 50, 25, 0.0), + (0.50, 10, 5, 0.0), ]) def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, balance_ratio, capital, - result1, result2, result3, limit_buy_order_open, + result1, result2, limit_buy_order_open, fee, mocker) -> None: mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -179,14 +179,6 @@ def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, balance_r result = freqtrade.wallets.get_trade_stake_amount('NEO/USDT') assert result == 0 - freqtrade.config['max_open_trades'] = 2 - freqtrade.config['dry_run_wallet'] = 1000 - freqtrade.wallets.start_cap = 1000 - freqtrade.config['position_adjustment_enable'] = True - freqtrade.config['base_stake_amount_ratio'] = 0.5 - result = freqtrade.wallets.get_trade_stake_amount('LTC/USDT') - assert result == result3 - @pytest.mark.parametrize('stake_amount,min_stake_amount,max_stake_amount,expected', [ (22, 11, 50, 22), From f61aaa8c0db409668359b33ff967fbdf81bce0c9 Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Fri, 24 Dec 2021 19:02:39 +0200 Subject: [PATCH 47/90] Improve documentation example --- docs/strategy-advanced.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index b82026817..165faa299 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -253,6 +253,8 @@ class DigDeeperStrategy(IStrategy): stoploss = -0.30 max_dca_orders = 3 + # This number is explained a bit further down + max_dca_multiplier = 5.5 # ... populate_* methods @@ -262,7 +264,7 @@ class DigDeeperStrategy(IStrategy): **kwargs) -> float: if self.config['stake_amount'] == 'unlimited': - return proposed_stake / 5.5 + return proposed_stake / self.max_dca_multiplier # Use default stake amount. return proposed_stake @@ -306,10 +308,15 @@ class DigDeeperStrategy(IStrategy): # If that falles down to -5% again, we buy 1.5x more # If that falles once again down to -5%, we buy 1.75x more # Total stake for this trade would be 1 + 1.25 + 1.5 + 1.75 = 5.5x of the initial allowed stake. + # That is why max_dca_multiplier is 5.5 # Hope you have a deep wallet! if 0 < count_of_buys <= self.max_dca_orders: try: + # This returns max stakes for one trade stake_amount = self.wallets.get_trade_stake_amount(pair, None) + # This calculates base order size + stake_amount = stake_amount / self.max_dca_multiplier + # This then calculates current safety order size stake_amount = stake_amount * (1 + (count_of_buys * 0.25)) return stake_amount except Exception as exception: From 3cbb2ff31fb79d7ebc4fccb8d659a75b78a1b969 Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Sat, 25 Dec 2021 10:35:08 +0200 Subject: [PATCH 48/90] Fix up documentation. --- docs/configuration.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 3bea83ad3..a445e6869 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -172,7 +172,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `user_data_dir` | Directory containing user data.
*Defaults to `./user_data/`*.
**Datatype:** String | `dataformat_ohlcv` | Data format to use to store historical candle (OHLCV) data.
*Defaults to `json`*.
**Datatype:** String | `dataformat_trades` | Data format to use to store historical trades data.
*Defaults to `jsongz`*.
**Datatype:** String -| `position_adjustment_enable` | Enables the strategy to use position adjustments (additional buys or sells). [More information here](strategy-callbacks.md#adjust-trade-position).
[Strategy Override](#parameters-in-the-strategy).
*Defaults to `true`.*
**Datatype:** Boolean +| `position_adjustment_enable` | Enables the strategy to use position adjustments (additional buys or sells). [More information here](strategy-callbacks.md#adjust-trade-position).
[Strategy Override](#parameters-in-the-strategy).
*Defaults to `false`.*
**Datatype:** Boolean ### Parameters in the strategy @@ -305,12 +305,12 @@ To allow the bot to trade all the available `stake_currency` in your account (mi It is therefore important to set `dry_run_wallet` to a sensible value (like 0.05 or 0.01 for BTC and 1000 or 100 for USDT, for example), otherwise, it may simulate trades with 100 BTC (or more) or 0.05 USDT (or less) at once - which may not correspond to your real available balance or is less than the exchange minimal limit for the order amount for the stake currency. #### Dynamic stake amount with position adjustment -When you want to use position adjustment with unlimited stakes, you must also set `base_stake_amount_ratio` to a reasonable value depending on your strategy. -Typical value would be in the range of 0.1 - 0.5, but depends highly on your strategy and how much you wish to leave into the wallet as position adjustment buffer. +When you want to use position adjustment with unlimited stakes, you must also implement `custom_stake_amount` to a return a value depending on your strategy. +Typical value would be in the range of 25% - 50% of the proposed stakes, but depends highly on your strategy and how much you wish to leave into the wallet as position adjustment buffer.
-For example if your position adjustment assumes it can do 2 additional buys with the same stake amounts then your `base_stake_amount_ratio` should be 0.333333 (33.3333%) of your calculated unlimited stake amount. +For example if your position adjustment assumes it can do 2 additional buys with the same stake amounts then your buffer should be 66.6667% of the initially proposed unlimited stake amount.
-Or another example if your position adjustment assumes it can do 1 additional buy with 3x the original stake amount then your `base_stake_amount_ratio` should be 0.25 (25%) of your calculated unlimited stake amount. +Or another example if your position adjustment assumes it can do 1 additional buy with 3x the original stake amount then `custom_stake_amount` should return 25% of proposed stake amount and leave 75% for possible later position adjustments. --8<-- "includes/pricing.md" From ea79eb55e979e08d89401904ee3b307e2d2dd3d3 Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Sat, 25 Dec 2021 10:43:25 +0200 Subject: [PATCH 49/90] Remove this test change from DCA branch. --- tests/optimize/test_hyperopt.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 5e8d899c8..a43c62376 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -1,5 +1,4 @@ # pragma pylint: disable=missing-docstring,W0212,C0103 -import os from datetime import datetime from pathlib import Path from unittest.mock import ANY, MagicMock @@ -170,7 +169,6 @@ def test_start_no_hyperopt_allowed(mocker, hyperopt_conf, caplog) -> None: def test_start_no_data(mocker, hyperopt_conf) -> None: - hyperopt_conf['user_data_dir'] = Path("tests") patched_configuration_load_config_file(mocker, hyperopt_conf) mocker.patch('freqtrade.data.history.load_pair_history', MagicMock(return_value=pd.DataFrame)) mocker.patch( @@ -191,12 +189,6 @@ def test_start_no_data(mocker, hyperopt_conf) -> None: with pytest.raises(OperationalException, match='No data found. Terminating.'): start_hyperopt(pargs) - # Cleanup since that failed hyperopt start leaves a lockfile. - try: - os.unlink(Hyperopt.get_lock_filename(hyperopt_conf)) - except Exception: - pass - def test_start_filelock(mocker, hyperopt_conf, caplog) -> None: hyperopt_mock = MagicMock(side_effect=Timeout(Hyperopt.get_lock_filename(hyperopt_conf))) From d3f3c49b13a45bbccc9daded6af96b63570b6328 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 26 Dec 2021 15:29:10 +0100 Subject: [PATCH 50/90] Fix minor "gotchas" --- docs/strategy-advanced.md | 6 +++--- freqtrade/freqtradebot.py | 6 ++++-- tests/test_freqtradebot.py | 6 +++--- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 165faa299..a2d8d1b06 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -238,7 +238,7 @@ For performance reasons, it's disabled by default and freqtrade will show a warn The strategy is expected to return a stake_amount if and when an additional buy order should be made (position is increased). If there is not enough funds in the wallet then nothing will happen. Additional orders also mean additional fees and those orders don't count towards `max_open_trades`. -Using unlimited stake amount with DCA orders requires you to also implement `custom_stake_amount` callback to avoid allocating all funcds to initial order. +Using unlimited stake amount with DCA orders requires you to also implement `custom_stake_amount` callback to avoid allocating all funds to initial order. !!! Warning Stoploss is still calculated from the initial opening price, not averaged price. @@ -305,8 +305,8 @@ class DigDeeperStrategy(IStrategy): # Allow up to 3 additional increasingly larger buys (4 in total) # Initial buy is 1x # If that falls to -5% profit, we buy 1.25x more, average profit should increase to roughly -2.2% - # If that falles down to -5% again, we buy 1.5x more - # If that falles once again down to -5%, we buy 1.75x more + # If that falls down to -5% again, we buy 1.5x more + # If that falls once again down to -5%, we buy 1.75x more # Total stake for this trade would be 1 + 1.25 + 1.5 + 1.75 = 5.5x of the initial allowed stake. # That is why max_dca_multiplier is 5.5 # Hope you have a deep wallet! diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 22fcc4e9a..c2c698859 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -7,7 +7,7 @@ import traceback from datetime import datetime, timezone from math import isclose from threading import Lock -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, Tuple import arrow @@ -650,7 +650,9 @@ class FreqtradeBot(LoggingMixin): logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}") return trade - def get_valid_enter_price_and_stake(self, pair, price, stake_amount, trade): + def get_valid_enter_price_and_stake( + self, pair: str, price: Optional[float], stake_amount: float, + trade: Optional[Trade]) -> Tuple[float, float]: if price: enter_limit_requested = price else: diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 18f720782..7bcd9b64e 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -4432,11 +4432,11 @@ def test_position_adjust(mocker, default_conf_usdt, fee) -> None: assert order assert order.order_id == '650' - def make_sure_its_651(*apos, **kwargs): + def make_sure_its_651(*args, **kwargs): - if apos[0] == '650': + if args[0] == '650': return closed_successful_buy_order - if apos[0] == '651': + if args[0] == '651': return open_dca_order_1 return None From 045225beefdb48613ef0be145a1ac5b20dd9676a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 26 Dec 2021 15:34:37 +0100 Subject: [PATCH 51/90] Slightly improve doc formatting --- docs/configuration.md | 5 +++-- docs/plotting.md | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index a445e6869..340ae2e72 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -305,11 +305,12 @@ To allow the bot to trade all the available `stake_currency` in your account (mi It is therefore important to set `dry_run_wallet` to a sensible value (like 0.05 or 0.01 for BTC and 1000 or 100 for USDT, for example), otherwise, it may simulate trades with 100 BTC (or more) or 0.05 USDT (or less) at once - which may not correspond to your real available balance or is less than the exchange minimal limit for the order amount for the stake currency. #### Dynamic stake amount with position adjustment + When you want to use position adjustment with unlimited stakes, you must also implement `custom_stake_amount` to a return a value depending on your strategy. Typical value would be in the range of 25% - 50% of the proposed stakes, but depends highly on your strategy and how much you wish to leave into the wallet as position adjustment buffer. -
+ For example if your position adjustment assumes it can do 2 additional buys with the same stake amounts then your buffer should be 66.6667% of the initially proposed unlimited stake amount. -
+ Or another example if your position adjustment assumes it can do 1 additional buy with 3x the original stake amount then `custom_stake_amount` should return 25% of proposed stake amount and leave 75% for possible later position adjustments. --8<-- "includes/pricing.md" diff --git a/docs/plotting.md b/docs/plotting.md index b81d57b7e..38635ae6e 100644 --- a/docs/plotting.md +++ b/docs/plotting.md @@ -273,7 +273,7 @@ def plot_config(self): !!! Warning `plotly` arguments are only supported with plotly library and will not work with freq-ui. -!!! Note +!!! Note "Trade position adjustments" If `position_adjustment_enable` / `adjust_trade_position()` is used, the trade initial buy price is averaged over multiple orders and the trade start price will most likely appear outside the candle range. ## Plot profit From 817a65b656943dadeb328dcf95469979b87a34dc Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Sun, 26 Dec 2021 20:01:48 +0200 Subject: [PATCH 52/90] This is not needed since backtesting does not have open orders. --- freqtrade/optimize/backtesting.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index b40f06a1e..70293e8b9 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -353,11 +353,6 @@ class Backtesting: def _get_adjust_trade_entry_for_candle(self, trade: LocalTrade, row: Tuple ) -> LocalTrade: - # If there is any open orders, wait for them to finish. - for order in trade.orders: - if order.ft_is_open: - return trade - current_profit = trade.calc_profit_ratio(row[OPEN_IDX]) stake_amount = strategy_safe_wrapper(self.strategy.adjust_trade_position, default_retval=None)( From 099dc07baf3ec88f9d777ececb25de85234ff5e7 Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Sun, 26 Dec 2021 20:02:20 +0200 Subject: [PATCH 53/90] No longer needed since recalc_trade_from_orders always calls it. --- freqtrade/persistence/models.py | 1 - 1 file changed, 1 deletion(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index d169c4d27..06b8279e5 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -424,7 +424,6 @@ class LocalTrade(): # Update open rate and actual amount self.open_rate = float(safe_value_fallback(order, 'average', 'price')) self.amount = float(safe_value_fallback(order, 'filled', 'amount')) - self.recalc_open_trade_value() if self.is_open: logger.info(f'{order_type.upper()}_BUY has been fulfilled for {self}.') self.open_order_id = None From bd5520bee22f8060ebbd470a5710174f34354d6a Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Sun, 26 Dec 2021 20:03:10 +0200 Subject: [PATCH 54/90] Adjust comments, fix stoploss_on_exchange for slower closed orders. --- freqtrade/freqtradebot.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index c2c698859..cf724f6a1 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -593,6 +593,7 @@ class FreqtradeBot(LoggingMixin): # Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker') + # This is a new trade if trade is None: trade = Trade( pair=pair, @@ -607,14 +608,17 @@ class FreqtradeBot(LoggingMixin): open_date=datetime.utcnow(), exchange=self.exchange.id, open_order_id=order_id, + fee_open_currency=None, strategy=self.strategy.get_strategy_name(), buy_tag=buy_tag, timeframe=timeframe_to_minutes(self.config['timeframe']) ) + else: + # This is additional buy, we reset fee_open_currency so timeout checking can work + trade.is_open = True + trade.fee_open_currency = None + trade.open_order_id = order_id - trade.is_open = True - trade.fee_open_currency = None - trade.open_order_id = order_id trade.orders.append(order_obj) trade.recalc_trade_from_orders() Trade.query.session.add(trade) @@ -1389,8 +1393,11 @@ class FreqtradeBot(LoggingMixin): Trade.commit() logger.info(f"Trade has been updated: {trade}") - # Updating wallets when order is closed if order['status'] in constants.NON_OPEN_EXCHANGE_STATES: + # If a buy order was closed, force update on stoploss on exchange + if order['side'] == 'buy': + trade = self.cancel_stoploss_on_exchange(trade) + # Updating wallets when order is closed self.wallets.update() if not trade.is_open: From bc8fc3ab09648f6018ec1126076d517b7db85bd2 Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Sun, 26 Dec 2021 20:09:18 +0200 Subject: [PATCH 55/90] We can actually call recalc_open_trade_value less since it's being called eventually anyway. --- freqtrade/freqtradebot.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index cf724f6a1..4e9cf9ab9 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1370,7 +1370,6 @@ class FreqtradeBot(LoggingMixin): logger.info(f"Updating order from exchange: {order}") trade.update_order(order) - trade.recalc_trade_from_orders() if self.exchange.check_order_canceled_empty(order): # Trade has been cancelled on exchange @@ -1384,7 +1383,6 @@ class FreqtradeBot(LoggingMixin): abs_tol=constants.MATH_CLOSE_PREC): order['amount'] = new_amount order.pop('filled', None) - trade.recalc_open_trade_value() except DependencyException as exception: logger.warning("Could not update trade amount: %s", exception) From 17f037cec62c838d37c877377babc2574d4e9a0f Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 27 Dec 2021 16:07:43 +0100 Subject: [PATCH 56/90] Extract order_fee handling from update_trade_state --- freqtrade/freqtradebot.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 4e9cf9ab9..5d0773437 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1359,7 +1359,7 @@ class FreqtradeBot(LoggingMixin): return False # Update trade with order values - logger.info('Found open order for %s', trade) + logger.info(f'Found open order for {trade}') try: order = action_order or self.exchange.fetch_order_or_stoploss_order(order_id, trade.pair, @@ -1376,15 +1376,7 @@ class FreqtradeBot(LoggingMixin): # Handling of this will happen in check_handle_timedout. return True - # Try update amount (binance-fix) - try: - new_amount = self.get_real_amount(trade, order) - if not isclose(safe_value_fallback(order, 'filled', 'amount'), new_amount, - abs_tol=constants.MATH_CLOSE_PREC): - order['amount'] = new_amount - order.pop('filled', None) - except DependencyException as exception: - logger.warning("Could not update trade amount: %s", exception) + order = self.handle_order_fee(trade, order) trade.update(order) trade.recalc_trade_from_orders() @@ -1439,6 +1431,18 @@ class FreqtradeBot(LoggingMixin): return real_amount return amount + def handle_order_fee(self, trade: Trade, order: Dict[str, Any]) -> Dict[str, Any]: + # Try update amount (binance-fix) + try: + new_amount = self.get_real_amount(trade, order) + if not isclose(safe_value_fallback(order, 'filled', 'amount'), new_amount, + abs_tol=constants.MATH_CLOSE_PREC): + order['amount'] = new_amount + order.pop('filled', None) + except DependencyException as exception: + logger.warning("Could not update trade amount: %s", exception) + return order + def get_real_amount(self, trade: Trade, order: Dict) -> float: """ Detect and update trade fee. From 2a728c676e5915f8fa25324e8078a1fe698006e1 Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Mon, 27 Dec 2021 19:41:33 +0200 Subject: [PATCH 57/90] Improve documentation. Fix bug. --- docs/strategy-advanced.md | 96 -------------------------------------- docs/strategy-callbacks.md | 91 +++++++++++++++++++++++++++++++++++- freqtrade/freqtradebot.py | 16 +++---- 3 files changed, 96 insertions(+), 107 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index a2d8d1b06..4cc607883 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -229,99 +229,3 @@ for val in self.buy_ema_short.range: # Append columns to existing dataframe merged_frame = pd.concat(frames, axis=1) ``` - -## Adjust trade position - -The `position_adjustment_enable` strategy property enables the usage of `adjust_trade_position()` callback in strategy. -For performance reasons, it's disabled by default and freqtrade will show a warning message on startup if enabled. -`adjust_trade_position()` can be used to perform additional orders to manage risk with DCA (Dollar Cost Averaging) for example. -The strategy is expected to return a stake_amount if and when an additional buy order should be made (position is increased). -If there is not enough funds in the wallet then nothing will happen. -Additional orders also mean additional fees and those orders don't count towards `max_open_trades`. -Using unlimited stake amount with DCA orders requires you to also implement `custom_stake_amount` callback to avoid allocating all funds to initial order. - -!!! Warning - Stoploss is still calculated from the initial opening price, not averaged price. - -``` python -from freqtrade.persistence import Trade - - -class DigDeeperStrategy(IStrategy): - - # Attempts to handle large drops with DCA. High stoploss is required. - stoploss = -0.30 - - max_dca_orders = 3 - # This number is explained a bit further down - max_dca_multiplier = 5.5 - - # ... populate_* methods - - # Let unlimited stakes leave funds open for DCA orders - def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float, - proposed_stake: float, min_stake: float, max_stake: float, - **kwargs) -> float: - - if self.config['stake_amount'] == 'unlimited': - return proposed_stake / self.max_dca_multiplier - - # Use default stake amount. - return proposed_stake - - def adjust_trade_position(self, pair: str, trade: Trade, - current_time: datetime, current_rate: float, current_profit: float, - **kwargs) -> Optional[float]: - """ - Custom trade adjustment logic, returning the stake amount that a trade should be increased. - This means extra buy orders with additional fees. - - :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: Stake amount to adjust your trade - """ - - if current_profit > -0.05: - return None - - # Obtain pair dataframe. - dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) - - # Only buy when not actively falling price. - last_candle = dataframe.iloc[-1].squeeze() - previous_candle = dataframe.iloc[-2].squeeze() - if last_candle['close'] < previous_candle['close']: - return None - - count_of_buys = 0 - for order in trade.orders: - if order.ft_order_side == 'buy' and order.status == "closed": - count_of_buys += 1 - - # Allow up to 3 additional increasingly larger buys (4 in total) - # Initial buy is 1x - # If that falls to -5% profit, we buy 1.25x more, average profit should increase to roughly -2.2% - # If that falls down to -5% again, we buy 1.5x more - # If that falls once again down to -5%, we buy 1.75x more - # Total stake for this trade would be 1 + 1.25 + 1.5 + 1.75 = 5.5x of the initial allowed stake. - # That is why max_dca_multiplier is 5.5 - # Hope you have a deep wallet! - if 0 < count_of_buys <= self.max_dca_orders: - try: - # This returns max stakes for one trade - stake_amount = self.wallets.get_trade_stake_amount(pair, None) - # This calculates base order size - stake_amount = stake_amount / self.max_dca_multiplier - # This then calculates current safety order size - stake_amount = stake_amount * (1 + (count_of_buys * 0.25)) - return stake_amount - except Exception as exception: - return None - - return None - -``` diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index 42a63bae7..3e45a9077 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -574,8 +574,95 @@ class AwesomeStrategy(IStrategy): The `position_adjustment_enable` strategy property enables the usage of `adjust_trade_position()` callback in strategy. For performance reasons, it's disabled by default and freqtrade will show a warning message on startup if enabled. -Enabling this does nothing unless the strategy also implements `adjust_trade_position()` callback. `adjust_trade_position()` can be used to perform additional orders, for example to manage risk with DCA (Dollar Cost Averaging). The strategy is expected to return a stake_amount if and when an additional buy order should be made (position is increased). +If there is not enough funds in the wallet then nothing will happen. +Additional orders also mean additional fees and those orders don't count towards `max_open_trades`. -[See Advanced Strategies for an example](strategy-advanced.md#adjust-trade-position) +!!! Note About stake size + Using fixed stake size means it will be the amount used for the first order just like without position adjustment. + Using 'unlimited' stake amount with DCA orders requires you to also implement custom_stake_amount callback to avoid allocating all funds to initial order. + +!!! Warning + Stoploss is still calculated from the initial opening price, not averaged price. + +``` python +from freqtrade.persistence import Trade + + +class DigDeeperStrategy(IStrategy): + + # Attempts to handle large drops with DCA. High stoploss is required. + stoploss = -0.30 + + max_dca_orders = 3 + # This number is explained a bit further down + max_dca_multiplier = 5.5 + + # ... populate_* methods + + # This is called when placing the initial order (opening trade) + def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float, + proposed_stake: float, min_stake: float, max_stake: float, + **kwargs) -> float: + + # We need to leave most of the funds for possible further DCA orders + # This also applies to fixed stakes + return proposed_stake / self.max_dca_multiplier + + def adjust_trade_position(self, pair: str, trade: Trade, + current_time: datetime, current_rate: float, current_profit: float, + **kwargs) -> Optional[float]: + """ + Custom trade adjustment logic, returning the stake amount that a trade should be increased. + This means extra buy orders with additional fees. + + :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: Stake amount to adjust your trade + """ + + if current_profit > -0.05: + return None + + # Obtain pair dataframe (just to show how to access it) + dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) + # Only buy when not actively falling price. + last_candle = dataframe.iloc[-1].squeeze() + previous_candle = dataframe.iloc[-2].squeeze() + if last_candle['close'] < previous_candle['close']: + return None + + count_of_buys = 0 + initial_order_stake = + for order in trade.orders: + if order.ft_order_side == 'buy' and order.status == "closed": + count_of_buys += 1 + + # Allow up to 3 additional increasingly larger buys (4 in total) + # Initial buy is 1x + # If that falls to -5% profit, we buy 1.25x more, average profit should increase to roughly -2.2% + # If that falls down to -5% again, we buy 1.5x more + # If that falls once again down to -5%, we buy 1.75x more + # Total stake for this trade would be 1 + 1.25 + 1.5 + 1.75 = 5.5x of the initial allowed stake. + # That is why max_dca_multiplier is 5.5 + # Hope you have a deep wallet! + if 0 < count_of_buys <= self.max_dca_orders: + try: + # This returns max stakes for one trade + stake_amount = self.wallets.get_trade_stake_amount(pair, None) + # This calculates base order size + stake_amount = stake_amount / self.max_dca_multiplier + # This then calculates current safety order size + stake_amount = stake_amount * (1 + (count_of_buys * 0.25)) + return stake_amount + except Exception as exception: + return None + + return None + +``` diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 5d0773437..d1327eb4d 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -458,14 +458,12 @@ class FreqtradeBot(LoggingMixin): # Walk through each pair and check if it needs changes for trade in Trade.get_open_trades(): # If there is any open orders, wait for them to finish. - for order in trade.orders: - if order.ft_is_open: - break - try: - self.check_and_call_adjust_trade_position(trade) - except DependencyException as exception: - logger.warning('Unable to adjust position of trade for %s: %s', - trade.pair, exception) + if len([o for o in trade.orders if o.ft_is_open]) == 0: + try: + self.check_and_call_adjust_trade_position(trade) + except DependencyException as exception: + logger.warning('Unable to adjust position of trade for %s: %s', + trade.pair, exception) def check_and_call_adjust_trade_position(self, trade: Trade): """ @@ -1385,7 +1383,7 @@ class FreqtradeBot(LoggingMixin): if order['status'] in constants.NON_OPEN_EXCHANGE_STATES: # If a buy order was closed, force update on stoploss on exchange - if order['side'] == 'buy': + if order.get('side', None) == 'buy': trade = self.cancel_stoploss_on_exchange(trade) # Updating wallets when order is closed self.wallets.update() From 4b654b271375ce5ef5ad7cd5086131fc784382c2 Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Mon, 27 Dec 2021 19:48:18 +0200 Subject: [PATCH 58/90] Reduce logging. --- freqtrade/freqtradebot.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index d1327eb4d..f4342fe05 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1366,7 +1366,6 @@ class FreqtradeBot(LoggingMixin): logger.warning('Unable to fetch order %s: %s', order_id, exception) return False - logger.info(f"Updating order from exchange: {order}") trade.update_order(order) if self.exchange.check_order_canceled_empty(order): @@ -1379,7 +1378,6 @@ class FreqtradeBot(LoggingMixin): trade.update(order) trade.recalc_trade_from_orders() Trade.commit() - logger.info(f"Trade has been updated: {trade}") if order['status'] in constants.NON_OPEN_EXCHANGE_STATES: # If a buy order was closed, force update on stoploss on exchange From f965e9177cecdb7542874a837ae67d99ecfa3acd Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Mon, 27 Dec 2021 19:56:45 +0200 Subject: [PATCH 59/90] Fix title --- docs/strategy-callbacks.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index 3e45a9077..e3cc72ba1 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -579,8 +579,9 @@ The strategy is expected to return a stake_amount if and when an additional buy If there is not enough funds in the wallet then nothing will happen. Additional orders also mean additional fees and those orders don't count towards `max_open_trades`. -!!! Note About stake size +!!! Note "About stake size" Using fixed stake size means it will be the amount used for the first order just like without position adjustment. + If you wish to buy additional orders with DCA then make sure to leave enough funds in the wallet for that. Using 'unlimited' stake amount with DCA orders requires you to also implement custom_stake_amount callback to avoid allocating all funds to initial order. !!! Warning From 3d336a736e6dafd0d168a75be2a62f99a3255083 Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Wed, 29 Dec 2021 14:51:57 +0200 Subject: [PATCH 60/90] Improve documentation. --- docs/strategy-callbacks.md | 2 +- freqtrade/strategy/interface.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index e3cc72ba1..129e15b32 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -621,7 +621,7 @@ class DigDeeperStrategy(IStrategy): :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_rate: Current buy rate. Use `exchange.get_rate` if you need sell rate. :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: Stake amount to adjust your trade diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 92a79488c..3a3b53df4 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -398,7 +398,7 @@ class IStrategy(ABC, HyperStrategyMixin): :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_rate: Current buy rate. Use `exchange.get_rate` if you need sell rate. :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: Stake amount to adjust your trade From 2116b0729fe0b2e673bd07372bd316c9c59ac4b6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 2 Jan 2022 20:20:56 +0100 Subject: [PATCH 61/90] Integration-test for DCA order --- docs/strategy-callbacks.md | 1 - freqtrade/wallets.py | 1 - tests/test_integration.py | 67 ++++++++++++++++++++++++++++++++++++-- 3 files changed, 65 insertions(+), 4 deletions(-) diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index 129e15b32..55db40b7b 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -639,7 +639,6 @@ class DigDeeperStrategy(IStrategy): return None count_of_buys = 0 - initial_order_stake = for order in trade.orders: if order.ft_order_side == 'buy' and order.status == "closed": count_of_buys += 1 diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index 90672c315..98a39ea2d 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -184,7 +184,6 @@ class Wallets: return 0 possible_stake = (available_amount + val_tied_up) / self._config['max_open_trades'] - # Theoretical amount can be above available amount - therefore limit to available amount! return min(possible_stake, available_amount) diff --git a/tests/test_integration.py b/tests/test_integration.py index a3484d438..21b01126f 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -127,8 +127,7 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee, (1, 200), (0.99, 198), ]) -def test_forcebuy_last_unlimited(default_conf, ticker, fee, limit_buy_order, mocker, balance_ratio, - result1) -> None: +def test_forcebuy_last_unlimited(default_conf, ticker, fee, mocker, balance_ratio, result1) -> None: """ Tests workflow unlimited stake-amount Buy 4 trades, forcebuy a 5th trade @@ -207,3 +206,67 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, limit_buy_order, moc assert len(bals2) == 5 assert 'LTC' in bals assert 'LTC' not in bals2 + + +def test_dca_buying(default_conf_usdt, ticker_usdt, fee, mocker) -> None: + default_conf_usdt['position_adjustment_enable'] = True + + freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + fetch_ticker=ticker_usdt, + get_fee=fee, + amount_to_precision=lambda s, x, y: y, + price_to_precision=lambda s, x, y: y, + ) + + patch_get_signal(freqtrade) + freqtrade.enter_positions() + + assert len(Trade.get_trades().all()) == 1 + trade = Trade.get_trades().first() + assert len(trade.orders) == 1 + assert trade.stake_amount == 60 + assert trade.open_rate == 2.0 + # No adjustment + freqtrade.process() + trade = Trade.get_trades().first() + assert len(trade.orders) == 1 + assert trade.stake_amount == 60 + + # Reduce bid amount + ticker_usdt_modif = ticker_usdt.return_value + ticker_usdt_modif['bid'] = ticker_usdt_modif['bid'] * 0.995 + mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', return_value=ticker_usdt_modif) + + # additional buy order + freqtrade.process() + trade = Trade.get_trades().first() + assert len(trade.orders) == 2 + assert trade.stake_amount == 120 + + # Open-rate averaged between 2.0 and 2.0 * 0.995 + assert trade.open_rate < 2.0 + assert trade.open_rate > 2.0 * 0.995 + + # No action - profit raised above 1% (the bar set in the strategy). + freqtrade.process() + trade = Trade.get_trades().first() + assert len(trade.orders) == 2 + assert trade.stake_amount == 120 + assert trade.orders[0].amount == 30 + assert trade.orders[1].amount == 60 / ticker_usdt_modif['bid'] + + assert trade.amount == trade.orders[0].amount + trade.orders[1].amount + + # Sell + patch_get_signal(freqtrade, value=(False, True, None, None)) + freqtrade.process() + trade = Trade.get_trades().first() + assert trade.is_open is False + assert trade.orders[0].amount == 30 + assert trade.orders[0].side == 'buy' + assert trade.orders[1].amount == 60 / ticker_usdt_modif['bid'] + # Sold everything + assert trade.orders[-1].side == 'sell' + assert trade.orders[2].amount == trade.amount From fac6956eebd31f418b2128aac2324d8298127c9d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 2 Jan 2022 22:25:40 +0100 Subject: [PATCH 62/90] Fix test failure after merge --- tests/optimize/test_backtesting_adjust_position.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/optimize/test_backtesting_adjust_position.py b/tests/optimize/test_backtesting_adjust_position.py index 38d0a27d2..a7f953008 100644 --- a/tests/optimize/test_backtesting_adjust_position.py +++ b/tests/optimize/test_backtesting_adjust_position.py @@ -1,5 +1,7 @@ # pragma pylint: disable=missing-docstring, W0212, line-too-long, C0103, unused-argument +from copy import deepcopy + import pandas as pd from arrow import Arrow @@ -31,7 +33,7 @@ def test_backtest_position_adjustment(default_conf, fee, mocker, testdatadir) -> processed = backtesting.strategy.advise_all_indicators(data) min_date, max_date = get_timerange(processed) result = backtesting.backtest( - processed=processed, + processed=deepcopy(processed), start_date=min_date, end_date=max_date, max_open_trades=10, From 94631c7d6499153fb1dbc93c39e81b1a55056ef4 Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Sat, 8 Jan 2022 15:06:05 +0200 Subject: [PATCH 63/90] Add performance warnings for backtesting and implementation. --- docs/strategy-callbacks.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index 55db40b7b..fd7c8ce2d 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -578,6 +578,7 @@ For performance reasons, it's disabled by default and freqtrade will show a warn The strategy is expected to return a stake_amount if and when an additional buy order should be made (position is increased). If there is not enough funds in the wallet then nothing will happen. Additional orders also mean additional fees and those orders don't count towards `max_open_trades`. +This callback is called very frequently, so you must keep your implementation as fast as possible. !!! Note "About stake size" Using fixed stake size means it will be the amount used for the first order just like without position adjustment. @@ -587,6 +588,9 @@ Additional orders also mean additional fees and those orders don't count towards !!! Warning Stoploss is still calculated from the initial opening price, not averaged price. +!!! Warning "Backtesting" + During backtesting this callback is called for each timeframe or `timeframe-detail`, so performance will be affected. + ``` python from freqtrade.persistence import Trade From 813a2cd23b0d9fa9f384c8c1f0b558ca5c4363e2 Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Sat, 8 Jan 2022 17:18:37 +0200 Subject: [PATCH 64/90] Add useful helper methods for adjust_trade_position implementation --- freqtrade/persistence/models.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 06b8279e5..d515d8782 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -614,6 +614,24 @@ class LocalTrade(): else: return None + def nr_of_successful_buys(self) -> int: + """ + Helper function to count the number of buy orders that have been filled. + :return: int count of buy orders that have been filled for this trade. + """ + return len([o for o in self.orders if o.ft_order_side == 'buy' and + o.status in NON_OPEN_EXCHANGE_STATES and + o.filled > 0]) + + def nr_of_successful_sells(self) -> int: + """ + Helper function to count the number of sell orders that have been filled. + :return: int count of sell orders that have been filled for this trade. + """ + return len([o for o in self.orders if o.ft_order_side == 'sell' and + o.status in NON_OPEN_EXCHANGE_STATES and + o.filled > 0]) + @staticmethod def get_trades_proxy(*, pair: str = None, is_open: bool = None, open_date: datetime = None, close_date: datetime = None, From 0bca07a32a0fca4f286d515cd06afe1cd46d9041 Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Sat, 8 Jan 2022 17:20:02 +0200 Subject: [PATCH 65/90] Added min_stake, max_stake. Removed pair as its included in trade. --- docs/strategy-callbacks.md | 7 +-- freqtrade/freqtradebot.py | 10 +++-- freqtrade/optimize/backtesting.py | 6 ++- freqtrade/strategy/interface.py | 11 ++--- tests/strategy/strats/strategy_test_v2.py | 6 +-- tests/test_integration.py | 4 ++ tests/test_persistence.py | 54 ++++++++++++++++++----- 7 files changed, 69 insertions(+), 29 deletions(-) diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index fd7c8ce2d..454c62072 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -625,7 +625,7 @@ class DigDeeperStrategy(IStrategy): :param pair: Pair that's currently analyzed :param trade: trade object. :param current_time: datetime object, containing the current datetime - :param current_rate: Current buy rate. Use `exchange.get_rate` if you need sell rate. + :param current_rate: Current buy rate. :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: Stake amount to adjust your trade @@ -642,10 +642,7 @@ class DigDeeperStrategy(IStrategy): if last_candle['close'] < previous_candle['close']: return None - count_of_buys = 0 - for order in trade.orders: - if order.ft_order_side == 'buy' and order.status == "closed": - count_of_buys += 1 + count_of_buys = trade.nr_of_successful_buys() # Allow up to 3 additional increasingly larger buys (4 in total) # Initial buy is 1x diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 0ef2d8fe1..508bf9b73 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -459,7 +459,7 @@ class FreqtradeBot(LoggingMixin): # Walk through each pair and check if it needs changes for trade in Trade.get_open_trades(): # If there is any open orders, wait for them to finish. - if len([o for o in trade.orders if o.ft_is_open]) == 0: + if trade.open_order_id is None: try: self.check_and_call_adjust_trade_position(trade) except DependencyException as exception: @@ -474,11 +474,15 @@ class FreqtradeBot(LoggingMixin): """ current_rate = self.exchange.get_rate(trade.pair, refresh=True, side="buy") current_profit = trade.calc_profit_ratio(current_rate) + min_stake_amount = self.exchange.get_min_pair_stake_amount(trade.pair, + current_rate, + self.strategy.stoploss) + max_stake_amount = self.wallets.get_available_stake_amount() logger.debug(f"Calling adjust_trade_position for pair {trade.pair}") stake_amount = strategy_safe_wrapper(self.strategy.adjust_trade_position, default_retval=None)( - pair=trade.pair, trade=trade, current_time=datetime.now(timezone.utc), - current_rate=current_rate, current_profit=current_profit) + trade=trade, current_time=datetime.now(timezone.utc), current_rate=current_rate, + current_profit=current_profit, min_stake=min_stake_amount, max_stake=max_stake_amount) if stake_amount is not None and stake_amount > 0.0: # We should increase our position diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index ceb4c36dc..7d3869a2c 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -358,10 +358,12 @@ class Backtesting: ) -> LocalTrade: current_profit = trade.calc_profit_ratio(row[OPEN_IDX]) + min_stake = self.exchange.get_min_pair_stake_amount(trade.pair, row[OPEN_IDX], -0.1) + max_stake = self.wallets.get_available_stake_amount() stake_amount = strategy_safe_wrapper(self.strategy.adjust_trade_position, default_retval=None)( - pair=trade.pair, trade=trade, current_time=row[DATE_IDX].to_pydatetime(), - current_rate=row[OPEN_IDX], current_profit=current_profit) + trade=trade, current_time=row[DATE_IDX].to_pydatetime(), current_rate=row[OPEN_IDX], + current_profit=current_profit, min_stake=min_stake, max_stake=max_stake) # Check if we should increase our position if stake_amount is not None and stake_amount > 0.0: diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 335ffb3e5..36061dc20 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -384,9 +384,9 @@ class IStrategy(ABC, HyperStrategyMixin): """ return proposed_stake - def adjust_trade_position(self, pair: str, trade: Trade, current_time: datetime, - current_rate: float, current_profit: float, **kwargs - ) -> Optional[float]: + def adjust_trade_position(self, trade: Trade, current_time: datetime, + current_rate: float, current_profit: float, min_stake: float, + max_stake: float, **kwargs) -> Optional[float]: """ Custom trade adjustment logic, returning the stake amount that a trade should be increased. This means extra buy orders with additional fees. @@ -395,11 +395,12 @@ class IStrategy(ABC, HyperStrategyMixin): When not implemented by a strategy, returns None - :param pair: Pair that's currently analyzed :param trade: trade object. :param current_time: datetime object, containing the current datetime - :param current_rate: Current buy rate. Use `exchange.get_rate` if you need sell rate. + :param current_rate: Current buy rate. :param current_profit: Current profit (as ratio), calculated based on current_rate. + :param min_stake: Minimal stake size allowed by exchange. + :param max_stake: Balance available for trading. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. :return float: Stake amount to adjust your trade """ diff --git a/tests/strategy/strats/strategy_test_v2.py b/tests/strategy/strats/strategy_test_v2.py index 0b0e533c3..1aadd0d4e 100644 --- a/tests/strategy/strats/strategy_test_v2.py +++ b/tests/strategy/strats/strategy_test_v2.py @@ -162,12 +162,12 @@ class StrategyTestV2(IStrategy): 'sell'] = 1 return dataframe - def adjust_trade_position(self, pair: str, trade: Trade, current_time: datetime, - current_rate: float, current_profit: float, **kwargs): + def adjust_trade_position(self, trade: Trade, current_time: datetime, current_rate: float, + current_profit: float, min_stake: float, max_stake: float, **kwargs): if current_profit < -0.0075: try: - return self.wallets.get_trade_stake_amount(pair, None) + return self.wallets.get_trade_stake_amount(trade.pair, None) except DependencyException: pass diff --git a/tests/test_integration.py b/tests/test_integration.py index 21b01126f..e1cbf3d2d 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -259,6 +259,8 @@ def test_dca_buying(default_conf_usdt, ticker_usdt, fee, mocker) -> None: assert trade.amount == trade.orders[0].amount + trade.orders[1].amount + assert trade.nr_of_successful_buys() == 2 + # Sell patch_get_signal(freqtrade, value=(False, True, None, None)) freqtrade.process() @@ -270,3 +272,5 @@ def test_dca_buying(default_conf_usdt, ticker_usdt, fee, mocker) -> None: # Sold everything assert trade.orders[-1].side == 'sell' assert trade.orders[2].amount == trade.amount + + assert trade.nr_of_successful_buys() == 2 diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 1a0c3c408..60d71d007 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -1553,20 +1553,21 @@ def test_recalc_trade_from_orders_ignores_bad_orders(fee): assert trade.open_rate == o1_rate assert trade.fee_open_cost == o1_fee_cost assert trade.open_trade_value == o1_trade_val + assert trade.nr_of_successful_buys() == 1 order2 = Order( ft_order_side='buy', ft_pair=trade.pair, ft_is_open=True, - status="closed", + status="open", symbol=trade.pair, order_type="market", side="buy", - price=1, - average=2, - filled=0, - remaining=4, - cost=5, + price=o1_rate, + average=o1_rate, + filled=o1_amount, + remaining=0, + cost=o1_cost, order_date=arrow.utcnow().shift(hours=-1).datetime, order_filled_date=arrow.utcnow().shift(hours=-1).datetime, ) @@ -1579,8 +1580,9 @@ def test_recalc_trade_from_orders_ignores_bad_orders(fee): assert trade.open_rate == o1_rate assert trade.fee_open_cost == o1_fee_cost assert trade.open_trade_value == o1_trade_val + assert trade.nr_of_successful_buys() == 1 - # Let's try with some other orders +# Let's try with some other orders order3 = Order( ft_order_side='buy', ft_pair=trade.pair, @@ -1606,6 +1608,34 @@ def test_recalc_trade_from_orders_ignores_bad_orders(fee): assert trade.open_rate == o1_rate assert trade.fee_open_cost == o1_fee_cost assert trade.open_trade_value == o1_trade_val + assert trade.nr_of_successful_buys() == 1 + + order4 = Order( + ft_order_side='buy', + ft_pair=trade.pair, + ft_is_open=False, + status="closed", + symbol=trade.pair, + order_type="market", + side="buy", + price=o1_rate, + average=o1_rate, + filled=o1_amount, + remaining=0, + cost=o1_cost, + order_date=arrow.utcnow().shift(hours=-1).datetime, + order_filled_date=arrow.utcnow().shift(hours=-1).datetime, + ) + trade.orders.append(order4) + trade.recalc_trade_from_orders() + + # Validate that the trade values have been changed + assert trade.amount == 2 * o1_amount + assert trade.stake_amount == 2 * o1_amount + assert trade.open_rate == o1_rate + assert trade.fee_open_cost == 2 * o1_fee_cost + assert trade.open_trade_value == 2 * o1_trade_val + assert trade.nr_of_successful_buys() == 2 # Just to make sure sell orders are ignored, let's calculate one more time. sell1 = Order( @@ -1627,8 +1657,10 @@ def test_recalc_trade_from_orders_ignores_bad_orders(fee): trade.orders.append(sell1) trade.recalc_trade_from_orders() - assert trade.amount == o1_amount - assert trade.stake_amount == o1_amount + assert trade.amount == 2 * o1_amount + assert trade.stake_amount == 2 * o1_amount assert trade.open_rate == o1_rate - assert trade.fee_open_cost == o1_fee_cost - assert trade.open_trade_value == o1_trade_val + assert trade.fee_open_cost == 2 * o1_fee_cost + assert trade.open_trade_value == 2 * o1_trade_val + assert trade.nr_of_successful_buys() == 2 + From c929d428b2294cd3e633a23c42608df307484a98 Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Sat, 8 Jan 2022 17:21:32 +0200 Subject: [PATCH 66/90] Remove blank line. --- tests/test_persistence.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 60d71d007..14988d85b 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -1663,4 +1663,3 @@ def test_recalc_trade_from_orders_ignores_bad_orders(fee): assert trade.fee_open_cost == 2 * o1_fee_cost assert trade.open_trade_value == 2 * o1_trade_val assert trade.nr_of_successful_buys() == 2 - From 195d601b8e808107663b4ac40c26a16c48ccb6ec Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Sat, 8 Jan 2022 17:41:59 +0200 Subject: [PATCH 67/90] Fix notification message showing "Current rate" as the initial buy order desired rate. --- freqtrade/freqtradebot.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 508bf9b73..618fde05a 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -620,6 +620,7 @@ class FreqtradeBot(LoggingMixin): # This is additional buy, we reset fee_open_currency so timeout checking can work trade.is_open = True trade.fee_open_currency = None + trade.open_rate_requested = enter_limit_requested trade.open_order_id = order_id trade.orders.append(order_obj) From 91b89c8c42f4d06c7fbca4c1bef9c795301a3fa1 Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Sat, 8 Jan 2022 21:30:42 +0200 Subject: [PATCH 68/90] Improve docs, fix telegram message to show current rate. --- docs/strategy-callbacks.md | 2 +- freqtrade/freqtradebot.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index 454c62072..bad96d990 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -589,7 +589,7 @@ This callback is called very frequently, so you must keep your implementation as Stoploss is still calculated from the initial opening price, not averaged price. !!! Warning "Backtesting" - During backtesting this callback is called for each timeframe or `timeframe-detail`, so performance will be affected. + During backtesting this callback is called for each candle in `timeframe` or `timeframe_detail`, so performance will be affected. ``` python from freqtrade.persistence import Trade diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 618fde05a..14a38beb5 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -695,6 +695,8 @@ class FreqtradeBot(LoggingMixin): if open_rate is None: open_rate = trade.open_rate + current_rate = self.exchange.get_rate(trade.pair, refresh=False, side="buy") + msg = { 'trade_id': trade.id, 'type': RPCMessageType.BUY_FILL if fill else RPCMessageType.BUY, @@ -709,7 +711,7 @@ class FreqtradeBot(LoggingMixin): 'fiat_currency': self.config.get('fiat_display_currency', None), 'amount': safe_value_fallback(order, 'filled', 'amount') or trade.amount, 'open_date': trade.open_date or datetime.utcnow(), - 'current_rate': trade.open_rate_requested, + 'current_rate': current_rate, } # Send the message From 30d293bfec903036da3480af824cc110a6f0e70a Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Mon, 10 Jan 2022 20:16:11 +0200 Subject: [PATCH 69/90] Fix bug with None in backtesting. --- freqtrade/persistence/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index d515d8782..1bc4cfc05 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -621,7 +621,7 @@ class LocalTrade(): """ return len([o for o in self.orders if o.ft_order_side == 'buy' and o.status in NON_OPEN_EXCHANGE_STATES and - o.filled > 0]) + (o.filled or 0) > 0]) def nr_of_successful_sells(self) -> int: """ @@ -630,7 +630,7 @@ class LocalTrade(): """ return len([o for o in self.orders if o.ft_order_side == 'sell' and o.status in NON_OPEN_EXCHANGE_STATES and - o.filled > 0]) + (o.filled or 0) > 0]) @staticmethod def get_trades_proxy(*, pair: str = None, is_open: bool = None, From 26f2db47770cd665627c97d0bc35fb979869f139 Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Mon, 10 Jan 2022 20:30:32 +0200 Subject: [PATCH 70/90] Fix notify_enter attempting to fetch rate during testing. --- freqtrade/freqtradebot.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 14a38beb5..3fdf31a37 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -16,7 +16,7 @@ from freqtrade.configuration import validate_config_consistency from freqtrade.data.converter import order_book_to_dataframe from freqtrade.data.dataprovider import DataProvider from freqtrade.edge import Edge -from freqtrade.enums import RPCMessageType, SellType, State +from freqtrade.enums import RPCMessageType, SellType, State, RunMode from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError, InvalidOrderException, PricingError) from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds @@ -695,7 +695,9 @@ class FreqtradeBot(LoggingMixin): if open_rate is None: open_rate = trade.open_rate - current_rate = self.exchange.get_rate(trade.pair, refresh=False, side="buy") + current_rate = trade.open_rate_requested + if self.dataprovider.runmode in (RunMode.DRY_RUN, RunMode.LIVE): + current_rate = self.exchange.get_rate(trade.pair, refresh=False, side="buy") msg = { 'trade_id': trade.id, From 3b7167ab07b1de12b91dde239acf2a025eca3283 Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Mon, 10 Jan 2022 20:30:40 +0200 Subject: [PATCH 71/90] Fix backtesting missing filled amounts in orders. --- freqtrade/optimize/backtesting.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 7d3869a2c..04fa8854f 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -531,6 +531,7 @@ class Backtesting: price=propose_rate, average=propose_rate, amount=amount, + filled=amount, cost=stake_amount + trade.fee_open ) trade.orders.append(order) From fbf026ac43c93136e2023b13b1bb272fa4505c01 Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Mon, 10 Jan 2022 20:43:57 +0200 Subject: [PATCH 72/90] Fix sorting of imports. --- freqtrade/freqtradebot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 3fdf31a37..495cb4b30 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -16,7 +16,7 @@ from freqtrade.configuration import validate_config_consistency from freqtrade.data.converter import order_book_to_dataframe from freqtrade.data.dataprovider import DataProvider from freqtrade.edge import Edge -from freqtrade.enums import RPCMessageType, SellType, State, RunMode +from freqtrade.enums import RPCMessageType, RunMode, SellType, State from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError, InvalidOrderException, PricingError) from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds From 94f2c99989885b86e8e256730e2a26470ace886d Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Tue, 11 Jan 2022 11:43:32 +0200 Subject: [PATCH 73/90] Temporary fix for lazy loading. Probably we can do it better. --- freqtrade/freqtradebot.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 495cb4b30..7fbed58d0 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -474,6 +474,7 @@ class FreqtradeBot(LoggingMixin): """ current_rate = self.exchange.get_rate(trade.pair, refresh=True, side="buy") current_profit = trade.calc_profit_ratio(current_rate) + nr_of_buys = trade.nr_of_successful_buys() # FIXME This is only here to lazyload orders. min_stake_amount = self.exchange.get_min_pair_stake_amount(trade.pair, current_rate, self.strategy.stoploss) From e50b07ecb473b5b1349345e7cc4a8529d4ff753c Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Tue, 11 Jan 2022 12:05:57 +0200 Subject: [PATCH 74/90] Make code compatible. --- freqtrade/freqtradebot.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 7fbed58d0..a056adabe 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -474,7 +474,9 @@ class FreqtradeBot(LoggingMixin): """ current_rate = self.exchange.get_rate(trade.pair, refresh=True, side="buy") current_profit = trade.calc_profit_ratio(current_rate) - nr_of_buys = trade.nr_of_successful_buys() # FIXME This is only here to lazyload orders. + + # FIXME This is only here to lazyload orders. + trade.nr_of_successful_buys() min_stake_amount = self.exchange.get_min_pair_stake_amount(trade.pair, current_rate, self.strategy.stoploss) From db3483c827f5e176f7e2b3e972541c4b60cbf2d4 Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Wed, 12 Jan 2022 05:02:42 +0200 Subject: [PATCH 75/90] Our example should also set position_adjustment_enable --- docs/strategy-callbacks.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index bad96d990..f313b6ae2 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -597,15 +597,18 @@ from freqtrade.persistence import Trade class DigDeeperStrategy(IStrategy): + position_adjustment_enable = True + # Attempts to handle large drops with DCA. High stoploss is required. stoploss = -0.30 + # ... populate_* methods + + # Example specific variables max_dca_orders = 3 # This number is explained a bit further down max_dca_multiplier = 5.5 - # ... populate_* methods - # This is called when placing the initial order (opening trade) def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float, proposed_stake: float, min_stake: float, max_stake: float, From af3d220ffc7c7f48ba843790972b73aa21a28c0f Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Wed, 12 Jan 2022 05:09:52 +0200 Subject: [PATCH 76/90] Update example method signature. --- docs/strategy-callbacks.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index f313b6ae2..8f6cd635e 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -618,18 +618,19 @@ class DigDeeperStrategy(IStrategy): # This also applies to fixed stakes return proposed_stake / self.max_dca_multiplier - def adjust_trade_position(self, pair: str, trade: Trade, - current_time: datetime, current_rate: float, current_profit: float, - **kwargs) -> Optional[float]: + def adjust_trade_position(self, trade: Trade, current_time: datetime, + current_rate: float, current_profit: float, min_stake: float, + max_stake: float, **kwargs): """ Custom trade adjustment logic, returning the stake amount that a trade should be increased. This means extra buy orders with additional fees. - :param pair: Pair that's currently analyzed :param trade: trade object. :param current_time: datetime object, containing the current datetime :param current_rate: Current buy rate. :param current_profit: Current profit (as ratio), calculated based on current_rate. + :param min_stake: Minimal stake size allowed by exchange. + :param max_stake: Balance available for trading. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. :return float: Stake amount to adjust your trade """ From 8643b20a0ea1d7a1dd4976f8e073655cdda4d0d1 Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Wed, 12 Jan 2022 06:06:23 +0200 Subject: [PATCH 77/90] Improve documentation about /stopbuy command --- docs/strategy-callbacks.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index 8f6cd635e..1adb06ae7 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -581,13 +581,16 @@ Additional orders also mean additional fees and those orders don't count towards This callback is called very frequently, so you must keep your implementation as fast as possible. !!! Note "About stake size" - Using fixed stake size means it will be the amount used for the first order just like without position adjustment. - If you wish to buy additional orders with DCA then make sure to leave enough funds in the wallet for that. - Using 'unlimited' stake amount with DCA orders requires you to also implement custom_stake_amount callback to avoid allocating all funds to initial order. + Using fixed stake size means it will be the amount used for the first order, just like without position adjustment. + If you wish to buy additional orders with DCA, then make sure to leave enough funds in the wallet for that. + Using 'unlimited' stake amount with DCA orders requires you to also implement custom_stake_amount callback to avoid allocating all funds to the initial order. !!! Warning Stoploss is still calculated from the initial opening price, not averaged price. +!!! Warning "/stopbuy" + While `/stopbuy` command stops the bot from entering new trades, the position adjustment feature will continue buying new orders on existing trades. + !!! Warning "Backtesting" During backtesting this callback is called for each candle in `timeframe` or `timeframe_detail`, so performance will be affected. From 7cd844865661de5509ffda31c040081bc7fa3658 Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Wed, 12 Jan 2022 06:09:06 +0200 Subject: [PATCH 78/90] Fix documentation example. --- docs/strategy-callbacks.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index 1adb06ae7..fb4dc2594 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -642,7 +642,7 @@ class DigDeeperStrategy(IStrategy): return None # Obtain pair dataframe (just to show how to access it) - dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) + dataframe, _ = self.dp.get_analyzed_dataframe(trade.pair, self.timeframe) # Only buy when not actively falling price. last_candle = dataframe.iloc[-1].squeeze() previous_candle = dataframe.iloc[-2].squeeze() @@ -662,7 +662,7 @@ class DigDeeperStrategy(IStrategy): if 0 < count_of_buys <= self.max_dca_orders: try: # This returns max stakes for one trade - stake_amount = self.wallets.get_trade_stake_amount(pair, None) + stake_amount = self.wallets.get_trade_stake_amount(trade.pair, None) # This calculates base order size stake_amount = stake_amount / self.max_dca_multiplier # This then calculates current safety order size From 7344f88ad5bed492464950297af3a45772e06e70 Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Wed, 12 Jan 2022 09:21:52 +0200 Subject: [PATCH 79/90] Add a note about not being called when there's an open order. --- docs/strategy-callbacks.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index fb4dc2594..849eb50dc 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -579,6 +579,7 @@ The strategy is expected to return a stake_amount if and when an additional buy If there is not enough funds in the wallet then nothing will happen. Additional orders also mean additional fees and those orders don't count towards `max_open_trades`. This callback is called very frequently, so you must keep your implementation as fast as possible. +This callback is NOT called when there is an open order (either buy or sell) waiting for execution. !!! Note "About stake size" Using fixed stake size means it will be the amount used for the first order, just like without position adjustment. From faa35cb16737d3400d7e4be37281de08a40d805b Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 13 Jan 2022 17:18:07 +0100 Subject: [PATCH 80/90] Small minor fixes --- docs/strategy-callbacks.md | 4 ++-- freqtrade/freqtradebot.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index 849eb50dc..c9403265c 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -623,8 +623,8 @@ class DigDeeperStrategy(IStrategy): return proposed_stake / self.max_dca_multiplier def adjust_trade_position(self, trade: Trade, current_time: datetime, - current_rate: float, current_profit: float, min_stake: float, - max_stake: float, **kwargs): + current_rate: float, current_profit: float, min_stake: float, + max_stake: float, **kwargs): """ Custom trade adjustment logic, returning the stake amount that a trade should be increased. This means extra buy orders with additional fees. diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index a056adabe..ee5d1545c 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -181,7 +181,8 @@ class FreqtradeBot(LoggingMixin): # Check if we need to adjust our current positions before attempting to buy new trades. if self.strategy.position_adjustment_enable: - self.process_open_trade_positions() + with self._exit_lock: + self.process_open_trade_positions() # Then looking for buy opportunities if self.get_free_open_trades(): From 678be0b773c2606b1613d8b7d00ca89512b5ef1e Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Thu, 13 Jan 2022 20:16:45 +0200 Subject: [PATCH 81/90] Slightly move code. --- freqtrade/optimize/backtesting.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 04fa8854f..ced716d18 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -460,13 +460,6 @@ class Backtesting: def _enter_trade(self, pair: str, row: Tuple, stake_amount: Optional[float] = None, trade: Optional[LocalTrade] = None) -> Optional[LocalTrade]: - pos_adjust = trade is not None - if stake_amount is None: - try: - stake_amount = self.wallets.get_trade_stake_amount(pair, None) - except DependencyException: - return trade - # let's call the custom entry price, using the open price as default price propose_rate = strategy_safe_wrapper(self.strategy.custom_entry_price, default_retval=row[OPEN_IDX])( @@ -479,6 +472,13 @@ class Backtesting: min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, propose_rate, -0.05) or 0 max_stake_amount = self.wallets.get_available_stake_amount() + pos_adjust = trade is not None + if stake_amount is None: + try: + stake_amount = self.wallets.get_trade_stake_amount(pair, None) + except DependencyException: + return trade + if not pos_adjust: stake_amount = strategy_safe_wrapper(self.strategy.custom_stake_amount, default_retval=stake_amount)( From 13bc5c5d8fb7567402d02972e77d619186aa0b31 Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Thu, 13 Jan 2022 20:24:21 +0200 Subject: [PATCH 82/90] Fine, this does look better. --- freqtrade/optimize/backtesting.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index ced716d18..754b46d81 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -473,13 +473,12 @@ class Backtesting: max_stake_amount = self.wallets.get_available_stake_amount() pos_adjust = trade is not None - if stake_amount is None: + if not pos_adjust: try: stake_amount = self.wallets.get_trade_stake_amount(pair, None) except DependencyException: return trade - if not pos_adjust: stake_amount = strategy_safe_wrapper(self.strategy.custom_stake_amount, default_retval=stake_amount)( pair=pair, current_time=row[DATE_IDX].to_pydatetime(), current_rate=propose_rate, From ffe69535d89f93b3baa82ba9df9cf16713e350e3 Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Thu, 13 Jan 2022 20:31:03 +0200 Subject: [PATCH 83/90] These could be properties. --- freqtrade/freqtradebot.py | 5 +++-- freqtrade/persistence/models.py | 2 ++ tests/test_integration.py | 4 ++-- tests/test_persistence.py | 10 +++++----- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index ee5d1545c..32eefe7de 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -476,8 +476,9 @@ class FreqtradeBot(LoggingMixin): current_rate = self.exchange.get_rate(trade.pair, refresh=True, side="buy") current_profit = trade.calc_profit_ratio(current_rate) - # FIXME This is only here to lazyload orders. - trade.nr_of_successful_buys() + # TODO: Is there a better way to force lazy-load? + len(trade.orders) + min_stake_amount = self.exchange.get_min_pair_stake_amount(trade.pair, current_rate, self.strategy.stoploss) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 1bc4cfc05..60826cfd3 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -614,6 +614,7 @@ class LocalTrade(): else: return None + @property def nr_of_successful_buys(self) -> int: """ Helper function to count the number of buy orders that have been filled. @@ -623,6 +624,7 @@ class LocalTrade(): o.status in NON_OPEN_EXCHANGE_STATES and (o.filled or 0) > 0]) + @property def nr_of_successful_sells(self) -> int: """ Helper function to count the number of sell orders that have been filled. diff --git a/tests/test_integration.py b/tests/test_integration.py index e1cbf3d2d..356ea5c62 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -259,7 +259,7 @@ def test_dca_buying(default_conf_usdt, ticker_usdt, fee, mocker) -> None: assert trade.amount == trade.orders[0].amount + trade.orders[1].amount - assert trade.nr_of_successful_buys() == 2 + assert trade.nr_of_successful_buys == 2 # Sell patch_get_signal(freqtrade, value=(False, True, None, None)) @@ -273,4 +273,4 @@ def test_dca_buying(default_conf_usdt, ticker_usdt, fee, mocker) -> None: assert trade.orders[-1].side == 'sell' assert trade.orders[2].amount == trade.amount - assert trade.nr_of_successful_buys() == 2 + assert trade.nr_of_successful_buys == 2 diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 14988d85b..729a7f64c 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -1553,7 +1553,7 @@ def test_recalc_trade_from_orders_ignores_bad_orders(fee): assert trade.open_rate == o1_rate assert trade.fee_open_cost == o1_fee_cost assert trade.open_trade_value == o1_trade_val - assert trade.nr_of_successful_buys() == 1 + assert trade.nr_of_successful_buys == 1 order2 = Order( ft_order_side='buy', @@ -1580,7 +1580,7 @@ def test_recalc_trade_from_orders_ignores_bad_orders(fee): assert trade.open_rate == o1_rate assert trade.fee_open_cost == o1_fee_cost assert trade.open_trade_value == o1_trade_val - assert trade.nr_of_successful_buys() == 1 + assert trade.nr_of_successful_buys == 1 # Let's try with some other orders order3 = Order( @@ -1608,7 +1608,7 @@ def test_recalc_trade_from_orders_ignores_bad_orders(fee): assert trade.open_rate == o1_rate assert trade.fee_open_cost == o1_fee_cost assert trade.open_trade_value == o1_trade_val - assert trade.nr_of_successful_buys() == 1 + assert trade.nr_of_successful_buys == 1 order4 = Order( ft_order_side='buy', @@ -1635,7 +1635,7 @@ def test_recalc_trade_from_orders_ignores_bad_orders(fee): assert trade.open_rate == o1_rate assert trade.fee_open_cost == 2 * o1_fee_cost assert trade.open_trade_value == 2 * o1_trade_val - assert trade.nr_of_successful_buys() == 2 + assert trade.nr_of_successful_buys == 2 # Just to make sure sell orders are ignored, let's calculate one more time. sell1 = Order( @@ -1662,4 +1662,4 @@ def test_recalc_trade_from_orders_ignores_bad_orders(fee): assert trade.open_rate == o1_rate assert trade.fee_open_cost == 2 * o1_fee_cost assert trade.open_trade_value == 2 * o1_trade_val - assert trade.nr_of_successful_buys() == 2 + assert trade.nr_of_successful_buys == 2 From 7699fde3806a05bc317d4a7ac72c8fd7c76cac7e Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Thu, 13 Jan 2022 20:33:40 +0200 Subject: [PATCH 84/90] Update documentation about trade.nr_of_successful_buys --- docs/strategy-callbacks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index c9403265c..447f96dfe 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -650,7 +650,7 @@ class DigDeeperStrategy(IStrategy): if last_candle['close'] < previous_candle['close']: return None - count_of_buys = trade.nr_of_successful_buys() + count_of_buys = trade.nr_of_successful_buys # Allow up to 3 additional increasingly larger buys (4 in total) # Initial buy is 1x From 08cae6f0674e465dc653aa1bd1b648248d79a6c6 Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Thu, 13 Jan 2022 20:44:03 +0200 Subject: [PATCH 85/90] Fix horrible whitespace mistake. --- freqtrade/freqtradebot.py | 1 - 1 file changed, 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 32eefe7de..5cbd25eea 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -478,7 +478,6 @@ class FreqtradeBot(LoggingMixin): # TODO: Is there a better way to force lazy-load? len(trade.orders) - min_stake_amount = self.exchange.get_min_pair_stake_amount(trade.pair, current_rate, self.strategy.stoploss) From 320c9ccf9052c44c36b7ef894ff4de31fe35bd57 Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Fri, 14 Jan 2022 20:02:35 +0200 Subject: [PATCH 86/90] Unify functions and make it easy to get a list of filled buy orders --- freqtrade/persistence/models.py | 20 +++++++++----- tests/test_persistence.py | 46 +++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 6 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 60826cfd3..7d109ec7c 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -614,15 +614,25 @@ class LocalTrade(): else: return None + def select_filled_orders(self, order_side: str) -> List['Order']: + """ + Finds filled orders for this orderside. + :param order_side: Side of the order (either 'buy' or 'sell') + :return: array of Order objects + """ + return [o for o in self.orders if o.ft_order_side == order_side and + o.ft_is_open is False and + (o.filled or 0) > 0 and + o.status in NON_OPEN_EXCHANGE_STATES] + @property def nr_of_successful_buys(self) -> int: """ Helper function to count the number of buy orders that have been filled. :return: int count of buy orders that have been filled for this trade. """ - return len([o for o in self.orders if o.ft_order_side == 'buy' and - o.status in NON_OPEN_EXCHANGE_STATES and - (o.filled or 0) > 0]) + + return len(self.select_filled_orders('buy')) @property def nr_of_successful_sells(self) -> int: @@ -630,9 +640,7 @@ class LocalTrade(): Helper function to count the number of sell orders that have been filled. :return: int count of sell orders that have been filled for this trade. """ - return len([o for o in self.orders if o.ft_order_side == 'sell' and - o.status in NON_OPEN_EXCHANGE_STATES and - (o.filled or 0) > 0]) + return len(self.select_filled_orders('sell')) @staticmethod def get_trades_proxy(*, pair: str = None, is_open: bool = None, diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 729a7f64c..958f3c42e 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -1663,3 +1663,49 @@ def test_recalc_trade_from_orders_ignores_bad_orders(fee): assert trade.fee_open_cost == 2 * o1_fee_cost assert trade.open_trade_value == 2 * o1_trade_val assert trade.nr_of_successful_buys == 2 + + +@pytest.mark.usefixtures("init_persistence") +def test_select_filled_orders(fee): + create_mock_trades(fee) + + trades = Trade.get_trades().all() + + # Closed buy order, no sell order + orders = trades[0].select_filled_orders('buy') + assert orders is not None + assert len(orders) == 1 + order = orders[0] + assert order.amount > 0 + assert order.filled > 0 + assert order.side == 'buy' + assert order.ft_order_side == 'buy' + assert order.status == 'closed' + orders = trades[0].select_filled_orders('sell') + assert orders is not None + assert len(orders) == 0 + + # closed buy order, and closed sell order + orders = trades[1].select_filled_orders('buy') + assert orders is not None + assert len(orders) == 1 + + orders = trades[1].select_filled_orders('sell') + assert orders is not None + assert len(orders) == 1 + + # Has open buy order + orders = trades[3].select_filled_orders('buy') + assert orders is not None + assert len(orders) == 0 + orders = trades[3].select_filled_orders('sell') + assert orders is not None + assert len(orders) == 0 + + # Open sell order + orders = trades[4].select_filled_orders('buy') + assert orders is not None + assert len(orders) == 1 + orders = trades[4].select_filled_orders('sell') + assert orders is not None + assert len(orders) == 0 From 93adb436f818d4f5da949611079e4dfe9fc72fe5 Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Fri, 14 Jan 2022 20:25:29 +0200 Subject: [PATCH 87/90] Fix flake8 intention issue. --- freqtrade/persistence/models.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 7d109ec7c..8eed60304 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -621,9 +621,9 @@ class LocalTrade(): :return: array of Order objects """ return [o for o in self.orders if o.ft_order_side == order_side and - o.ft_is_open is False and - (o.filled or 0) > 0 and - o.status in NON_OPEN_EXCHANGE_STATES] + o.ft_is_open is False and + (o.filled or 0) > 0 and + o.status in NON_OPEN_EXCHANGE_STATES] @property def nr_of_successful_buys(self) -> int: From 9f9e2a8722aa86a8dde7921eac2f64a7d20ddf66 Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Fri, 14 Jan 2022 20:46:16 +0200 Subject: [PATCH 88/90] Remove references to wallets.get_trade_stake_amount --- docs/strategy-callbacks.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index 447f96dfe..fd8323fe7 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -650,8 +650,9 @@ class DigDeeperStrategy(IStrategy): if last_candle['close'] < previous_candle['close']: return None - count_of_buys = trade.nr_of_successful_buys - + filled_buys = trade.select_filled_orders('buy') + count_of_buys = len(filled_buys) + # Allow up to 3 additional increasingly larger buys (4 in total) # Initial buy is 1x # If that falls to -5% profit, we buy 1.25x more, average profit should increase to roughly -2.2% @@ -662,10 +663,8 @@ class DigDeeperStrategy(IStrategy): # Hope you have a deep wallet! if 0 < count_of_buys <= self.max_dca_orders: try: - # This returns max stakes for one trade - stake_amount = self.wallets.get_trade_stake_amount(trade.pair, None) - # This calculates base order size - stake_amount = stake_amount / self.max_dca_multiplier + # This returns first order stake size + stake_amount = filled_buys[0].stake_amount # This then calculates current safety order size stake_amount = stake_amount * (1 + (count_of_buys * 0.25)) return stake_amount From 6c0eef94bbd2eb9d63ccc51349cdbc9d069aa018 Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Fri, 14 Jan 2022 21:05:05 +0200 Subject: [PATCH 89/90] Fix typo. --- docs/strategy-callbacks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index fd8323fe7..e56526e99 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -664,7 +664,7 @@ class DigDeeperStrategy(IStrategy): if 0 < count_of_buys <= self.max_dca_orders: try: # This returns first order stake size - stake_amount = filled_buys[0].stake_amount + stake_amount = filled_buys[0].cost # This then calculates current safety order size stake_amount = stake_amount * (1 + (count_of_buys * 0.25)) return stake_amount From 66a479c26a0f1bb0fb582b54142c2a9b6c81f2aa Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 15 Jan 2022 15:11:13 +0100 Subject: [PATCH 90/90] Small doc improvements --- docs/bot-basics.md | 6 ++---- docs/strategy-callbacks.md | 16 +++++++++------- tests/test_persistence.py | 4 +--- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/docs/bot-basics.md b/docs/bot-basics.md index 92ecfc901..a9a2628f6 100644 --- a/docs/bot-basics.md +++ b/docs/bot-basics.md @@ -38,8 +38,7 @@ By default, loop runs every few seconds (`internals.process_throttle_secs`) and * Considers stoploss, ROI and sell-signal, `custom_sell()` and `custom_stoploss()`. * Determine sell-price based on `ask_strategy` configuration setting or by using the `custom_exit_price()` callback. * Before a sell order is placed, `confirm_trade_exit()` strategy callback is called. -* Check position adjustments for open trades if enabled. - * Call `adjust_trade_position()` strategy callback and place additional order if required. +* Check position adjustments for open trades if enabled by calling `adjust_trade_position()` and place additional order if required. * Check if trade-slots are still available (if `max_open_trades` is reached). * Verifies buy signal trying to enter new positions. * Determine buy-price based on `bid_strategy` configuration setting, or by using the `custom_entry_price()` callback. @@ -60,10 +59,9 @@ This loop will be repeated again and again until the bot is stopped. * Confirm trade buy / sell (calls `confirm_trade_entry()` and `confirm_trade_exit()` if implemented in the strategy). * Call `custom_entry_price()` (if implemented in the strategy) to determine entry price (Prices are moved to be within the opening candle). * Determine stake size by calling the `custom_stake_amount()` callback. + * Check position adjustments for open trades if enabled and call `adjust_trade_position()` to determine if an additional order is requested. * Call `custom_stoploss()` and `custom_sell()` to find custom exit points. * For sells based on sell-signal and custom-sell: Call `custom_exit_price()` to determine exit price (Prices are moved to be within the closing candle). - * Check position adjustments for open trades if enabled and call `adjust_trade_position()` determine additional order is required. - * Generate backtest report output !!! Note diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index e56526e99..6edc549a4 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -572,19 +572,21 @@ class AwesomeStrategy(IStrategy): ## Adjust trade position -The `position_adjustment_enable` strategy property enables the usage of `adjust_trade_position()` callback in strategy. +The `position_adjustment_enable` strategy property enables the usage of `adjust_trade_position()` callback in the strategy. For performance reasons, it's disabled by default and freqtrade will show a warning message on startup if enabled. `adjust_trade_position()` can be used to perform additional orders, for example to manage risk with DCA (Dollar Cost Averaging). -The strategy is expected to return a stake_amount if and when an additional buy order should be made (position is increased). -If there is not enough funds in the wallet then nothing will happen. -Additional orders also mean additional fees and those orders don't count towards `max_open_trades`. -This callback is called very frequently, so you must keep your implementation as fast as possible. -This callback is NOT called when there is an open order (either buy or sell) waiting for execution. + +The strategy is expected to return a stake_amount (in stake currency) between `min_stake` and `max_stake` if and when an additional buy order should be made (position is increased). +If there are not enough funds in the wallet (the return value is above `max_stake`) then the signal will be ignored. +Additional orders also result in additional fees and those orders don't count towards `max_open_trades`. + +This callback is **not** called when there is an open order (either buy or sell) waiting for execution. +`adjust_trade_position()` is called very frequently for the duration of a trade, so you must keep your implementation as performant as possible. !!! Note "About stake size" Using fixed stake size means it will be the amount used for the first order, just like without position adjustment. If you wish to buy additional orders with DCA, then make sure to leave enough funds in the wallet for that. - Using 'unlimited' stake amount with DCA orders requires you to also implement custom_stake_amount callback to avoid allocating all funds to the initial order. + Using 'unlimited' stake amount with DCA orders requires you to also implement the `custom_stake_amount()` callback to avoid allocating all funds to the initial order. !!! Warning Stoploss is still calculated from the initial opening price, not averaged price. diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 958f3c42e..d98238f6f 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -1498,8 +1498,6 @@ def test_recalc_trade_from_orders(fee): trade.orders.append(sell1) trade.recalc_trade_from_orders() - avg_price = (o1_cost + o2_cost + o3_cost) / (o1_amount + o2_amount + o3_amount) - assert trade.amount == o1_amount + o2_amount + o3_amount assert trade.stake_amount == o1_cost + o2_cost + o3_cost assert trade.open_rate == avg_price @@ -1582,7 +1580,7 @@ def test_recalc_trade_from_orders_ignores_bad_orders(fee): assert trade.open_trade_value == o1_trade_val assert trade.nr_of_successful_buys == 1 -# Let's try with some other orders + # Let's try with some other orders order3 = Order( ft_order_side='buy', ft_pair=trade.pair,