Repository: freqtrade/freqtrade

Issue:
adjust_trade_position()'s stake_amount handled differently between backtesting and dry-run/live while using leverage?
## Describe your environment

  * Operating system: Docker's value -> Debian GNU/Linux 11 (bullseye)
  * Python Version: Docker's value -> 3.11.4
  * CCXT version: Docker's value -> 3.1.44
  * Freqtrade Version: Docker's value -> 2023.6
  
## Your question

I am dry-running  and backtesting a strategy with `position_adjustment_enable = True`. I found out that `stake_amount` returned from  `adjust_trade_position()` during *dry-run* has become very high, even higher than wallet balance. Eventually the bot used whole wallet balance to increase the position.

However, during *backtesting* the same pair and timerange, this isn't the behaviour. The bot calculates the `stake_amount` as per intended. Here's a real life example (reproducible code below):

My DCA calculation is:
```
stake_amount = 2 * previous_stake_amount
```

Both backtesting and dry-run have same initial stake_amount of $10, leverage 10x, wallet balance of $50:

in backtesting
```
previous_stake_amount is: 9.982392999999998
after DCA, current position was increased by stake_amount of: 19.964785999999997

which is the intended behaviour where stake_amount = 2 * 9.982392999999998 = 19.964785999999997
```

however in dry-run:
```
previous_stake_amount is: 99.99028000000001
after DCA, current position (supposedly) increased by stake_amount of: 199.98056000000003
which is greater than wallet balance.

and this is big discrepancy against backtesting calculation
```

I noticed during `dry-run`, the calculation factored in `leverage` (in my case is 10x) while `backtesting` does not.

Hope you can understand the situation. I am confused why dry run calculates it differently and in theory could be in live mode too. IMO calculation in backtesting is the intended behaviour and accurate too.

Some perhaps relevant information
```
Exchange: Binance
Mode: Futures (isolated)
Pair: StaticPairList, WAVES/USDT:USDT only
--timerange 20230714-
"dry_run_wallet": 50 (same as in backtest too)
stake_amount: 10
leverage: 10
```

tldr; why dry-run handles `stake_amount` wrongly by using leverage?

Thank you and let me know if you need more info.

Here is the reproducible code (do not run in LIVE).
```python
from freqtrade.strategy import IStrategy
from pandas import DataFrame
from pandas_ta import sma
from freqtrade.persistence import Trade
from datetime import datetime
from typing import Optional
import logging

logger = logging.getLogger(__name__)

class Reaper(IStrategy):
    INTERFACE_VERSION: int = 3
    can_short = True

    # ROI table:
    minimal_roi = {
        "0": 0.1
    }

    # Stoploss:
    stoploss = -0.5

    max_open_trades = 1
    timeframe = '5m'
    startup_candle_count: int = 12
    position_adjustment_enable = True

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe.loc[:, f"fast_sma"] = sma(dataframe['close'], 6)
        dataframe.loc[:, f"slow_sma"] = sma(dataframe['close'], 12)
        return dataframe
    
    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        # DON'T RUN IN LIVE, purposely made to be losing
        dataframe.loc[
            (
                dataframe['fast_sma'] < dataframe['slow_sma']
            ),
            ['enter_long', 'enter_tag']
        ] = (1, 'sma_crossed_over')
        
        dataframe.loc[
            (
                dataframe['fast_sma'] > dataframe['slow_sma']
            ),
            ['enter_short', 'enter_tag']
        ] = (1, 'sma_crossed_below')
        
        return dataframe

    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        return dataframe

    def adjust_trade_position(self, trade: Trade, current_time: datetime,
                              current_rate: float, current_profit: float,
                              min_stake: Optional[float], max_stake: float,
                              current_entry_rate: float, current_exit_rate: float,
                              current_entry_profit: float, current_exit_profit: float,
                              **kwargs) -> Optional[float]:
        
        if current_profit > -0.01:
            return None
        
        filled_entries = trade.select_filled_orders(trade.entry_side)
        try:
            # This returns last order stake size.
            previous_stake_amount = filled_entries[-1].cost
            # double down previous entry
            stake_amount = 2 * previous_stake_amount

            if self.wallets:
                available_balance = self.wallets.get_free("USDT")
                
            logger.info(f"##### log for debugging stake_amount became very high during dry-run")
            logger.info(f"##### trade id: {trade.id}, prev_stake_amount: {previous_stake_amount}, stake_amount: {stake_amount}, wallet_balance: {available_balance}")
            
            return stake_amount
        except Exception as exception:
            return None

        return None

    def leverage(self, pair: str, current_time: datetime, current_rate: float,
                 proposed_leverage: float, max_leverage: float, entry_tag: Optional[str], side: str,
                 **kwargs) -> float:
        return 10
    
    def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
                            proposed_stake: float, min_stake: Optional[float], max_stake: float,
                            leverage: float, entry_tag: Optional[str], side: str,
                            **kwargs) -> float:
        return 10

```
adjust_trade_position()'s stake_amount handled differently between backtesting and dry-run/live while using leverage?
## Describe your environment

  * Operating system: Docker's value -> Debian GNU/Linux 11 (bullseye)
  * Python Version: Docker's value -> 3.11.4
  * CCXT version: Docker's value -> 3.1.44
  * Freqtrade Version: Docker's value -> 2023.6
  
