Merge pull request #6400 from freqtrade/short_dca

trade-adjustment for short trades
This commit is contained in:
Matthias 2022-02-28 19:47:50 +01:00 committed by GitHub
commit e005439720
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 191 additions and 74 deletions

View File

@ -593,6 +593,8 @@ Additional orders also result in additional fees and those orders don't count to
This callback is **not** called when there is an open order (either buy or sell) waiting for execution, or when you have reached the maximum amount of extra buys that you have set on `max_entry_position_adjustment`.
`adjust_trade_position()` is called very frequently for the duration of a trade, so you must keep your implementation as performant as possible.
Position adjustments will always be applied in the direction of the trade, so a positive value will always increase your position, no matter if it's a long or short trade. Modifications to leverage are not possible.
!!! Note "About stake size"
Using fixed stake size means it will be the amount used for the first order, just like without position adjustment.
If you wish to buy additional orders with DCA, then make sure to leave enough funds in the wallet for that.
@ -663,7 +665,7 @@ class DigDeeperStrategy(IStrategy):
return None
filled_buys = trade.select_filled_orders('buy')
count_of_buys = trade.nr_of_successful_buys
count_of_entries = trade.nr_of_successful_entries
# Allow up to 3 additional increasingly larger buys (4 in total)
# Initial buy is 1x
# If that falls to -5% profit, we buy 1.25x more, average profit should increase to roughly -2.2%
@ -676,7 +678,7 @@ class DigDeeperStrategy(IStrategy):
# This returns first order stake size
stake_amount = filled_buys[0].cost
# This then calculates current safety order size
stake_amount = stake_amount * (1 + (count_of_buys * 0.25))
stake_amount = stake_amount * (1 + (count_of_entries * 0.25))
return stake_amount
except Exception as exception:
return None

View File

