Refactoring to use strategy based configuration

This commit is contained in:
Reigo Reinmets 2021-12-24 12:38:43 +02:00
parent ac690e9215
commit de79d25caf
13 changed files with 117 additions and 200 deletions

View File

@ -83,7 +83,6 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| `stake_amount` | **Required.** Amount of crypto-currency your bot will use for each trade. Set it to `"unlimited"` to allow the bot to use all available balance. [More information below](#configuring-amount-per-trade). <br> **Datatype:** Positive float or `"unlimited"`. | `stake_amount` | **Required.** Amount of crypto-currency your bot will use for each trade. Set it to `"unlimited"` to allow the bot to use all available balance. [More information below](#configuring-amount-per-trade). <br> **Datatype:** Positive float or `"unlimited"`.
| `tradable_balance_ratio` | Ratio of the total account balance the bot is allowed to trade. [More information below](#configuring-amount-per-trade). <br>*Defaults to `0.99` 99%).*<br> **Datatype:** Positive float between `0.1` and `1.0`. | `tradable_balance_ratio` | Ratio of the total account balance the bot is allowed to trade. [More information below](#configuring-amount-per-trade). <br>*Defaults to `0.99` 99%).*<br> **Datatype:** Positive float between `0.1` and `1.0`.
| `available_capital` | Available starting capital for the bot. Useful when running multiple bots on the same exchange account.[More information below](#configuring-amount-per-trade). <br> **Datatype:** Positive float. | `available_capital` | Available starting capital for the bot. Useful when running multiple bots on the same exchange account.[More information below](#configuring-amount-per-trade). <br> **Datatype:** Positive float.
| `base_stake_amount_ratio` | When using position adjustment with unlimited stakes, the strategy often requires that some funds are left for additional buy orders. You can define the ratio that the initial buy order can use from the calculated unlimited stake amount. [More information below](#configuring-amount-per-trade). <br>*Defaults to `1.0`.* <br> **Datatype:** Float (as ratio)
| `amend_last_stake_amount` | Use reduced last stake amount if necessary. [More information below](#configuring-amount-per-trade). <br>*Defaults to `false`.* <br> **Datatype:** Boolean | `amend_last_stake_amount` | Use reduced last stake amount if necessary. [More information below](#configuring-amount-per-trade). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
| `last_stake_amount_min_ratio` | Defines minimum stake amount that has to be left and executed. Applies only to the last stake amount when it's amended to a reduced value (i.e. if `amend_last_stake_amount` is set to `true`). [More information below](#configuring-amount-per-trade). <br>*Defaults to `0.5`.* <br> **Datatype:** Float (as ratio) | `last_stake_amount_min_ratio` | Defines minimum stake amount that has to be left and executed. Applies only to the last stake amount when it's amended to a reduced value (i.e. if `amend_last_stake_amount` is set to `true`). [More information below](#configuring-amount-per-trade). <br>*Defaults to `0.5`.* <br> **Datatype:** Float (as ratio)
| `amount_reserve_percent` | Reserve some amount in min pair stake amount. The bot will reserve `amount_reserve_percent` + stoploss value when calculating min pair stake amount in order to avoid possible trade refusals. <br>*Defaults to `0.05` (5%).* <br> **Datatype:** Positive Float as ratio. | `amount_reserve_percent` | Reserve some amount in min pair stake amount. The bot will reserve `amount_reserve_percent` + stoploss value when calculating min pair stake amount in order to avoid possible trade refusals. <br>*Defaults to `0.05` (5%).* <br> **Datatype:** Positive Float as ratio.
@ -173,7 +172,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| `user_data_dir` | Directory containing user data. <br> *Defaults to `./user_data/`*. <br> **Datatype:** String | `user_data_dir` | Directory containing user data. <br> *Defaults to `./user_data/`*. <br> **Datatype:** String
| `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 below. <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 `true`.*<br> **Datatype:** Boolean
### Parameters in the strategy ### Parameters in the strategy
@ -198,6 +197,7 @@ Values set in the configuration file always overwrite values set in the strategy
* `sell_profit_offset` * `sell_profit_offset`
* `ignore_roi_if_buy_signal` * `ignore_roi_if_buy_signal`
* `ignore_buying_expired_candle_after` * `ignore_buying_expired_candle_after`
* `position_adjustment_enable`
### Configuring amount per trade ### Configuring amount per trade
@ -594,15 +594,6 @@ export HTTPS_PROXY="http://addr:port"
freqtrade freqtrade
``` ```
### Understand position_adjustment_enable
The `position_adjustment_enable` configuration parameter enables the usage of `adjust_trade_position()` callback in strategy.
For performance reasons, it's disabled by default, and freqtrade will show a warning message on startup if enabled.
Enabling this does nothing unless the strategy also implements `adjust_trade_position()` callback.
See [the strategy callbacks](strategy-callbacks.md) for details on usage.
## Next step ## Next step
Now you have configured your config.json, the next step is to [start your bot](bot-usage.md). Now you have configured your config.json, the next step is to [start your bot](bot-usage.md).

View File

@ -232,14 +232,13 @@ merged_frame = pd.concat(frames, axis=1)
## Adjust trade position ## Adjust trade position
The `position_adjustment_enable` strategy property enables the usage of `adjust_trade_position()` callback in strategy.
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 to manage risk with DCA (Dollar Cost Averaging) for example. `adjust_trade_position()` can be used to perform additional orders to manage risk with DCA (Dollar Cost Averaging) for example.
The strategy is expected to return a stake_amount if and when an additional buy order should be made (position is increased). The strategy is expected to return a stake_amount if and when an additional buy order should be made (position is increased).
If there is not enough funds in the wallet then nothing will happen. If there is not enough funds in the wallet then nothing will happen.
Additional orders also mean additional fees and those orders don't count towards `max_open_trades`. Additional orders also mean additional fees and those orders don't count towards `max_open_trades`.
Unlimited stake amount with trade position increasing is highly not recommended as your DCA orders would compete with your normal trade open orders. Using unlimited stake amount with DCA orders requires you to also implement `custom_stake_amount` callback to avoid allocating all funcds to initial order.
!!! Note
The `position_adjustment_enable` configuration parameter must be enabled to use adjust_trade_position callback in strategy.
!!! Warning !!! Warning
Stoploss is still calculated from the initial opening price, not averaged price. Stoploss is still calculated from the initial opening price, not averaged price.
@ -253,8 +252,21 @@ class DigDeeperStrategy(IStrategy):
# Attempts to handle large drops with DCA. High stoploss is required. # Attempts to handle large drops with DCA. High stoploss is required.
stoploss = -0.30 stoploss = -0.30
max_dca_orders = 3
# ... populate_* methods # ... populate_* methods
# Let unlimited stakes leave funds open for DCA orders
def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
proposed_stake: float, min_stake: float, max_stake: float,
**kwargs) -> float:
if self.config['stake_amount'] == 'unlimited':
return proposed_stake / 5.5
# Use default stake amount.
return proposed_stake
def adjust_trade_position(self, pair: str, trade: Trade, def adjust_trade_position(self, pair: str, trade: Trade,
current_time: datetime, current_rate: float, current_profit: float, current_time: datetime, current_rate: float, current_profit: float,
**kwargs) -> Optional[float]: **kwargs) -> Optional[float]:
@ -270,7 +282,7 @@ class DigDeeperStrategy(IStrategy):
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return float: Stake amount to adjust your trade :return float: Stake amount to adjust your trade
""" """
if current_profit > -0.05: if current_profit > -0.05:
return None return None
@ -285,9 +297,6 @@ class DigDeeperStrategy(IStrategy):
count_of_buys = 0 count_of_buys = 0
for order in trade.orders: for order in trade.orders:
# Instantly stop when there's an open order
if order.ft_is_open:
return None
if order.ft_order_side == 'buy' and order.status == "closed": if order.ft_order_side == 'buy' and order.status == "closed":
count_of_buys += 1 count_of_buys += 1
@ -298,7 +307,7 @@ class DigDeeperStrategy(IStrategy):
# If that falles once again down to -5%, we buy 1.75x more # If that falles once again down to -5%, we buy 1.75x more
# 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.
# Hope you have a deep wallet! # Hope you have a deep wallet!
if 0 < count_of_buys <= 3: if 0 < count_of_buys <= self.max_dca_orders:
try: try:
stake_amount = self.wallets.get_trade_stake_amount(pair, None) stake_amount = self.wallets.get_trade_stake_amount(pair, None)
stake_amount = stake_amount * (1 + (count_of_buys * 0.25)) stake_amount = stake_amount * (1 + (count_of_buys * 0.25))

View File

@ -572,54 +572,10 @@ class AwesomeStrategy(IStrategy):
## Adjust trade position ## Adjust trade position
`adjust_trade_position()` can be used to perform additional orders to manage risk with DCA (Dollar Cost Averaging). The `position_adjustment_enable` strategy property enables the usage of `adjust_trade_position()` callback in strategy.
For performance reasons, it's disabled by default and freqtrade will show a warning message on startup if enabled.
Enabling this does nothing unless the strategy also implements `adjust_trade_position()` callback.
`adjust_trade_position()` can be used to perform additional orders, for example to manage risk with DCA (Dollar Cost Averaging).
The strategy is expected to return a stake_amount if and when an additional buy order should be made (position is increased). The strategy is expected to return a stake_amount if and when an additional buy order should be made (position is increased).
If there is not enough funds in the wallet then nothing will happen.
Additional orders also mean additional fees and those orders don't count towards `max_open_trades`.
Unlimited stake amount with trade position increasing is highly not recommended as your DCA orders would compete with your normal trade open orders.
!!! Note [See Advanced Strategies for an example](strategy-advanced.md#adjust-trade-position)
Current implementation does not support decreasing position size with partial sales!
!!! Tip
The `position_adjustment_enable` configuration parameter must be enabled to use adjust_trade_position callback in strategy.
!!! Warning
Stoploss is still calculated from the initial opening price, not averaged price.
So if you do 3 additional buys at -7% and have a stoploss at -10% then you will most likely trigger stoploss while the UI will be showing you an average profit of -3%.
``` python
from freqtrade.persistence import Trade
class AwesomeStrategy(IStrategy):
# ... populate_* methods
def adjust_trade_position(self, pair: str, trade: Trade,
current_time: datetime, current_rate: float, current_profit: float,
**kwargs) -> Optional[float]:
"""
Custom trade adjustment logic, returning the stake amount that a trade should be increased.
This means extra buy orders with additional fees.
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
When not implemented by a strategy, returns None
:param pair: Pair that's currently analyzed
:param trade: trade object.
:param current_time: datetime object, containing the current datetime
:param current_rate: Rate, calculated based on pricing settings in ask_strategy.
:param current_profit: Current profit (as ratio), calculated based on current_rate.
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return float: Stake amount to adjust your trade
"""
# Example: If 10% loss / -10% profit then buy more the same amount we had before.
if current_profit < -0.10:
return trade.stake_amount
return None
```

View File

@ -173,9 +173,6 @@ class Configuration:
if 'sd_notify' in self.args and self.args['sd_notify']: if 'sd_notify' in self.args and self.args['sd_notify']:
config['internals'].update({'sd_notify': True}) config['internals'].update({'sd_notify': True})
if config.get('position_adjustment_enable', False):
logger.warning('`position_adjustment` has been enabled for strategy.')
def _process_datadir_options(self, config: Dict[str, Any]) -> None: def _process_datadir_options(self, config: Dict[str, Any]) -> None:
""" """
Extract information for sys.argv and load directory configurations Extract information for sys.argv and load directory configurations

View File

@ -102,9 +102,6 @@ class FreqtradeBot(LoggingMixin):
self._exit_lock = Lock() self._exit_lock = Lock()
LoggingMixin.__init__(self, logger, timeframe_to_seconds(self.strategy.timeframe)) LoggingMixin.__init__(self, logger, timeframe_to_seconds(self.strategy.timeframe))
# Is Position Adjustment enabled?
self.position_adjustment = bool(self.config.get('position_adjustment_enable', False))
def notify_status(self, msg: str) -> None: def notify_status(self, msg: str) -> None:
""" """
Public method for users of this class (worker, etc.) to send notifications Public method for users of this class (worker, etc.) to send notifications
@ -182,7 +179,7 @@ class FreqtradeBot(LoggingMixin):
self.exit_positions(trades) self.exit_positions(trades)
# Check if we need to adjust our current positions before attempting to buy new trades. # Check if we need to adjust our current positions before attempting to buy new trades.
if self.position_adjustment: if self.strategy.position_adjustment_enable:
self.process_open_trade_positions() self.process_open_trade_positions()
# Then looking for buy opportunities # Then looking for buy opportunities
@ -460,26 +457,25 @@ class FreqtradeBot(LoggingMixin):
""" """
# Walk through each pair and check if it needs changes # Walk through each pair and check if it needs changes
for trade in Trade.get_open_trades(): for trade in Trade.get_open_trades():
# If there is any open orders, wait for them to finish.
for order in trade.orders:
if order.ft_is_open:
break
try: try:
self.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('Unable to adjust position of trade for %s: %s',
trade.pair, exception) trade.pair, exception)
def adjust_trade_position(self, trade: Trade): def check_and_call_adjust_trade_position(self, trade: Trade):
""" """
Check the implemented trading strategy for adjustment command. Check the implemented trading strategy for adjustment command.
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 there is any open orders, wait for them to finish.
for order in trade.orders:
if order.ft_is_open:
return
logger.debug(f"adjust_trade_position for pair {trade.pair}")
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)
logger.debug(f"Calling adjust_trade_position for pair {trade.pair}")
stake_amount = strategy_safe_wrapper(self.strategy.adjust_trade_position, stake_amount = strategy_safe_wrapper(self.strategy.adjust_trade_position,
default_retval=None)( default_retval=None)(
pair=trade.pair, trade=trade, current_time=datetime.now(timezone.utc), pair=trade.pair, trade=trade, current_time=datetime.now(timezone.utc),

View File

@ -118,7 +118,6 @@ class Backtesting:
# Add maximum startup candle count to configuration for informative pairs support # Add maximum startup candle count to configuration for informative pairs support
self.config['startup_candle_count'] = self.required_startup self.config['startup_candle_count'] = self.required_startup
self.exchange.validate_required_startup_candles(self.required_startup, self.timeframe) self.exchange.validate_required_startup_candles(self.required_startup, self.timeframe)
self.position_adjustment = bool(self.config.get('position_adjustment_enable', False))
self.init_backtest() self.init_backtest()
def __del__(self): def __del__(self):
@ -354,8 +353,12 @@ class Backtesting:
def _get_adjust_trade_entry_for_candle(self, trade: LocalTrade, row: Tuple def _get_adjust_trade_entry_for_candle(self, trade: LocalTrade, row: Tuple
) -> LocalTrade: ) -> LocalTrade:
current_profit = trade.calc_profit_ratio(row[OPEN_IDX]) # If there is any open orders, wait for them to finish.
for order in trade.orders:
if order.ft_is_open:
return trade
current_profit = trade.calc_profit_ratio(row[OPEN_IDX])
stake_amount = strategy_safe_wrapper(self.strategy.adjust_trade_position, stake_amount = strategy_safe_wrapper(self.strategy.adjust_trade_position,
default_retval=None)( default_retval=None)(
pair=trade.pair, trade=trade, current_time=row[DATE_IDX].to_pydatetime(), pair=trade.pair, trade=trade, current_time=row[DATE_IDX].to_pydatetime(),
@ -363,56 +366,17 @@ class Backtesting:
# Check if we should increase our position # Check if we should increase our position
if stake_amount is not None and stake_amount > 0.0: if stake_amount is not None and stake_amount > 0.0:
return self._execute_trade_position_change(trade, row, stake_amount) pos_trade = self._enter_trade(trade.pair, row, stake_amount, trade)
if pos_trade is not None:
return pos_trade
return trade return trade
def _execute_trade_position_change(self, trade: LocalTrade, row: Tuple,
stake_amount: float) -> LocalTrade:
current_price = row[OPEN_IDX]
propose_rate = min(max(current_price, row[LOW_IDX]), row[HIGH_IDX])
available_amount = self.wallets.get_available_stake_amount()
try:
min_stake_amount = self.exchange.get_min_pair_stake_amount(
trade.pair, propose_rate, -0.05) or 0
stake_amount = self.wallets.validate_stake_amount(trade.pair,
stake_amount, min_stake_amount)
stake_amount = self.wallets._check_available_stake_amount(stake_amount,
available_amount)
except DependencyException:
logger.debug(f"{trade.pair} adjustment failed, "
f"wallet is smaller than asked stake {stake_amount}")
return trade
amount = stake_amount / current_price
if amount <= 0:
logger.debug(f"{trade.pair} adjustment failed, amount ended up being zero {amount}")
return trade
buy_order = Order(
ft_is_open=False,
ft_pair=trade.pair,
symbol=trade.pair,
ft_order_side="buy",
side="buy",
order_type="market",
status="closed",
price=propose_rate,
average=propose_rate,
amount=amount,
cost=stake_amount
)
trade.orders.append(buy_order)
trade.recalc_trade_from_orders()
self.wallets.update()
return trade
def _get_sell_trade_entry_for_candle(self, trade: LocalTrade, def _get_sell_trade_entry_for_candle(self, trade: LocalTrade,
sell_row: Tuple) -> Optional[LocalTrade]: sell_row: Tuple) -> Optional[LocalTrade]:
# Check if we need to adjust our current positions # Check if we need to adjust our current positions
if self.position_adjustment: if self.strategy.position_adjustment_enable:
trade = self._get_adjust_trade_entry_for_candle(trade, sell_row) 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()
@ -490,11 +454,16 @@ class Backtesting:
else: else:
return self._get_sell_trade_entry_for_candle(trade, sell_row) return self._get_sell_trade_entry_for_candle(trade, sell_row)
def _enter_trade(self, pair: str, row: List) -> Optional[LocalTrade]: def _enter_trade(self, pair: str, row: Tuple, stake_amount: Optional[float] = None,
try: trade: Optional[LocalTrade] = None) -> Optional[LocalTrade]:
stake_amount = self.wallets.get_trade_stake_amount(pair, None)
except DependencyException: pos_adjust = trade is not None
return None if stake_amount is None:
try:
stake_amount = self.wallets.get_trade_stake_amount(pair, None)
except DependencyException:
return trade
# let's call the custom entry price, using the open price as default price # let's call the custom entry price, using the open price as default price
propose_rate = strategy_safe_wrapper(self.strategy.custom_entry_price, propose_rate = strategy_safe_wrapper(self.strategy.custom_entry_price,
default_retval=row[OPEN_IDX])( default_retval=row[OPEN_IDX])(
@ -507,38 +476,47 @@ class Backtesting:
min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, propose_rate, -0.05) or 0 min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, propose_rate, -0.05) or 0
max_stake_amount = self.wallets.get_available_stake_amount() max_stake_amount = self.wallets.get_available_stake_amount()
stake_amount = strategy_safe_wrapper(self.strategy.custom_stake_amount, if not pos_adjust:
default_retval=stake_amount)( stake_amount = strategy_safe_wrapper(self.strategy.custom_stake_amount,
pair=pair, current_time=row[DATE_IDX].to_pydatetime(), current_rate=propose_rate, default_retval=stake_amount)(
proposed_stake=stake_amount, min_stake=min_stake_amount, max_stake=max_stake_amount) pair=pair, current_time=row[DATE_IDX].to_pydatetime(), current_rate=propose_rate,
proposed_stake=stake_amount, min_stake=min_stake_amount, max_stake=max_stake_amount)
stake_amount = self.wallets.validate_stake_amount(pair, stake_amount, min_stake_amount) stake_amount = self.wallets.validate_stake_amount(pair, stake_amount, min_stake_amount)
if not stake_amount: if not stake_amount:
return None # In case of pos adjust, still return the original trade
# If not pos adjust, trade is None
return trade
order_type = self.strategy.order_types['buy'] order_type = self.strategy.order_types['buy']
time_in_force = self.strategy.order_time_in_force['sell'] time_in_force = self.strategy.order_time_in_force['sell']
# Confirm trade entry: # Confirm trade entry:
if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)( if not pos_adjust:
pair=pair, order_type=order_type, amount=stake_amount, rate=propose_rate, if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)(
time_in_force=time_in_force, current_time=row[DATE_IDX].to_pydatetime()): pair=pair, order_type=order_type, amount=stake_amount, rate=propose_rate,
return None time_in_force=time_in_force, current_time=row[DATE_IDX].to_pydatetime()):
return None
if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount): if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount):
# Enter trade amount = round(stake_amount / propose_rate, 8)
has_buy_tag = len(row) >= BUY_TAG_IDX + 1 if trade is None:
trade = LocalTrade( # Enter trade
pair=pair, has_buy_tag = len(row) >= BUY_TAG_IDX + 1
open_rate=propose_rate, trade = LocalTrade(
open_date=row[DATE_IDX].to_pydatetime(), pair=pair,
stake_amount=stake_amount, open_rate=propose_rate,
amount=round(stake_amount / propose_rate, 8), open_date=row[DATE_IDX].to_pydatetime(),
fee_open=self.fee, stake_amount=stake_amount,
fee_close=self.fee, amount=amount,
is_open=True, fee_open=self.fee,
buy_tag=row[BUY_TAG_IDX] if has_buy_tag else None, fee_close=self.fee,
exchange='backtesting', is_open=True,
) buy_tag=row[BUY_TAG_IDX] if has_buy_tag else None,
exchange='backtesting',
orders=[]
)
order = Order( order = Order(
ft_is_open=False, ft_is_open=False,
ft_pair=trade.pair, ft_pair=trade.pair,
@ -547,15 +525,16 @@ class Backtesting:
side="buy", side="buy",
order_type="market", order_type="market",
status="closed", status="closed",
price=trade.open_rate, price=propose_rate,
average=trade.open_rate, average=propose_rate,
amount=trade.amount, amount=amount,
cost=trade.stake_amount + trade.fee_open cost=stake_amount + trade.fee_open
) )
trade.orders = []
trade.orders.append(order) trade.orders.append(order)
return trade if pos_adjust:
return None trade.recalc_trade_from_orders()
return trade
def handle_left_open(self, open_trades: Dict[str, List[LocalTrade]], def handle_left_open(self, open_trades: Dict[str, List[LocalTrade]],
data: Dict[str, List[Tuple]]) -> List[LocalTrade]: data: Dict[str, List[Tuple]]) -> List[LocalTrade]:

View File

@ -96,7 +96,8 @@ class StrategyResolver(IResolver):
("ignore_roi_if_buy_signal", False), ("ignore_roi_if_buy_signal", False),
("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)
] ]
for attribute, default in attributes: for attribute, default in attributes:
StrategyResolver._override_attribute_helper(strategy, config, StrategyResolver._override_attribute_helper(strategy, config,

View File

@ -721,7 +721,7 @@ class RPC:
# check if pair already has an open pair # check if pair already has an open pair
trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first() trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first()
if trade: if trade:
if not self._config.get('position_adjustment_enable', False): if not self._freqtrade.strategy.position_adjustment_enable:
raise RPCException(f'position for {pair} already open - id: {trade.id}') raise RPCException(f'position for {pair} already open - id: {trade.id}')
# gen stake amount # gen stake amount

View File

@ -106,6 +106,9 @@ class IStrategy(ABC, HyperStrategyMixin):
sell_profit_offset: float sell_profit_offset: float
ignore_roi_if_buy_signal: bool ignore_roi_if_buy_signal: bool
# Position adjustment is disabled by default
position_adjustment_enable: bool = False
# 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

@ -185,16 +185,6 @@ class Wallets:
possible_stake = (available_amount + val_tied_up) / self._config['max_open_trades'] possible_stake = (available_amount + val_tied_up) / self._config['max_open_trades']
# Position Adjustment dynamic base order size
try:
if self._config.get('position_adjustment_enable', False):
base_stake_amount_ratio = self._config.get('base_stake_amount_ratio', 1.0)
if base_stake_amount_ratio > 0.0:
possible_stake = possible_stake * base_stake_amount_ratio
except Exception as e:
logger.warning("Invalid base_stake_amount_ratio", e)
return 0
# Theoretical amount can be above available amount - therefore limit to available amount! # Theoretical amount can be above available amount - therefore limit to available amount!
return min(possible_stake, available_amount) return min(possible_stake, available_amount)

View File

@ -17,7 +17,6 @@ def test_backtest_position_adjustment(default_conf, fee, mocker, testdatadir) ->
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
patch_exchange(mocker) patch_exchange(mocker)
default_conf.update({ default_conf.update({
"position_adjustment_enable": True,
"stake_amount": 100.0, "stake_amount": 100.0,
"dry_run_wallet": 1000.0, "dry_run_wallet": 1000.0,
"strategy": "StrategyTestV2" "strategy": "StrategyTestV2"
@ -28,6 +27,7 @@ def test_backtest_position_adjustment(default_conf, fee, mocker, testdatadir) ->
timerange = TimeRange('date', None, 1517227800, 0) timerange = TimeRange('date', None, 1517227800, 0)
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)
backtesting.strategy.position_adjustment_enable = True
processed = backtesting.strategy.advise_all_indicators(data) processed = backtesting.strategy.advise_all_indicators(data)
min_date, max_date = get_timerange(processed) min_date, max_date = get_timerange(processed)
result = backtesting.backtest( result = backtesting.backtest(

View File

@ -6,6 +6,7 @@ import talib.abstract as ta
from pandas import DataFrame from pandas import DataFrame
import freqtrade.vendor.qtpylib.indicators as qtpylib import freqtrade.vendor.qtpylib.indicators as qtpylib
from freqtrade.exceptions import DependencyException
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.strategy.interface import IStrategy from freqtrade.strategy.interface import IStrategy
@ -51,6 +52,9 @@ class StrategyTestV2(IStrategy):
'sell': 'gtc', 'sell': 'gtc',
} }
# By default this strategy does not use Position Adjustments
position_adjustment_enable = False
def informative_pairs(self): def informative_pairs(self):
""" """
Define additional, informative pair/interval combinations to be cached from the exchange. Define additional, informative pair/interval combinations to be cached from the exchange.
@ -162,10 +166,9 @@ class StrategyTestV2(IStrategy):
current_rate: float, current_profit: float, **kwargs): current_rate: float, current_profit: float, **kwargs):
if current_profit < -0.0075: if current_profit < -0.0075:
for order in trade.orders: try:
if order.ft_is_open: return self.wallets.get_trade_stake_amount(pair, None)
return None except DependencyException:
pass
return self.wallets.get_trade_stake_amount(pair, None)
return None return None

View File

@ -121,19 +121,19 @@ def test_get_trade_stake_amount_no_stake_amount(default_conf, mocker) -> None:
freqtrade.wallets.get_trade_stake_amount('ETH/BTC') freqtrade.wallets.get_trade_stake_amount('ETH/BTC')
@pytest.mark.parametrize("balance_ratio,capital,result1,result2,result3", [ @pytest.mark.parametrize("balance_ratio,capital,result1,result2", [
(1, None, 50, 66.66666, 250), (1, None, 50, 66.66666),
(0.99, None, 49.5, 66.0, 247.5), (0.99, None, 49.5, 66.0),
(0.50, None, 25, 33.3333, 125), (0.50, None, 25, 33.3333),
# Tests with capital ignore balance_ratio # Tests with capital ignore balance_ratio
(1, 100, 50, 0.0, 0.0), (1, 100, 50, 0.0),
(0.99, 200, 50, 66.66666, 50), (0.99, 200, 50, 66.66666),
(0.99, 150, 50, 50, 37.5), (0.99, 150, 50, 50),
(0.50, 50, 25, 0.0, 0.0), (0.50, 50, 25, 0.0),
(0.50, 10, 5, 0.0, 0.0), (0.50, 10, 5, 0.0),
]) ])
def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, balance_ratio, capital, def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, balance_ratio, capital,
result1, result2, result3, limit_buy_order_open, result1, result2, limit_buy_order_open,
fee, mocker) -> None: fee, mocker) -> None:
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
@ -179,14 +179,6 @@ def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, balance_r
result = freqtrade.wallets.get_trade_stake_amount('NEO/USDT') result = freqtrade.wallets.get_trade_stake_amount('NEO/USDT')
assert result == 0 assert result == 0
freqtrade.config['max_open_trades'] = 2
freqtrade.config['dry_run_wallet'] = 1000
freqtrade.wallets.start_cap = 1000
freqtrade.config['position_adjustment_enable'] = True
freqtrade.config['base_stake_amount_ratio'] = 0.5
result = freqtrade.wallets.get_trade_stake_amount('LTC/USDT')
assert result == result3
@pytest.mark.parametrize('stake_amount,min_stake_amount,max_stake_amount,expected', [ @pytest.mark.parametrize('stake_amount,min_stake_amount,max_stake_amount,expected', [
(22, 11, 50, 22), (22, 11, 50, 22),