Source code for tradeexecutor.utils.leverage_calculations

"""Helpers for leverage calculations.

- Collateral/borrow size

- Fees needed to pay

- Liquidation price

"""
from decimal import Decimal
from dataclasses import dataclass

from tradeexecutor.state.identifier import TradingPairIdentifier, TradingPairKind
from tradeexecutor.state.types import LeverageMultiplier, USDollarAmount, USDollarPrice, Percent


[docs]@dataclass class LeverageEstimate: """Estimate token quantities and fees for a leverage position. A helper class to make sense out of fees when doing leveraged trading on 1delta / Aave. - When increasing short, the fees are allocated to the collateral we need to borrow **Opening short** - Doing 3x ETH short - Start with 10 USDC - Deposit in Aave - ETH price is 1,500 USD/ETH - Using swap exact out method - The short should be 20 USD worth of ETH, 29.99 USD collateral - Deposit 10 USDC to Aave - Open position with 1delta protocol contract - 1delta takes inputs - 1delta initiates swap for 0.0133333333333 WETH to 20 USDC (minus fees) - Uniswap v3 calls back 1delta - 1delta mints out USDC aToken from USDC we received from the swap - We have now total 10 (originak deposit) + 19.99 USDC (new loan) in Aave - 1delta borrows WETH for 0.0133333333333 WETH - Uniswap is happy has we have WETH we did not have at the start of the process - Fee calculations - Token in: Borrowed ETH (no fees) 13.3333333333 - Token out: 19.99 USDC (0.01 USD paid in fees) - Final outcome - vWETH 0.0133333333 - aUSDC 29.99 - `Example transaction <https://dashboard.tenderly.co/tx/polygon/0xaf9bddedc174dc051abcdb28e6be6bf7f337ce73a9d9ba47bf51b42c04fe0df1?trace=0.0.0>`__ ** Close short ** - Closing 3x short as described above - Closing position with 1delta - Assume price is - Get exact wWETH debt amount: 0.0112236255452078143 vWETH - Start a swap process on Uniswap with WETH -> USDC for this amount - There is no WETH yet in this point - Uniswap will tell us how much USDC we will need later down the chain - Uniswap calls 1delta fallback called with incoming WETH from the swap - Aave loan is repaid with WETH - Uniswap tells os the USDC needed to cover the swap cost - Atoken USDC colleteral is converted back to USDC to cover the cost of the swap - The amount of USDC here is fee inclusive to match 0.0112236255452078143 vWETH, so it is wWETH price + fees - Total swap cost is = (0.0112236255452078143 / 0.9995) * ETH price - Fees are 0.0005 * (0.0112236255452078143 / 0.9995) * ETH price = ETH amount * (fee / (1-fee)) - `Example transaction <https://dashboard.tenderly.co/tx/polygon/0x887bafca8fbe39a5188e385e638fa522146065b57e4ca6aa495926a840566272>`__ """ #: Amount of USDC reserve we use for this position. #: #: Set to 0 when closing/reducing short position as the position is covered from the collateral. #: starting_reserve: Decimal #: What was the leverage multiplier we used. #: #: Short open: This is the leverage the user desired. #: #: Short close/reduce: This is the leverage remaining. #: #: leverage: LeverageMultiplier #: Amount of the borrowed token we short. #: #: Positive if we increase our borrow. #: #: Negative if we reduce or borrow. #: borrowed_quantity: Decimal #: What is the borrowed asset value in USD #: borrowed_value: USDollarAmount #: How much additional collateral we are going to take. # #: This is the output when we buy/sell borrowed asset. #: #: Positive: We are selling WETH and adding this USDC to our debt. #: #: Negative: We are buying WETH and need to convert this much of collateral to USDC to match the #: cost. #: additional_collateral_quantity: Decimal #: Amount of total collateral we have #: #: Short open: This is starting reserve + additional collateral borrowed. #: #: Short close: This is remaining collateral after converting it to #: cover the trade to close the short. #: total_collateral_quantity: Decimal #: What is the total borrow amount after this ooperation #: total_borrowed_quantity: Decimal #: What's the price for the borrowed token. #: #: - Assume collateral = USDC # - Assume USDC 1:1 with USD #: borrowed_asset_price: USDollarPrice #: What was our swap fee tier #: #: E.g ``0.0005`` for 5 BPS. fee_tier: Percent #: How much fees we are going to be. #: #: Estimate swap fees. #: lp_fees: USDollarAmount #: What is the liquidation price for this position. #: If the price goes below this, the loan is liquidated. #: liquidation_price: USDollarAmount | None = None def __repr__(self): return f"<Leverage estimate\n" \ f" leverage: {self.leverage}\n" \ f" reserve allocated (USDC): {self.starting_reserve}\n" \ f" borrowed (vToken): {self.borrowed_quantity}\n" \ f" total collateral (aToken): {self.total_collateral_quantity}\n" \ f" LP fees: {self.lp_fees} USD\n" \ f">\n"
[docs] @staticmethod def open_short( starting_reserve: USDollarAmount | Decimal, leverage: LeverageMultiplier, borrowed_asset_price: USDollarAmount, shorting_pair: TradingPairIdentifier, fee: Percent = 0, ) -> "LeverageEstimate": """Get borrow and colleteral size for a loan in leverage protocol trading. See :py:func:`calculate_sizes_for_leverage`. .. note :: Short only. Stablecoin collateral only. Example: .. code-block:: python from tradeexecutor.strategy.lending_protocol_leverage import LeverageEstimate # Start with 10 USD starting_capital = 10.0 leverage = 3.0 eth_price = 1634.4869 # This will borrow additional # - 20 USDC as collateral # - 0.01228645 WETH estimate = LeverageEstimate.open_short( starting_capital, leverage, eth_price, fee=0.0005, ) print("Estimated amounts for the short:", estimate) Example output: .. code-block:: text Estimated amounts for the short: <Leverage estimate leverage: 3.0 reserve allocated (USDC): 10 borrowed (vToken): 0.01223625591615325807353339610 total collateral (aToken): 29.98999999999999999979183318 LP fees: 0.01 USD > :param starting_capital: How much USDC we are going to deposit :param leverage: How much leverage we take :param token_price: What is the price of a token we short :param shorting_pair: The synthetic trading pair for the lending pool short. With aToken and vToken. :param fee: What is the trading fee for swapping the borrowed asset to collateral. TODO: Use the fee from the trading pair. :return: borrow quantity, collateral quantity for the constructed loan """ assert shorting_pair.kind == TradingPairKind.lending_protocol_short max_leverage = shorting_pair.get_max_leverage_at_open(side="short") assert leverage < max_leverage, f"Max short leverage for {shorting_pair.quote.underlying.token_symbol} is {max_leverage}, got {leverage}" if type(starting_reserve) == float: starting_reserve = Decimal(starting_reserve) assert leverage > 0 assert starting_reserve > 0 # Assume collateral is USDC total_collateral_quantity = starting_reserve * (Decimal(leverage) + 1) borrow_value_usdc = total_collateral_quantity - starting_reserve additional_collateral_quantity_no_fee = total_collateral_quantity - starting_reserve swapped_out = additional_collateral_quantity_no_fee * (Decimal(1) - Decimal(fee)) paid_fee = float(additional_collateral_quantity_no_fee) * fee borrow_quantity = borrow_value_usdc / Decimal(borrowed_asset_price) liquidation_price = calculate_liquidation_price( collateral_size=total_collateral_quantity, borrow_quantity=borrow_quantity, shorting_pair=shorting_pair, ) return LeverageEstimate( starting_reserve=Decimal(starting_reserve), leverage=leverage, borrowed_quantity=borrow_quantity, borrowed_value=float(borrow_value_usdc), additional_collateral_quantity=swapped_out, total_collateral_quantity=swapped_out + starting_reserve, total_borrowed_quantity=borrow_quantity, borrowed_asset_price=borrowed_asset_price, fee_tier=fee, lp_fees=paid_fee, liquidation_price=liquidation_price, )
[docs] @staticmethod def close_short( start_collateral: Decimal, start_borrowed: Decimal, close_size: Decimal, borrowed_asset_price: USDollarAmount, fee: Percent | None = 0, ) -> "LeverageEstimate": """Reduce or close short position. Calculate the trade mounts needed to close a short position. - Buy back shorted tokens - Release any collateral See :py:class:`LeverageEstimate` for fee calculation example. Example: .. code-block:: python estimate = LeverageEstimate.close_short( start_collateral=short_position.loan.collateral.quantity, start_borrowed=short_position.loan.borrowed.quantity, close_size=short_position.loan.borrowed.quantity, fee=weth_short_identifier_5bps.fee, borrowed_asset_price=1500.0, ) assert estimate.leverage == 1.0 # Reduced USDC leverage to 1.0 assert estimate.additional_collateral_quantity == pytest.approx(Decimal(-20010.00500250125062552103147)) # USDC needed to reduce from collateral to close position + fees assert estimate.borrowed_quantity == pytest.approx(Decimal(-13.33333333333333333333333333)) # How much ETH is bought to close the short assert estimate.total_collateral_quantity == pytest.approx(Decimal(9979.99499749874937427080171)) # Collateral left after closing the position assert estimate.total_borrowed_quantity == 0 # open vWETH debt left after close assert estimate.lp_fees == pytest.approx(10.005002501250626) We assume collateral is 1:1 USD. :param start_collateral: How much collateral we have at start. :param close_size: How much debt to reduce. Expressed in the amount of borrowed token quantity. """ assert close_size > 0 matching_usdc_amount = Decimal(borrowed_asset_price) * close_size if fee: assert fee > 0 fee_decimal = Decimal(fee) else: fee_decimal = Decimal(0) matching_usdc_amount_with_fees = matching_usdc_amount / (Decimal(1) - fee_decimal) paid_fee = Decimal(matching_usdc_amount_with_fees * fee_decimal) total_collateral_quantity = start_collateral - matching_usdc_amount_with_fees total_borrowed_quantity = start_borrowed - close_size total_borrowed_usd = total_borrowed_quantity * Decimal(borrowed_asset_price) leverage = total_collateral_quantity / (total_collateral_quantity - total_borrowed_usd) return LeverageEstimate( starting_reserve=Decimal(0), leverage=float(leverage), borrowed_quantity=-close_size, borrowed_value=float(matching_usdc_amount), additional_collateral_quantity=-matching_usdc_amount_with_fees, total_collateral_quantity=total_collateral_quantity, total_borrowed_quantity=total_borrowed_quantity, borrowed_asset_price=borrowed_asset_price, fee_tier=fee, lp_fees=float(paid_fee), )
[docs]def calculate_sizes_for_leverage( starting_reserve: USDollarAmount, leverage: LeverageMultiplier, ) -> tuple[USDollarAmount, USDollarAmount, Decimal]: """Calculate the collateral and borrow loan size to hit the target leverage with a starting capital. - When calculating the loan size using this function, the loan net asset value will be the same as starting capital - Because loan net asset value is same is deposited reserve, portfolio total NAV stays intact Notes: .. code-block:: text nav = col - borrow leverage = borrow / nav leverage = col / nav - 1 borrow = nav * leverage col = nav * leverage + nav col = nav * (leverage + 1) # Calculate leverage for 4x and 1000 USD nav (starting reserve) nav = 1000 borrow = 1000 * 4 = 4000 col = 1000 * 4 + 1000 = 5000 col = 1000 * (4 + 1) = 5000 col = 1000 + 4000 = 5000 :param starting_reserve: Initial deposit in lending protocol :param shorting_pair: Leverage short trading pair :return: Tuple (borrow value, collateral value) in dollars """ collateral_size = starting_reserve + (leverage + 1) borrow_size = collateral_size - starting_reserve return borrow_size, collateral_size
[docs]def calculate_liquidation_price( collateral_size: USDollarAmount, borrow_quantity: Decimal, shorting_pair: TradingPairIdentifier, ) -> USDollarAmount: """Calculate the liquidation price for a short position. lP = buy_USD * cfBuy / sell where: buy_USD: buy/deposit asset amount in USD sell: sell/borrow asset amount in sell currency - `See 1delta documentation <https://docs.1delta.io/lenders/metrics>`__. :param collateral_size: Collateral size in USD :param borrow_quantity: Borrow quantity in sell currency :param shorting_pair: Leverage short trading pair :return: Liquidation price in USD """ assert shorting_pair.is_leverage() return Decimal(collateral_size) * Decimal(shorting_pair.get_collateral_factor()) / Decimal(borrow_quantity)