mirror of
https://github.com/freqtrade/freqtrade.git
synced 2025-11-29 08:33:07 +00:00
Merge pull request #9515 from stash86/bt-metrics
In partial exit, do full exit if remaining == 0
This commit is contained in:
@@ -760,22 +760,31 @@ The `position_adjustment_enable` strategy property enables the usage of `adjust_
|
|||||||
For performance reasons, it's disabled by default and freqtrade will show a warning message on startup if enabled.
|
For performance reasons, it's disabled by default and freqtrade will show a warning message on startup if enabled.
|
||||||
`adjust_trade_position()` can be used to perform additional orders, for example to manage risk with DCA (Dollar Cost Averaging) or to increase or decrease positions.
|
`adjust_trade_position()` can be used to perform additional orders, for example to manage risk with DCA (Dollar Cost Averaging) or to increase or decrease positions.
|
||||||
|
|
||||||
`max_entry_position_adjustment` property is used to limit the number of additional entries per trade (on top of the first entry order) that the bot can execute. By default, the value is -1 which means the bot have no limit on number of adjustment entries.
|
|
||||||
|
|
||||||
The strategy is expected to return a stake_amount (in stake currency) between `min_stake` and `max_stake` if and when an additional entry order should be made (position is increased -> buy order for long trades, sell order for short trades).
|
|
||||||
If there are not enough funds in the wallet (the return value is above `max_stake`) then the signal will be ignored.
|
|
||||||
Additional orders also result in additional fees and those orders don't count towards `max_open_trades`.
|
Additional orders also result in additional fees and those orders don't count towards `max_open_trades`.
|
||||||
|
|
||||||
This callback is **not** called when there is an open order (either buy or sell) waiting for execution.
|
This callback is **not** called when there is an open order (either buy or sell) waiting for execution.
|
||||||
|
|
||||||
`adjust_trade_position()` is called very frequently for the duration of a trade, so you must keep your implementation as performant as possible.
|
`adjust_trade_position()` is called very frequently for the duration of a trade, so you must keep your implementation as performant as possible.
|
||||||
|
|
||||||
Additional entries are ignored once you have reached the maximum amount of extra entries that you have set on `max_entry_position_adjustment`, but the callback is called anyway looking for partial exits.
|
|
||||||
|
|
||||||
Position adjustments will always be applied in the direction of the trade, so a positive value will always increase your position (negative values will decrease your position), no matter if it's a long or short trade.
|
Position adjustments will always be applied in the direction of the trade, so a positive value will always increase your position (negative values will decrease your position), no matter if it's a long or short trade.
|
||||||
|
|
||||||
Modifications to leverage are not possible, and the stake-amount returned is assumed to be before applying leverage.
|
Modifications to leverage are not possible, and the stake-amount returned is assumed to be before applying leverage.
|
||||||
|
|
||||||
|
### Increase position
|
||||||
|
|
||||||
|
The strategy is expected to return a positive **stake_amount** (in stake currency) between `min_stake` and `max_stake` if and when an additional entry order should be made (position is increased -> buy order for long trades, sell order for short trades).
|
||||||
|
|
||||||
|
If there are not enough funds in the wallet (the return value is above `max_stake`) then the signal will be ignored.
|
||||||
|
`max_entry_position_adjustment` property is used to limit the number of additional entries per trade (on top of the first entry order) that the bot can execute. By default, the value is -1 which means the bot have no limit on number of adjustment entries.
|
||||||
|
|
||||||
|
Additional entries are ignored once you have reached the maximum amount of extra entries that you have set on `max_entry_position_adjustment`, but the callback is called anyway looking for partial exits.
|
||||||
|
|
||||||
|
### Decrease position
|
||||||
|
|
||||||
|
The strategy is expected to return a negative stake_amount (in stake currency) for a partial exit.
|
||||||
|
Returning the full owned stake at that point (based on the current price) (`-(trade.amount / trade.leverage) * current_exit_rate`) results in a full exit.
|
||||||
|
Returning a value more than the above (so remaining stake_amount would become negative) will result in the bot ignoring the signal.
|
||||||
|
|
||||||
!!! Note "About stake size"
|
!!! Note "About stake size"
|
||||||
Using fixed stake size means it will be the amount used for the first order, just like without position adjustment.
|
Using fixed stake size means it will be the amount used for the first order, just like without position adjustment.
|
||||||
If you wish to buy additional orders with DCA, then make sure to leave enough funds in the wallet for that.
|
If you wish to buy additional orders with DCA, then make sure to leave enough funds in the wallet for that.
|
||||||
|
|||||||
@@ -673,20 +673,13 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
amount = self.exchange.amount_to_contract_precision(
|
amount = self.exchange.amount_to_contract_precision(
|
||||||
trade.pair,
|
trade.pair,
|
||||||
abs(float(FtPrecise(stake_amount * trade.leverage) / FtPrecise(current_exit_rate))))
|
abs(float(FtPrecise(stake_amount * trade.leverage) / FtPrecise(current_exit_rate))))
|
||||||
if amount > trade.amount:
|
|
||||||
# This is currently ineffective as remaining would become < min tradable
|
|
||||||
# Fixing this would require checking for 0.0 there -
|
|
||||||
# if we decide that this callback is allowed to "fully exit"
|
|
||||||
logger.info(
|
|
||||||
f"Adjusting amount to trade.amount as it is higher. {amount} > {trade.amount}")
|
|
||||||
amount = trade.amount
|
|
||||||
|
|
||||||
if amount == 0.0:
|
if amount == 0.0:
|
||||||
logger.info("Amount to exit is 0.0 due to exchange limits - not exiting.")
|
logger.info("Amount to exit is 0.0 due to exchange limits - not exiting.")
|
||||||
return
|
return
|
||||||
|
|
||||||
remaining = (trade.amount - amount) * current_exit_rate
|
remaining = (trade.amount - amount) * current_exit_rate
|
||||||
if min_exit_stake and remaining < min_exit_stake:
|
if min_exit_stake and remaining != 0 and remaining < min_exit_stake:
|
||||||
logger.info(f"Remaining amount of {remaining} would be smaller "
|
logger.info(f"Remaining amount of {remaining} would be smaller "
|
||||||
f"than the minimum of {min_exit_stake}.")
|
f"than the minimum of {min_exit_stake}.")
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -531,7 +531,7 @@ class Backtesting:
|
|||||||
def _get_adjust_trade_entry_for_candle(
|
def _get_adjust_trade_entry_for_candle(
|
||||||
self, trade: LocalTrade, row: Tuple, current_time: datetime
|
self, trade: LocalTrade, row: Tuple, current_time: datetime
|
||||||
) -> LocalTrade:
|
) -> LocalTrade:
|
||||||
current_rate = row[OPEN_IDX]
|
current_rate: float = row[OPEN_IDX]
|
||||||
current_profit = trade.calc_profit_ratio(current_rate)
|
current_profit = trade.calc_profit_ratio(current_rate)
|
||||||
min_stake = self.exchange.get_min_pair_stake_amount(trade.pair, current_rate, -0.1)
|
min_stake = self.exchange.get_min_pair_stake_amount(trade.pair, current_rate, -0.1)
|
||||||
max_stake = self.exchange.get_max_pair_stake_amount(trade.pair, current_rate)
|
max_stake = self.exchange.get_max_pair_stake_amount(trade.pair, current_rate)
|
||||||
@@ -564,11 +564,8 @@ class Backtesting:
|
|||||||
self.precision_mode, trade.contract_size)
|
self.precision_mode, trade.contract_size)
|
||||||
if amount == 0.0:
|
if amount == 0.0:
|
||||||
return trade
|
return trade
|
||||||
if amount > trade.amount:
|
|
||||||
# This is currently ineffective as remaining would become < min tradable
|
|
||||||
amount = trade.amount
|
|
||||||
remaining = (trade.amount - amount) * current_rate
|
remaining = (trade.amount - amount) * current_rate
|
||||||
if remaining < min_stake:
|
if min_stake and remaining != 0 and remaining < min_stake:
|
||||||
# Remaining stake is too low to be sold.
|
# Remaining stake is too low to be sold.
|
||||||
return trade
|
return trade
|
||||||
exit_ = ExitCheckTuple(ExitType.PARTIAL_EXIT)
|
exit_ = ExitCheckTuple(ExitType.PARTIAL_EXIT)
|
||||||
|
|||||||
@@ -650,28 +650,42 @@ def test_dca_exiting(default_conf_usdt, ticker_usdt, fee, mocker, caplog, levera
|
|||||||
caplog.clear()
|
caplog.clear()
|
||||||
|
|
||||||
# Sell more than what we got (we got ~20 coins left)
|
# Sell more than what we got (we got ~20 coins left)
|
||||||
# First adjusts the amount to 20 - then rejects.
|
# Doesn't exit, as the amount is too high.
|
||||||
freqtrade.strategy.adjust_trade_position = MagicMock(return_value=-50)
|
freqtrade.strategy.adjust_trade_position = MagicMock(return_value=-50)
|
||||||
freqtrade.process()
|
freqtrade.process()
|
||||||
assert log_has_re("Adjusting amount to trade.amount as it is higher.*", caplog)
|
|
||||||
assert log_has_re("Remaining amount of 0.0 would be smaller than the minimum of 10.", caplog)
|
|
||||||
trade = Trade.get_trades().first()
|
trade = Trade.get_trades().first()
|
||||||
assert len(trade.orders) == 2
|
assert len(trade.orders) == 2
|
||||||
|
|
||||||
|
# Amount too low...
|
||||||
|
freqtrade.strategy.adjust_trade_position = MagicMock(return_value=-(trade.stake_amount * 0.99))
|
||||||
|
freqtrade.process()
|
||||||
|
|
||||||
|
trade = Trade.get_trades().first()
|
||||||
|
assert len(trade.orders) == 2
|
||||||
|
|
||||||
|
# Amount exactly comes out as exactly 0
|
||||||
|
freqtrade.strategy.adjust_trade_position = MagicMock(
|
||||||
|
return_value=-(trade.amount / trade.leverage * 2.02))
|
||||||
|
freqtrade.process()
|
||||||
|
|
||||||
|
trade = Trade.get_trades().first()
|
||||||
|
assert len(trade.orders) == 3
|
||||||
|
|
||||||
assert trade.orders[-1].ft_order_side == 'sell'
|
assert trade.orders[-1].ft_order_side == 'sell'
|
||||||
assert pytest.approx(trade.stake_amount) == 40.198
|
assert pytest.approx(trade.stake_amount) == 40.198
|
||||||
assert trade.is_open
|
assert trade.is_open is False
|
||||||
|
|
||||||
# use amount that would trunc to 0.0 once selling
|
# use amount that would trunc to 0.0 once selling
|
||||||
mocker.patch(f"{EXMS}.amount_to_contract_precision", lambda s, p, v: round(v, 1))
|
mocker.patch(f"{EXMS}.amount_to_contract_precision", lambda s, p, v: round(v, 1))
|
||||||
freqtrade.strategy.adjust_trade_position = MagicMock(return_value=-0.01)
|
freqtrade.strategy.adjust_trade_position = MagicMock(return_value=-0.01)
|
||||||
freqtrade.process()
|
freqtrade.process()
|
||||||
trade = Trade.get_trades().first()
|
trade = Trade.get_trades().first()
|
||||||
assert len(trade.orders) == 2
|
assert len(trade.orders) == 3
|
||||||
assert trade.orders[-1].ft_order_side == 'sell'
|
assert trade.orders[-1].ft_order_side == 'sell'
|
||||||
assert pytest.approx(trade.stake_amount) == 40.198
|
assert pytest.approx(trade.stake_amount) == 40.198
|
||||||
assert trade.is_open
|
assert trade.is_open is False
|
||||||
assert log_has_re('Amount to exit is 0.0 due to exchange limits - not exiting.', caplog)
|
assert log_has_re('Amount to exit is 0.0 due to exchange limits - not exiting.', caplog)
|
||||||
expected_profit = starting_amount - 40.1980 + trade.realized_profit
|
expected_profit = starting_amount - 60 + trade.realized_profit
|
||||||
assert pytest.approx(freqtrade.wallets.get_free('USDT')) == expected_profit
|
assert pytest.approx(freqtrade.wallets.get_free('USDT')) == expected_profit
|
||||||
if spot:
|
if spot:
|
||||||
assert pytest.approx(freqtrade.wallets.get_total('USDT')) == expected_profit
|
assert pytest.approx(freqtrade.wallets.get_total('USDT')) == expected_profit
|
||||||
|
|||||||
Reference in New Issue
Block a user