Refactoring to use strategy based configuration
This commit is contained in:
parent
ac690e9215
commit
de79d25caf
@ -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).
|
||||||
|
@ -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]:
|
||||||
@ -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))
|
||||||
|
@ -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
|
|
||||||
|
|
||||||
```
|
|
||||||
|
@ -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
|
||||||
|
@ -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),
|
||||||
|
@ -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,
|
||||||
|
trade: Optional[LocalTrade] = None) -> Optional[LocalTrade]:
|
||||||
|
|
||||||
|
pos_adjust = trade is not None
|
||||||
|
if stake_amount is None:
|
||||||
try:
|
try:
|
||||||
stake_amount = self.wallets.get_trade_stake_amount(pair, None)
|
stake_amount = self.wallets.get_trade_stake_amount(pair, None)
|
||||||
except DependencyException:
|
except DependencyException:
|
||||||
return None
|
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,24 +476,31 @@ 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()
|
||||||
|
|
||||||
|
if not pos_adjust:
|
||||||
stake_amount = strategy_safe_wrapper(self.strategy.custom_stake_amount,
|
stake_amount = strategy_safe_wrapper(self.strategy.custom_stake_amount,
|
||||||
default_retval=stake_amount)(
|
default_retval=stake_amount)(
|
||||||
pair=pair, current_time=row[DATE_IDX].to_pydatetime(), current_rate=propose_rate,
|
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)
|
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 pos_adjust:
|
||||||
if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)(
|
if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)(
|
||||||
pair=pair, order_type=order_type, amount=stake_amount, rate=propose_rate,
|
pair=pair, order_type=order_type, amount=stake_amount, rate=propose_rate,
|
||||||
time_in_force=time_in_force, current_time=row[DATE_IDX].to_pydatetime()):
|
time_in_force=time_in_force, current_time=row[DATE_IDX].to_pydatetime()):
|
||||||
return None
|
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):
|
||||||
|
amount = round(stake_amount / propose_rate, 8)
|
||||||
|
if trade is None:
|
||||||
# Enter trade
|
# Enter trade
|
||||||
has_buy_tag = len(row) >= BUY_TAG_IDX + 1
|
has_buy_tag = len(row) >= BUY_TAG_IDX + 1
|
||||||
trade = LocalTrade(
|
trade = LocalTrade(
|
||||||
@ -532,13 +508,15 @@ class Backtesting:
|
|||||||
open_rate=propose_rate,
|
open_rate=propose_rate,
|
||||||
open_date=row[DATE_IDX].to_pydatetime(),
|
open_date=row[DATE_IDX].to_pydatetime(),
|
||||||
stake_amount=stake_amount,
|
stake_amount=stake_amount,
|
||||||
amount=round(stake_amount / propose_rate, 8),
|
amount=amount,
|
||||||
fee_open=self.fee,
|
fee_open=self.fee,
|
||||||
fee_close=self.fee,
|
fee_close=self.fee,
|
||||||
is_open=True,
|
is_open=True,
|
||||||
buy_tag=row[BUY_TAG_IDX] if has_buy_tag else None,
|
buy_tag=row[BUY_TAG_IDX] if has_buy_tag else None,
|
||||||
exchange='backtesting',
|
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)
|
||||||
|
if pos_adjust:
|
||||||
|
trade.recalc_trade_from_orders()
|
||||||
|
|
||||||
return trade
|
return trade
|
||||||
return None
|
|
||||||
|
|
||||||
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]:
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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(
|
||||||
|
@ -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 None
|
|
||||||
|
|
||||||
return self.wallets.get_trade_stake_amount(pair, None)
|
return self.wallets.get_trade_stake_amount(pair, None)
|
||||||
|
except DependencyException:
|
||||||
|
pass
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
@ -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),
|
||||||
|
Loading…
Reference in New Issue
Block a user