Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[pull] develop from freqtrade:develop #412

Merged
merged 5 commits into from
Mar 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/backtesting.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ A backtesting result will look like that:
| Sortino | 1.88 |
| Sharpe | 2.97 |
| Calmar | 6.29 |
| SQN | 2.45 |
| Profit factor | 1.11 |
| Expectancy (Ratio) | -0.15 (-0.05) |
| Avg. stake amount | 0.001 BTC |
Expand Down
11 changes: 6 additions & 5 deletions docs/strategy-callbacks.md
Original file line number Diff line number Diff line change
Expand Up @@ -758,7 +758,7 @@ For performance reasons, it's disabled by default and freqtrade will show a warn

Additional orders also result in additional fees and those orders don't count towards `max_open_trades`.

This callback is also called when there is an open order (either buy or sell) waiting for execution - and will cancel the existing open order to place a new order if the amount, price or direction is different.
This callback is also called when there is an open order (either buy or sell) waiting for execution - and will cancel the existing open order to place a new order if the amount, price or direction is different. Also partially filled orders will be canceled, and will be replaced with the new amount as returned by the callback.

`adjust_trade_position()` is called very frequently for the duration of a trade, so you must keep your implementation as performant as possible.

Expand All @@ -770,9 +770,10 @@ Modifications to leverage are not possible, and the stake-amount returned is ass
The combined stake currently allocated to the position is held in `trade.stake_amount`. Therefore `trade.stake_amount` will always be updated on every additional entry and partial exit made through `adjust_trade_position()`.

!!! Danger "Loose Logic"
On dry and live run, this function will be called every `throttle_process_secs` (default to 5s). If you have a loose logic, for example your logic for extra entry is only to check RSI of last candle is below 30, then when such condition fulfilled, your bot will do extra re-entry every 5 secs until either it run out of money, it hit the `max_position_adjustment` limit, or a new candle with RSI more than 30 arrived.
On dry and live run, this function will be called every `throttle_process_secs` (default to 5s). If you have a loose logic, (e.g. increase position if RSI of the last candle is below 30), your bot will do extra re-entry every 5 secs until you either it run out of money, hit the `max_position_adjustment` limit, or a new candle with RSI more than 30 arrived.

Same thing also can happen with partial exit. So be sure to have a strict logic and/or check for the last filled order.
Same thing also can happen with partial exit.
So be sure to have a strict logic and/or check for the last filled order and if an order is already open.

!!! Warning "Performance with many position adjustments"
Position adjustments can be a good approach to increase a strategy's output - but it can also have drawbacks if using this feature extensively.
Expand Down Expand Up @@ -976,7 +977,7 @@ class AwesomeStrategy(IStrategy):
side: str,
is_entry: bool,
**kwargs,
) -> float:
) -> float | None:
"""
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)
Expand All @@ -998,7 +999,7 @@ class AwesomeStrategy(IStrategy):
: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
:return float or None: New entry price value if provided
"""

# Limit entry orders to use and follow SMA200 as price target for the first 10 minutes since entry trigger for BTC/USDT pair.
Expand Down
100 changes: 61 additions & 39 deletions freqtrade/freqtradebot.py
Original file line number Diff line number Diff line change
Expand Up @@ -789,6 +789,7 @@ def check_and_call_adjust_trade_position(self, trade: Trade):
return
else:
logger.debug("Max adjustment entries is set to unlimited.")

self.execute_entry(
trade.pair,
stake_amount,
Expand Down Expand Up @@ -1711,47 +1712,68 @@ def replace_order(self, order: CcxtOrder, order_obj: Order | None, trade: Trade)
cancel_reason = constants.CANCEL_REASON["USER_CANCEL"]

if order_obj.safe_placement_price != adjusted_price:
# cancel existing order if new price is supplied or None
res = self.handle_cancel_order(
order, order_obj, trade, cancel_reason, replacing=replacing
self.handle_replace_order(
order,
order_obj,
trade,
adjusted_price,
is_entry,
cancel_reason,
replacing=replacing,
)
if not res:
self.replace_order_failed(
trade, f"Could not fully cancel order for {trade}, therefore not replacing."

def handle_replace_order(
self,
order: CcxtOrder | None,
order_obj: Order,
trade: Trade,
new_order_price: float | None,
is_entry: bool,
cancel_reason: str,
replacing: bool = False,
) -> None:
"""
Cancel existing order if new price is supplied, and if the cancel is successful,
places a new order with the remaining capital.
"""
if not order:
order = self.exchange.fetch_order(order_obj.order_id, trade.pair)
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 new_order_price:
# place new order only if new price is supplied
try:
if is_entry:
succeeded = self.execute_entry(
pair=trade.pair,
stake_amount=(
order_obj.safe_remaining * order_obj.safe_price / trade.leverage
),
price=new_order_price,
trade=trade,
is_short=trade.is_short,
mode="replace",
)
return
if adjusted_price:
# place new order only if new price is supplied
try:
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}."
)
except DependencyException as exception:
logger.warning(f"Unable to replace order for {trade.pair}: {exception}")
self.replace_order_failed(trade, f"Could not replace order for {trade}.")
else:
succeeded = self.execute_trade_exit(
trade,
new_order_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}.")
except DependencyException as exception:
logger.warning(f"Unable to replace order for {trade.pair}: {exception}")
self.replace_order_failed(trade, f"Could not replace order for {trade}.")

def cancel_open_orders_of_trade(
self, trade: Trade, sides: list[str], reason: str, replacing: bool = False
Expand Down
12 changes: 6 additions & 6 deletions freqtrade/strategy/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -664,7 +664,7 @@ def adjust_entry_price(
entry_tag: str | None,
side: str,
**kwargs,
) -> float:
) -> float | None:
"""
Entry 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)
Expand All @@ -685,7 +685,7 @@ def adjust_entry_price(
: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 float or None: New entry price value if provided

"""
return current_order_rate
Expand All @@ -701,7 +701,7 @@ def adjust_exit_price(
entry_tag: str | None,
side: str,
**kwargs,
) -> float:
) -> float | None:
"""
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)
Expand All @@ -722,7 +722,7 @@ def adjust_exit_price(
: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 float or None: New exit price value if provided

"""
return current_order_rate
Expand All @@ -739,7 +739,7 @@ def adjust_order_price(
side: str,
is_entry: bool,
**kwargs,
) -> float:
) -> float | None:
"""
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)
Expand All @@ -761,7 +761,7 @@ def adjust_order_price(
: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
:return float or None: New entry price value if provided
"""
if is_entry:
return self.adjust_entry_price(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def adjust_order_price(
side: str,
is_entry: bool,
**kwargs,
) -> float:
) -> float | None:
"""
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)
Expand All @@ -74,8 +74,7 @@ def adjust_order_price(
: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

:return float or None: New entry price value if provided
"""
return current_order_rate

Expand Down
Loading