mirror of
https://github.com/freqtrade/freqtrade.git
synced 2025-11-29 08:33:07 +00:00
Merge pull request #9955 from Axel-CH/feature/trade-lifecycle-callbacks
Feature: trade lifecycle callbacks
This commit is contained in:
@@ -33,7 +33,6 @@ For spot pairs, naming will be `base/quote` (e.g. `ETH/USDT`).
|
||||
|
||||
For futures pairs, naming will be `base/quote:settle` (e.g. `ETH/USDT:USDT`).
|
||||
|
||||
|
||||
## Bot execution logic
|
||||
|
||||
Starting freqtrade in dry-run or live mode (using `freqtrade trade`) will start the bot and start the bot iteration loop.
|
||||
@@ -50,7 +49,9 @@ By default, the bot loop runs every few seconds (`internals.process_throttle_sec
|
||||
* Call `populate_indicators()`
|
||||
* Call `populate_entry_trend()`
|
||||
* Call `populate_exit_trend()`
|
||||
* Check timeouts for open orders.
|
||||
* Update trades open order state from exchange.
|
||||
* Call `order_filled()` strategy callback for filled orders.
|
||||
* 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.
|
||||
@@ -86,8 +87,10 @@ This loop will be repeated again and again until the bot is stopped.
|
||||
* In Margin and Futures mode, `leverage()` strategy callback is called to determine the desired leverage.
|
||||
* Determine stake size by calling the `custom_stake_amount()` callback.
|
||||
* Check position adjustments for open trades if enabled and call `adjust_trade_position()` to determine if an additional order is requested.
|
||||
* Call `order_filled()` strategy callback for filled entry orders.
|
||||
* Call `custom_stoploss()` and `custom_exit()` to find custom exit points.
|
||||
* For exits based on exit-signal, custom-exit and partial exits: Call `custom_exit_price()` to determine exit price (Prices are moved to be within the closing candle).
|
||||
* Call `order_filled()` strategy callback for filled exit orders.
|
||||
* Generate backtest report output
|
||||
|
||||
!!! Note
|
||||
|
||||
@@ -19,6 +19,7 @@ Currently available callbacks:
|
||||
* [`adjust_trade_position()`](#adjust-trade-position)
|
||||
* [`adjust_entry_price()`](#adjust-entry-price)
|
||||
* [`leverage()`](#leverage-callback)
|
||||
* [`order_filled()`](#order-filled-callback)
|
||||
|
||||
!!! Tip "Callback calling sequence"
|
||||
You can find the callback calling sequence in [bot-basics](bot-basics.md#bot-execution-logic)
|
||||
@@ -1022,3 +1023,33 @@ class AwesomeStrategy(IStrategy):
|
||||
|
||||
All profit calculations include leverage. Stoploss / ROI also include leverage in their calculation.
|
||||
Defining a stoploss of 10% at 10x leverage would trigger the stoploss with a 1% move to the downside.
|
||||
|
||||
## Order filled Callback
|
||||
|
||||
The `order_filled()` callback may be used to perform specific actions based on the current trade state after an order is filled.
|
||||
It will be called independently of the order type (entry, exit, stoploss or position adjustment).
|
||||
|
||||
Assuming that your strategy needs to store the high value of the candle at trade entry, this is possible with this callback as the following example show.
|
||||
|
||||
``` python
|
||||
class AwesomeStrategy(IStrategy):
|
||||
def order_filled(self, pair: str, trade: Trade, order: Order, current_time: datetime, **kwargs) -> None:
|
||||
"""
|
||||
Called right after an order fills.
|
||||
Will be called for all order types (entry, exit, stoploss, position adjustment).
|
||||
:param pair: Pair for trade
|
||||
:param trade: trade object.
|
||||
:param order: Order object.
|
||||
:param current_time: datetime object, containing the current datetime
|
||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||
"""
|
||||
# Obtain pair dataframe (just to show how to access it)
|
||||
dataframe, _ = self.dp.get_analyzed_dataframe(trade.pair, self.timeframe)
|
||||
last_candle = dataframe.iloc[-1].squeeze()
|
||||
|
||||
if (trade.nr_of_successful_entries == 1) and (order.ft_order_side == trade.entry_side):
|
||||
trade.set_custom_data(key='entry_candle_high', value=last_candle['high'])
|
||||
|
||||
return None
|
||||
|
||||
```
|
||||
|
||||
@@ -1944,6 +1944,9 @@ class FreqtradeBot(LoggingMixin):
|
||||
|
||||
def _update_trade_after_fill(self, trade: Trade, order: Order) -> Trade:
|
||||
if order.status in constants.NON_OPEN_EXCHANGE_STATES:
|
||||
strategy_safe_wrapper(
|
||||
self.strategy.order_filled, default_retval=None)(
|
||||
pair=trade.pair, trade=trade, order=order, current_time=datetime.now(timezone.utc))
|
||||
# If a entry order was closed, force update on stoploss on exchange
|
||||
if order.ft_order_side == trade.entry_side:
|
||||
trade = self.cancel_stoploss_on_exchange(trade)
|
||||
|
||||
@@ -603,6 +603,11 @@ class Backtesting:
|
||||
if order and self._get_order_filled(order.ft_price, row):
|
||||
order.close_bt_order(current_date, trade)
|
||||
self._run_funding_fees(trade, current_date, force=True)
|
||||
strategy_safe_wrapper(
|
||||
self.strategy.order_filled,
|
||||
default_retval=None)(
|
||||
pair=trade.pair, trade=trade, # type: ignore[arg-type]
|
||||
order=order, current_time=current_date)
|
||||
|
||||
if not (order.ft_order_side == trade.exit_side and order.safe_amount == trade.amount):
|
||||
# trade is still open
|
||||
|
||||
@@ -372,6 +372,19 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
"""
|
||||
return True
|
||||
|
||||
def order_filled(self, pair: str, trade: Trade, order: Order,
|
||||
current_time: datetime, **kwargs) -> None:
|
||||
"""
|
||||
Called right after an order fills.
|
||||
Will be called for all order types (entry, exit, stoploss, position adjustment).
|
||||
:param pair: Pair for trade
|
||||
:param trade: trade object.
|
||||
:param order: Order object.
|
||||
:param current_time: datetime object, containing the current datetime
|
||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||
"""
|
||||
pass
|
||||
|
||||
def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, current_rate: float,
|
||||
current_profit: float, after_fill: bool, **kwargs) -> Optional[float]:
|
||||
"""
|
||||
|
||||
@@ -300,3 +300,17 @@ def leverage(self, pair: str, current_time: datetime, current_rate: float,
|
||||
:return: A leverage amount, which is between 1.0 and max_leverage.
|
||||
"""
|
||||
return 1.0
|
||||
|
||||
|
||||
def order_filled(self, pair: str, trade: 'Trade', order: 'Order',
|
||||
current_time: datetime, **kwargs) -> None:
|
||||
"""
|
||||
Called right after an order fills.
|
||||
Will be called for all order types (entry, exit, stoploss, position adjustment).
|
||||
:param pair: Pair for trade
|
||||
:param trade: trade object.
|
||||
:param order: Order object.
|
||||
:param current_time: datetime object, containing the current datetime
|
||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||
"""
|
||||
pass
|
||||
|
||||
@@ -1233,6 +1233,7 @@ def test_update_trade_state(mocker, default_conf_usdt, limit_order, is_short, ca
|
||||
order_id=order_id,
|
||||
|
||||
))
|
||||
freqtrade.strategy.order_filled = MagicMock(return_value=None)
|
||||
assert not freqtrade.update_trade_state(trade, None)
|
||||
assert log_has_re(r'Orderid for trade .* is empty.', caplog)
|
||||
caplog.clear()
|
||||
@@ -1243,6 +1244,7 @@ def test_update_trade_state(mocker, default_conf_usdt, limit_order, is_short, ca
|
||||
caplog.clear()
|
||||
assert not trade.has_open_orders
|
||||
assert trade.amount == order['amount']
|
||||
assert freqtrade.strategy.order_filled.call_count == 1
|
||||
|
||||
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', return_value=0.01)
|
||||
assert trade.amount == 30.0
|
||||
@@ -1260,11 +1262,13 @@ def test_update_trade_state(mocker, default_conf_usdt, limit_order, is_short, ca
|
||||
limit_buy_order_usdt_new['filled'] = 0.0
|
||||
limit_buy_order_usdt_new['status'] = 'canceled'
|
||||
|
||||
freqtrade.strategy.order_filled = MagicMock(return_value=None)
|
||||
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', side_effect=ValueError)
|
||||
mocker.patch(f'{EXMS}.fetch_order', return_value=limit_buy_order_usdt_new)
|
||||
res = freqtrade.update_trade_state(trade, order_id)
|
||||
# Cancelled empty
|
||||
assert res is True
|
||||
assert freqtrade.strategy.order_filled.call_count == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("is_short", [False, True])
|
||||
|
||||
@@ -146,10 +146,12 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_
|
||||
'amount': enter_order['amount'],
|
||||
})
|
||||
mocker.patch(f'{EXMS}.fetch_stoploss_order', stoploss_order_hit)
|
||||
freqtrade.strategy.order_filled = MagicMock(return_value=None)
|
||||
assert freqtrade.handle_stoploss_on_exchange(trade) is True
|
||||
assert log_has_re(r'STOP_LOSS_LIMIT is hit for Trade\(id=1, .*\)\.', caplog)
|
||||
assert len(trade.open_sl_orders) == 0
|
||||
assert trade.is_open is False
|
||||
assert freqtrade.strategy.order_filled.call_count == 1
|
||||
caplog.clear()
|
||||
|
||||
mocker.patch(f'{EXMS}.create_stoploss', side_effect=ExchangeError())
|
||||
|
||||
@@ -698,6 +698,7 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None:
|
||||
data = history.load_data(datadir=testdatadir, timeframe='5m', pairs=['UNITTEST/BTC'],
|
||||
timerange=timerange)
|
||||
processed = backtesting.strategy.advise_all_indicators(data)
|
||||
backtesting.strategy.order_filled = MagicMock()
|
||||
min_date, max_date = get_timerange(processed)
|
||||
|
||||
result = backtesting.backtest(
|
||||
@@ -760,6 +761,8 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None:
|
||||
pd.testing.assert_frame_equal(results, expected)
|
||||
assert 'orders' in results.columns
|
||||
data_pair = processed[pair]
|
||||
# Called once per order
|
||||
assert backtesting.strategy.order_filled.call_count == 4
|
||||
for _, t in results.iterrows():
|
||||
assert len(t['orders']) == 2
|
||||
ln = data_pair.loc[data_pair["date"] == t["open_date"]]
|
||||
|
||||
Reference in New Issue
Block a user