@ -103,7 +103,6 @@ class FreqtradeBot(LoggingMixin):
self._exit_lock = Lock()
LoggingMixin.__init__(self, logger, timeframe_to_seconds(self.strategy.timeframe))
self.liquidation_buffer = float(self.config.get('liquidation_buffer', '0.05'))
self.trading_mode: TradingMode = self.config.get('trading_mode', TradingMode.SPOT)
self.margin_mode_type: Optional[MarginMode] = None
if 'margin_mode' in self.config:
@ -510,7 +509,7 @@ class FreqtradeBot(LoggingMixin):
"""
# TODO-lev: Check what changes are necessary for DCA in relation to shorts.
if self.strategy.max_entry_position_adjustment > -1:
count_of_buys = trade.nr_of_successful_buys
count_of_buys = trade.nr_of_successful_entries
if count_of_buys > self.strategy.max_entry_position_adjustment:
logger.debug(f"Max adjustment entries for {trade.pair} has been reached.")
return
@ -533,7 +532,7 @@ class FreqtradeBot(LoggingMixin):
if stake_amount is not None and stake_amount > 0.0:
# We should increase our position
self.execute_entry(trade.pair, stake_amount, trade=trade)
self.execute_entry(trade.pair, stake_amount, trade=trade, is_short=trade.is_short)
if stake_amount is not None and stake_amount < 0.0:
# We should decrease our position
@ -643,18 +642,21 @@ class FreqtradeBot(LoggingMixin):
if not stake_amount:
return False
max_leverage = self.exchange.get_max_leverage(pair, stake_amount)
leverage = strategy_safe_wrapper(self.strategy.leverage, default_retval=1.0)(
pair=pair,
current_time=datetime.now(timezone.utc),
current_rate=enter_limit_requested,
proposed_leverage=1.0,
max_leverage=max_leverage,
side=trade_side,
) if self.trading_mode != TradingMode.SPOT else 1.0
# Cap leverage between 1.0 and max_leverage.
leverage = min(max(leverage, 1.0), max_leverage)
if not pos_adjust:
max_leverage = self.exchange.get_max_leverage(pair, stake_amount)
leverage = strategy_safe_wrapper(self.strategy.leverage, default_retval=1.0)(
pair=pair,
current_time=datetime.now(timezone.utc),
current_rate=enter_limit_requested,
proposed_leverage=1.0,
max_leverage=max_leverage,
side=trade_side,
) if self.trading_mode != TradingMode.SPOT else 1.0
# Cap leverage between 1.0 and max_leverage.
leverage = min(max(leverage, 1.0), max_leverage)
else:
# Changing leverage currently not possible
leverage = trade.leverage if trade else 1.0
if pos_adjust:
logger.info(f"Position adjust: about to create a new order for {pair} with stake: "
f"{stake_amount} for {trade}")
@ -724,6 +726,7 @@ class FreqtradeBot(LoggingMixin):
amount = safe_value_fallback(order, 'filled', 'amount')
enter_limit_filled_price = safe_value_fallback(order, 'average', 'price')
# TODO: this might be unnecessary, as we're calling it in update_trade_state.
interest_rate, isolated_liq = self.leverage_prep(
leverage=leverage,
pair=pair,
@ -1596,9 +1599,19 @@ class FreqtradeBot(LoggingMixin):
Trade.commit()
if order['status'] in constants.NON_OPEN_EXCHANGE_STATES:
# If a buy order was closed, force update on stoploss on exchange
if order.get('side', None) == 'buy':
# If a entry order was closed, force update on stoploss on exchange
if order.get('side', None) == trade.enter_side:
trade = self.cancel_stoploss_on_exchange(trade)
# TODO: Margin will need to use interest_rate as well.
_, isolated_liq = self.leverage_prep(
leverage=trade.leverage,
pair=trade.pair,
amount=trade.amount,
open_rate=trade.open_rate,
is_short=trade.is_short
)
if isolated_liq:
trade.set_isolated_liq(isolated_liq)
# Updating wallets when order is closed
self.wallets.update()
@ -1607,7 +1620,7 @@ class FreqtradeBot(LoggingMixin):
self._notify_exit(trade, '', True)
self.handle_protections(trade.pair)
elif send_msg and not trade.open_order_id:
# Buy fill
# Enter fill
self._notify_enter(trade, order, fill=True)
return False

View File

@ -464,11 +464,11 @@ class Backtesting:
# Check if we need to adjust our current positions
if self.strategy.position_adjustment_enable:
check_adjust_buy = True
check_adjust_entry = True
if self.strategy.max_entry_position_adjustment > -1:
count_of_buys = trade.nr_of_successful_buys
check_adjust_buy = (count_of_buys <= self.strategy.max_entry_position_adjustment)
if check_adjust_buy:
entry_count = trade.nr_of_successful_entries
check_adjust_entry = (entry_count <= self.strategy.max_entry_position_adjustment)
if check_adjust_entry:
trade = self._get_adjust_trade_entry_for_candle(trade, sell_row)
sell_candle_time: datetime = sell_row[DATE_IDX].to_pydatetime()
@ -639,17 +639,20 @@ class Backtesting:
# If not pos adjust, trade is None
return trade
max_leverage = self.exchange.get_max_leverage(pair, stake_amount)
leverage = strategy_safe_wrapper(self.strategy.leverage, default_retval=1.0)(
pair=pair,
current_time=current_time,
current_rate=row[OPEN_IDX],
proposed_leverage=1.0,
max_leverage=max_leverage,
side=direction,
) if self._can_short else 1.0
# Cap leverage between 1.0 and max_leverage.
leverage = min(max(leverage, 1.0), max_leverage)
if not pos_adjust:
max_leverage = self.exchange.get_max_leverage(pair, stake_amount)
leverage = strategy_safe_wrapper(self.strategy.leverage, default_retval=1.0)(
pair=pair,
current_time=current_time,
current_rate=row[OPEN_IDX],
proposed_leverage=1.0,
max_leverage=max_leverage,
side=direction,
) if self._can_short else 1.0
# Cap leverage between 1.0 and max_leverage.
leverage = min(max(leverage, 1.0), max_leverage)
else:
leverage = trade.leverage if trade else 1.0
order_type = self.strategy.order_types['buy']
time_in_force = self.strategy.order_time_in_force['buy']
@ -729,7 +732,7 @@ class Backtesting:
for pair in open_trades.keys():
if len(open_trades[pair]) > 0:
for trade in open_trades[pair]:
if trade.open_order_id and trade.nr_of_successful_buys == 0:
if trade.open_order_id and trade.nr_of_successful_entries == 0:
# Ignore trade if buy-order did not fill yet
continue
sell_row = data[pair][-1]
@ -782,7 +785,7 @@ class Backtesting:
if timedout:
if order.side == 'buy':
self.timedout_entry_orders += 1
if trade.nr_of_successful_buys == 0:
if trade.nr_of_successful_entries == 0:
# Remove trade due to buy timeout expiration.
return True
else:

View File

@ -867,7 +867,7 @@ class LocalTrade():
def recalc_trade_from_orders(self):
# We need at least 2 entry orders for averaging amounts and rates.
if len(self.select_filled_orders('buy')) < 2:
if len(self.select_filled_orders(self.enter_side)) < 2:
# Just in case, still recalc open trade value
self.recalc_open_trade_value()
return
@ -889,8 +889,9 @@ class LocalTrade():
total_stake += tmp_price * tmp_amount
if total_amount > 0:
# Leverage not updated, as we don't allow changing leverage through DCA at the moment.
self.open_rate = total_stake / total_amount
self.stake_amount = total_stake
self.stake_amount = total_stake / (self.leverage or 1.0)
self.amount = total_amount
self.fee_open_cost = self.fee_open * self.stake_amount
self.recalc_open_trade_value()
@ -936,10 +937,28 @@ class LocalTrade():
(o.filled or 0) > 0 and
o.status in NON_OPEN_EXCHANGE_STATES]
@property
def nr_of_successful_entries(self) -> int:
"""
Helper function to count the number of entry orders that have been filled.
:return: int count of entry orders that have been filled for this trade.
"""
return len(self.select_filled_orders(self.enter_side))
@property
def nr_of_successful_exits(self) -> int:
"""
Helper function to count the number of exit orders that have been filled.
:return: int count of exit orders that have been filled for this trade.
"""
return len(self.select_filled_orders(self.exit_side))
@property
def nr_of_successful_buys(self) -> int:
"""
Helper function to count the number of buy orders that have been filled.
WARNING: Please use nr_of_successful_entries for short support.
:return: int count of buy orders that have been filled for this trade.
"""
@ -949,6 +968,7 @@ class LocalTrade():
def nr_of_successful_sells(self) -> int:
"""
Helper function to count the number of sell orders that have been filled.
WARNING: Please use nr_of_successful_exits for short support.
:return: int count of sell orders that have been filled for this trade.
"""
return len(self.select_filled_orders('sell'))

View File

@ -261,11 +261,11 @@ class RPC:
profit_str
]
if self._config.get('position_adjustment_enable', False):
max_buy_str = ''
max_entry_str = ''
if self._config.get('max_entry_position_adjustment', -1) > 0:
max_buy_str = f"/{self._config['max_entry_position_adjustment'] + 1}"
filled_buys = trade.nr_of_successful_buys
detail_trade.append(f"{filled_buys}{max_buy_str}")
max_entry_str = f"/{self._config['max_entry_position_adjustment'] + 1}"
filled_entries = trade.nr_of_successful_entries
detail_trade.append(f"{filled_entries}{max_entry_str}")
trades_list.append(detail_trade)
profitcol = "Profit"
if self._fiat_converter:
@ -696,19 +696,18 @@ class RPC:
if trade.open_order_id:
order = self._freqtrade.exchange.fetch_order(trade.open_order_id, trade.pair)
if order['side'] == 'buy':
if order['side'] == trade.enter_side:
fully_canceled = self._freqtrade.handle_cancel_enter(
trade, order, CANCEL_REASON['FORCE_SELL'])
if order['side'] == 'sell':
if order['side'] == trade.exit_side:
# Cancel order - so it is placed anew with a fresh price.
self._freqtrade.handle_cancel_exit(trade, order, CANCEL_REASON['FORCE_SELL'])
if not fully_canceled:
# Get current rate and execute sell
closing_side = "buy" if trade.is_short else "sell"
current_rate = self._freqtrade.exchange.get_rate(
trade.pair, refresh=False, side=closing_side)
trade.pair, refresh=False, side=trade.exit_side)
sell_reason = SellCheckTuple(sell_type=SellType.FORCE_SELL)
order_type = ordertype or self._freqtrade.strategy.order_types.get(
"forcesell", self._freqtrade.strategy.order_types["sell"])
@ -769,8 +768,10 @@ class RPC:
# check if valid pair
# check if pair already has an open pair
trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first()
trade: Trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first()
is_short = (order_side == SignalDirection.SHORT)
if trade:
is_short = trade.is_short
if not self._freqtrade.strategy.position_adjustment_enable:
raise RPCException(f'position for {pair} already open - id: {trade.id}')
@ -784,7 +785,7 @@ class RPC:
'forcebuy', self._freqtrade.strategy.order_types['buy'])
if self._freqtrade.execute_entry(pair, stake_amount, price,
ordertype=order_type, trade=trade,
is_short=(order_side == SignalDirection.SHORT),
is_short=is_short,
enter_tag=enter_tag,
):
Trade.commit()

View File

@ -183,7 +183,7 @@ class StrategyTestV3(IStrategy):
current_profit: float, min_stake: float, max_stake: float, **kwargs):
if current_profit < -0.0075:
orders = trade.select_filled_orders('buy')
orders = trade.select_filled_orders(trade.enter_side)
return round(orders[0].cost, 0)
return None

View File

@ -231,13 +231,13 @@ def test_dca_buying(default_conf_usdt, ticker_usdt, fee, mocker) -> None:
assert len(Trade.get_trades().all()) == 1
trade = Trade.get_trades().first()
assert len(trade.orders) == 1
assert trade.stake_amount == 60
assert pytest.approx(trade.stake_amount) == 60
assert trade.open_rate == 2.0
# No adjustment
freqtrade.process()
trade = Trade.get_trades().first()
assert len(trade.orders) == 1
assert trade.stake_amount == 60
assert pytest.approx(trade.stake_amount) == 60
# Reduce bid amount
ticker_usdt_modif = ticker_usdt.return_value
@ -266,6 +266,7 @@ def test_dca_buying(default_conf_usdt, ticker_usdt, fee, mocker) -> None:
assert trade.amount == trade.orders[0].amount + trade.orders[1].amount
assert trade.nr_of_successful_buys == 2
assert trade.nr_of_successful_entries == 2
# Sell
patch_get_signal(freqtrade, enter_long=False, exit_long=True)
@ -280,3 +281,75 @@ def test_dca_buying(default_conf_usdt, ticker_usdt, fee, mocker) -> None:
assert trade.orders[2].amount == trade.amount
assert trade.nr_of_successful_buys == 2
assert trade.nr_of_successful_entries == 2
def test_dca_short(default_conf_usdt, ticker_usdt, fee, mocker) -> None:
default_conf_usdt['position_adjustment_enable'] = True
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
fetch_ticker=ticker_usdt,
get_fee=fee,
amount_to_precision=lambda s, x, y: y,
price_to_precision=lambda s, x, y: y,
)
patch_get_signal(freqtrade, enter_long=False, enter_short=True)
freqtrade.enter_positions()
assert len(Trade.get_trades().all()) == 1
trade = Trade.get_trades().first()
assert len(trade.orders) == 1
assert pytest.approx(trade.stake_amount) == 60
assert trade.open_rate == 2.02
# No adjustment
freqtrade.process()
trade = Trade.get_trades().first()
assert len(trade.orders) == 1
assert pytest.approx(trade.stake_amount) == 60
# Reduce bid amount
ticker_usdt_modif = ticker_usdt.return_value
ticker_usdt_modif['ask'] = ticker_usdt_modif['ask'] * 1.015
ticker_usdt_modif['bid'] = ticker_usdt_modif['bid'] * 1.0125
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', return_value=ticker_usdt_modif)
# additional buy order
freqtrade.process()
trade = Trade.get_trades().first()
assert len(trade.orders) == 2
for o in trade.orders:
assert o.status == "closed"
assert pytest.approx(trade.stake_amount) == 120
# Open-rate averaged between 2.0 and 2.0 * 1.015
assert trade.open_rate >= 2.02
assert trade.open_rate < 2.02 * 1.015
# No action - profit raised above 1% (the bar set in the strategy).
freqtrade.process()
trade = Trade.get_trades().first()
assert len(trade.orders) == 2
assert pytest.approx(trade.stake_amount) == 120
# assert trade.orders[0].amount == 30
assert trade.orders[1].amount == 60 / ticker_usdt_modif['ask']
assert trade.amount == trade.orders[0].amount + trade.orders[1].amount
assert trade.nr_of_successful_entries == 2
# Buy
patch_get_signal(freqtrade, enter_long=False, exit_short=True)
freqtrade.process()
trade = Trade.get_trades().first()
assert trade.is_open is False
# assert trade.orders[0].amount == 30
assert trade.orders[0].side == 'sell'
assert trade.orders[1].amount == 60 / ticker_usdt_modif['ask']
# Sold everything
assert trade.orders[-1].side == 'buy'
assert trade.orders[2].amount == trade.amount
assert trade.nr_of_successful_entries == 2
assert trade.nr_of_successful_exits == 1

View File

@ -2371,13 +2371,16 @@ def test_recalc_trade_from_orders(fee):
assert pytest.approx(trade.open_trade_value) == o1_trade_val + o2_trade_val + o3_trade_val
def test_recalc_trade_from_orders_ignores_bad_orders(fee):
@pytest.mark.parametrize('is_short', [True, False])
def test_recalc_trade_from_orders_ignores_bad_orders(fee, is_short):
o1_amount = 100
o1_rate = 1
o1_cost = o1_amount * o1_rate
o1_fee_cost = o1_cost * fee.return_value
o1_trade_val = o1_cost + o1_fee_cost
o1_trade_val = o1_cost - o1_fee_cost if is_short else o1_cost + o1_fee_cost
enter_side = "sell" if is_short else "buy"
exit_side = "buy" if is_short else "sell"
trade = Trade(
pair='ADA/USDT',
@ -2389,17 +2392,18 @@ def test_recalc_trade_from_orders_ignores_bad_orders(fee):
exchange='binance',
open_rate=o1_rate,
max_rate=o1_rate,
is_short=is_short,
)
trade.update_fee(o1_fee_cost, 'BNB', fee.return_value, 'buy')
trade.update_fee(o1_fee_cost, 'BNB', fee.return_value, enter_side)
# Check with 1 order
order1 = Order(
ft_order_side='buy',
ft_order_side=enter_side,
ft_pair=trade.pair,
ft_is_open=False,
status="closed",
symbol=trade.pair,
order_type="market",
side="buy",
side=enter_side,
price=o1_rate,
average=o1_rate,
filled=o1_amount,
@ -2417,16 +2421,16 @@ def test_recalc_trade_from_orders_ignores_bad_orders(fee):
assert trade.open_rate == o1_rate
assert trade.fee_open_cost == o1_fee_cost
assert trade.open_trade_value == o1_trade_val
assert trade.nr_of_successful_buys == 1
assert trade.nr_of_successful_entries == 1
order2 = Order(
ft_order_side='buy',
ft_order_side=enter_side,
ft_pair=trade.pair,
ft_is_open=True,
status="open",
symbol=trade.pair,
order_type="market",
side="buy",
side=enter_side,
price=o1_rate,
average=o1_rate,
filled=o1_amount,
@ -2444,17 +2448,17 @@ def test_recalc_trade_from_orders_ignores_bad_orders(fee):
assert trade.open_rate == o1_rate
assert trade.fee_open_cost == o1_fee_cost
assert trade.open_trade_value == o1_trade_val
assert trade.nr_of_successful_buys == 1
assert trade.nr_of_successful_entries == 1
# Let's try with some other orders
order3 = Order(
ft_order_side='buy',
ft_order_side=enter_side,
ft_pair=trade.pair,
ft_is_open=False,
status="cancelled",
symbol=trade.pair,
order_type="market",
side="buy",
side=enter_side,
price=1,
average=2,
filled=0,
@ -2472,16 +2476,16 @@ def test_recalc_trade_from_orders_ignores_bad_orders(fee):
assert trade.open_rate == o1_rate
assert trade.fee_open_cost == o1_fee_cost
assert trade.open_trade_value == o1_trade_val
assert trade.nr_of_successful_buys == 1
assert trade.nr_of_successful_entries == 1
order4 = Order(
ft_order_side='buy',
ft_order_side=enter_side,
ft_pair=trade.pair,
ft_is_open=False,
status="closed",
symbol=trade.pair,
order_type="market",
side="buy",
side=enter_side,
price=o1_rate,
average=o1_rate,
filled=o1_amount,
@ -2499,17 +2503,17 @@ def test_recalc_trade_from_orders_ignores_bad_orders(fee):
assert trade.open_rate == o1_rate
assert trade.fee_open_cost == 2 * o1_fee_cost
assert trade.open_trade_value == 2 * o1_trade_val
assert trade.nr_of_successful_buys == 2
assert trade.nr_of_successful_entries == 2
# Just to make sure sell orders are ignored, let's calculate one more time.
# Just to make sure exit orders are ignored, let's calculate one more time.
sell1 = Order(
ft_order_side='sell',
ft_order_side=exit_side,
ft_pair=trade.pair,
ft_is_open=False,
status="closed",
symbol=trade.pair,
order_type="market",
side="sell",
side=exit_side,
price=4,
average=3,
filled=2,
@ -2526,16 +2530,17 @@ def test_recalc_trade_from_orders_ignores_bad_orders(fee):
assert trade.open_rate == o1_rate
assert trade.fee_open_cost == 2 * o1_fee_cost
assert trade.open_trade_value == 2 * o1_trade_val
assert trade.nr_of_successful_buys == 2
assert trade.nr_of_successful_entries == 2
# Check with 1 order
order_noavg = Order(
ft_order_side='buy',
ft_order_side=enter_side,
ft_pair=trade.pair,
ft_is_open=False,
status="closed",
symbol=trade.pair,
order_type="market",
side="buy",
side=enter_side,
price=o1_rate,
average=None,
filled=o1_amount,
@ -2553,7 +2558,7 @@ def test_recalc_trade_from_orders_ignores_bad_orders(fee):
assert trade.open_rate == o1_rate
assert trade.fee_open_cost == 3 * o1_fee_cost
assert trade.open_trade_value == 3 * o1_trade_val
assert trade.nr_of_successful_buys == 3
assert trade.nr_of_successful_entries == 3
@pytest.mark.usefixtures("init_persistence")