Merge pull request #6555 from freqtrade/inverse_leverage_minstake
Inverse leverage with stake detection
This commit is contained in:
commit
a64ca541a2
@ -42,8 +42,8 @@ By default, loop runs every few seconds (`internals.process_throttle_secs`) and
|
|||||||
* Check if trade-slots are still available (if `max_open_trades` is reached).
|
* Check if trade-slots are still available (if `max_open_trades` is reached).
|
||||||
* Verifies buy signal trying to enter new positions.
|
* Verifies buy signal trying to enter new positions.
|
||||||
* Determine buy-price based on `bid_strategy` configuration setting, or by using the `custom_entry_price()` callback.
|
* Determine buy-price based on `bid_strategy` configuration setting, or by using the `custom_entry_price()` callback.
|
||||||
* Determine stake size by calling the `custom_stake_amount()` callback.
|
|
||||||
* In Margin and Futures mode, `leverage()` strategy callback is called to determine the desired leverage.
|
* In Margin and Futures mode, `leverage()` strategy callback is called to determine the desired leverage.
|
||||||
|
* Determine stake size by calling the `custom_stake_amount()` callback.
|
||||||
* Before a buy order is placed, `confirm_trade_entry()` strategy callback is called.
|
* Before a buy order is placed, `confirm_trade_entry()` strategy callback is called.
|
||||||
|
|
||||||
This loop will be repeated again and again until the bot is stopped.
|
This loop will be repeated again and again until the bot is stopped.
|
||||||
@ -59,8 +59,8 @@ This loop will be repeated again and again until the bot is stopped.
|
|||||||
* Loops per candle simulating entry and exit points.
|
* Loops per candle simulating entry and exit points.
|
||||||
* Confirm trade buy / sell (calls `confirm_trade_entry()` and `confirm_trade_exit()` if implemented in the strategy).
|
* Confirm trade buy / sell (calls `confirm_trade_entry()` and `confirm_trade_exit()` if implemented in the strategy).
|
||||||
* Call `custom_entry_price()` (if implemented in the strategy) to determine entry price (Prices are moved to be within the opening candle).
|
* Call `custom_entry_price()` (if implemented in the strategy) to determine entry price (Prices are moved to be within the opening candle).
|
||||||
* Determine stake size by calling the `custom_stake_amount()` callback.
|
|
||||||
* In Margin and Futures mode, `leverage()` strategy callback is called to determine the desired leverage.
|
* In Margin and Futures mode, `leverage()` strategy callback is called to determine the desired leverage.
|
||||||
|
* Determine stake size by calling the `custom_stake_amount()` callback.
|
||||||
* Check position adjustments for open trades if enabled and call `adjust_trade_position()` to determine if an additional order is requested.
|
* Check position adjustments for open trades if enabled and call `adjust_trade_position()` to determine if an additional order is requested.
|
||||||
* Call `custom_stoploss()` and `custom_sell()` to find custom exit points.
|
* Call `custom_stoploss()` and `custom_sell()` to find custom exit points.
|
||||||
* For sells based on sell-signal and custom-sell: Call `custom_exit_price()` to determine exit price (Prices are moved to be within the closing candle).
|
* For sells based on sell-signal and custom-sell: Call `custom_exit_price()` to determine exit price (Prices are moved to be within the closing candle).
|
||||||
|
@ -600,26 +600,12 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
trade_side = 'short' if is_short else 'long'
|
trade_side = 'short' if is_short else 'long'
|
||||||
pos_adjust = trade is not None
|
pos_adjust = trade is not None
|
||||||
|
|
||||||
enter_limit_requested, stake_amount = self.get_valid_enter_price_and_stake(
|
enter_limit_requested, stake_amount, leverage = self.get_valid_enter_price_and_stake(
|
||||||
pair, price, stake_amount, side, trade_side, enter_tag, trade)
|
pair, price, stake_amount, side, trade_side, enter_tag, trade)
|
||||||
|
|
||||||
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)
|
|
||||||
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:
|
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}")
|
||||||
@ -775,7 +761,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
side: str, trade_side: str,
|
side: str, trade_side: str,
|
||||||
entry_tag: Optional[str],
|
entry_tag: Optional[str],
|
||||||
trade: Optional[Trade]
|
trade: Optional[Trade]
|
||||||
) -> Tuple[float, float]:
|
) -> Tuple[float, float, float]:
|
||||||
|
|
||||||
if price:
|
if price:
|
||||||
enter_limit_requested = price
|
enter_limit_requested = price
|
||||||
@ -792,13 +778,30 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
if not enter_limit_requested:
|
if not enter_limit_requested:
|
||||||
raise PricingError(f'Could not determine {side} price.')
|
raise PricingError(f'Could not determine {side} price.')
|
||||||
|
|
||||||
|
if trade is None:
|
||||||
|
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
|
||||||
|
|
||||||
# Min-stake-amount should actually include Leverage - this way our "minimal"
|
# Min-stake-amount should actually include Leverage - this way our "minimal"
|
||||||
# stake- amount might be higher than necessary.
|
# stake- amount might be higher than necessary.
|
||||||
# We do however also need min-stake to determine leverage, therefore this is ignored as
|
# We do however also need min-stake to determine leverage, therefore this is ignored as
|
||||||
# edge-case for now.
|
# edge-case for now.
|
||||||
min_stake_amount = self.exchange.get_min_pair_stake_amount(
|
min_stake_amount = self.exchange.get_min_pair_stake_amount(
|
||||||
pair, enter_limit_requested, self.strategy.stoploss)
|
pair, enter_limit_requested, self.strategy.stoploss, leverage)
|
||||||
max_stake_amount = self.exchange.get_max_pair_stake_amount(pair, enter_limit_requested)
|
max_stake_amount = self.exchange.get_max_pair_stake_amount(
|
||||||
|
pair, enter_limit_requested, leverage)
|
||||||
|
|
||||||
if not self.edge and trade is None:
|
if not self.edge and trade is None:
|
||||||
stake_available = self.wallets.get_available_stake_amount()
|
stake_available = self.wallets.get_available_stake_amount()
|
||||||
@ -817,7 +820,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
max_stake_amount=max_stake_amount,
|
max_stake_amount=max_stake_amount,
|
||||||
)
|
)
|
||||||
|
|
||||||
return enter_limit_requested, stake_amount
|
return enter_limit_requested, stake_amount, leverage
|
||||||
|
|
||||||
def _notify_enter(self, trade: Trade, order: Dict, order_type: Optional[str] = None,
|
def _notify_enter(self, trade: Trade, order: Dict, order_type: Optional[str] = None,
|
||||||
fill: bool = False) -> None:
|
fill: bool = False) -> None:
|
||||||
|
@ -635,18 +635,15 @@ class Backtesting:
|
|||||||
else:
|
else:
|
||||||
return self._get_sell_trade_entry_for_candle(trade, sell_row)
|
return self._get_sell_trade_entry_for_candle(trade, sell_row)
|
||||||
|
|
||||||
def _enter_trade(self, pair: str, row: Tuple, direction: str,
|
def get_valid_price_and_stake(
|
||||||
stake_amount: Optional[float] = None,
|
self, pair: str, row: Tuple, propose_rate: float, stake_amount: Optional[float],
|
||||||
trade: Optional[LocalTrade] = None) -> Optional[LocalTrade]:
|
direction: str, current_time: datetime, entry_tag: Optional[str],
|
||||||
|
trade: Optional[LocalTrade], order_type: str
|
||||||
|
) -> Tuple[float, float, float, float]:
|
||||||
|
|
||||||
current_time = row[DATE_IDX].to_pydatetime()
|
|
||||||
entry_tag = row[ENTER_TAG_IDX] if len(row) >= ENTER_TAG_IDX + 1 else None
|
|
||||||
# let's call the custom entry price, using the open price as default price
|
|
||||||
order_type = self.strategy.order_types['entry']
|
|
||||||
propose_rate = row[OPEN_IDX]
|
|
||||||
if order_type == 'limit':
|
if order_type == 'limit':
|
||||||
propose_rate = strategy_safe_wrapper(self.strategy.custom_entry_price,
|
propose_rate = strategy_safe_wrapper(self.strategy.custom_entry_price,
|
||||||
default_retval=row[OPEN_IDX])(
|
default_retval=propose_rate)(
|
||||||
pair=pair, current_time=current_time,
|
pair=pair, current_time=current_time,
|
||||||
proposed_rate=propose_rate, entry_tag=entry_tag) # default value is the open rate
|
proposed_rate=propose_rate, entry_tag=entry_tag) # default value is the open rate
|
||||||
# We can't place orders higher than current high (otherwise it'd be a stop limit buy)
|
# We can't place orders higher than current high (otherwise it'd be a stop limit buy)
|
||||||
@ -656,39 +653,14 @@ class Backtesting:
|
|||||||
else:
|
else:
|
||||||
propose_rate = min(propose_rate, row[HIGH_IDX])
|
propose_rate = min(propose_rate, row[HIGH_IDX])
|
||||||
|
|
||||||
min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, propose_rate, -0.05) or 0
|
|
||||||
max_stake_amount = self.exchange.get_max_pair_stake_amount(pair, propose_rate)
|
|
||||||
stake_available = self.wallets.get_available_stake_amount()
|
|
||||||
|
|
||||||
pos_adjust = trade is not None
|
pos_adjust = trade is not None
|
||||||
|
leverage = trade.leverage if trade else 1.0
|
||||||
if not pos_adjust:
|
if not pos_adjust:
|
||||||
try:
|
try:
|
||||||
stake_amount = self.wallets.get_trade_stake_amount(pair, None, update=False)
|
stake_amount = self.wallets.get_trade_stake_amount(pair, None, update=False)
|
||||||
except DependencyException:
|
except DependencyException:
|
||||||
return None
|
return 0, 0, 0, 0
|
||||||
|
|
||||||
stake_amount = strategy_safe_wrapper(self.strategy.custom_stake_amount,
|
|
||||||
default_retval=stake_amount)(
|
|
||||||
pair=pair, current_time=current_time, current_rate=propose_rate,
|
|
||||||
proposed_stake=stake_amount, min_stake=min_stake_amount,
|
|
||||||
max_stake=min(stake_available, max_stake_amount),
|
|
||||||
entry_tag=entry_tag, side=direction)
|
|
||||||
|
|
||||||
stake_amount = self.wallets.validate_stake_amount(
|
|
||||||
pair=pair,
|
|
||||||
stake_amount=stake_amount,
|
|
||||||
min_stake_amount=min_stake_amount,
|
|
||||||
max_stake_amount=max_stake_amount,
|
|
||||||
)
|
|
||||||
|
|
||||||
if not stake_amount:
|
|
||||||
# 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['entry']
|
|
||||||
time_in_force = self.strategy.order_time_in_force['entry']
|
|
||||||
|
|
||||||
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,
|
||||||
@ -701,14 +673,57 @@ class Backtesting:
|
|||||||
# 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)
|
||||||
|
|
||||||
|
min_stake_amount = self.exchange.get_min_pair_stake_amount(
|
||||||
|
pair, propose_rate, -0.05, leverage=leverage) or 0
|
||||||
|
max_stake_amount = self.exchange.get_max_pair_stake_amount(
|
||||||
|
pair, propose_rate, leverage=leverage)
|
||||||
|
stake_available = self.wallets.get_available_stake_amount()
|
||||||
|
|
||||||
|
if not pos_adjust:
|
||||||
|
stake_amount = strategy_safe_wrapper(self.strategy.custom_stake_amount,
|
||||||
|
default_retval=stake_amount)(
|
||||||
|
pair=pair, current_time=current_time, current_rate=propose_rate,
|
||||||
|
proposed_stake=stake_amount, min_stake=min_stake_amount,
|
||||||
|
max_stake=min(stake_available, max_stake_amount),
|
||||||
|
entry_tag=entry_tag, side=direction)
|
||||||
|
|
||||||
|
stake_amount_val = self.wallets.validate_stake_amount(
|
||||||
|
pair=pair,
|
||||||
|
stake_amount=stake_amount,
|
||||||
|
min_stake_amount=min_stake_amount,
|
||||||
|
max_stake_amount=max_stake_amount,
|
||||||
|
)
|
||||||
|
|
||||||
|
return propose_rate, stake_amount_val, leverage, min_stake_amount
|
||||||
|
|
||||||
|
def _enter_trade(self, pair: str, row: Tuple, direction: str,
|
||||||
|
stake_amount: Optional[float] = None,
|
||||||
|
trade: Optional[LocalTrade] = None) -> Optional[LocalTrade]:
|
||||||
|
|
||||||
|
current_time = row[DATE_IDX].to_pydatetime()
|
||||||
|
entry_tag = row[ENTER_TAG_IDX] if len(row) >= ENTER_TAG_IDX + 1 else None
|
||||||
|
# let's call the custom entry price, using the open price as default price
|
||||||
|
order_type = self.strategy.order_types['entry']
|
||||||
|
pos_adjust = trade is not None
|
||||||
|
|
||||||
|
propose_rate, stake_amount, leverage, min_stake_amount = self.get_valid_price_and_stake(
|
||||||
|
pair, row, row[OPEN_IDX], stake_amount, direction, current_time, entry_tag, trade,
|
||||||
|
order_type
|
||||||
|
)
|
||||||
|
|
||||||
|
if not stake_amount:
|
||||||
|
# In case of pos adjust, still return the original trade
|
||||||
|
# If not pos adjust, trade is None
|
||||||
|
return trade
|
||||||
|
time_in_force = self.strategy.order_time_in_force['entry']
|
||||||
|
|
||||||
|
if not pos_adjust:
|
||||||
# Confirm trade entry:
|
# Confirm trade entry:
|
||||||
if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)(
|
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,
|
pair=pair, order_type=order_type, amount=stake_amount, rate=propose_rate,
|
||||||
time_in_force=time_in_force, current_time=current_time,
|
time_in_force=time_in_force, current_time=current_time,
|
||||||
entry_tag=entry_tag, side=direction):
|
entry_tag=entry_tag, side=direction):
|
||||||
return trade
|
return trade
|
||||||
else:
|
|
||||||
leverage = trade.leverage if trade else 1.0
|
|
||||||
|
|
||||||
if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount):
|
if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount):
|
||||||
self.order_id_counter += 1
|
self.order_id_counter += 1
|
||||||
|
@ -996,6 +996,36 @@ def test_execute_entry_confirm_error(mocker, default_conf_usdt, fee, limit_order
|
|||||||
assert not freqtrade.execute_entry(pair, stake_amount)
|
assert not freqtrade.execute_entry(pair, stake_amount)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("is_short", [False, True])
|
||||||
|
def test_execute_entry_min_leverage(mocker, default_conf_usdt, fee, limit_order, is_short) -> None:
|
||||||
|
default_conf_usdt['trading_mode'] = 'futures'
|
||||||
|
default_conf_usdt['margin_mode'] = 'isolated'
|
||||||
|
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.exchange.Exchange',
|
||||||
|
fetch_ticker=MagicMock(return_value={
|
||||||
|
'bid': 1.9,
|
||||||
|
'ask': 2.2,
|
||||||
|
'last': 1.9
|
||||||
|
}),
|
||||||
|
create_order=MagicMock(return_value=limit_order[enter_side(is_short)]),
|
||||||
|
get_rate=MagicMock(return_value=0.11),
|
||||||
|
# Minimum stake-amount is ~5$
|
||||||
|
get_maintenance_ratio_and_amt=MagicMock(return_value=(0.0, 0.0)),
|
||||||
|
_fetch_and_calculate_funding_fees=MagicMock(return_value=0),
|
||||||
|
get_fee=fee,
|
||||||
|
get_max_leverage=MagicMock(return_value=5.0),
|
||||||
|
)
|
||||||
|
stake_amount = 2
|
||||||
|
pair = 'SOL/BUSD:BUSD'
|
||||||
|
freqtrade.strategy.leverage = MagicMock(return_value=5.0)
|
||||||
|
|
||||||
|
assert freqtrade.execute_entry(pair, stake_amount, is_short=is_short)
|
||||||
|
trade = Trade.query.first()
|
||||||
|
assert trade.leverage == 5.0
|
||||||
|
# assert trade.stake_amount == 2
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("is_short", [False, True])
|
@pytest.mark.parametrize("is_short", [False, True])
|
||||||
def test_add_stoploss_on_exchange(mocker, default_conf_usdt, limit_order, is_short) -> None:
|
def test_add_stoploss_on_exchange(mocker, default_conf_usdt, limit_order, is_short) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
|
Loading…
Reference in New Issue
Block a user