From ac433eebfe8a9931810c3240812715021a3bf669 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Fri, 18 Feb 2022 05:43:16 -0600 Subject: [PATCH 01/15] stoploss in freqtradebot leverage adjustment --- freqtrade/freqtradebot.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index abd38859b..4d0316bb8 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1065,7 +1065,11 @@ class FreqtradeBot(LoggingMixin): # If enter order is fulfilled but there is no stoploss, we add a stoploss on exchange if not stoploss_order: - stoploss = self.edge.stoploss(pair=trade.pair) if self.edge else self.strategy.stoploss + stoploss = ( + self.edge.stoploss(pair=trade.pair) + if self.edge else + self.strategy.stoploss / trade.leverage + ) if trade.is_short: stop_price = trade.open_rate * (1 - stoploss) else: From 78194559f4cc2319e8407688fda93b0d35237474 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 26 Feb 2022 08:07:01 -0600 Subject: [PATCH 02/15] persistence.adjust_stop_loss accounts for leverage --- freqtrade/persistence/models.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 18491d687..ae4265374 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -566,13 +566,14 @@ class LocalTrade(): # Don't modify if called with initial and nothing to do return + leverage = self.leverage or 1.0 if self.is_short: - new_loss = float(current_price * (1 + abs(stoploss))) + new_loss = float(current_price * (1 + abs(stoploss / leverage))) # If trading with leverage, don't set the stoploss below the liquidation price if self.isolated_liq: new_loss = min(self.isolated_liq, new_loss) else: - new_loss = float(current_price * (1 - abs(stoploss))) + new_loss = float(current_price * (1 - abs(stoploss / leverage))) # If trading with leverage, don't set the stoploss below the liquidation price if self.isolated_liq: new_loss = max(self.isolated_liq, new_loss) From 499e21517bb4e7fbddfc0e4a605420fb204fbe65 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 26 Feb 2022 08:33:57 -0600 Subject: [PATCH 03/15] test_persistence tests for stoploss with leverage adjustements --- tests/optimize/test_backtest_detail.py | 2 +- tests/test_persistence.py | 117 +++++++++++++++++++------ 2 files changed, 90 insertions(+), 29 deletions(-) diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index 0d3cdbf9f..ea95a500f 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -634,7 +634,7 @@ tc39 = BTContainer(data=[ [3, 5010, 5010, 4986, 5010, 6172, 0, 1], [4, 5010, 5010, 4855, 4995, 6172, 0, 0], # Triggers stoploss + sellsignal acted on [5, 4995, 4995, 4950, 4950, 6172, 0, 0]], - stop_loss=-0.01, roi={"0": 1}, profit_perc=0.002 * 5.0, use_sell_signal=True, + stop_loss=-0.05, roi={"0": 1}, profit_perc=0.002 * 5.0, use_sell_signal=True, leverage=5.0, trades=[BTrade(sell_reason=SellType.SELL_SIGNAL, open_tick=1, close_tick=4)] ) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index f7273950a..efba25550 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -1749,6 +1749,67 @@ def test_stoploss_reinitialization(default_conf, fee): assert trade_adj.initial_stop_loss_pct == -0.04 +def test_stoploss_reinitialization_leverage(default_conf, fee): + init_db(default_conf['db_url']) + trade = Trade( + pair='ADA/USDT', + stake_amount=30.0, + fee_open=fee.return_value, + open_date=arrow.utcnow().shift(hours=-2).datetime, + amount=30.0, + fee_close=fee.return_value, + exchange='binance', + open_rate=1, + max_rate=1, + leverage=5.0, + ) + + trade.adjust_stop_loss(trade.open_rate, 0.1, True) + assert trade.stop_loss == 0.98 + assert trade.stop_loss_pct == -0.1 + assert trade.initial_stop_loss == 0.98 + assert trade.initial_stop_loss_pct == -0.1 + Trade.query.session.add(trade) + + # Lower stoploss + Trade.stoploss_reinitialization(0.15) + + trades = Trade.get_open_trades() + assert len(trades) == 1 + trade_adj = trades[0] + assert trade_adj.stop_loss == 0.97 + assert trade_adj.stop_loss_pct == -0.15 + assert trade_adj.initial_stop_loss == 0.97 + assert trade_adj.initial_stop_loss_pct == -0.15 + + # Raise stoploss + Trade.stoploss_reinitialization(0.05) + + trades = Trade.get_open_trades() + assert len(trades) == 1 + trade_adj = trades[0] + assert trade_adj.stop_loss == 0.99 + assert trade_adj.stop_loss_pct == -0.05 + assert trade_adj.initial_stop_loss == 0.99 + assert trade_adj.initial_stop_loss_pct == -0.05 + + # Trailing stoploss (move stoplos up a bit) + trade.adjust_stop_loss(1.02, 0.05) + assert trade_adj.stop_loss == 1.0098 + assert trade_adj.initial_stop_loss == 0.99 + + Trade.stoploss_reinitialization(0.05) + + trades = Trade.get_open_trades() + assert len(trades) == 1 + trade_adj = trades[0] + # Stoploss should not change in this case. + assert trade_adj.stop_loss == 1.0098 + assert trade_adj.stop_loss_pct == -0.05 + assert trade_adj.initial_stop_loss == 0.99 + assert trade_adj.initial_stop_loss_pct == -0.05 + + def test_stoploss_reinitialization_short(default_conf, fee): init_db(default_conf['db_url']) trade = Trade( @@ -1762,50 +1823,50 @@ def test_stoploss_reinitialization_short(default_conf, fee): open_rate=1, max_rate=1, is_short=True, - leverage=3.0, + leverage=5.0, ) - trade.adjust_stop_loss(trade.open_rate, -0.05, True) - assert trade.stop_loss == 1.05 - assert trade.stop_loss_pct == 0.05 - assert trade.initial_stop_loss == 1.05 - assert trade.initial_stop_loss_pct == 0.05 + trade.adjust_stop_loss(trade.open_rate, -0.1, True) + assert trade.stop_loss == 1.02 + assert trade.stop_loss_pct == 0.1 + assert trade.initial_stop_loss == 1.02 + assert trade.initial_stop_loss_pct == 0.1 Trade.query.session.add(trade) # Lower stoploss - Trade.stoploss_reinitialization(-0.06) + Trade.stoploss_reinitialization(-0.15) trades = Trade.get_open_trades() assert len(trades) == 1 trade_adj = trades[0] - assert trade_adj.stop_loss == 1.06 - assert trade_adj.stop_loss_pct == 0.06 - assert trade_adj.initial_stop_loss == 1.06 - assert trade_adj.initial_stop_loss_pct == 0.06 + assert trade_adj.stop_loss == 1.03 + assert trade_adj.stop_loss_pct == 0.15 + assert trade_adj.initial_stop_loss == 1.03 + assert trade_adj.initial_stop_loss_pct == 0.15 # Raise stoploss - Trade.stoploss_reinitialization(-0.04) + Trade.stoploss_reinitialization(-0.05) trades = Trade.get_open_trades() assert len(trades) == 1 trade_adj = trades[0] - assert trade_adj.stop_loss == 1.04 - assert trade_adj.stop_loss_pct == 0.04 - assert trade_adj.initial_stop_loss == 1.04 - assert trade_adj.initial_stop_loss_pct == 0.04 + assert trade_adj.stop_loss == 1.01 + assert trade_adj.stop_loss_pct == 0.05 + assert trade_adj.initial_stop_loss == 1.01 + assert trade_adj.initial_stop_loss_pct == 0.05 # Trailing stoploss - trade.adjust_stop_loss(0.98, -0.04) - assert trade_adj.stop_loss == 1.0192 - assert trade_adj.initial_stop_loss == 1.04 - Trade.stoploss_reinitialization(-0.04) + trade.adjust_stop_loss(0.98, -0.05) + assert trade_adj.stop_loss == 0.9898 + assert trade_adj.initial_stop_loss == 1.01 + Trade.stoploss_reinitialization(-0.05) trades = Trade.get_open_trades() assert len(trades) == 1 trade_adj = trades[0] # Stoploss should not change in this case. - assert trade_adj.stop_loss == 1.0192 - assert trade_adj.stop_loss_pct == 0.04 - assert trade_adj.initial_stop_loss == 1.04 - assert trade_adj.initial_stop_loss_pct == 0.04 + assert trade_adj.stop_loss == 0.9898 + assert trade_adj.stop_loss_pct == 0.05 + assert trade_adj.initial_stop_loss == 1.01 + assert trade_adj.initial_stop_loss_pct == 0.05 # Stoploss can't go above liquidation price - trade_adj.set_isolated_liq(1.0) - trade.adjust_stop_loss(0.97, -0.04) - assert trade_adj.stop_loss == 1.0 - assert trade_adj.stop_loss == 1.0 + trade_adj.set_isolated_liq(0.985) + trade.adjust_stop_loss(0.9799, -0.05) + assert trade_adj.stop_loss == 0.985 + assert trade_adj.stop_loss == 0.985 def test_update_fee(fee): From b363940baf1a6e15ed10dd13fceef428724be331 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 26 Feb 2022 08:42:18 -0600 Subject: [PATCH 04/15] Add TODO-lev comment in test_handle_stoploss_on_exchange --- tests/test_freqtradebot.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index d6930bc24..7b77375b6 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1128,6 +1128,9 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_ assert trade.is_open is False caplog.clear() + # TODO-lev: Test 2 identical orders but one with leverage, 1 without, and test that the + # leveraged trade is hit, but the other trade is not + mocker.patch( 'freqtrade.exchange.Binance.stoploss', side_effect=ExchangeError() From 7b9880035b9aaf452c626c1700746baf09f31138 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 27 Feb 2022 15:11:09 +0100 Subject: [PATCH 05/15] Remove wrong TODO-lev comment --- tests/test_freqtradebot.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 7b77375b6..d6930bc24 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1128,9 +1128,6 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_ assert trade.is_open is False caplog.clear() - # TODO-lev: Test 2 identical orders but one with leverage, 1 without, and test that the - # leveraged trade is hit, but the other trade is not - mocker.patch( 'freqtrade.exchange.Binance.stoploss', side_effect=ExchangeError() From bcfa73d492e3c150f0b909df58eb2c59ce6a15a6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 13 Feb 2022 15:10:09 +0100 Subject: [PATCH 06/15] Add "nr_of_successfull_entries" --- docs/strategy-callbacks.md | 6 ++++-- freqtrade/freqtradebot.py | 2 +- freqtrade/optimize/backtesting.py | 12 ++++++------ freqtrade/persistence/models.py | 21 +++++++++++++++++++++ freqtrade/rpc/rpc.py | 8 ++++---- tests/test_persistence.py | 2 ++ 6 files changed, 38 insertions(+), 13 deletions(-) diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index 9e0b33ab1..5cc7b9776 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -593,6 +593,8 @@ Additional orders also result in additional fees and those orders don't count to This callback is **not** called when there is an open order (either buy or sell) waiting for execution, or when you have reached the maximum amount of extra buys that you have set on `max_entry_position_adjustment`. `adjust_trade_position()` is called very frequently for the duration of a trade, so you must keep your implementation as performant as possible. +Position adjustments will always be applied in the direction of the trade, so a positive value will always increase your position, no matter if it's a long or short trade. + !!! 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. @@ -663,7 +665,7 @@ class DigDeeperStrategy(IStrategy): return None filled_buys = trade.select_filled_orders('buy') - count_of_buys = trade.nr_of_successful_buys + count_of_entries = trade.nr_of_successful_entries # 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% @@ -676,7 +678,7 @@ class DigDeeperStrategy(IStrategy): # This returns first order stake size stake_amount = filled_buys[0].cost # This then calculates current safety order size - stake_amount = stake_amount * (1 + (count_of_buys * 0.25)) + stake_amount = stake_amount * (1 + (count_of_entries * 0.25)) return stake_amount except Exception as exception: return None diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 4d0316bb8..20565120c 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -510,7 +510,7 @@ class FreqtradeBot(LoggingMixin): """ # TODO-lev: Check what changes are necessary for DCA in relation to shorts. if self.strategy.max_entry_position_adjustment > -1: - count_of_buys = trade.nr_of_successful_buys + count_of_buys = trade.nr_of_successful_entries if count_of_buys > self.strategy.max_entry_position_adjustment: logger.debug(f"Max adjustment entries for {trade.pair} has been reached.") return diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 0e3a70a93..6716c0133 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -464,11 +464,11 @@ class Backtesting: # Check if we need to adjust our current positions if self.strategy.position_adjustment_enable: - check_adjust_buy = True + check_adjust_entry = True if self.strategy.max_entry_position_adjustment > -1: - count_of_buys = trade.nr_of_successful_buys - check_adjust_buy = (count_of_buys <= self.strategy.max_entry_position_adjustment) - if check_adjust_buy: + entry_count = trade.nr_of_successful_entries + check_adjust_entry = (entry_count <= self.strategy.max_entry_position_adjustment) + if check_adjust_entry: trade = self._get_adjust_trade_entry_for_candle(trade, sell_row) sell_candle_time: datetime = sell_row[DATE_IDX].to_pydatetime() @@ -729,7 +729,7 @@ class Backtesting: for pair in open_trades.keys(): if len(open_trades[pair]) > 0: for trade in open_trades[pair]: - if trade.open_order_id and trade.nr_of_successful_buys == 0: + if trade.open_order_id and trade.nr_of_successful_entries == 0: # Ignore trade if buy-order did not fill yet continue sell_row = data[pair][-1] @@ -782,7 +782,7 @@ class Backtesting: if timedout: if order.side == 'buy': self.timedout_entry_orders += 1 - if trade.nr_of_successful_buys == 0: + if trade.nr_of_successful_entries == 0: # Remove trade due to buy timeout expiration. return True else: diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index ae4265374..fd73de49b 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -889,6 +889,8 @@ class LocalTrade(): total_stake += tmp_price * tmp_amount if total_amount > 0: + # TODO-lev: This should update leverage as well - + # as averaged trades might have different leverage self.open_rate = total_stake / total_amount self.stake_amount = total_stake self.amount = total_amount @@ -936,10 +938,28 @@ class LocalTrade(): (o.filled or 0) > 0 and o.status in NON_OPEN_EXCHANGE_STATES] + @property + def nr_of_successful_entries(self) -> int: + """ + Helper function to count the number of entry orders that have been filled. + :return: int count of entry orders that have been filled for this trade. + """ + + return len(self.select_filled_orders(self.enter_side)) + + @property + def nr_of_successful_exits(self) -> int: + """ + Helper function to count the number of exit orders that have been filled. + :return: int count of exit orders that have been filled for this trade. + """ + return len(self.select_filled_orders(self.exit_side)) + @property def nr_of_successful_buys(self) -> int: """ Helper function to count the number of buy orders that have been filled. + WARNING: Please use nr_of_successful_entries for short support. :return: int count of buy orders that have been filled for this trade. """ @@ -949,6 +969,7 @@ class LocalTrade(): def nr_of_successful_sells(self) -> int: """ Helper function to count the number of sell orders that have been filled. + WARNING: Please use nr_of_successful_exits for short support. :return: int count of sell orders that have been filled for this trade. """ return len(self.select_filled_orders('sell')) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 9b780d88d..1c73160a4 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -261,11 +261,11 @@ class RPC: profit_str ] if self._config.get('position_adjustment_enable', False): - max_buy_str = '' + max_entry_str = '' if self._config.get('max_entry_position_adjustment', -1) > 0: - max_buy_str = f"/{self._config['max_entry_position_adjustment'] + 1}" - filled_buys = trade.nr_of_successful_buys - detail_trade.append(f"{filled_buys}{max_buy_str}") + max_entry_str = f"/{self._config['max_entry_position_adjustment'] + 1}" + filled_entries = trade.nr_of_successful_entries + detail_trade.append(f"{filled_entries}{max_entry_str}") trades_list.append(detail_trade) profitcol = "Profit" if self._fiat_converter: diff --git a/tests/test_persistence.py b/tests/test_persistence.py index efba25550..2ceff216b 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -2418,6 +2418,7 @@ def test_recalc_trade_from_orders_ignores_bad_orders(fee): 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_entries == 1 order2 = Order( ft_order_side='buy', @@ -2554,6 +2555,7 @@ def test_recalc_trade_from_orders_ignores_bad_orders(fee): assert trade.fee_open_cost == 3 * o1_fee_cost assert trade.open_trade_value == 3 * o1_trade_val assert trade.nr_of_successful_buys == 3 + assert trade.nr_of_successful_entries == 3 @pytest.mark.usefixtures("init_persistence") From eed516a5c6f19bdb8ed54c50cd8338af4ed3bd02 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 13 Feb 2022 15:29:48 +0100 Subject: [PATCH 07/15] Update DCA logic to some extend --- freqtrade/persistence/models.py | 2 +- tests/test_persistence.py | 48 ++++++++++++++++++--------------- 2 files changed, 27 insertions(+), 23 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index fd73de49b..c736ff191 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -867,7 +867,7 @@ class LocalTrade(): def recalc_trade_from_orders(self): # We need at least 2 entry orders for averaging amounts and rates. - if len(self.select_filled_orders('buy')) < 2: + if len(self.select_filled_orders(self.enter_side)) < 2: # Just in case, still recalc open trade value self.recalc_open_trade_value() return diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 2ceff216b..fa135dfbb 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -2371,13 +2371,17 @@ def test_recalc_trade_from_orders(fee): 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): +@pytest.mark.parametrize('is_short', [True, False]) +# TODO-lev: this should also check with different leverages per entry order! +def test_recalc_trade_from_orders_ignores_bad_orders(fee, is_short): 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 + o1_trade_val = o1_cost - o1_fee_cost if is_short else o1_cost + o1_fee_cost + enter_side = "sell" if is_short else "buy" + exit_side = "buy" if is_short else "sell" trade = Trade( pair='ADA/USDT', @@ -2389,17 +2393,18 @@ def test_recalc_trade_from_orders_ignores_bad_orders(fee): exchange='binance', open_rate=o1_rate, max_rate=o1_rate, + is_short=is_short, ) - trade.update_fee(o1_fee_cost, 'BNB', fee.return_value, 'buy') + trade.update_fee(o1_fee_cost, 'BNB', fee.return_value, enter_side) # Check with 1 order order1 = Order( - ft_order_side='buy', + ft_order_side=enter_side, ft_pair=trade.pair, ft_is_open=False, status="closed", symbol=trade.pair, order_type="market", - side="buy", + side=enter_side, price=o1_rate, average=o1_rate, filled=o1_amount, @@ -2417,17 +2422,16 @@ 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_entries == 1 order2 = Order( - ft_order_side='buy', + ft_order_side=enter_side, ft_pair=trade.pair, ft_is_open=True, status="open", symbol=trade.pair, order_type="market", - side="buy", + side=enter_side, price=o1_rate, average=o1_rate, filled=o1_amount, @@ -2445,17 +2449,17 @@ 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_entries == 1 # Let's try with some other orders order3 = Order( - ft_order_side='buy', + ft_order_side=enter_side, ft_pair=trade.pair, ft_is_open=False, status="cancelled", symbol=trade.pair, order_type="market", - side="buy", + side=enter_side, price=1, average=2, filled=0, @@ -2473,16 +2477,16 @@ 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_entries == 1 order4 = Order( - ft_order_side='buy', + ft_order_side=enter_side, ft_pair=trade.pair, ft_is_open=False, status="closed", symbol=trade.pair, order_type="market", - side="buy", + side=enter_side, price=o1_rate, average=o1_rate, filled=o1_amount, @@ -2500,17 +2504,17 @@ 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_entries == 2 - # Just to make sure sell orders are ignored, let's calculate one more time. + # Just to make sure exit orders are ignored, let's calculate one more time. sell1 = Order( - ft_order_side='sell', + ft_order_side=exit_side, ft_pair=trade.pair, ft_is_open=False, status="closed", symbol=trade.pair, order_type="market", - side="sell", + side=exit_side, price=4, average=3, filled=2, @@ -2527,16 +2531,17 @@ 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_entries == 2 + # Check with 1 order order_noavg = Order( - ft_order_side='buy', + ft_order_side=enter_side, ft_pair=trade.pair, ft_is_open=False, status="closed", symbol=trade.pair, order_type="market", - side="buy", + side=enter_side, price=o1_rate, average=None, filled=o1_amount, @@ -2554,7 +2559,6 @@ def test_recalc_trade_from_orders_ignores_bad_orders(fee): assert trade.open_rate == o1_rate assert trade.fee_open_cost == 3 * o1_fee_cost assert trade.open_trade_value == 3 * o1_trade_val - assert trade.nr_of_successful_buys == 3 assert trade.nr_of_successful_entries == 3 From f0f5a509750d4ef24e8f1bffb3339e24af02caa1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 13 Feb 2022 16:01:44 +0100 Subject: [PATCH 08/15] Update tests to test DCA for shorts --- docs/strategy-callbacks.md | 2 +- freqtrade/freqtradebot.py | 2 +- tests/strategy/strats/strategy_test_v3.py | 2 +- tests/test_integration.py | 79 ++++++++++++++++++++++- 4 files changed, 80 insertions(+), 5 deletions(-) diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index 5cc7b9776..24dad1f56 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -593,7 +593,7 @@ Additional orders also result in additional fees and those orders don't count to This callback is **not** called when there is an open order (either buy or sell) waiting for execution, or when you have reached the maximum amount of extra buys that you have set on `max_entry_position_adjustment`. `adjust_trade_position()` is called very frequently for the duration of a trade, so you must keep your implementation as performant as possible. -Position adjustments will always be applied in the direction of the trade, so a positive value will always increase your position, no matter if it's a long or short trade. +Position adjustments will always be applied in the direction of the trade, so a positive value will always increase your position, no matter if it's a long or short trade. !!! Note "About stake size" Using fixed stake size means it will be the amount used for the first order, just like without position adjustment. diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 20565120c..554837860 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -533,7 +533,7 @@ class FreqtradeBot(LoggingMixin): 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) + self.execute_entry(trade.pair, stake_amount, trade=trade, is_short=trade.is_short) if stake_amount is not None and stake_amount < 0.0: # We should decrease our position diff --git a/tests/strategy/strats/strategy_test_v3.py b/tests/strategy/strats/strategy_test_v3.py index a056b316c..0b73c1271 100644 --- a/tests/strategy/strats/strategy_test_v3.py +++ b/tests/strategy/strats/strategy_test_v3.py @@ -183,7 +183,7 @@ class StrategyTestV3(IStrategy): current_profit: float, min_stake: float, max_stake: float, **kwargs): if current_profit < -0.0075: - orders = trade.select_filled_orders('buy') + orders = trade.select_filled_orders(trade.enter_side) return round(orders[0].cost, 0) return None diff --git a/tests/test_integration.py b/tests/test_integration.py index bb14aa03d..846d7a5db 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -214,6 +214,7 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, mocker, balance_rati def test_dca_buying(default_conf_usdt, ticker_usdt, fee, mocker) -> None: + # TODO-lev: this should also check with different leverages per entry order! default_conf_usdt['position_adjustment_enable'] = True freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) @@ -231,13 +232,13 @@ def test_dca_buying(default_conf_usdt, ticker_usdt, fee, mocker) -> None: assert len(Trade.get_trades().all()) == 1 trade = Trade.get_trades().first() assert len(trade.orders) == 1 - assert trade.stake_amount == 60 + assert pytest.approx(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 + assert pytest.approx(trade.stake_amount) == 60 # Reduce bid amount ticker_usdt_modif = ticker_usdt.return_value @@ -266,6 +267,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_entries == 2 # Sell patch_get_signal(freqtrade, enter_long=False, exit_long=True) @@ -280,3 +282,76 @@ def test_dca_buying(default_conf_usdt, ticker_usdt, fee, mocker) -> None: assert trade.orders[2].amount == trade.amount assert trade.nr_of_successful_buys == 2 + assert trade.nr_of_successful_entries == 2 + + +def test_dca_short(default_conf_usdt, ticker_usdt, fee, mocker) -> None: + # TODO-lev: this should also check with different leverages per entry order! + 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, enter_long=False, enter_short=True) + freqtrade.enter_positions() + + assert len(Trade.get_trades().all()) == 1 + trade = Trade.get_trades().first() + assert len(trade.orders) == 1 + assert pytest.approx(trade.stake_amount) == 60 + assert trade.open_rate == 2.02 + # No adjustment + freqtrade.process() + trade = Trade.get_trades().first() + assert len(trade.orders) == 1 + assert pytest.approx(trade.stake_amount) == 60 + + # Reduce bid amount + ticker_usdt_modif = ticker_usdt.return_value + ticker_usdt_modif['ask'] = ticker_usdt_modif['ask'] * 1.015 + ticker_usdt_modif['bid'] = ticker_usdt_modif['bid'] * 1.0125 + 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 + for o in trade.orders: + assert o.status == "closed" + assert pytest.approx(trade.stake_amount) == 120 + + # Open-rate averaged between 2.0 and 2.0 * 1.015 + assert trade.open_rate >= 2.02 + assert trade.open_rate < 2.02 * 1.015 + + # 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 pytest.approx(trade.stake_amount) == 120 + # assert trade.orders[0].amount == 30 + assert trade.orders[1].amount == 60 / ticker_usdt_modif['ask'] + + assert trade.amount == trade.orders[0].amount + trade.orders[1].amount + assert trade.nr_of_successful_entries == 2 + + # Buy + patch_get_signal(freqtrade, enter_long=False, exit_short=True) + freqtrade.process() + trade = Trade.get_trades().first() + assert trade.is_open is False + # assert trade.orders[0].amount == 30 + assert trade.orders[0].side == 'sell' + assert trade.orders[1].amount == 60 / ticker_usdt_modif['ask'] + # Sold everything + assert trade.orders[-1].side == 'buy' + assert trade.orders[2].amount == trade.amount + + assert trade.nr_of_successful_entries == 2 + assert trade.nr_of_successful_exits == 1 From 536f54cfc6ff8d5576921fc5f2d982956b2aa845 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 13 Feb 2022 16:28:49 +0100 Subject: [PATCH 09/15] is_short for forceentries --- 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 1c73160a4..3bb7e7a6d 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -769,8 +769,10 @@ class RPC: # check if valid pair # check if pair already has an open pair - trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first() + trade: Trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first() + is_short = (order_side == SignalDirection.SHORT) if trade: + is_short = trade.is_short if not self._freqtrade.strategy.position_adjustment_enable: raise RPCException(f'position for {pair} already open - id: {trade.id}') @@ -784,7 +786,7 @@ class RPC: 'forcebuy', self._freqtrade.strategy.order_types['buy']) if self._freqtrade.execute_entry(pair, stake_amount, price, ordertype=order_type, trade=trade, - is_short=(order_side == SignalDirection.SHORT), + is_short=is_short, enter_tag=enter_tag, ): Trade.commit() From 1b6548c8d883896bbd315ecf5bc9646169834474 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 21 Feb 2022 19:27:38 +0100 Subject: [PATCH 10/15] Don't modify leverage through DCA --- freqtrade/freqtradebot.py | 27 +++++++++++++++------------ freqtrade/optimize/backtesting.py | 25 ++++++++++++++----------- freqtrade/persistence/models.py | 3 +-- 3 files changed, 30 insertions(+), 25 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 554837860..909fa61ba 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -643,18 +643,21 @@ class FreqtradeBot(LoggingMixin): if not stake_amount: return False - - max_leverage = self.exchange.get_max_leverage(pair, stake_amount) - leverage = strategy_safe_wrapper(self.strategy.leverage, default_retval=1.0)( - pair=pair, - current_time=datetime.now(timezone.utc), - current_rate=enter_limit_requested, - proposed_leverage=1.0, - max_leverage=max_leverage, - side=trade_side, - ) if self.trading_mode != TradingMode.SPOT else 1.0 - # Cap leverage between 1.0 and max_leverage. - leverage = min(max(leverage, 1.0), max_leverage) + if not pos_adjust: + max_leverage = self.exchange.get_max_leverage(pair, stake_amount) + leverage = strategy_safe_wrapper(self.strategy.leverage, default_retval=1.0)( + pair=pair, + current_time=datetime.now(timezone.utc), + current_rate=enter_limit_requested, + proposed_leverage=1.0, + max_leverage=max_leverage, + side=trade_side, + ) if self.trading_mode != TradingMode.SPOT else 1.0 + # Cap leverage between 1.0 and max_leverage. + leverage = min(max(leverage, 1.0), max_leverage) + else: + # Changing leverage currently not possible + leverage = trade.leverage if trade else 1.0 if pos_adjust: logger.info(f"Position adjust: about to create a new order for {pair} with stake: " f"{stake_amount} for {trade}") diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 6716c0133..4e881d271 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -639,17 +639,20 @@ class Backtesting: # If not pos adjust, trade is None return trade - max_leverage = self.exchange.get_max_leverage(pair, stake_amount) - leverage = strategy_safe_wrapper(self.strategy.leverage, default_retval=1.0)( - pair=pair, - current_time=current_time, - current_rate=row[OPEN_IDX], - proposed_leverage=1.0, - max_leverage=max_leverage, - side=direction, - ) if self._can_short else 1.0 - # Cap leverage between 1.0 and max_leverage. - leverage = min(max(leverage, 1.0), max_leverage) + if not pos_adjust: + max_leverage = self.exchange.get_max_leverage(pair, stake_amount) + leverage = strategy_safe_wrapper(self.strategy.leverage, default_retval=1.0)( + pair=pair, + current_time=current_time, + current_rate=row[OPEN_IDX], + proposed_leverage=1.0, + max_leverage=max_leverage, + side=direction, + ) if self._can_short else 1.0 + # Cap leverage between 1.0 and max_leverage. + leverage = min(max(leverage, 1.0), max_leverage) + else: + leverage = trade.leverage if trade else 1.0 order_type = self.strategy.order_types['buy'] time_in_force = self.strategy.order_time_in_force['buy'] diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index c736ff191..de3ed3154 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -889,8 +889,7 @@ class LocalTrade(): total_stake += tmp_price * tmp_amount if total_amount > 0: - # TODO-lev: This should update leverage as well - - # as averaged trades might have different leverage + # Leverage not updated, as we don't allow changing leverage through DCA at the moment. self.open_rate = total_stake / total_amount self.stake_amount = total_stake self.amount = total_amount From 5c2cca50e5dc58cfac49817967145eb760179249 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 23 Feb 2022 20:09:15 +0100 Subject: [PATCH 11/15] Minor updates, document no leverage changes --- docs/strategy-callbacks.md | 2 +- tests/test_integration.py | 2 -- tests/test_persistence.py | 1 - 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index 24dad1f56..fb7bea5f3 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -593,7 +593,7 @@ Additional orders also result in additional fees and those orders don't count to This callback is **not** called when there is an open order (either buy or sell) waiting for execution, or when you have reached the maximum amount of extra buys that you have set on `max_entry_position_adjustment`. `adjust_trade_position()` is called very frequently for the duration of a trade, so you must keep your implementation as performant as possible. -Position adjustments will always be applied in the direction of the trade, so a positive value will always increase your position, no matter if it's a long or short trade. +Position adjustments will always be applied in the direction of the trade, so a positive value will always increase your position, no matter if it's a long or short trade. Modifications to leverage are not 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. diff --git a/tests/test_integration.py b/tests/test_integration.py index 846d7a5db..70ee1c52c 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -214,7 +214,6 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, mocker, balance_rati def test_dca_buying(default_conf_usdt, ticker_usdt, fee, mocker) -> None: - # TODO-lev: this should also check with different leverages per entry order! default_conf_usdt['position_adjustment_enable'] = True freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) @@ -286,7 +285,6 @@ def test_dca_buying(default_conf_usdt, ticker_usdt, fee, mocker) -> None: def test_dca_short(default_conf_usdt, ticker_usdt, fee, mocker) -> None: - # TODO-lev: this should also check with different leverages per entry order! default_conf_usdt['position_adjustment_enable'] = True freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index fa135dfbb..c287bf4fd 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -2372,7 +2372,6 @@ def test_recalc_trade_from_orders(fee): @pytest.mark.parametrize('is_short', [True, False]) -# TODO-lev: this should also check with different leverages per entry order! def test_recalc_trade_from_orders_ignores_bad_orders(fee, is_short): o1_amount = 100 From 33be14e7e22d9687235e85769a6be544afc05c00 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 27 Feb 2022 17:09:29 +0100 Subject: [PATCH 12/15] Update stake_amount calculation with multiple entries when using leverage --- freqtrade/persistence/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index de3ed3154..8d85c775b 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -891,7 +891,7 @@ class LocalTrade(): if total_amount > 0: # Leverage not updated, as we don't allow changing leverage through DCA at the moment. self.open_rate = total_stake / total_amount - self.stake_amount = total_stake + self.stake_amount = total_stake / (self.leverage or 1.0) self.amount = total_amount self.fee_open_cost = self.fee_open * self.stake_amount self.recalc_open_trade_value() From 437b12fab716597a30eecfdbd5e8be367b1b9ca4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 27 Feb 2022 17:15:33 +0100 Subject: [PATCH 13/15] Use trade.* props where possible --- freqtrade/freqtradebot.py | 6 +++--- freqtrade/rpc/rpc.py | 7 +++---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 909fa61ba..9cdf580e3 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1599,8 +1599,8 @@ class FreqtradeBot(LoggingMixin): Trade.commit() if order['status'] in constants.NON_OPEN_EXCHANGE_STATES: - # If a buy order was closed, force update on stoploss on exchange - if order.get('side', None) == 'buy': + # If a entry order was closed, force update on stoploss on exchange + if order.get('side', None) == trade.enter_side: trade = self.cancel_stoploss_on_exchange(trade) # Updating wallets when order is closed self.wallets.update() @@ -1610,7 +1610,7 @@ class FreqtradeBot(LoggingMixin): self._notify_exit(trade, '', True) self.handle_protections(trade.pair) elif send_msg and not trade.open_order_id: - # Buy fill + # Enter fill self._notify_enter(trade, order, fill=True) return False diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 3bb7e7a6d..98207bfea 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -696,19 +696,18 @@ class RPC: if trade.open_order_id: order = self._freqtrade.exchange.fetch_order(trade.open_order_id, trade.pair) - if order['side'] == 'buy': + if order['side'] == trade.enter_side: fully_canceled = self._freqtrade.handle_cancel_enter( trade, order, CANCEL_REASON['FORCE_SELL']) - if order['side'] == 'sell': + if order['side'] == trade.exit_side: # Cancel order - so it is placed anew with a fresh price. self._freqtrade.handle_cancel_exit(trade, order, CANCEL_REASON['FORCE_SELL']) if not fully_canceled: # Get current rate and execute sell - closing_side = "buy" if trade.is_short else "sell" current_rate = self._freqtrade.exchange.get_rate( - trade.pair, refresh=False, side=closing_side) + trade.pair, refresh=False, side=trade.exit_side) sell_reason = SellCheckTuple(sell_type=SellType.FORCE_SELL) order_type = ordertype or self._freqtrade.strategy.order_types.get( "forcesell", self._freqtrade.strategy.order_types["sell"]) From 7948224892255fd3c93b6e1df9111a59474007de Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 27 Feb 2022 21:14:28 +0100 Subject: [PATCH 14/15] leverage_prep should also becalled after filling a entry ordre --- freqtrade/freqtradebot.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 9cdf580e3..45b9e75da 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -103,7 +103,6 @@ class FreqtradeBot(LoggingMixin): self._exit_lock = Lock() LoggingMixin.__init__(self, logger, timeframe_to_seconds(self.strategy.timeframe)) - self.liquidation_buffer = float(self.config.get('liquidation_buffer', '0.05')) self.trading_mode: TradingMode = self.config.get('trading_mode', TradingMode.SPOT) self.margin_mode_type: Optional[MarginMode] = None if 'margin_mode' in self.config: @@ -1602,6 +1601,16 @@ class FreqtradeBot(LoggingMixin): # If a entry order was closed, force update on stoploss on exchange if order.get('side', None) == trade.enter_side: trade = self.cancel_stoploss_on_exchange(trade) + # TODO: Margin will need to use interest_rate as well. + _, isolated_liq = self.leverage_prep( + leverage=trade.leverage, + pair=trade.pair, + amount=trade.amount, + open_rate=trade.open_rate, + is_short=trade.is_short + ) + if isolated_liq: + trade.set_isolated_liq(isolated_liq) # Updating wallets when order is closed self.wallets.update() From bc922254419d5f5164b2ff4cc629901447edb6d6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 28 Feb 2022 19:23:14 +0100 Subject: [PATCH 15/15] Add todo about leverage_prep --- freqtrade/freqtradebot.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 45b9e75da..0026667d3 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -726,6 +726,7 @@ class FreqtradeBot(LoggingMixin): amount = safe_value_fallback(order, 'filled', 'amount') enter_limit_filled_price = safe_value_fallback(order, 'average', 'price') + # TODO: this might be unnecessary, as we're calling it in update_trade_state. interest_rate, isolated_liq = self.leverage_prep( leverage=leverage, pair=pair,