diff --git a/docs/bot-basics.md b/docs/bot-basics.md index a19f005db..1c88559c0 100644 --- a/docs/bot-basics.md +++ b/docs/bot-basics.md @@ -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,10 +49,12 @@ 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. - * 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. +* 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. * 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. @@ -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 diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index c49324be7..4134289a7 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -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 + +``` diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 898f36eb1..0184741ff 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -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) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 51acb2e44..326c48c62 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -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 diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 2630c3547..f8a890d5d 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -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]: """ diff --git a/freqtrade/templates/strategy_subtemplates/strategy_methods_advanced.j2 b/freqtrade/templates/strategy_subtemplates/strategy_methods_advanced.j2 index 6fad129c7..541c26e87 100644 --- a/freqtrade/templates/strategy_subtemplates/strategy_methods_advanced.j2 +++ b/freqtrade/templates/strategy_subtemplates/strategy_methods_advanced.j2 @@ -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 diff --git a/tests/freqtradebot/test_freqtradebot.py b/tests/freqtradebot/test_freqtradebot.py index 27a18c0df..0106e1b77 100644 --- a/tests/freqtradebot/test_freqtradebot.py +++ b/tests/freqtradebot/test_freqtradebot.py @@ -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]) diff --git a/tests/freqtradebot/test_stoploss_on_exchange.py b/tests/freqtradebot/test_stoploss_on_exchange.py index 325fe549f..04a04ea9f 100644 --- a/tests/freqtradebot/test_stoploss_on_exchange.py +++ b/tests/freqtradebot/test_stoploss_on_exchange.py @@ -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()) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 603fcc310..8fc42b474 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -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"]]