## Your question

I am dry-running  and backtesting a strategy with `position_adjustment_enable = True`. I found out that `stake_amount` returned from  `adjust_trade_position()` during *dry-run* has become very high, even higher than wallet balance. Eventually the bot used whole wallet balance to increase the position.

However, during *backtesting* the same pair and timerange, this isn't the behaviour. The bot calculates the `stake_amount` as per intended. Here's a real life example (reproducible code below):

My DCA calculation is:
```
stake_amount = 2 * previous_stake_amount
```

Both backtesting and dry-run have same initial stake_amount of $10, leverage 10x, wallet balance of $50:

in backtesting
```
previous_stake_amount is: 9.982392999999998
after DCA, current position was increased by stake_amount of: 19.964785999999997

which is the intended behaviour where stake_amount = 2 * 9.982392999999998 = 19.964785999999997
```

however in dry-run:
```
previous_stake_amount is: 99.99028000000001
after DCA, current position (supposedly) increased by stake_amount of: 199.98056000000003
which is greater than wallet balance.

and this is big discrepancy against backtesting calculation
```

I noticed during `dry-run`, the calculation factored in `leverage` (in my case is 10x) while `backtesting` does not.

Hope you can understand the situation. I am confused why dry run calculates it differently and in theory could be in live mode too. IMO calculation in backtesting is the intended behaviour and accurate too.

Some perhaps relevant information
```
Exchange: Binance
Mode: Futures (isolated)
Pair: StaticPairList, WAVES/USDT:USDT only
--timerange 20230714-
"dry_run_wallet": 50 (same as in backtest too)
stake_amount: 10
leverage: 10
```

tldr; why dry-run handles `stake_amount` wrongly by using leverage?

Thank you and let me know if you need more info.

Here is the reproducible code (do not run in LIVE).
```python
from freqtrade.strategy import IStrategy
from pandas import DataFrame
from pandas_ta import sma
from freqtrade.persistence import Trade
from datetime import datetime
from typing import Optional
import logging

logger = logging.getLogger(__name__)

class Reaper(IStrategy):
    INTERFACE_VERSION: int = 3
    can_short = True

    # ROI table:
    minimal_roi = {
        "0": 0.1
    }

    # Stoploss:
    stoploss = -0.5

    max_open_trades = 1
    timeframe = '5m'
    startup_candle_count: int = 12
    position_adjustment_enable = True

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe.loc[:, f"fast_sma"] = sma(dataframe['close'], 6)
        dataframe.loc[:, f"slow_sma"] = sma(dataframe['close'], 12)
        return dataframe
    
    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        # DON'T RUN IN LIVE, purposely made to be losing
        dataframe.loc[
            (
                dataframe['fast_sma'] < dataframe['slow_sma']
            ),
            ['enter_long', 'enter_tag']
        ] = (1, 'sma_crossed_over')
        
        dataframe.loc[
            (
                dataframe['fast_sma'] > dataframe['slow_sma']
            ),
            ['enter_short', 'enter_tag']
        ] = (1, 'sma_crossed_below')
        
        return dataframe

    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        return dataframe

    def adjust_trade_position(self, trade: Trade, current_time: datetime,
                              current_rate: float, current_profit: float,
                              min_stake: Optional[float], max_stake: float,
                              current_entry_rate: float, current_exit_rate: float,
                              current_entry_profit: float, current_exit_profit: float,
                              **kwargs) -> Optional[float]:
        
        if current_profit > -0.01:
            return None
        
        filled_entries = trade.select_filled_orders(trade.entry_side)
        try:
            # This returns last order stake size.
            previous_stake_amount = filled_entries[-1].cost
            # double down previous entry
            stake_amount = 2 * previous_stake_amount

            if self.wallets:
                available_balance = self.wallets.get_free("USDT")
                
            logger.info(f"##### log for debugging stake_amount became very high during dry-run")
            logger.info(f"##### trade id: {trade.id}, prev_stake_amount: {previous_stake_amount}, stake_amount: {stake_amount}, wallet_balance: {available_balance}")
            
            return stake_amount
        except Exception as exception:
            return None

        return None

    def leverage(self, pair: str, current_time: datetime, current_rate: float,
                 proposed_leverage: float, max_leverage: float, entry_tag: Optional[str], side: str,
                 **kwargs) -> float:
        return 10
    
    def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
                            proposed_stake: float, min_stake: Optional[float], max_stake: float,
                            leverage: float, entry_tag: Optional[str], side: str,
                            **kwargs) -> float:
        return 10

```


