Merge pull request #6260 from stash86/pos_adjust

Add max_buy_position_adjustment as attribute
This commit is contained in:
Matthias 2022-01-27 20:13:51 +01:00 committed by GitHub
commit 82e193d9f0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 85 additions and 21 deletions

View File

@ -173,6 +173,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| `dataformat_ohlcv` | Data format to use to store historical candle (OHLCV) data. <br> *Defaults to `json`*. <br> **Datatype:** String | `dataformat_ohlcv` | Data format to use to store historical candle (OHLCV) data. <br> *Defaults to `json`*. <br> **Datatype:** String
| `dataformat_trades` | Data format to use to store historical trades data. <br> *Defaults to `jsongz`*. <br> **Datatype:** String | `dataformat_trades` | Data format to use to store historical trades data. <br> *Defaults to `jsongz`*. <br> **Datatype:** String
| `position_adjustment_enable` | Enables the strategy to use position adjustments (additional buys or sells). [More information here](strategy-callbacks.md#adjust-trade-position). <br> [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.*<br> **Datatype:** Boolean | `position_adjustment_enable` | Enables the strategy to use position adjustments (additional buys or sells). [More information here](strategy-callbacks.md#adjust-trade-position). <br> [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.*<br> **Datatype:** Boolean
| `max_entry_position_adjustment` | Maximum additional buy(s) for each open trade on top of the first entry Order. Set it to `-1` for unlimited additional additional orders. [More information here](strategy-callbacks.md#adjust-trade-position). <br> [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `-1`.*<br> **Datatype:** Positive Integer or -1
### Parameters in the strategy ### Parameters in the strategy
@ -198,6 +199,7 @@ Values set in the configuration file always overwrite values set in the strategy
* `ignore_roi_if_buy_signal` * `ignore_roi_if_buy_signal`
* `ignore_buying_expired_candle_after` * `ignore_buying_expired_candle_after`
* `position_adjustment_enable` * `position_adjustment_enable`
* `max_entry_position_adjustment`
### Configuring amount per trade ### Configuring amount per trade

View File

@ -579,11 +579,13 @@ 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). `adjust_trade_position()` can be used to perform additional orders, for example to manage risk with DCA (Dollar Cost Averaging).
`max_entry_position_adjustment` property is used to limit the number of additional buys per trade (on top of the first buy) that the bot can execute. By default, the value is -1 which means the bot have no limit on number of adjustment buys.
The strategy is expected to return a stake_amount (in stake currency) between `min_stake` and `max_stake` if and when an additional buy order should be made (position is increased). The strategy is expected to return a stake_amount (in stake currency) between `min_stake` and `max_stake` if and when an additional buy order should be made (position is increased).
If there are not enough funds in the wallet (the return value is above `max_stake`) then the signal will be ignored. 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, or when you have reached the maximum amount of extra buys that you have set on `max_entry_position_adjustment`.
`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.
!!! Note "About stake size" !!! Note "About stake size"
@ -614,7 +616,7 @@ class DigDeeperStrategy(IStrategy):
# ... populate_* methods # ... populate_* methods
# Example specific variables # Example specific variables
max_dca_orders = 3 max_entry_position_adjustment = 3
# This number is explained a bit further down # This number is explained a bit further down
max_dca_multiplier = 5.5 max_dca_multiplier = 5.5
@ -656,8 +658,7 @@ class DigDeeperStrategy(IStrategy):
return None return None
filled_buys = trade.select_filled_orders('buy') filled_buys = trade.select_filled_orders('buy')
count_of_buys = len(filled_buys) count_of_buys = trade.nr_of_successful_buys
# Allow up to 3 additional increasingly larger buys (4 in total) # Allow up to 3 additional increasingly larger buys (4 in total)
# Initial buy is 1x # Initial buy is 1x
# If that falls to -5% profit, we buy 1.25x more, average profit should increase to roughly -2.2% # If that falls to -5% profit, we buy 1.25x more, average profit should increase to roughly -2.2%
@ -666,15 +667,14 @@ class DigDeeperStrategy(IStrategy):
# Total stake for this trade would be 1 + 1.25 + 1.5 + 1.75 = 5.5x of the initial allowed stake. # Total stake for this trade would be 1 + 1.25 + 1.5 + 1.75 = 5.5x of the initial allowed stake.
# That is why max_dca_multiplier is 5.5 # That is why max_dca_multiplier is 5.5
# Hope you have a deep wallet! # Hope you have a deep wallet!
if 0 < count_of_buys <= self.max_dca_orders: try:
try: # This returns first order stake size
# This returns first order stake size stake_amount = filled_buys[0].cost
stake_amount = filled_buys[0].cost # This then calculates current safety order size
# This then calculates current safety order size stake_amount = stake_amount * (1 + (count_of_buys * 0.25))
stake_amount = stake_amount * (1 + (count_of_buys * 0.25)) return stake_amount
return stake_amount except Exception as exception:
except Exception as exception: return None
return None
return None return None

View File

@ -371,7 +371,9 @@ CONF_SCHEMA = {
'type': 'string', 'type': 'string',
'enum': AVAILABLE_DATAHANDLERS, 'enum': AVAILABLE_DATAHANDLERS,
'default': 'jsongz' 'default': 'jsongz'
} },
'position_adjustment_enable': {'type': 'boolean', 'default': False},
'max_entry_position_adjustment': {'type': ['integer', 'number'], 'minimum': -1},
}, },
'definitions': { 'definitions': {
'exchange': { 'exchange': {

View File

@ -462,8 +462,8 @@ class FreqtradeBot(LoggingMixin):
try: try:
self.check_and_call_adjust_trade_position(trade) self.check_and_call_adjust_trade_position(trade)
except DependencyException as exception: except DependencyException as exception:
logger.warning('Unable to adjust position of trade for %s: %s', logger.warning(
trade.pair, exception) f"Unable to adjust position of trade for {trade.pair}: {exception}")
def check_and_call_adjust_trade_position(self, trade: Trade): def check_and_call_adjust_trade_position(self, trade: Trade):
""" """
@ -471,6 +471,13 @@ class FreqtradeBot(LoggingMixin):
If the strategy triggers the adjustment, a new order gets issued. If the strategy triggers the adjustment, a new order gets issued.
Once that completes, the existing trade is modified to match new data. Once that completes, the existing trade is modified to match new data.
""" """
if self.strategy.max_entry_position_adjustment > -1:
count_of_buys = trade.nr_of_successful_buys
if count_of_buys > self.strategy.max_entry_position_adjustment:
logger.debug(f"Max adjustment entries for {trade.pair} has been reached.")
return
else:
logger.debug("Max adjustment entries is set to unlimited.")
current_rate = self.exchange.get_rate(trade.pair, refresh=True, side="buy") current_rate = self.exchange.get_rate(trade.pair, refresh=True, side="buy")
current_profit = trade.calc_profit_ratio(current_rate) current_profit = trade.calc_profit_ratio(current_rate)

View File

@ -381,7 +381,12 @@ class Backtesting:
# Check if we need to adjust our current positions # Check if we need to adjust our current positions
if self.strategy.position_adjustment_enable: if self.strategy.position_adjustment_enable:
trade = self._get_adjust_trade_entry_for_candle(trade, sell_row) check_adjust_buy = True
if self.strategy.max_entry_position_adjustment > -1:
count_of_buys = trade.nr_of_successful_buys
check_adjust_buy = (count_of_buys <= self.strategy.max_entry_position_adjustment)
if check_adjust_buy:
trade = self._get_adjust_trade_entry_for_candle(trade, sell_row)
sell_candle_time = sell_row[DATE_IDX].to_pydatetime() sell_candle_time = sell_row[DATE_IDX].to_pydatetime()
sell = self.strategy.should_sell(trade, sell_row[OPEN_IDX], # type: ignore sell = self.strategy.should_sell(trade, sell_row[OPEN_IDX], # type: ignore

View File

@ -97,7 +97,8 @@ class StrategyResolver(IResolver):
("sell_profit_offset", 0.0), ("sell_profit_offset", 0.0),
("disable_dataframe_checks", False), ("disable_dataframe_checks", False),
("ignore_buying_expired_candle_after", 0), ("ignore_buying_expired_candle_after", 0),
("position_adjustment_enable", False) ("position_adjustment_enable", False),
("max_entry_position_adjustment", -1),
] ]
for attribute, default in attributes: for attribute, default in attributes:
StrategyResolver._override_attribute_helper(strategy, config, StrategyResolver._override_attribute_helper(strategy, config,

View File

@ -173,6 +173,8 @@ class ShowConfig(BaseModel):
bot_name: str bot_name: str
state: str state: str
runmode: str runmode: str
position_adjustment_enable: bool
max_entry_position_adjustment: int
class TradeSchema(BaseModel): class TradeSchema(BaseModel):

View File

@ -136,7 +136,12 @@ class RPC:
'ask_strategy': config.get('ask_strategy', {}), 'ask_strategy': config.get('ask_strategy', {}),
'bid_strategy': config.get('bid_strategy', {}), 'bid_strategy': config.get('bid_strategy', {}),
'state': str(botstate), 'state': str(botstate),
'runmode': config['runmode'].value 'runmode': config['runmode'].value,
'position_adjustment_enable': config.get('position_adjustment_enable', False),
'max_entry_position_adjustment': (
config['max_entry_position_adjustment']
if config['max_entry_position_adjustment'] != float('inf')
else -1)
} }
return val return val
@ -247,8 +252,9 @@ class RPC:
profit_str profit_str
] ]
if self._config.get('position_adjustment_enable', False): if self._config.get('position_adjustment_enable', False):
filled_buys = trade.select_filled_orders('buy') max_buy = self._config['max_entry_position_adjustment'] + 1
detail_trade.append(str(len(filled_buys))) filled_buys = trade.nr_of_successful_buys
detail_trade.append(f"{filled_buys}/{max_buy}")
trades_list.append(detail_trade) trades_list.append(detail_trade)
profitcol = "Profit" profitcol = "Profit"
if self._fiat_converter: if self._fiat_converter:

View File

@ -1347,6 +1347,14 @@ class Telegram(RPCHandler):
else: else:
sl_info = f"*Stoploss:* `{val['stoploss']}`\n" sl_info = f"*Stoploss:* `{val['stoploss']}`\n"
if val['position_adjustment_enable']:
pa_info = (
f"*Position adjustment:* On\n"
f"*Max enter position adjustment:* `{val['max_entry_position_adjustment']}`\n"
)
else:
pa_info = "*Position adjustment:* Off\n"
self._send_msg( self._send_msg(
f"*Mode:* `{'Dry-run' if val['dry_run'] else 'Live'}`\n" f"*Mode:* `{'Dry-run' if val['dry_run'] else 'Live'}`\n"
f"*Exchange:* `{val['exchange']}`\n" f"*Exchange:* `{val['exchange']}`\n"
@ -1356,6 +1364,7 @@ class Telegram(RPCHandler):
f"*Ask strategy:* ```\n{json.dumps(val['ask_strategy'])}```\n" f"*Ask strategy:* ```\n{json.dumps(val['ask_strategy'])}```\n"
f"*Bid strategy:* ```\n{json.dumps(val['bid_strategy'])}```\n" f"*Bid strategy:* ```\n{json.dumps(val['bid_strategy'])}```\n"
f"{sl_info}" f"{sl_info}"
f"{pa_info}"
f"*Timeframe:* `{val['timeframe']}`\n" f"*Timeframe:* `{val['timeframe']}`\n"
f"*Strategy:* `{val['strategy']}`\n" f"*Strategy:* `{val['strategy']}`\n"
f"*Current state:* `{val['state']}`" f"*Current state:* `{val['state']}`"

View File

@ -108,6 +108,7 @@ class IStrategy(ABC, HyperStrategyMixin):
# Position adjustment is disabled by default # Position adjustment is disabled by default
position_adjustment_enable: bool = False position_adjustment_enable: bool = False
max_entry_position_adjustment: int = -1
# Number of seconds after which the candle will no longer result in a buy on expired candles # Number of seconds after which the candle will no longer result in a buy on expired candles
ignore_buying_expired_candle_after: int = 0 ignore_buying_expired_candle_after: int = 0

View File

@ -4550,3 +4550,32 @@ def test_position_adjust(mocker, default_conf_usdt, fee) -> None:
# Make sure the closed order is found as the second order. # Make sure the closed order is found as the second order.
order = trade.select_order('buy', False) order = trade.select_order('buy', False)
assert order.order_id == '652' assert order.order_id == '652'
def test_process_open_trade_positions_exception(mocker, default_conf_usdt, fee, caplog) -> None:
default_conf_usdt.update({
"position_adjustment_enable": True,
})
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.check_and_call_adjust_trade_position',
side_effect=DependencyException())
create_mock_trades(fee)
freqtrade.process_open_trade_positions()
assert log_has_re(r"Unable to adjust position of trade for .*", caplog)
def test_check_and_call_adjust_trade_position(mocker, default_conf_usdt, fee, caplog) -> None:
default_conf_usdt.update({
"position_adjustment_enable": True,
"max_entry_position_adjustment": 0,
})
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
create_mock_trades(fee)
caplog.set_level(logging.DEBUG)
freqtrade.process_open_trade_positions()
assert log_has_re(r"Max adjustment entries for .* has been reached\.", caplog)