mirror of
https://github.com/freqtrade/freqtrade.git
synced 2025-11-29 08:33:07 +00:00
Merge pull request #11412 from freqtrade/feat/adjust_order_price
Add adjust_order_price callback
This commit is contained in:
@@ -54,11 +54,13 @@ By default, the bot loop runs every few seconds (`internals.process_throttle_sec
|
||||
* Check timeouts for open orders.
|
||||
* Calls `check_entry_timeout()` strategy callback for open entry orders.
|
||||
* Calls `check_exit_timeout()` strategy callback for open exit orders.
|
||||
* Calls `adjust_entry_price()` strategy callback for open entry orders.
|
||||
* Calls `adjust_order_price()` strategy callback for open orders.
|
||||
* Calls `adjust_entry_price()` strategy callback for open entry orders. *only called when `adjust_order_price()` is not implemented*
|
||||
* Calls `adjust_exit_price()` strategy callback for open exit orders. *only called when `adjust_order_price()` is not implemented*
|
||||
* Verifies existing positions and eventually places exit orders.
|
||||
* Considers stoploss, ROI and exit-signal, `custom_exit()` and `custom_stoploss()`.
|
||||
* Determine exit-price based on `exit_pricing` configuration setting or by using the `custom_exit_price()` callback.
|
||||
* Before a exit order is placed, `confirm_trade_exit()` strategy callback is called.
|
||||
* Before an exit order is placed, `confirm_trade_exit()` strategy callback is called.
|
||||
* 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 entry signal trying to enter new positions.
|
||||
@@ -80,7 +82,9 @@ This loop will be repeated again and again until the bot is stopped.
|
||||
* Loops per candle simulating entry and exit points.
|
||||
* Calls `bot_loop_start()` strategy callback.
|
||||
* Check for Order timeouts, either via the `unfilledtimeout` configuration, or via `check_entry_timeout()` / `check_exit_timeout()` strategy callbacks.
|
||||
* Calls `adjust_entry_price()` strategy callback for open entry orders.
|
||||
* Calls `adjust_order_price()` strategy callback for open orders.
|
||||
* Calls `adjust_entry_price()` strategy callback for open entry orders. *only called when `adjust_order_price()` is not implemented!*
|
||||
* Calls `adjust_exit_price()` strategy callback for open exit orders. *only called when `adjust_order_price()` is not implemented!*
|
||||
* Check for trade entry signals (`enter_long` / `enter_short` columns).
|
||||
* Confirm trade entry / exits (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).
|
||||
|
||||
@@ -934,28 +934,25 @@ class DigDeeperStrategy(IStrategy):
|
||||
|
||||
The total profit for this trade was 950$ on a 3350$ investment (`100@8$ + 100@9$ + 150@11$`). As such - the final relative profit is 28.35% (`950 / 3350`).
|
||||
|
||||
## Adjust Entry Price
|
||||
## Adjust order Price
|
||||
|
||||
The `adjust_entry_price()` callback may be used by strategy developer to refresh/replace limit orders upon arrival of new candles.
|
||||
The `adjust_order_price()` callback may be used by strategy developer to refresh/replace limit orders upon arrival of new candles.
|
||||
This callback is called once every iteration unless the order has been (re)placed within the current candle - limiting the maximum (re)placement of each order to once per candle.
|
||||
This also means that the first call will be at the start of the next candle after the initial order was placed.
|
||||
|
||||
Be aware that `custom_entry_price()` is still the one dictating initial entry limit order price target at the time of entry trigger.
|
||||
Be aware that `custom_entry_price()`/`custom_exit_price()` is still the one dictating initial limit order price target at the time of the signal.
|
||||
|
||||
Orders can be cancelled out of this callback by returning `None`.
|
||||
|
||||
Returning `current_order_rate` will keep the order on the exchange "as is".
|
||||
Returning any other price will cancel the existing order, and replace it with a new order.
|
||||
|
||||
The trade open-date (`trade.open_date_utc`) will remain at the time of the very first order placed.
|
||||
Please make sure to be aware of this - and eventually adjust your logic in other callbacks to account for this, and use the date of the first filled order instead.
|
||||
|
||||
If the cancellation of the original order fails, then the order will not be replaced - though the order will most likely have been canceled on exchange. Having this happen on initial entries will result in the deletion of the order, while on position adjustment orders, it'll result in the trade size remaining as is.
|
||||
If the order has been partially filled, the order will not be replaced. You can however use [`adjust_trade_position()`](#adjust-trade-position) to adjust the trade size to the full, expected position size, should this be necessary / desired.
|
||||
If the order has been partially filled, the order will not be replaced. You can however use [`adjust_trade_position()`](#adjust-trade-position) to adjust the trade size to the expected position size, should this be necessary / desired.
|
||||
|
||||
!!! Warning "Regular timeout"
|
||||
Entry `unfilledtimeout` mechanism (as well as `check_entry_timeout()`) takes precedence over this.
|
||||
Entry Orders that are cancelled via the above methods will not have this callback called. Be sure to update timeout values to match your expectations.
|
||||
Entry `unfilledtimeout` mechanism (as well as `check_entry_timeout()`/`check_exit_timeout()`) takes precedence over this callback.
|
||||
Orders that are cancelled via the above methods will not have this callback called. Be sure to update timeout values to match your expectations.
|
||||
|
||||
```python
|
||||
# Default imports
|
||||
@@ -964,14 +961,26 @@ class AwesomeStrategy(IStrategy):
|
||||
|
||||
# ... populate_* methods
|
||||
|
||||
def adjust_entry_price(self, trade: Trade, order: Order | None, pair: str,
|
||||
current_time: datetime, proposed_rate: float, current_order_rate: float,
|
||||
entry_tag: str | None, side: str, **kwargs) -> float:
|
||||
def adjust_order_price(
|
||||
self,
|
||||
trade: Trade,
|
||||
order: Order | None,
|
||||
pair: str,
|
||||
current_time: datetime,
|
||||
proposed_rate: float,
|
||||
current_order_rate: float,
|
||||
entry_tag: str | None,
|
||||
side: str,
|
||||
is_entry: bool,
|
||||
**kwargs,
|
||||
) -> float:
|
||||
"""
|
||||
Entry price re-adjustment logic, returning the user desired limit price.
|
||||
Exit and entry order price re-adjustment logic, returning the user desired limit price.
|
||||
This only executes when a order was already placed, still open (unfilled fully or partially)
|
||||
and not timed out on subsequent candles after entry trigger.
|
||||
|
||||
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-callbacks/
|
||||
|
||||
When not implemented by a strategy, returns current_order_rate as default.
|
||||
If current_order_rate is returned then the existing order is maintained.
|
||||
If None is returned then order gets canceled but not replaced by a new one.
|
||||
@@ -983,14 +992,16 @@ class AwesomeStrategy(IStrategy):
|
||||
:param proposed_rate: Rate, calculated based on pricing settings in entry_pricing.
|
||||
:param current_order_rate: Rate of the existing order in place.
|
||||
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
|
||||
:param side: "long" or "short" - indicating the direction of the proposed trade
|
||||
:param side: 'long' or 'short' - indicating the direction of the proposed trade
|
||||
:param is_entry: True if the order is an entry order, False if it's an exit order.
|
||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||
:return float: New entry price value if provided
|
||||
|
||||
"""
|
||||
# Limit orders to use and follow SMA200 as price target for the first 10 minutes since entry trigger for BTC/USDT pair.
|
||||
|
||||
# Limit entry orders to use and follow SMA200 as price target for the first 10 minutes since entry trigger for BTC/USDT pair.
|
||||
if (
|
||||
pair == "BTC/USDT"
|
||||
is_entry
|
||||
and pair == "BTC/USDT"
|
||||
and entry_tag == "long_sma200"
|
||||
and side == "long"
|
||||
and (current_time - timedelta(minutes=10)) <= trade.open_date_utc
|
||||
@@ -1007,6 +1018,26 @@ class AwesomeStrategy(IStrategy):
|
||||
return current_order_rate
|
||||
```
|
||||
|
||||
!!! danger "Incompatibility with `adjust_*_price()`"
|
||||
If you have both `adjust_order_price()` and `adjust_entry_price()`/`adjust_exit_price()` implemented, only `adjust_order_price()` will be used.
|
||||
If you need to adjust entry/exit prices, you can either implement the logic in `adjust_order_price()`, or use the split `adjust_entry_price()` / `adjust_exit_price()` callbacks, but not both.
|
||||
Mixing these is not supported and will raise an error during bot startup.
|
||||
|
||||
### Adjust Entry Price
|
||||
|
||||
The `adjust_entry_price()` callback may be used by strategy developer to refresh/replace entry limit orders upon arrival.
|
||||
It's a sub-set of `adjust_order_price()` and is called only for entry orders.
|
||||
All remaining behavior is identical to `adjust_order_price()`.
|
||||
|
||||
The trade open-date (`trade.open_date_utc`) will remain at the time of the very first order placed.
|
||||
Please make sure to be aware of this - and eventually adjust your logic in other callbacks to account for this, and use the date of the first filled order instead.
|
||||
|
||||
### Adjust Exit Price
|
||||
|
||||
The `adjust_exit_price()` callback may be used by strategy developer to refresh/replace exit limit orders upon arrival.
|
||||
It's a sub-set of `adjust_order_price()` and is called only for exit orders.
|
||||
All remaining behavior is identical to `adjust_order_price()`.
|
||||
|
||||
## Leverage Callback
|
||||
|
||||
When trading in markets that allow leverage, this method must return the desired Leverage (Defaults to 1 -> No leverage).
|
||||
|
||||
@@ -1607,27 +1607,29 @@ class FreqtradeBot(LoggingMixin):
|
||||
self.replace_order(order, open_order, trade)
|
||||
|
||||
def handle_cancel_order(
|
||||
self, order: CcxtOrder, order_obj: Order, trade: Trade, reason: str
|
||||
) -> None:
|
||||
self, order: CcxtOrder, order_obj: Order, trade: Trade, reason: str, replacing: bool = False
|
||||
) -> bool:
|
||||
"""
|
||||
Check if current analyzed order timed out and cancel if necessary.
|
||||
:param order: Order dict grabbed with exchange.fetch_order()
|
||||
:param order_obj: Order object from the database.
|
||||
:param trade: Trade object.
|
||||
:return: None
|
||||
:return: True if the order was canceled, False otherwise.
|
||||
"""
|
||||
if order["side"] == trade.entry_side:
|
||||
self.handle_cancel_enter(trade, order, order_obj, reason)
|
||||
return self.handle_cancel_enter(trade, order, order_obj, reason, replacing)
|
||||
else:
|
||||
canceled = self.handle_cancel_exit(trade, order, order_obj, reason)
|
||||
canceled_count = trade.get_canceled_exit_order_count()
|
||||
max_timeouts = self.config.get("unfilledtimeout", {}).get("exit_timeout_count", 0)
|
||||
if canceled and max_timeouts > 0 and canceled_count >= max_timeouts:
|
||||
logger.warning(
|
||||
f"Emergency exiting trade {trade}, as the exit order "
|
||||
f"timed out {max_timeouts} times. force selling {order['amount']}."
|
||||
)
|
||||
self.emergency_exit(trade, order["price"], order["amount"])
|
||||
if not replacing:
|
||||
canceled_count = trade.get_canceled_exit_order_count()
|
||||
max_timeouts = self.config.get("unfilledtimeout", {}).get("exit_timeout_count", 0)
|
||||
if canceled and max_timeouts > 0 and canceled_count >= max_timeouts:
|
||||
logger.warning(
|
||||
f"Emergency exiting trade {trade}, as the exit order "
|
||||
f"timed out {max_timeouts} times. force selling {order['amount']}."
|
||||
)
|
||||
self.emergency_exit(trade, order["price"], order["amount"])
|
||||
return canceled
|
||||
|
||||
def emergency_exit(
|
||||
self, trade: Trade, price: float, sub_trade_amt: float | None = None
|
||||
@@ -1663,9 +1665,9 @@ class FreqtradeBot(LoggingMixin):
|
||||
def replace_order(self, order: CcxtOrder, order_obj: Order | None, trade: Trade) -> None:
|
||||
"""
|
||||
Check if current analyzed entry order should be replaced or simply cancelled.
|
||||
To simply cancel the existing order(no replacement) adjust_entry_price() should return None
|
||||
To maintain existing order adjust_entry_price() should return order_obj.price
|
||||
To replace existing order adjust_entry_price() should return desired price for limit order
|
||||
To simply cancel the existing order(no replacement) adjust_order_price() should return None
|
||||
To maintain existing order adjust_order_price() should return order_obj.price
|
||||
To replace existing order adjust_order_price() should return desired price for limit order
|
||||
:param order: Order dict grabbed with exchange.fetch_order()
|
||||
:param order_obj: Order object.
|
||||
:param trade: Trade object.
|
||||
@@ -1679,17 +1681,17 @@ class FreqtradeBot(LoggingMixin):
|
||||
self.strategy.timeframe, latest_candle_open_date
|
||||
)
|
||||
# Check if new candle
|
||||
if (
|
||||
order_obj
|
||||
and order_obj.side == trade.entry_side
|
||||
and latest_candle_close_date > order_obj.order_date_utc
|
||||
):
|
||||
if order_obj and latest_candle_close_date > order_obj.order_date_utc:
|
||||
is_entry = order_obj.side == trade.entry_side
|
||||
# New candle
|
||||
proposed_rate = self.exchange.get_rate(
|
||||
trade.pair, side="entry", is_short=trade.is_short, refresh=True
|
||||
trade.pair,
|
||||
side="entry" if is_entry else "exit",
|
||||
is_short=trade.is_short,
|
||||
refresh=True,
|
||||
)
|
||||
adjusted_entry_price = strategy_safe_wrapper(
|
||||
self.strategy.adjust_entry_price, default_retval=order_obj.safe_placement_price
|
||||
adjusted_price = strategy_safe_wrapper(
|
||||
self.strategy.adjust_order_price, default_retval=order_obj.safe_placement_price
|
||||
)(
|
||||
trade=trade,
|
||||
order=order_obj,
|
||||
@@ -1699,36 +1701,51 @@ class FreqtradeBot(LoggingMixin):
|
||||
current_order_rate=order_obj.safe_placement_price,
|
||||
entry_tag=trade.enter_tag,
|
||||
side=trade.trade_direction,
|
||||
is_entry=is_entry,
|
||||
)
|
||||
|
||||
replacing = True
|
||||
cancel_reason = constants.CANCEL_REASON["REPLACE"]
|
||||
if not adjusted_entry_price:
|
||||
if not adjusted_price:
|
||||
replacing = False
|
||||
cancel_reason = constants.CANCEL_REASON["USER_CANCEL"]
|
||||
if order_obj.safe_placement_price != adjusted_entry_price:
|
||||
|
||||
if order_obj.safe_placement_price != adjusted_price:
|
||||
# cancel existing order if new price is supplied or None
|
||||
res = self.handle_cancel_enter(
|
||||
trade, order, order_obj, cancel_reason, replacing=replacing
|
||||
res = self.handle_cancel_order(
|
||||
order, order_obj, trade, cancel_reason, replacing=replacing
|
||||
)
|
||||
if not res:
|
||||
self.replace_order_failed(
|
||||
trade, f"Could not fully cancel order for {trade}, therefore not replacing."
|
||||
)
|
||||
return
|
||||
if adjusted_entry_price:
|
||||
if adjusted_price:
|
||||
# place new order only if new price is supplied
|
||||
try:
|
||||
if not self.execute_entry(
|
||||
pair=trade.pair,
|
||||
stake_amount=(
|
||||
order_obj.safe_remaining * order_obj.safe_price / trade.leverage
|
||||
),
|
||||
price=adjusted_entry_price,
|
||||
trade=trade,
|
||||
is_short=trade.is_short,
|
||||
mode="replace",
|
||||
):
|
||||
if is_entry:
|
||||
succeeded = self.execute_entry(
|
||||
pair=trade.pair,
|
||||
stake_amount=(
|
||||
order_obj.safe_remaining * order_obj.safe_price / trade.leverage
|
||||
),
|
||||
price=adjusted_price,
|
||||
trade=trade,
|
||||
is_short=trade.is_short,
|
||||
mode="replace",
|
||||
)
|
||||
else:
|
||||
succeeded = self.execute_trade_exit(
|
||||
trade,
|
||||
adjusted_price,
|
||||
exit_check=ExitCheckTuple(
|
||||
exit_type=ExitType.CUSTOM_EXIT,
|
||||
exit_reason=order_obj.ft_order_tag or "order_replaced",
|
||||
),
|
||||
ordertype="limit",
|
||||
sub_trade_amt=order_obj.safe_remaining,
|
||||
)
|
||||
if not succeeded:
|
||||
self.replace_order_failed(
|
||||
trade, f"Could not replace order for {trade}."
|
||||
)
|
||||
|
||||
@@ -396,6 +396,8 @@ class Backtesting:
|
||||
self.canceled_trade_entries = 0
|
||||
self.canceled_entry_orders = 0
|
||||
self.replaced_entry_orders = 0
|
||||
self.canceled_exit_orders = 0
|
||||
self.replaced_exit_orders = 0
|
||||
self.dataprovider.clear_cache()
|
||||
if enable_protections:
|
||||
self._load_protections(self.strategy)
|
||||
@@ -1234,8 +1236,8 @@ class Backtesting:
|
||||
for order in [o for o in trade.orders if o.ft_is_open]:
|
||||
if order.side == trade.entry_side:
|
||||
self.canceled_entry_orders += 1
|
||||
# elif order.side == trade.exit_side:
|
||||
# self.canceled_exit_orders += 1
|
||||
elif order.side == trade.exit_side:
|
||||
self.canceled_exit_orders += 1
|
||||
# canceled orders are removed from the trade
|
||||
del trade.orders[trade.orders.index(order)]
|
||||
|
||||
@@ -1299,9 +1301,10 @@ class Backtesting:
|
||||
Returns True if the trade should be deleted.
|
||||
"""
|
||||
# only check on new candles for open entry orders
|
||||
if order.side == trade.entry_side and current_time > order.order_date_utc:
|
||||
if current_time > order.order_date_utc:
|
||||
is_entry = order.side == trade.entry_side
|
||||
requested_rate = strategy_safe_wrapper(
|
||||
self.strategy.adjust_entry_price, default_retval=order.ft_price
|
||||
self.strategy.adjust_order_price, default_retval=order.ft_price
|
||||
)(
|
||||
trade=trade, # type: ignore[arg-type]
|
||||
order=order,
|
||||
@@ -1311,6 +1314,7 @@ class Backtesting:
|
||||
current_order_rate=order.ft_price,
|
||||
entry_tag=trade.enter_tag,
|
||||
side=trade.trade_direction,
|
||||
is_entry=is_entry,
|
||||
) # default value is current order price
|
||||
|
||||
# cancel existing order whenever a new rate is requested (or None)
|
||||
@@ -1319,22 +1323,35 @@ class Backtesting:
|
||||
return False
|
||||
else:
|
||||
del trade.orders[trade.orders.index(order)]
|
||||
self.canceled_entry_orders += 1
|
||||
if is_entry:
|
||||
self.canceled_entry_orders += 1
|
||||
else:
|
||||
self.canceled_exit_orders += 1
|
||||
|
||||
# place new order if result was not None
|
||||
if requested_rate:
|
||||
self._enter_trade(
|
||||
pair=trade.pair,
|
||||
row=row,
|
||||
trade=trade,
|
||||
requested_rate=requested_rate,
|
||||
requested_stake=(order.safe_remaining * order.ft_price / trade.leverage),
|
||||
direction="short" if trade.is_short else "long",
|
||||
)
|
||||
if is_entry:
|
||||
self._enter_trade(
|
||||
pair=trade.pair,
|
||||
row=row,
|
||||
trade=trade,
|
||||
requested_rate=requested_rate,
|
||||
requested_stake=(order.safe_remaining * order.ft_price / trade.leverage),
|
||||
direction="short" if trade.is_short else "long",
|
||||
)
|
||||
self.replaced_entry_orders += 1
|
||||
else:
|
||||
self._exit_trade(
|
||||
trade=trade,
|
||||
sell_row=row,
|
||||
close_rate=requested_rate,
|
||||
amount=order.safe_remaining,
|
||||
exit_reason=order.ft_order_tag,
|
||||
)
|
||||
self.replaced_exit_orders += 1
|
||||
# Delete trade if no successful entries happened (if placing the new order failed)
|
||||
if not trade.has_open_orders and trade.nr_of_successful_entries == 0:
|
||||
if not trade.has_open_orders and is_entry and trade.nr_of_successful_entries == 0:
|
||||
return True
|
||||
self.replaced_entry_orders += 1
|
||||
else:
|
||||
# assumption: there can't be multiple open entry orders at any given time
|
||||
return trade.nr_of_successful_entries == 0
|
||||
|
||||
@@ -242,6 +242,14 @@ class StrategyResolver(IResolver):
|
||||
if has_after_fill:
|
||||
strategy._ft_stop_uses_after_fill = True
|
||||
|
||||
if check_override(strategy, IStrategy, "adjust_order_price") and (
|
||||
check_override(strategy, IStrategy, "adjust_entry_price")
|
||||
or check_override(strategy, IStrategy, "adjust_exit_price")
|
||||
):
|
||||
raise OperationalException(
|
||||
"If you implement `adjust_order_price`, `adjust_entry_price` and "
|
||||
"`adjust_exit_price` will not be used. Please pick one approach for your strategy."
|
||||
)
|
||||
return strategy
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -690,6 +690,104 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
"""
|
||||
return current_order_rate
|
||||
|
||||
def adjust_exit_price(
|
||||
self,
|
||||
trade: Trade,
|
||||
order: Order | None,
|
||||
pair: str,
|
||||
current_time: datetime,
|
||||
proposed_rate: float,
|
||||
current_order_rate: float,
|
||||
entry_tag: str | None,
|
||||
side: str,
|
||||
**kwargs,
|
||||
) -> float:
|
||||
"""
|
||||
Exit price re-adjustment logic, returning the user desired limit price.
|
||||
This only executes when a order was already placed, still open (unfilled fully or partially)
|
||||
and not timed out on subsequent candles after entry trigger.
|
||||
|
||||
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-callbacks/
|
||||
|
||||
When not implemented by a strategy, returns current_order_rate as default.
|
||||
If current_order_rate is returned then the existing order is maintained.
|
||||
If None is returned then order gets canceled but not replaced by a new one.
|
||||
|
||||
:param pair: Pair that's currently analyzed
|
||||
:param trade: Trade object.
|
||||
:param order: Order object
|
||||
:param current_time: datetime object, containing the current datetime
|
||||
:param proposed_rate: Rate, calculated based on pricing settings in entry_pricing.
|
||||
:param current_order_rate: Rate of the existing order in place.
|
||||
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
|
||||
:param side: 'long' or 'short' - indicating the direction of the proposed trade
|
||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||
:return float: New entry price value if provided
|
||||
|
||||
"""
|
||||
return current_order_rate
|
||||
|
||||
def adjust_order_price(
|
||||
self,
|
||||
trade: Trade,
|
||||
order: Order | None,
|
||||
pair: str,
|
||||
current_time: datetime,
|
||||
proposed_rate: float,
|
||||
current_order_rate: float,
|
||||
entry_tag: str | None,
|
||||
side: str,
|
||||
is_entry: bool,
|
||||
**kwargs,
|
||||
) -> float:
|
||||
"""
|
||||
Exit and entry order price re-adjustment logic, returning the user desired limit price.
|
||||
This only executes when a order was already placed, still open (unfilled fully or partially)
|
||||
and not timed out on subsequent candles after entry trigger.
|
||||
|
||||
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-callbacks/
|
||||
|
||||
When not implemented by a strategy, returns current_order_rate as default.
|
||||
If current_order_rate is returned then the existing order is maintained.
|
||||
If None is returned then order gets canceled but not replaced by a new one.
|
||||
|
||||
:param pair: Pair that's currently analyzed
|
||||
:param trade: Trade object.
|
||||
:param order: Order object
|
||||
:param current_time: datetime object, containing the current datetime
|
||||
:param proposed_rate: Rate, calculated based on pricing settings in entry_pricing.
|
||||
:param current_order_rate: Rate of the existing order in place.
|
||||
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
|
||||
:param side: 'long' or 'short' - indicating the direction of the proposed trade
|
||||
:param is_entry: True if the order is an entry order, False if it's an exit order.
|
||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||
:return float: New entry price value if provided
|
||||
"""
|
||||
if is_entry:
|
||||
return self.adjust_entry_price(
|
||||
trade=trade,
|
||||
order=order,
|
||||
pair=pair,
|
||||
current_time=current_time,
|
||||
proposed_rate=proposed_rate,
|
||||
current_order_rate=current_order_rate,
|
||||
entry_tag=entry_tag,
|
||||
side=side,
|
||||
**kwargs,
|
||||
)
|
||||
else:
|
||||
return self.adjust_exit_price(
|
||||
trade=trade,
|
||||
order=order,
|
||||
pair=pair,
|
||||
current_time=current_time,
|
||||
proposed_rate=proposed_rate,
|
||||
current_order_rate=current_order_rate,
|
||||
entry_tag=entry_tag,
|
||||
side=side,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def leverage(
|
||||
self,
|
||||
pair: str,
|
||||
|
||||
@@ -40,7 +40,7 @@ def custom_entry_price(
|
||||
"""
|
||||
return proposed_rate
|
||||
|
||||
def adjust_entry_price(
|
||||
def adjust_order_price(
|
||||
self,
|
||||
trade: Trade,
|
||||
order: Order | None,
|
||||
@@ -50,10 +50,11 @@ def adjust_entry_price(
|
||||
current_order_rate: float,
|
||||
entry_tag: str | None,
|
||||
side: str,
|
||||
is_entry: bool,
|
||||
**kwargs,
|
||||
) -> float:
|
||||
"""
|
||||
Entry price re-adjustment logic, returning the user desired limit price.
|
||||
Exit and entry order price re-adjustment logic, returning the user desired limit price.
|
||||
This only executes when a order was already placed, still open (unfilled fully or partially)
|
||||
and not timed out on subsequent candles after entry trigger.
|
||||
|
||||
@@ -71,6 +72,7 @@ def adjust_entry_price(
|
||||
:param current_order_rate: Rate of the existing order in place.
|
||||
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
|
||||
:param side: 'long' or 'short' - indicating the direction of the proposed trade
|
||||
:param is_entry: True if the order is an entry order, False if it's an exit order.
|
||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||
:return float: New entry price value if provided
|
||||
|
||||
|
||||
@@ -436,6 +436,7 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker)
|
||||
|
||||
# Replace new order with diff. order at a lower price
|
||||
freqtrade.strategy.adjust_entry_price = MagicMock(return_value=1.95)
|
||||
freqtrade.strategy.adjust_exit_price = MagicMock(side_effect=ValueError)
|
||||
freqtrade.strategy.adjust_trade_position = MagicMock(return_value=None)
|
||||
freqtrade.process()
|
||||
trade = Trade.get_trades().first()
|
||||
@@ -445,6 +446,8 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker)
|
||||
assert pytest.approx(trade.stake_amount) == 60
|
||||
assert trade.orders[-1].price == 1.95
|
||||
assert pytest.approx(trade.orders[-1].cost) == 120 * leverage
|
||||
assert freqtrade.strategy.adjust_entry_price.call_count == 1
|
||||
assert freqtrade.strategy.adjust_exit_price.call_count == 0
|
||||
|
||||
# Fill DCA order
|
||||
freqtrade.strategy.adjust_trade_position = MagicMock(return_value=None)
|
||||
@@ -469,6 +472,7 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker)
|
||||
mocker.patch(f"{EXMS}._dry_is_price_crossed", return_value=False)
|
||||
freqtrade.strategy.custom_exit = MagicMock(return_value="Exit now")
|
||||
freqtrade.strategy.adjust_entry_price = MagicMock(return_value=2.02)
|
||||
freqtrade.strategy.adjust_exit_price = MagicMock(side_effect=ValueError)
|
||||
freqtrade.process()
|
||||
trade = Trade.get_trades().first()
|
||||
assert len(trade.orders) == 5
|
||||
@@ -478,8 +482,9 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker)
|
||||
assert pytest.approx(trade.amount) == 91.689215 * leverage
|
||||
assert pytest.approx(trade.orders[-1].amount) == 91.689215 * leverage
|
||||
assert freqtrade.strategy.adjust_entry_price.call_count == 0
|
||||
assert freqtrade.strategy.adjust_exit_price.call_count == 0
|
||||
|
||||
# Process again, should not adjust entry price
|
||||
# Process again, should not adjust price
|
||||
freqtrade.process()
|
||||
trade = Trade.get_trades().first()
|
||||
|
||||
@@ -490,6 +495,21 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker)
|
||||
assert trade.orders[-1].price == 2.02
|
||||
# Adjust entry price cannot be called - this is an exit order
|
||||
assert freqtrade.strategy.adjust_entry_price.call_count == 0
|
||||
assert freqtrade.strategy.adjust_exit_price.call_count == 1
|
||||
|
||||
freqtrade.strategy.adjust_exit_price = MagicMock(return_value=2.03)
|
||||
|
||||
# Process again, should adjust exit price
|
||||
freqtrade.process()
|
||||
trade = Trade.get_trades().first()
|
||||
|
||||
assert trade.orders[-2].status == "canceled"
|
||||
assert len(trade.orders) == 6
|
||||
assert trade.orders[-1].side == trade.exit_side
|
||||
assert trade.orders[-1].status == "open"
|
||||
assert trade.orders[-1].price == 2.03
|
||||
assert freqtrade.strategy.adjust_entry_price.call_count == 0
|
||||
assert freqtrade.strategy.adjust_exit_price.call_count == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize("leverage", [1, 2])
|
||||
|
||||
@@ -45,6 +45,7 @@ class BTContainer(NamedTuple):
|
||||
leverage: float = 1.0
|
||||
timeout: int | None = None
|
||||
adjust_entry_price: float | None = None
|
||||
adjust_exit_price: float | None = None
|
||||
adjust_trade_position: list[float] | None = None
|
||||
|
||||
|
||||
|
||||
@@ -1217,6 +1217,46 @@ tc57 = BTContainer(
|
||||
],
|
||||
)
|
||||
|
||||
# Test 58: Custom-exit-price short - below all candles
|
||||
tc58 = BTContainer(
|
||||
data=[
|
||||
# D O H L C V EL XL ES Xs BT
|
||||
[0, 5000, 5050, 4950, 5000, 6172, 0, 0, 1, 0],
|
||||
[1, 5000, 5200, 4951, 5000, 6172, 0, 0, 0, 0], # enter trade (signal on last candle)
|
||||
[2, 4900, 5250, 4900, 5100, 6172, 0, 0, 0, 1], # Exit - delayed
|
||||
[3, 5100, 5100, 4650, 4750, 6172, 0, 0, 0, 0], #
|
||||
[4, 4750, 5100, 4350, 4750, 6172, 0, 0, 0, 0],
|
||||
],
|
||||
stop_loss=-0.10,
|
||||
roi={"0": 1.00},
|
||||
profit_perc=-0.01,
|
||||
use_exit_signal=True,
|
||||
timeout=1000,
|
||||
custom_exit_price=4300,
|
||||
adjust_exit_price=5050,
|
||||
trades=[BTrade(exit_reason=ExitType.EXIT_SIGNAL, open_tick=1, close_tick=4, is_short=True)],
|
||||
)
|
||||
|
||||
# Test 59: Custom-exit-price above all candles - readjust order
|
||||
tc59 = BTContainer(
|
||||
data=[
|
||||
# D O H L C V EL XL ES Xs BT
|
||||
[0, 5000, 5050, 4950, 5000, 6172, 1, 0],
|
||||
[1, 5000, 5500, 4951, 5000, 6172, 0, 0],
|
||||
[2, 4900, 5250, 4500, 5100, 6172, 0, 1], # exit
|
||||
[3, 5100, 5100, 4650, 4750, 6172, 0, 0], # order readjust
|
||||
[4, 4750, 4950, 4350, 4750, 6172, 0, 0],
|
||||
],
|
||||
stop_loss=-0.2,
|
||||
roi={"0": 0.10},
|
||||
profit_perc=-0.02,
|
||||
use_exit_signal=True,
|
||||
timeout=1000,
|
||||
custom_exit_price=5300,
|
||||
adjust_exit_price=4900,
|
||||
trades=[BTrade(exit_reason=ExitType.EXIT_SIGNAL, open_tick=1, close_tick=4, is_short=False)],
|
||||
)
|
||||
|
||||
|
||||
TESTS = [
|
||||
tc0,
|
||||
@@ -1277,6 +1317,8 @@ TESTS = [
|
||||
tc55,
|
||||
tc56,
|
||||
tc57,
|
||||
tc58,
|
||||
tc59,
|
||||
]
|
||||
|
||||
|
||||
@@ -1330,6 +1372,8 @@ def test_backtest_results(default_conf, mocker, caplog, data: BTContainer) -> No
|
||||
)
|
||||
if data.adjust_entry_price:
|
||||
backtesting.strategy.adjust_entry_price = MagicMock(return_value=data.adjust_entry_price)
|
||||
if data.adjust_exit_price:
|
||||
backtesting.strategy.adjust_exit_price = MagicMock(return_value=data.adjust_exit_price)
|
||||
|
||||
backtesting.strategy.use_custom_stoploss = data.use_custom_stoploss
|
||||
backtesting.strategy.leverage = lambda **kwargs: data.leverage
|
||||
|
||||
@@ -21,10 +21,12 @@ class TestStrategyNoImplementSell(TestStrategyNoImplements):
|
||||
return super().populate_entry_trend(dataframe, metadata)
|
||||
|
||||
|
||||
class TestStrategyImplementCustomSell(TestStrategyNoImplementSell):
|
||||
class TestStrategyImplementEmptyWorking(TestStrategyNoImplementSell):
|
||||
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
return super().populate_exit_trend(dataframe, metadata)
|
||||
|
||||
|
||||
class TestStrategyImplementCustomSell(TestStrategyImplementEmptyWorking):
|
||||
def custom_sell(
|
||||
self,
|
||||
pair: str,
|
||||
@@ -55,3 +57,34 @@ class TestStrategyImplementSellTimeout(TestStrategyNoImplementSell):
|
||||
self, pair: str, trade, order: Order, current_time: datetime, **kwargs
|
||||
) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
class TestStrategyAdjustOrderPrice(TestStrategyImplementEmptyWorking):
|
||||
def adjust_entry_price(
|
||||
self,
|
||||
trade,
|
||||
order,
|
||||
pair,
|
||||
current_time,
|
||||
proposed_rate,
|
||||
current_order_rate,
|
||||
entry_tag,
|
||||
side,
|
||||
**kwargs,
|
||||
):
|
||||
return proposed_rate
|
||||
|
||||
def adjust_order_price(
|
||||
self,
|
||||
trade,
|
||||
order,
|
||||
pair,
|
||||
current_time,
|
||||
proposed_rate,
|
||||
current_order_rate,
|
||||
entry_tag,
|
||||
side,
|
||||
is_entry,
|
||||
**kwargs,
|
||||
):
|
||||
return proposed_rate
|
||||
|
||||
@@ -460,6 +460,10 @@ def test_missing_implements(default_conf, caplog):
|
||||
):
|
||||
StrategyResolver.load_strategy(default_conf)
|
||||
|
||||
default_conf["strategy"] = "TestStrategyAdjustOrderPrice"
|
||||
with pytest.raises(OperationalException, match=r"If you implement `adjust_order_price`.*"):
|
||||
StrategyResolver.load_strategy(default_conf)
|
||||
|
||||
|
||||
def test_call_deprecated_function(default_conf):
|
||||
default_location = Path(__file__).parent / "strats/broken_strats/"
|
||||
|
||||
Reference in New Issue
Block a user