Refactoring to use strategy based configuration
This commit is contained in:
@@ -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
|
||||
|
@@ -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),
|
||||
|
@@ -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]:
|
||||
|
@@ -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,
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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)
|
||||
|
||||
|
Reference in New Issue
Block a user