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"`.
| `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.
| `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
| `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.
@ -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
| `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
| `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
@ -198,6 +197,7 @@ Values set in the configuration file always overwrite values set in the strategy
* `sell_profit_offset`
* `ignore_roi_if_buy_signal`
* `ignore_buying_expired_candle_after`
* `position_adjustment_enable`
### Configuring amount per trade
@ -594,15 +594,6 @@ export HTTPS_PROXY="http://addr:port"
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
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
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.
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
The `position_adjustment_enable` configuration parameter must be enabled to use adjust_trade_position callback in strategy.
Using unlimited stake amount with DCA orders requires you to also implement `custom_stake_amount` callback to avoid allocating all funcds to initial order.
!!! Warning
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.
stoploss = -0.30
max_dca_orders = 3
# ... 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,
current_time: datetime, current_rate: float, current_profit: 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.
:return float: Stake amount to adjust your trade
"""
if current_profit > -0.05:
return None
@ -285,9 +297,6 @@ class DigDeeperStrategy(IStrategy):
count_of_buys = 0
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":
count_of_buys += 1
@ -298,7 +307,7 @@ class DigDeeperStrategy(IStrategy):
# 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.
# Hope you have a deep wallet!
if 0 < count_of_buys <= 3:
if 0 < count_of_buys <= self.max_dca_orders:
try:
stake_amount = self.wallets.get_trade_stake_amount(pair, None)
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()` 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).
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
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
```
[See Advanced Strategies for an example](strategy-advanced.md#adjust-trade-position)

View File

@ -173,9 +173,6 @@ class Configuration:
if 'sd_notify' in self.args and self.args['sd_notify']:
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:
"""
Extract information for sys.argv and load directory configurations

View File

@ -102,9 +102,6 @@ class FreqtradeBot(LoggingMixin):
self._exit_lock = Lock()
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:
"""
Public method for users of this class (worker, etc.) to send notifications
@ -182,7 +179,7 @@ class FreqtradeBot(LoggingMixin):
self.exit_positions(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()
# Then looking for buy opportunities
@ -460,26 +457,25 @@ class FreqtradeBot(LoggingMixin):
"""
# Walk through each pair and check if it needs changes
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:
self.adjust_trade_position(trade)
self.check_and_call_adjust_trade_position(trade)
except DependencyException as exception:
logger.warning('Unable to adjust position of trade for %s: %s',
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.
If the strategy triggers the adjustment, a new order gets issued.
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_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,
default_retval=None)(
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
self.config['startup_candle_count'] = self.required_startup
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()
def __del__(self):
@ -354,8 +353,12 @@ class Backtesting:
def _get_adjust_trade_entry_for_candle(self, trade: LocalTrade, row: Tuple
) -> 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,
default_retval=None)(
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
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
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,
sell_row: Tuple) -> Optional[LocalTrade]:
# 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)
sell_candle_time = sell_row[DATE_IDX].to_pydatetime()
@ -490,11 +454,16 @@ class Backtesting:
else:
return self._get_sell_trade_entry_for_candle(trade, sell_row)
def _enter_trade(self, pair: str, row: List) -> Optional[LocalTrade]:
try:
stake_amount = self.wallets.get_trade_stake_amount(pair, None)
except DependencyException:
return None
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:
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
propose_rate = strategy_safe_wrapper(self.strategy.custom_entry_price,
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
max_stake_amount = self.wallets.get_available_stake_amount()
stake_amount = strategy_safe_wrapper(self.strategy.custom_stake_amount,
default_retval=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)
if not pos_adjust:
stake_amount = strategy_safe_wrapper(self.strategy.custom_stake_amount,
default_retval=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)
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']
time_in_force = self.strategy.order_time_in_force['sell']
# Confirm trade entry:
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,
time_in_force=time_in_force, current_time=row[DATE_IDX].to_pydatetime()):
return None
if not pos_adjust:
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,
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):
# Enter trade
has_buy_tag = len(row) >= BUY_TAG_IDX + 1
trade = LocalTrade(
pair=pair,
open_rate=propose_rate,
open_date=row[DATE_IDX].to_pydatetime(),
stake_amount=stake_amount,
amount=round(stake_amount / propose_rate, 8),
fee_open=self.fee,
fee_close=self.fee,
is_open=True,
buy_tag=row[BUY_TAG_IDX] if has_buy_tag else None,
exchange='backtesting',
)
amount = round(stake_amount / propose_rate, 8)
if trade is None:
# Enter trade
has_buy_tag = len(row) >= BUY_TAG_IDX + 1
trade = LocalTrade(
pair=pair,
open_rate=propose_rate,
open_date=row[DATE_IDX].to_pydatetime(),
stake_amount=stake_amount,
amount=amount,
fee_open=self.fee,
fee_close=self.fee,
is_open=True,
buy_tag=row[BUY_TAG_IDX] if has_buy_tag else None,
exchange='backtesting',
orders=[]
)
order = Order(
ft_is_open=False,
ft_pair=trade.pair,
@ -547,15 +525,16 @@ class Backtesting:
side="buy",
order_type="market",
status="closed",
price=trade.open_rate,
average=trade.open_rate,
amount=trade.amount,
cost=trade.stake_amount + trade.fee_open
price=propose_rate,
average=propose_rate,
amount=amount,
cost=stake_amount + trade.fee_open
)
trade.orders = []
trade.orders.append(order)
return trade
return None
if pos_adjust:
trade.recalc_trade_from_orders()
return trade
def handle_left_open(self, open_trades: Dict[str, List[LocalTrade]],
data: Dict[str, List[Tuple]]) -> List[LocalTrade]:

View File

@ -96,7 +96,8 @@ class StrategyResolver(IResolver):
("ignore_roi_if_buy_signal", False),
("sell_profit_offset", 0.0),
("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:
StrategyResolver._override_attribute_helper(strategy, config,

View File

@ -721,7 +721,7 @@ class RPC:
# check if pair already has an open pair
trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first()
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}')
# gen stake amount

View File

@ -106,6 +106,9 @@ class IStrategy(ABC, HyperStrategyMixin):
sell_profit_offset: float
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
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']
# 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!
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)
patch_exchange(mocker)
default_conf.update({
"position_adjustment_enable": True,
"stake_amount": 100.0,
"dry_run_wallet": 1000.0,
"strategy": "StrategyTestV2"
@ -28,6 +27,7 @@ def test_backtest_position_adjustment(default_conf, fee, mocker, testdatadir) ->
timerange = TimeRange('date', None, 1517227800, 0)
data = history.load_data(datadir=testdatadir, timeframe='5m', pairs=['UNITTEST/BTC'],
timerange=timerange)
backtesting.strategy.position_adjustment_enable = True
processed = backtesting.strategy.advise_all_indicators(data)
min_date, max_date = get_timerange(processed)
result = backtesting.backtest(

View File

@ -6,6 +6,7 @@ import talib.abstract as ta
from pandas import DataFrame
import freqtrade.vendor.qtpylib.indicators as qtpylib
from freqtrade.exceptions import DependencyException
from freqtrade.persistence import Trade
from freqtrade.strategy.interface import IStrategy
@ -51,6 +52,9 @@ class StrategyTestV2(IStrategy):
'sell': 'gtc',
}
# By default this strategy does not use Position Adjustments
position_adjustment_enable = False
def informative_pairs(self):
"""
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):
if current_profit < -0.0075:
for order in trade.orders:
if order.ft_is_open:
return None
return self.wallets.get_trade_stake_amount(pair, None)
try:
return self.wallets.get_trade_stake_amount(pair, None)
except DependencyException:
pass
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')
@pytest.mark.parametrize("balance_ratio,capital,result1,result2,result3", [
(1, None, 50, 66.66666, 250),
(0.99, None, 49.5, 66.0, 247.5),
(0.50, None, 25, 33.3333, 125),
@pytest.mark.parametrize("balance_ratio,capital,result1,result2", [
(1, None, 50, 66.66666),
(0.99, None, 49.5, 66.0),
(0.50, None, 25, 33.3333),
# Tests with capital ignore balance_ratio
(1, 100, 50, 0.0, 0.0),
(0.99, 200, 50, 66.66666, 50),
(0.99, 150, 50, 50, 37.5),
(0.50, 50, 25, 0.0, 0.0),
(0.50, 10, 5, 0.0, 0.0),
(1, 100, 50, 0.0),
(0.99, 200, 50, 66.66666),
(0.99, 150, 50, 50),
(0.50, 50, 25, 0.0),
(0.50, 10, 5, 0.0),
])
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:
mocker.patch.multiple(
'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')
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', [
(22, 11, 50, 22),