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

@@ -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)