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`).
|
For futures pairs, naming will be `base/quote:settle` (e.g. `ETH/USDT:USDT`).
|
||||||
|
|
||||||
|
|
||||||
## Bot execution logic
|
## Bot execution logic
|
||||||
|
|
||||||
Starting freqtrade in dry-run or live mode (using `freqtrade trade`) will start the bot and start the bot iteration loop.
|
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_indicators()`
|
||||||
* Call `populate_entry_trend()`
|
* Call `populate_entry_trend()`
|
||||||
* Call `populate_exit_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_entry_timeout()` strategy callback for open entry orders.
|
||||||
* Calls `check_exit_timeout()` strategy callback for open exit orders.
|
* Calls `check_exit_timeout()` strategy callback for open exit orders.
|
||||||
* Calls `adjust_entry_price()` strategy callback for open entry 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.
|
* 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.
|
* 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.
|
* 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.
|
* 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).
|
* 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
|
* Generate backtest report output
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ Currently available callbacks:
|
|||||||
* [`adjust_trade_position()`](#adjust-trade-position)
|
* [`adjust_trade_position()`](#adjust-trade-position)
|
||||||
* [`adjust_entry_price()`](#adjust-entry-price)
|
* [`adjust_entry_price()`](#adjust-entry-price)
|
||||||
* [`leverage()`](#leverage-callback)
|
* [`leverage()`](#leverage-callback)
|
||||||
|
* [`order_filled()`](#order-filled-callback)
|
||||||
|
|
||||||
!!! Tip "Callback calling sequence"
|
!!! Tip "Callback calling sequence"
|
||||||
You can find the callback calling sequence in [bot-basics](bot-basics.md#bot-execution-logic)
|
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.
|
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.
|
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:
|
def _update_trade_after_fill(self, trade: Trade, order: Order) -> Trade:
|
||||||
if order.status in constants.NON_OPEN_EXCHANGE_STATES:
|
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 a entry order was closed, force update on stoploss on exchange
|
||||||
if order.ft_order_side == trade.entry_side:
|
if order.ft_order_side == trade.entry_side:
|
||||||
trade = self.cancel_stoploss_on_exchange(trade)
|
trade = self.cancel_stoploss_on_exchange(trade)
|
||||||
|
|||||||
@@ -603,6 +603,11 @@ class Backtesting:
|
|||||||
if order and self._get_order_filled(order.ft_price, row):
|
if order and self._get_order_filled(order.ft_price, row):
|
||||||
order.close_bt_order(current_date, trade)
|
order.close_bt_order(current_date, trade)
|
||||||
self._run_funding_fees(trade, current_date, force=True)
|
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):
|
if not (order.ft_order_side == trade.exit_side and order.safe_amount == trade.amount):
|
||||||
# trade is still open
|
# trade is still open
|
||||||
|
|||||||
@@ -372,6 +372,19 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
"""
|
"""
|
||||||
return True
|
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,
|
def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, current_rate: float,
|
||||||
current_profit: float, after_fill: bool, **kwargs) -> Optional[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: A leverage amount, which is between 1.0 and max_leverage.
|
||||||
"""
|
"""
|
||||||
return 1.0
|
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,
|
order_id=order_id,
|
||||||
|
|
||||||
))
|
))
|
||||||
|
freqtrade.strategy.order_filled = MagicMock(return_value=None)
|
||||||
assert not freqtrade.update_trade_state(trade, None)
|
assert not freqtrade.update_trade_state(trade, None)
|
||||||
assert log_has_re(r'Orderid for trade .* is empty.', caplog)
|
assert log_has_re(r'Orderid for trade .* is empty.', caplog)
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
@@ -1243,6 +1244,7 @@ def test_update_trade_state(mocker, default_conf_usdt, limit_order, is_short, ca
|
|||||||
caplog.clear()
|
caplog.clear()
|
||||||
assert not trade.has_open_orders
|
assert not trade.has_open_orders
|
||||||
assert trade.amount == order['amount']
|
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)
|
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', return_value=0.01)
|
||||||
assert trade.amount == 30.0
|
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['filled'] = 0.0
|
||||||
limit_buy_order_usdt_new['status'] = 'canceled'
|
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('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', side_effect=ValueError)
|
||||||
mocker.patch(f'{EXMS}.fetch_order', return_value=limit_buy_order_usdt_new)
|
mocker.patch(f'{EXMS}.fetch_order', return_value=limit_buy_order_usdt_new)
|
||||||
res = freqtrade.update_trade_state(trade, order_id)
|
res = freqtrade.update_trade_state(trade, order_id)
|
||||||
# Cancelled empty
|
# Cancelled empty
|
||||||
assert res is True
|
assert res is True
|
||||||
|
assert freqtrade.strategy.order_filled.call_count == 0
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("is_short", [False, True])
|
@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'],
|
'amount': enter_order['amount'],
|
||||||
})
|
})
|
||||||
mocker.patch(f'{EXMS}.fetch_stoploss_order', stoploss_order_hit)
|
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 freqtrade.handle_stoploss_on_exchange(trade) is True
|
||||||
assert log_has_re(r'STOP_LOSS_LIMIT is hit for Trade\(id=1, .*\)\.', caplog)
|
assert log_has_re(r'STOP_LOSS_LIMIT is hit for Trade\(id=1, .*\)\.', caplog)
|
||||||
assert len(trade.open_sl_orders) == 0
|
assert len(trade.open_sl_orders) == 0
|
||||||
assert trade.is_open is False
|
assert trade.is_open is False
|
||||||
|
assert freqtrade.strategy.order_filled.call_count == 1
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
|
|
||||||
mocker.patch(f'{EXMS}.create_stoploss', side_effect=ExchangeError())
|
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'],
|
data = history.load_data(datadir=testdatadir, timeframe='5m', pairs=['UNITTEST/BTC'],
|
||||||
timerange=timerange)
|
timerange=timerange)
|
||||||
processed = backtesting.strategy.advise_all_indicators(data)
|
processed = backtesting.strategy.advise_all_indicators(data)
|
||||||
|
backtesting.strategy.order_filled = MagicMock()
|
||||||
min_date, max_date = get_timerange(processed)
|
min_date, max_date = get_timerange(processed)
|
||||||
|
|
||||||
result = backtesting.backtest(
|
result = backtesting.backtest(
|
||||||
@@ -760,6 +761,8 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None:
|
|||||||
pd.testing.assert_frame_equal(results, expected)
|
pd.testing.assert_frame_equal(results, expected)
|
||||||
assert 'orders' in results.columns
|
assert 'orders' in results.columns
|
||||||
data_pair = processed[pair]
|
data_pair = processed[pair]
|
||||||
|
# Called once per order
|
||||||
|
assert backtesting.strategy.order_filled.call_count == 4
|
||||||
for _, t in results.iterrows():
|
for _, t in results.iterrows():
|
||||||
assert len(t['orders']) == 2
|
assert len(t['orders']) == 2
|
||||||
ln = data_pair.loc[data_pair["date"] == t["open_date"]]
|
ln = data_pair.loc[data_pair["date"] == t["open_date"]]
|
||||||
|
|||||||
Reference in New Issue
Block a user