Repository: freqtrade/freqtrade

Issue:
Support for custom entry-prices and exit-prices during backtesting.

Custom order entry and exit price is good.
and i can set the trade price directly. 

This thing work only in "trade" mode. but not in "backtesting"











Discussion:
This is correct, and was an explicit decision at the time of implementing custom entry/exit pricing.

As backtesting always fills orders, we'll need to be very careful to only allow "sane" values (which is not necessarily the whole candle).
 
Obviously i can always buy at "candle low" during backtesting - but will that get filled realistically? probably not.  backtesting does not have the data necessary to simulate "unfilled" orders,  so we'll have to assume that everything that's between high/low of a candle will fill.

That's however not how reality will work (low might have already happened) - so allowing a (even remotely) realistic simulation of entry prices is not as easy as it sounds.


Resolving patch:
diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py
--- a/freqtrade/optimize/backtesting.py
+++ b/freqtrade/optimize/backtesting.py
@@ -342,10 +342,7 @@ def _get_close_rate(self, sell_row: Tuple, trade: LocalTrade, sell: SellCheckTup
                     # use Open rate if open_rate > calculated sell rate
                     return sell_row[OPEN_IDX]
 
-                # Use the maximum between close_rate and low as we
-                # cannot sell outside of a candle.
-                # Applies when a new ROI setting comes in place and the whole candle is above that.
-                return min(max(close_rate, sell_row[LOW_IDX]), sell_row[HIGH_IDX])
+                return close_rate
 
             else:
                 # This should not be reached...
@@ -366,6 +363,17 @@ def _get_sell_trade_entry_for_candle(self, trade: LocalTrade,
 
             trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60)
             closerate = self._get_close_rate(sell_row, trade, sell, trade_dur)
+            # call the custom exit price,with default value as previous closerate
+            current_profit = trade.calc_profit_ratio(closerate)
+            if sell.sell_type in (SellType.SELL_SIGNAL, SellType.CUSTOM_SELL):
+                # Custom exit pricing only for sell-signals
+                closerate = strategy_safe_wrapper(self.strategy.custom_exit_price,
+                                                  default_retval=closerate)(
+                    pair=trade.pair, trade=trade,
+                    current_time=sell_row[DATE_IDX],
+                    proposed_rate=closerate, current_profit=current_profit)
+            # Use the maximum between close_rate and low as we cannot sell outside of a candle.
+            closerate = min(max(closerate, sell_row[LOW_IDX]), sell_row[HIGH_IDX])
 
             # Confirm trade exit:
             time_in_force = self.strategy.order_time_in_force['sell']
@@ -424,13 +432,21 @@ def _enter_trade(self, pair: str, row: List) -> Optional[LocalTrade]:
             stake_amount = self.wallets.get_trade_stake_amount(pair, None)
         except DependencyException:
             return None
+        # 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])(
+            pair=pair, current_time=row[DATE_IDX].to_pydatetime(),
+            proposed_rate=row[OPEN_IDX])  # default value is the open rate
 
-        min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, row[OPEN_IDX], -0.05) or 0
+        # Move rate to within the candle's low/high rate
+        propose_rate = min(max(propose_rate, row[LOW_IDX]), row[HIGH_IDX])
+
+        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=row[OPEN_IDX],
+            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)
 
@@ -441,7 +457,7 @@ def _enter_trade(self, pair: str, row: List) -> Optional[LocalTrade]:
         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=row[OPEN_IDX],
+                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
 
@@ -450,10 +466,10 @@ def _enter_trade(self, pair: str, row: List) -> Optional[LocalTrade]:
             has_buy_tag = len(row) >= BUY_TAG_IDX + 1
             trade = LocalTrade(
                 pair=pair,
-                open_rate=row[OPEN_IDX],
+                open_rate=propose_rate,
                 open_date=row[DATE_IDX].to_pydatetime(),
                 stake_amount=stake_amount,
-                amount=round(stake_amount / row[OPEN_IDX], 8),
+                amount=round(stake_amount / propose_rate, 8),
                 fee_open=self.fee,
                 fee_close=self.fee,
                 is_open=True,


Test patch: