mirror of
https://github.com/freqtrade/freqtrade.git
synced 2024-11-11 02:33:55 +00:00
Merge pull request #6400 from freqtrade/short_dca
trade-adjustment for short trades
This commit is contained in:
commit
e005439720
|
@ -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`.
|
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.
|
`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"
|
!!! Note "About stake size"
|
||||||
Using fixed stake size means it will be the amount used for the first order, just like without position adjustment.
|
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.
|
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
|
return None
|
||||||
|
|
||||||
filled_buys = trade.select_filled_orders('buy')
|
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)
|
# Allow up to 3 additional increasingly larger buys (4 in total)
|
||||||
# Initial buy is 1x
|
# Initial buy is 1x
|
||||||
# If that falls to -5% profit, we buy 1.25x more, average profit should increase to roughly -2.2%
|
# 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
|
# This returns first order stake size
|
||||||
stake_amount = filled_buys[0].cost
|
stake_amount = filled_buys[0].cost
|
||||||
# This then calculates current safety order size
|
# 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
|
return stake_amount
|
||||||
except Exception as exception:
|
except Exception as exception:
|
||||||
return None
|
return None
|
||||||
|
|
|
@ -103,7 +103,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))
|
||||||
|
|
||||||
self.liquidation_buffer = float(self.config.get('liquidation_buffer', '0.05'))
|
|
||||||
self.trading_mode: TradingMode = self.config.get('trading_mode', TradingMode.SPOT)
|
self.trading_mode: TradingMode = self.config.get('trading_mode', TradingMode.SPOT)
|
||||||
self.margin_mode_type: Optional[MarginMode] = None
|
self.margin_mode_type: Optional[MarginMode] = None
|
||||||
if 'margin_mode' in self.config:
|
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.
|
# TODO-lev: Check what changes are necessary for DCA in relation to shorts.
|
||||||
if self.strategy.max_entry_position_adjustment > -1:
|
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:
|
if count_of_buys > self.strategy.max_entry_position_adjustment:
|
||||||
logger.debug(f"Max adjustment entries for {trade.pair} has been reached.")
|
logger.debug(f"Max adjustment entries for {trade.pair} has been reached.")
|
||||||
return
|
return
|
||||||
|
@ -533,7 +532,7 @@ class FreqtradeBot(LoggingMixin):
|
||||||
|
|
||||||
if stake_amount is not None and stake_amount > 0.0:
|
if stake_amount is not None and stake_amount > 0.0:
|
||||||
# We should increase our position
|
# 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:
|
if stake_amount is not None and stake_amount < 0.0:
|
||||||
# We should decrease our position
|
# We should decrease our position
|
||||||
|
@ -643,18 +642,21 @@ class FreqtradeBot(LoggingMixin):
|
||||||
|
|
||||||
if not stake_amount:
|
if not stake_amount:
|
||||||
return False
|
return False
|
||||||
|
if not pos_adjust:
|
||||||
max_leverage = self.exchange.get_max_leverage(pair, stake_amount)
|
max_leverage = self.exchange.get_max_leverage(pair, stake_amount)
|
||||||
leverage = strategy_safe_wrapper(self.strategy.leverage, default_retval=1.0)(
|
leverage = strategy_safe_wrapper(self.strategy.leverage, default_retval=1.0)(
|
||||||
pair=pair,
|
pair=pair,
|
||||||
current_time=datetime.now(timezone.utc),
|
current_time=datetime.now(timezone.utc),
|
||||||
current_rate=enter_limit_requested,
|
current_rate=enter_limit_requested,
|
||||||
proposed_leverage=1.0,
|
proposed_leverage=1.0,
|
||||||
max_leverage=max_leverage,
|
max_leverage=max_leverage,
|
||||||
side=trade_side,
|
side=trade_side,
|
||||||
) if self.trading_mode != TradingMode.SPOT else 1.0
|
) if self.trading_mode != TradingMode.SPOT else 1.0
|
||||||
# Cap leverage between 1.0 and max_leverage.
|
# Cap leverage between 1.0 and max_leverage.
|
||||||
leverage = min(max(leverage, 1.0), 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:
|
if pos_adjust:
|
||||||
logger.info(f"Position adjust: about to create a new order for {pair} with stake: "
|
logger.info(f"Position adjust: about to create a new order for {pair} with stake: "
|
||||||
f"{stake_amount} for {trade}")
|
f"{stake_amount} for {trade}")
|
||||||
|
@ -724,6 +726,7 @@ class FreqtradeBot(LoggingMixin):
|
||||||
amount = safe_value_fallback(order, 'filled', 'amount')
|
amount = safe_value_fallback(order, 'filled', 'amount')
|
||||||
enter_limit_filled_price = safe_value_fallback(order, 'average', 'price')
|
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(
|
interest_rate, isolated_liq = self.leverage_prep(
|
||||||
leverage=leverage,
|
leverage=leverage,
|
||||||
pair=pair,
|
pair=pair,
|
||||||
|
@ -1596,9 +1599,19 @@ class FreqtradeBot(LoggingMixin):
|
||||||
Trade.commit()
|
Trade.commit()
|
||||||
|
|
||||||
if order['status'] in constants.NON_OPEN_EXCHANGE_STATES:
|
if order['status'] in constants.NON_OPEN_EXCHANGE_STATES:
|
||||||
# If a buy order was closed, force update on stoploss on exchange
|
# If a entry order was closed, force update on stoploss on exchange
|
||||||
if order.get('side', None) == 'buy':
|
if order.get('side', None) == trade.enter_side:
|
||||||
trade = self.cancel_stoploss_on_exchange(trade)
|
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
|
# Updating wallets when order is closed
|
||||||
self.wallets.update()
|
self.wallets.update()
|
||||||
|
|
||||||
|
@ -1607,7 +1620,7 @@ class FreqtradeBot(LoggingMixin):
|
||||||
self._notify_exit(trade, '', True)
|
self._notify_exit(trade, '', True)
|
||||||
self.handle_protections(trade.pair)
|
self.handle_protections(trade.pair)
|
||||||
elif send_msg and not trade.open_order_id:
|
elif send_msg and not trade.open_order_id:
|
||||||
# Buy fill
|
# Enter fill
|
||||||
self._notify_enter(trade, order, fill=True)
|
self._notify_enter(trade, order, fill=True)
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
|
@ -464,11 +464,11 @@ class Backtesting:
|
||||||
|
|
||||||
# Check if we need to adjust our current positions
|
# Check if we need to adjust our current positions
|
||||||
if self.strategy.position_adjustment_enable:
|
if self.strategy.position_adjustment_enable:
|
||||||
check_adjust_buy = True
|
check_adjust_entry = True
|
||||||
if self.strategy.max_entry_position_adjustment > -1:
|
if self.strategy.max_entry_position_adjustment > -1:
|
||||||
count_of_buys = trade.nr_of_successful_buys
|
entry_count = trade.nr_of_successful_entries
|
||||||
check_adjust_buy = (count_of_buys <= self.strategy.max_entry_position_adjustment)
|
check_adjust_entry = (entry_count <= self.strategy.max_entry_position_adjustment)
|
||||||
if check_adjust_buy:
|
if check_adjust_entry:
|
||||||
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: datetime = sell_row[DATE_IDX].to_pydatetime()
|
sell_candle_time: datetime = sell_row[DATE_IDX].to_pydatetime()
|
||||||
|
@ -639,17 +639,20 @@ class Backtesting:
|
||||||
# If not pos adjust, trade is None
|
# If not pos adjust, trade is None
|
||||||
return trade
|
return trade
|
||||||
|
|
||||||
max_leverage = self.exchange.get_max_leverage(pair, stake_amount)
|
if not pos_adjust:
|
||||||
leverage = strategy_safe_wrapper(self.strategy.leverage, default_retval=1.0)(
|
max_leverage = self.exchange.get_max_leverage(pair, stake_amount)
|
||||||
pair=pair,
|
leverage = strategy_safe_wrapper(self.strategy.leverage, default_retval=1.0)(
|
||||||
current_time=current_time,
|
pair=pair,
|
||||||
current_rate=row[OPEN_IDX],
|
current_time=current_time,
|
||||||
proposed_leverage=1.0,
|
current_rate=row[OPEN_IDX],
|
||||||
max_leverage=max_leverage,
|
proposed_leverage=1.0,
|
||||||
side=direction,
|
max_leverage=max_leverage,
|
||||||
) if self._can_short else 1.0
|
side=direction,
|
||||||
# Cap leverage between 1.0 and max_leverage.
|
) if self._can_short else 1.0
|
||||||
leverage = min(max(leverage, 1.0), max_leverage)
|
# 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']
|
order_type = self.strategy.order_types['buy']
|
||||||
time_in_force = self.strategy.order_time_in_force['buy']
|
time_in_force = self.strategy.order_time_in_force['buy']
|
||||||
|
@ -729,7 +732,7 @@ class Backtesting:
|
||||||
for pair in open_trades.keys():
|
for pair in open_trades.keys():
|
||||||
if len(open_trades[pair]) > 0:
|
if len(open_trades[pair]) > 0:
|
||||||
for trade in open_trades[pair]:
|
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
|
# Ignore trade if buy-order did not fill yet
|
||||||
continue
|
continue
|
||||||
sell_row = data[pair][-1]
|
sell_row = data[pair][-1]
|
||||||
|
@ -782,7 +785,7 @@ class Backtesting:
|
||||||
if timedout:
|
if timedout:
|
||||||
if order.side == 'buy':
|
if order.side == 'buy':
|
||||||
self.timedout_entry_orders += 1
|
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.
|
# Remove trade due to buy timeout expiration.
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -867,7 +867,7 @@ class LocalTrade():
|
||||||
|
|
||||||
def recalc_trade_from_orders(self):
|
def recalc_trade_from_orders(self):
|
||||||
# We need at least 2 entry orders for averaging amounts and rates.
|
# 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
|
# Just in case, still recalc open trade value
|
||||||
self.recalc_open_trade_value()
|
self.recalc_open_trade_value()
|
||||||
return
|
return
|
||||||
|
@ -889,8 +889,9 @@ class LocalTrade():
|
||||||
total_stake += tmp_price * tmp_amount
|
total_stake += tmp_price * tmp_amount
|
||||||
|
|
||||||
if total_amount > 0:
|
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.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.amount = total_amount
|
||||||
self.fee_open_cost = self.fee_open * self.stake_amount
|
self.fee_open_cost = self.fee_open * self.stake_amount
|
||||||
self.recalc_open_trade_value()
|
self.recalc_open_trade_value()
|
||||||
|
@ -936,10 +937,28 @@ class LocalTrade():
|
||||||
(o.filled or 0) > 0 and
|
(o.filled or 0) > 0 and
|
||||||
o.status in NON_OPEN_EXCHANGE_STATES]
|
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
|
@property
|
||||||
def nr_of_successful_buys(self) -> int:
|
def nr_of_successful_buys(self) -> int:
|
||||||
"""
|
"""
|
||||||
Helper function to count the number of buy orders that have been filled.
|
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.
|
: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:
|
def nr_of_successful_sells(self) -> int:
|
||||||
"""
|
"""
|
||||||
Helper function to count the number of sell orders that have been filled.
|
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: int count of sell orders that have been filled for this trade.
|
||||||
"""
|
"""
|
||||||
return len(self.select_filled_orders('sell'))
|
return len(self.select_filled_orders('sell'))
|
||||||
|
|
|
@ -261,11 +261,11 @@ class RPC:
|
||||||
profit_str
|
profit_str
|
||||||
]
|
]
|
||||||
if self._config.get('position_adjustment_enable', False):
|
if self._config.get('position_adjustment_enable', False):
|
||||||
max_buy_str = ''
|
max_entry_str = ''
|
||||||
if self._config.get('max_entry_position_adjustment', -1) > 0:
|
if self._config.get('max_entry_position_adjustment', -1) > 0:
|
||||||
max_buy_str = f"/{self._config['max_entry_position_adjustment'] + 1}"
|
max_entry_str = f"/{self._config['max_entry_position_adjustment'] + 1}"
|
||||||
filled_buys = trade.nr_of_successful_buys
|
filled_entries = trade.nr_of_successful_entries
|
||||||
detail_trade.append(f"{filled_buys}{max_buy_str}")
|
detail_trade.append(f"{filled_entries}{max_entry_str}")
|
||||||
trades_list.append(detail_trade)
|
trades_list.append(detail_trade)
|
||||||
profitcol = "Profit"
|
profitcol = "Profit"
|
||||||
if self._fiat_converter:
|
if self._fiat_converter:
|
||||||
|
@ -696,19 +696,18 @@ class RPC:
|
||||||
if trade.open_order_id:
|
if trade.open_order_id:
|
||||||
order = self._freqtrade.exchange.fetch_order(trade.open_order_id, trade.pair)
|
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(
|
fully_canceled = self._freqtrade.handle_cancel_enter(
|
||||||
trade, order, CANCEL_REASON['FORCE_SELL'])
|
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.
|
# Cancel order - so it is placed anew with a fresh price.
|
||||||
self._freqtrade.handle_cancel_exit(trade, order, CANCEL_REASON['FORCE_SELL'])
|
self._freqtrade.handle_cancel_exit(trade, order, CANCEL_REASON['FORCE_SELL'])
|
||||||
|
|
||||||
if not fully_canceled:
|
if not fully_canceled:
|
||||||
# Get current rate and execute sell
|
# Get current rate and execute sell
|
||||||
closing_side = "buy" if trade.is_short else "sell"
|
|
||||||
current_rate = self._freqtrade.exchange.get_rate(
|
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)
|
sell_reason = SellCheckTuple(sell_type=SellType.FORCE_SELL)
|
||||||
order_type = ordertype or self._freqtrade.strategy.order_types.get(
|
order_type = ordertype or self._freqtrade.strategy.order_types.get(
|
||||||
"forcesell", self._freqtrade.strategy.order_types["sell"])
|
"forcesell", self._freqtrade.strategy.order_types["sell"])
|
||||||
|
@ -769,8 +768,10 @@ class RPC:
|
||||||
# check if valid pair
|
# check if valid pair
|
||||||
|
|
||||||
# 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 = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first()
|
||||||
|
is_short = (order_side == SignalDirection.SHORT)
|
||||||
if trade:
|
if trade:
|
||||||
|
is_short = trade.is_short
|
||||||
if not self._freqtrade.strategy.position_adjustment_enable:
|
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}')
|
||||||
|
|
||||||
|
@ -784,7 +785,7 @@ class RPC:
|
||||||
'forcebuy', self._freqtrade.strategy.order_types['buy'])
|
'forcebuy', self._freqtrade.strategy.order_types['buy'])
|
||||||
if self._freqtrade.execute_entry(pair, stake_amount, price,
|
if self._freqtrade.execute_entry(pair, stake_amount, price,
|
||||||
ordertype=order_type, trade=trade,
|
ordertype=order_type, trade=trade,
|
||||||
is_short=(order_side == SignalDirection.SHORT),
|
is_short=is_short,
|
||||||
enter_tag=enter_tag,
|
enter_tag=enter_tag,
|
||||||
):
|
):
|
||||||
Trade.commit()
|
Trade.commit()
|
||||||
|
|
|
@ -183,7 +183,7 @@ class StrategyTestV3(IStrategy):
|
||||||
current_profit: float, min_stake: float, max_stake: float, **kwargs):
|
current_profit: float, min_stake: float, max_stake: float, **kwargs):
|
||||||
|
|
||||||
if current_profit < -0.0075:
|
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 round(orders[0].cost, 0)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
|
@ -231,13 +231,13 @@ def test_dca_buying(default_conf_usdt, ticker_usdt, fee, mocker) -> None:
|
||||||
assert len(Trade.get_trades().all()) == 1
|
assert len(Trade.get_trades().all()) == 1
|
||||||
trade = Trade.get_trades().first()
|
trade = Trade.get_trades().first()
|
||||||
assert len(trade.orders) == 1
|
assert len(trade.orders) == 1
|
||||||
assert trade.stake_amount == 60
|
assert pytest.approx(trade.stake_amount) == 60
|
||||||
assert trade.open_rate == 2.0
|
assert trade.open_rate == 2.0
|
||||||
# No adjustment
|
# No adjustment
|
||||||
freqtrade.process()
|
freqtrade.process()
|
||||||
trade = Trade.get_trades().first()
|
trade = Trade.get_trades().first()
|
||||||
assert len(trade.orders) == 1
|
assert len(trade.orders) == 1
|
||||||
assert trade.stake_amount == 60
|
assert pytest.approx(trade.stake_amount) == 60
|
||||||
|
|
||||||
# Reduce bid amount
|
# Reduce bid amount
|
||||||
ticker_usdt_modif = ticker_usdt.return_value
|
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.amount == trade.orders[0].amount + trade.orders[1].amount
|
||||||
assert trade.nr_of_successful_buys == 2
|
assert trade.nr_of_successful_buys == 2
|
||||||
|
assert trade.nr_of_successful_entries == 2
|
||||||
|
|
||||||
# Sell
|
# Sell
|
||||||
patch_get_signal(freqtrade, enter_long=False, exit_long=True)
|
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.orders[2].amount == trade.amount
|
||||||
|
|
||||||
assert trade.nr_of_successful_buys == 2
|
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
|
||||||
|
|
|
@ -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
|
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_amount = 100
|
||||||
o1_rate = 1
|
o1_rate = 1
|
||||||
o1_cost = o1_amount * o1_rate
|
o1_cost = o1_amount * o1_rate
|
||||||
o1_fee_cost = o1_cost * fee.return_value
|
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(
|
trade = Trade(
|
||||||
pair='ADA/USDT',
|
pair='ADA/USDT',
|
||||||
|
@ -2389,17 +2392,18 @@ def test_recalc_trade_from_orders_ignores_bad_orders(fee):
|
||||||
exchange='binance',
|
exchange='binance',
|
||||||
open_rate=o1_rate,
|
open_rate=o1_rate,
|
||||||
max_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
|
# Check with 1 order
|
||||||
order1 = Order(
|
order1 = Order(
|
||||||
ft_order_side='buy',
|
ft_order_side=enter_side,
|
||||||
ft_pair=trade.pair,
|
ft_pair=trade.pair,
|
||||||
ft_is_open=False,
|
ft_is_open=False,
|
||||||
status="closed",
|
status="closed",
|
||||||
symbol=trade.pair,
|
symbol=trade.pair,
|
||||||
order_type="market",
|
order_type="market",
|
||||||
side="buy",
|
side=enter_side,
|
||||||
price=o1_rate,
|
price=o1_rate,
|
||||||
average=o1_rate,
|
average=o1_rate,
|
||||||
filled=o1_amount,
|
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.open_rate == o1_rate
|
||||||
assert trade.fee_open_cost == o1_fee_cost
|
assert trade.fee_open_cost == o1_fee_cost
|
||||||
assert trade.open_trade_value == o1_trade_val
|
assert trade.open_trade_value == o1_trade_val
|
||||||
assert trade.nr_of_successful_buys == 1
|
assert trade.nr_of_successful_entries == 1
|
||||||
|
|
||||||
order2 = Order(
|
order2 = Order(
|
||||||
ft_order_side='buy',
|
ft_order_side=enter_side,
|
||||||
ft_pair=trade.pair,
|
ft_pair=trade.pair,
|
||||||
ft_is_open=True,
|
ft_is_open=True,
|
||||||
status="open",
|
status="open",
|
||||||
symbol=trade.pair,
|
symbol=trade.pair,
|
||||||
order_type="market",
|
order_type="market",
|
||||||
side="buy",
|
side=enter_side,
|
||||||
price=o1_rate,
|
price=o1_rate,
|
||||||
average=o1_rate,
|
average=o1_rate,
|
||||||
filled=o1_amount,
|
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.open_rate == o1_rate
|
||||||
assert trade.fee_open_cost == o1_fee_cost
|
assert trade.fee_open_cost == o1_fee_cost
|
||||||
assert trade.open_trade_value == o1_trade_val
|
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
|
# Let's try with some other orders
|
||||||
order3 = Order(
|
order3 = Order(
|
||||||
ft_order_side='buy',
|
ft_order_side=enter_side,
|
||||||
ft_pair=trade.pair,
|
ft_pair=trade.pair,
|
||||||
ft_is_open=False,
|
ft_is_open=False,
|
||||||
status="cancelled",
|
status="cancelled",
|
||||||
symbol=trade.pair,
|
symbol=trade.pair,
|
||||||
order_type="market",
|
order_type="market",
|
||||||
side="buy",
|
side=enter_side,
|
||||||
price=1,
|
price=1,
|
||||||
average=2,
|
average=2,
|
||||||
filled=0,
|
filled=0,
|
||||||
|
@ -2472,16 +2476,16 @@ def test_recalc_trade_from_orders_ignores_bad_orders(fee):
|
||||||
assert trade.open_rate == o1_rate
|
assert trade.open_rate == o1_rate
|
||||||
assert trade.fee_open_cost == o1_fee_cost
|
assert trade.fee_open_cost == o1_fee_cost
|
||||||
assert trade.open_trade_value == o1_trade_val
|
assert trade.open_trade_value == o1_trade_val
|
||||||
assert trade.nr_of_successful_buys == 1
|
assert trade.nr_of_successful_entries == 1
|
||||||
|
|
||||||
order4 = Order(
|
order4 = Order(
|
||||||
ft_order_side='buy',
|
ft_order_side=enter_side,
|
||||||
ft_pair=trade.pair,
|
ft_pair=trade.pair,
|
||||||
ft_is_open=False,
|
ft_is_open=False,
|
||||||
status="closed",
|
status="closed",
|
||||||
symbol=trade.pair,
|
symbol=trade.pair,
|
||||||
order_type="market",
|
order_type="market",
|
||||||
side="buy",
|
side=enter_side,
|
||||||
price=o1_rate,
|
price=o1_rate,
|
||||||
average=o1_rate,
|
average=o1_rate,
|
||||||
filled=o1_amount,
|
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.open_rate == o1_rate
|
||||||
assert trade.fee_open_cost == 2 * o1_fee_cost
|
assert trade.fee_open_cost == 2 * o1_fee_cost
|
||||||
assert trade.open_trade_value == 2 * o1_trade_val
|
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(
|
sell1 = Order(
|
||||||
ft_order_side='sell',
|
ft_order_side=exit_side,
|
||||||
ft_pair=trade.pair,
|
ft_pair=trade.pair,
|
||||||
ft_is_open=False,
|
ft_is_open=False,
|
||||||
status="closed",
|
status="closed",
|
||||||
symbol=trade.pair,
|
symbol=trade.pair,
|
||||||
order_type="market",
|
order_type="market",
|
||||||
side="sell",
|
side=exit_side,
|
||||||
price=4,
|
price=4,
|
||||||
average=3,
|
average=3,
|
||||||
filled=2,
|
filled=2,
|
||||||
|
@ -2526,16 +2530,17 @@ def test_recalc_trade_from_orders_ignores_bad_orders(fee):
|
||||||
assert trade.open_rate == o1_rate
|
assert trade.open_rate == o1_rate
|
||||||
assert trade.fee_open_cost == 2 * o1_fee_cost
|
assert trade.fee_open_cost == 2 * o1_fee_cost
|
||||||
assert trade.open_trade_value == 2 * o1_trade_val
|
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
|
# Check with 1 order
|
||||||
order_noavg = Order(
|
order_noavg = Order(
|
||||||
ft_order_side='buy',
|
ft_order_side=enter_side,
|
||||||
ft_pair=trade.pair,
|
ft_pair=trade.pair,
|
||||||
ft_is_open=False,
|
ft_is_open=False,
|
||||||
status="closed",
|
status="closed",
|
||||||
symbol=trade.pair,
|
symbol=trade.pair,
|
||||||
order_type="market",
|
order_type="market",
|
||||||
side="buy",
|
side=enter_side,
|
||||||
price=o1_rate,
|
price=o1_rate,
|
||||||
average=None,
|
average=None,
|
||||||
filled=o1_amount,
|
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.open_rate == o1_rate
|
||||||
assert trade.fee_open_cost == 3 * o1_fee_cost
|
assert trade.fee_open_cost == 3 * o1_fee_cost
|
||||||
assert trade.open_trade_value == 3 * o1_trade_val
|
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")
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
|
|
Loading…
Reference in New Issue
Block a user