Discussion:
your `adjust_trade_position()` logic is unconditionally buying with 2x the prior stake.
if the initial stake is 100$ - the 2nd stake will be 200$, the next stake 400$, ... 
That seems intentional.

leverage has nothing to do with this - but your actual logic is causing deviations - which is something the docs are also warning about.

but to quote from the [corresponding docs](https://www.freqtrade.io/en/stable/strategy-callbacks/#adjust-trade-position) (it's in the very last Warning box before the example):

> During backtesting this callback is called for each candle in timeframe or timeframe_detail, so run-time performance will be affected. This can also cause deviating results between live and backtesting, since backtesting can adjust the trade only once per candle, whereas live could adjust the trade multiple times per candle.


Maybe making the above clearer:
backtesting -> 1 adjustment every 5 minutes
dry-run -> 1 adjustment every 5 seconds (roughly the time for one iteration).
Depending on network latency, and how many open orders there are, this can obviously deviate  and be higher - but i think you get the idea.

Thanks for the prompt reply. Yes, I do understand the doc, but my situation is different.
Let me try to explain again.

The deviation is between **dry-run** and **backtesting** while they both using same initial stake of $10, wallet balance of $50, **same max_open_trades=1**, same pair, same date, same leverage=10x.

The deviation is **9.99** (backtest) vs **99.99** (dry-run). The number too big for network or candle deviation.

And I think I have found where the deviation happened.

**backtesting**

- open a Long position with `stake_amount` = 10 USDT. This is `trade's entry` no. 1
- price go down by 1 percent, hence trigger the DCA
- this code triggered:
```
previous_stake_amount = filled_entries[-1].cost
logger.info(f"previous_stake_amount: {previous_stake_amount}")
```
- and the printed result is
```previous_stake_amount: 9.99428```
- note here in backtesting, `trade's entry no. 1` is **9.99428**

**dry-run**

- open a Long position with `stake_amount` = 10 USDT. This is `trade's entry` no. 1
- price go down by 1 percent, hence trigger the DCA
- this code triggered:
```
previous_stake_amount = filled_entries[-1].cost
logger.info(f"previous_stake_amount: {previous_stake_amount}")
```
- and the printed result is
```previous_stake_amount: 99.84479999999999```
- note here in dry-run, `trade's entry no. 1` is **99.84479999999999**

Both are the same entry number 1. **9.99428** VS **99.84479999999999**.

Exactly same config, strategy, stake amount, leverage etc. One is run in backtest and 1 is dry-run.
This code gave different result. Which ends up giving different behaviour
```
previous_stake_amount = filled_entries[-1].cost
```
well i think the problematic point is the usage of `order.cost`.
while in dry/backtesting, we can control it (and it seems misaligned) - we do NOT control it in live, but it's whatever the exchange returns as part of the field "cost".

While i know that some exchanges have this as "amount * price" - there's little guarantee this is valid across all supported futures exchanges - as the "real" cost would be the one before leverage.

One example for live OKX 3x trade for XRP/USDT:USDT:
an amount of 100, for a price of 0.51915.
Now you'd assume cost to be 51.9 - but the orders table contains 5191.5 - because XRP has a contract size of 100.

![image](https://github.com/freqtrade/freqtrade/assets/5024695/01c5747a-aa25-4b6d-b4e5-cf407e5ee1af)

![image](https://github.com/freqtrade/freqtrade/assets/5024695/a7cce599-02e5-4259-b48f-49c0e589788f)



Fixing this for backtesting will be easy - but for live, you'll need to double-check if the behavior for your particular exchange.

We'll probably need to adjust the example  to be clearer, as using .cost is pretty dangerous when using futures.

your `adjust_trade_position()` logic is unconditionally buying with 2x the prior stake.
if the initial stake is 100$ - the 2nd stake will be 200$, the next stake 400$, ... 
That seems intentional.

leverage has nothing to do with this - but your actual logic is causing deviations - which is something the docs are also warning about.

but to quote from the [corresponding docs](https://www.freqtrade.io/en/stable/strategy-callbacks/#adjust-trade-position) (it's in the very last Warning box before the example):

> During backtesting this callback is called for each candle in timeframe or timeframe_detail, so run-time performance will be affected. This can also cause deviating results between live and backtesting, since backtesting can adjust the trade only once per candle, whereas live could adjust the trade multiple times per candle.


Maybe making the above clearer:
backtesting -> 1 adjustment every 5 minutes
dry-run -> 1 adjustment every 5 seconds (roughly the time for one iteration).
Depending on network latency, and how many open orders there are, this can obviously deviate  and be higher - but i think you get the idea.

Thanks for the prompt reply. Yes, I do understand the doc, but my situation is different.
Let me try to explain again.

The deviation is between **dry-run** and **backtesting** while they both using same initial stake of $10, wallet balance of $50, **same max_open_trades=1**, same pair, same date, same leverage=10x.

The deviation is **9.99** (backtest) vs **99.99** (dry-run). The number too big for network or candle deviation.

And I think I have found where the deviation happened.

**backtesting**

- open a Long position with `stake_amount` = 10 USDT. This is `trade's entry` no. 1
- price go down by 1 percent, hence trigger the DCA
- this code triggered:
```
previous_stake_amount = filled_entries[-1].cost
logger.info(f"previous_stake_amount: {previous_stake_amount}")
```
- and the printed result is
```previous_stake_amount: 9.99428```
- note here in backtesting, `trade's entry no. 1` is **9.99428**

**dry-run**

- open a Long position with `stake_amount` = 10 USDT. This is `trade's entry` no. 1
- price go down by 1 percent, hence trigger the DCA
- this code triggered:
```
previous_stake_amount = filled_entries[-1].cost
logger.info(f"previous_stake_amount: {previous_stake_amount}")
```
- and the printed result is
```previous_stake_amount: 99.84479999999999```
- note here in dry-run, `trade's entry no. 1` is **99.84479999999999**

Both are the same entry number 1. **9.99428** VS **99.84479999999999**.

Exactly same config, strategy, stake amount, leverage etc. One is run in backtest and 1 is dry-run.
This code gave different result. Which ends up giving different behaviour
```
previous_stake_amount = filled_entries[-1].cost
```
well i think the problematic point is the usage of `order.cost`.
while in dry/backtesting, we can control it (and it seems misaligned) - we do NOT control it in live, but it's whatever the exchange returns as part of the field "cost".

While i know that some exchanges have this as "amount * price" - there's little guarantee this is valid across all supported futures exchanges - as the "real" cost would be the one before leverage.

One example for live OKX 3x trade for XRP/USDT:USDT:
an amount of 100, for a price of 0.51915.
Now you'd assume cost to be 51.9 - but the orders table contains 5191.5 - because XRP has a contract size of 100.

![image](https://github.com/freqtrade/freqtrade/assets/5024695/01c5747a-aa25-4b6d-b4e5-cf407e5ee1af)

![image](https://github.com/freqtrade/freqtrade/assets/5024695/a7cce599-02e5-4259-b48f-49c0e589788f)



Fixing this for backtesting will be easy - but for live, you'll need to double-check if the behavior for your particular exchange.

We'll probably need to adjust the example  to be clearer, as using .cost is pretty dangerous when using futures.


Resolving patch:
diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py
--- a/freqtrade/optimize/backtesting.py
+++ b/freqtrade/optimize/backtesting.py
@@ -679,6 +679,7 @@ def _exit_trade(self, trade: LocalTrade, sell_row: Tuple,
             remaining=amount,
             cost=amount * close_rate,
         )
+        order._trade_bt = trade
         trade.orders.append(order)
         return trade
 
@@ -901,8 +902,9 @@ def _enter_trade(self, pair: str, row: Tuple, direction: LongShort,
                 amount=amount,
                 filled=0,
                 remaining=amount,
-                cost=stake_amount + trade.fee_open,
+                cost=amount * propose_rate + trade.fee_open,
             )
+            order._trade_bt = trade
             trade.orders.append(order)
             if pos_adjust and self._get_order_filled(order.ft_price, row):
                 order.close_bt_order(current_time, trade)
diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py
--- a/freqtrade/persistence/trade_model.py
+++ b/freqtrade/persistence/trade_model.py
@@ -38,6 +38,7 @@ class Order(ModelBase):
     Mirrors CCXT Order structure
     """
     __tablename__ = 'orders'
+    __allow_unmapped__ = True
     session: ClassVar[SessionType]
 
     # Uniqueness should be ensured over pair, order_id
@@ -47,7 +48,8 @@ class Order(ModelBase):
     id: Mapped[int] = mapped_column(Integer, primary_key=True)
     ft_trade_id: Mapped[int] = mapped_column(Integer, ForeignKey('trades.id'), index=True)
 
-    trade: Mapped["Trade"] = relationship("Trade", back_populates="orders")
+    _trade_live: Mapped["Trade"] = relationship("Trade", back_populates="orders")
+    _trade_bt: "LocalTrade" = None  # type: ignore
 
     # order_side can only be 'buy', 'sell' or 'stoploss'
     ft_order_side: Mapped[str] = mapped_column(String(25), nullable=False)
@@ -119,6 +121,15 @@ def safe_fee_base(self) -> float:
     def safe_amount_after_fee(self) -> float:
         return self.safe_filled - self.safe_fee_base
 
+    @property
+    def trade(self) -> "LocalTrade":
+        return self._trade_bt or self._trade_live
+
+    @property
+    def stake_amount(self) -> float:
+        """ Amount in stake currency used for this order"""
+        return self.safe_amount * self.safe_price / self.trade.leverage
+
     def __repr__(self):
 
         return (f"Order(id={self.id}, trade={self.ft_trade_id}, order_id={self.order_id}, "


Test patch:
diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py
--- a/tests/optimize/test_backtesting.py
+++ b/tests/optimize/test_backtesting.py
@@ -601,6 +601,9 @@ def test_backtest__enter_trade_futures(default_conf_usdt, fee, mocker) -> None:
 
     trade = backtesting._enter_trade(pair, row=row, direction='short')
     assert pytest.approx(trade.liquidation_price) == 0.11787191
+    assert pytest.approx(trade.orders[0].cost) == (
+        trade.stake_amount * trade.leverage + trade.fee_open)
+    assert pytest.approx(trade.orders[-1].stake_amount) == trade.stake_amount
 
     # Stake-amount too high!
     mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=600.0)
diff --git a/tests/persistence/test_persistence.py b/tests/persistence/test_persistence.py
--- a/tests/persistence/test_persistence.py
+++ b/tests/persistence/test_persistence.py
@@ -563,14 +563,14 @@ def test_calc_open_close_trade_price(
     trade.open_order_id = f'something-{is_short}-{lev}-{exchange}'
 
     oobj = Order.parse_from_ccxt_object(entry_order, 'ADA/USDT', trade.entry_side)
-    oobj.trade = trade
+    oobj._trade_live = trade
     oobj.update_from_ccxt_object(entry_order)
     trade.update_trade(oobj)
 
     trade.funding_fees = funding_fees
 
     oobj = Order.parse_from_ccxt_object(exit_order, 'ADA/USDT', trade.exit_side)
-    oobj.trade = trade
+    oobj._trade_live = trade
     oobj.update_from_ccxt_object(exit_order)
     trade.update_trade(oobj)
 
diff --git a/tests/test_integration.py b/tests/test_integration.py
--- a/tests/test_integration.py
+++ b/tests/test_integration.py
@@ -429,6 +429,7 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker)
     assert pytest.approx(trade.stop_loss) == 1.99 * (1 - 0.1 / leverage)
     assert pytest.approx(trade.initial_stop_loss) == 1.96 * (1 - 0.1 / leverage)
     assert trade.initial_stop_loss_pct == -0.1
+    assert pytest.approx(trade.orders[-1].stake_amount) == trade.stake_amount
 
     # 2nd order - not filling
     freqtrade.strategy.adjust_trade_position = MagicMock(return_value=120)