Impermanent Loss

Background

Impermanent loss is a concept in decentralized finance (DeFi) that occurs when providing liquidity to automated market makers (AMMs) such as Uniswap. It represents the difference between holding tokens in a liquidity pool and simply holding them in a wallet. It is important for liquidity providers to understand and monitor impermanent loss as they could incur losses while trying to earn passive income.

When users provide liquidity to a pool, they deposit pairs of assets (like USDC and WETH) in equal value amounts. As trades occur in the pool, the ratio of these assets changes, causing the liquidity provider’s share of each asset to adjust. If the price of one asset increases or decreases relative to the other, the provider's share of assets will diverge from the initial deposit amounts.

Impermanent loss happens when this divergence leads to a scenario where the total value of the assets, when withdrawn from the pool, is less than what it would have been if the provider had simply held the assets without providing liquidity. This loss is "impermanent" because it only becomes permanent if the liquidity provider withdraws their assets when the price ratio is different from the initial one. If the price ratio returns to its original value, the impermanent loss can disappear.

Tracking impermanent loss is crucial for liquidity providers because it can offset the gains made from trading fees earned in the pool. Understanding and monitoring impermanent loss helps liquidity providers make informed decisions about whether the potential returns justify the risk, and when it might be optimal to exit a liquidity pool to minimize losses.

Impermanent Loss

Tracking Impermanent Loss

We provide 2 examples to track impermanent loss. The first example uses the ImpermanentLossPolicy with a normal agent and the second example uses the PassiveConcentratedLP policy with an ImpermanentLossAgent. They accomplish the same task and have the same result. However, the ImpermanentLossPolicy provides us with more signals for research, whereas the second example shows impermanent loss as the reward for the agent.

Example Using The ImpermanentLossPolicy

Impermanent Loss Formula

This is the formula we used to calculate impermanent loss in units of token0 and token1 but note that over time the term impermanent loss has been used interchangeably for different PnL measures. You can read more about this here: A Guide through Impermanent Loss on Uniswap.

The first action that the agent performs is providing liquidity using all the tokens they have.

Afterwards, the agent doesn't perform any trade because we're observing the impermanent loss that occurs.

We track the number of tokens we would have if we held onto the tokens, the current number of tokens in the adjusted LP position, the current number of tokens in the adjsted LP position including the fees, and the impermanent loss in the units of token0 or token1.

Impermanent Loss Simulation

Plotting the "Hodl Value in Token0" with "Current Token0 Value with Fees" shows us the difference between holding the tokens in a wallet and holding the tokens in a liquidity pool. This difference value is shown on the "Impermanent Loss" signal on the dashboard.

Impermanent Loss Simulation

Example Using The ImpermanentLossAgent

Impermanent Loss Formula

This is the formula we used to calculate impermanent loss but note that over time the term impermanent loss has been used interchangeably for different PnL measures. You can read more about this here: A Guide through Impermanent Loss on Uniswap.

This agent implements the PassiveConcentratedLP policy which makes the agent provide liquidity passively to the pool. Its first action is a swap of tokens which is necessary to have equal value amounts of each token. Then the agent will provide liquidity using all its tokens. Afterwards it performs no other action. At each block, the agent will update its reward on the dashboard to be the percentage of impermanent loss.


How To Run

Installation

Follow our Getting Started guide to install the dojo library and other required tools.

Then clone the dojo_examples repository and go into the relevant directory.

Terminal
git clone https://github.com/CompassLabs/dojo_examples.git
cd dojo_examples/examples/impermanent_loss_tracking

Running

Download the dashboard to view the simulation results. To view example simulation data, download results.db and click 'Add A Simulation' on the dashboard.

To run the strategy which uses the policy, use the following command.

Terminal
python run_with_policy.py

To run the strategy which uses the agent, use the following command.

Terminal
python run_with_agent.py

This command will setup your local blockchain, contracts, accounts and agents. You can then access your results on your Dojo dashboard by connecting to a running simulation.


Step-By-Step Explanation

Initialization

We create a class called ImpermanentLossPolicy which inherits from the BasePolicy class and initializes the self.has_executed_lp_action variable which makes sure that we only provide liquidity once.

policy.py
class ImpermanentLossPolicy(BasePolicy):  # type: ignore
  def __init__(self, agent: BaseAgent) -> None:
      super().__init__(agent=agent)
      self.has_provided_liquidity = False
      self.has_executed_lp_action = False
      self.initial_lp_positions: dict[str, Decimal] = {}

Providing Liquidity

We provide liquidity to the pool at the start. Therefore, we return a UniswapV3Quote object with the relevant parameters set only the first time we run this simulation. We set quantities=[portfolio[token0], portfolio[token1]] which means that we give all of our tokens to the pool. However, the pool will calculate and only take the max amount of tokens in equal value amounts. For example, if the agent has 10,000 USDC and 1 WETH (worth 2100 USDC), the pool will take 2100 USDC and 1 WETH (not exact amounts).

policy.py
if not self.has_executed_lp_action:
  self.has_executed_lp_action = True
  portfolio = self.agent.portfolio()
  spot_price = obs.price(token0, token1, pool)
 
  decimals0 = obs.token_decimals(token0)
  decimals1 = obs.token_decimals(token1)
 
  lower_price_range = Decimal(0.95) * spot_price
  upper_price_range = Decimal(1.05) * spot_price
  tick_spacing = obs.tick_spacing(pool)
 
  lower_tick = uniswapV3.price_to_active_tick(
      lower_price_range, tick_spacing, (decimals0, decimals1)
  )
  upper_tick = uniswapV3.price_to_active_tick(
      upper_price_range, tick_spacing, (decimals0, decimals1)
  )
  action = UniswapV3Quote(
      agent=self.agent,
      pool=pool,
      quantities=(portfolio[token0], portfolio[token1]),
      tick_range=(lower_tick, upper_tick),
  )

Signal Calculation

Signals allow us to easily view data on our Dojo dashboard. In this example, we are adding 8 signals (essentially 4 signals but we display each signal in units of token0 or token1):

  • Hodl Value - number of total tokens we would have if we held onto the tokens
  • Current Token Value - the number of tokens the agent would get back if it withdrew liquidity.
  • Current Token Value with Fees - the number of tokens the agent would get back if it withdrew liquidity, including the accrued LP fees.
  • Impermanent Loss - if above 0, the agent made profit. If below 0, the agent made a loss as it could have simply held onto the tokens.

We can then add bookmarks on the dashboard to examine when impermanent loss happened, at which block, the number of tokens at that exact time etc.

policy.py
def compute_signals(self, obs: UniswapV3Observation) -> None:
  pool = obs.pools[0]
  token0, token1 = obs.pool_tokens(pool)
  token_ids = self.agent.get_liquidity_ownership_tokens()
 
  current_portfolio = obs.lp_total_potential_tokens_on_withdrawal(token_ids)
  current_quantities = obs.lp_quantities(token_ids)
 
  if current_portfolio == {}:
      token0_amount, token1_amount = self.calculate_initial_signal(obs)
      current_portfolio.update({token0: token0_amount, token1: token1_amount})
      current_quantities.update({token0: token0_amount, token1: token1_amount})
      self.initial_lp_positions.update(
          {token0: token0_amount, token1: token1_amount}
      )
  elif not self.has_provided_liquidity:
      self.has_provided_liquidity = True
      self.initial_lp_positions.update(
          {token0: current_quantities[token0], token1: current_quantities[token1]}
      )
 
  value_if_held0 = self.initial_lp_positions[token0] + self.initial_lp_positions[
      token1
  ] * obs.price(token1, token0, pool)
  value_if_held1 = (
      self.initial_lp_positions[token0] * obs.price(token0, token1, pool)
      + self.initial_lp_positions[token1]
  )
 
  current_wealth0 = current_portfolio[token0] + current_portfolio[
      token1
  ] * obs.price(token1, token0, pool)
  current_wealth1 = current_portfolio[token1] + current_portfolio[
      token0
  ] * obs.price(token0, token1, pool)
 
  obs.add_signal("Hodl Value in Token0", value_if_held0)
  obs.add_signal("Hodl Value in Token1", value_if_held1)
  obs.add_signal("Current Token0 Value", current_quantities[token0])
  obs.add_signal("Current Token1 Value", current_quantities[token1])
  obs.add_signal("Current Token0 Value With Fees", current_wealth0)
  obs.add_signal("Current Token1 Value With Fees", current_wealth1)
  obs.add_signal("Token0 Impermanent Loss", current_wealth0 - value_if_held0)
  obs.add_signal("Token1 Impermanent Loss", current_wealth1 - value_if_held1)

The get_liquidity_ownership_tokens() function returns the ids of tokens that the agent provided liquidity for.

The lp_total_potential_tokens_on_withdrawal(token_ids) function returns the total number of tokens the agent will get back upon withdrawing liquidity. This includes the quantities of each token and their uncollected fees.

The lp_quantities(token_ids) function returns the number of tokens the agent will get back upon withdrawing liquidity. It doesn't include the uncollected LP fees.

The current_portfolio dictionary may be empty if the agent hasn't provided liquidity yet. So we calculate an estimate for the initial signal and populate the dictionary accordingly.

If the current_portfolio is not empty but the self.has_provided_lp is still set to False, we set initial_lp_positions to the quantities we have at that time. This will be used to calculate how much money we would have had if we simply held onto those tokens, instead of providing liquidity to the pool.

The obs.price(token, unit, pool) function returns the price of a token in the units of another token in a specific pool at the current block.

ImpermanentLossAgent Explanation

The _pool_wealth function keeps track of the value of the given portfolio. This could be the hold_portfolio or the current portfolio.

uniswapV3_impermanent_loss.py
def _pool_wealth(
  self, obs: UniswapV3Observation, portfolio: dict[str, Decimal]
) -> float:
  """Calculate the wealth of a portfolio denoted in the y asset of the pool.
 
  :param portfolio: Portfolio to calculate wealth for.
  :raises ValueError: If agent token is not in pool.
  """
  wealth = Decimal(0)
  if len(portfolio) == 0:
      return float(wealth)
 
  pool = obs.pools[0]
  pool_tokens = obs.pool_tokens(pool=pool)
  for token, quantity in portfolio.items():
      if token not in pool_tokens:
          raise ValueError(f"{token} not in pool, so it can't be priced.")
      price = obs.price(token=token, unit=pool_tokens[1], pool=pool)
      wealth += quantity * price
  return float(wealth)

The output of the reward function will be displayed on our dashboard. Therefore, we return the percentage of impermanent loss in this function by calculating the difference in value if we held onto the tokens and if we provided liquidity with them.

uniswapV3_impermanent_loss.py
def reward(self, obs: UniswapV3Observation) -> float:  # type: ignore
  """Impermanent loss of the agent denoted in the y asset of the pool."""
  token_ids = self.get_liquidity_ownership_tokens()
  if not self.hold_portfolio:
      self.hold_portfolio = obs.lp_quantities(token_ids)
  hold_wealth = self._pool_wealth(obs, self.hold_portfolio)
  lp_wealth = self._pool_wealth(obs, obs.lp_portfolio(token_ids))
  if hold_wealth == 0:
      return 0.0
  return (lp_wealth - hold_wealth) / hold_wealth

In the run.py file, we create a Uniswap environment and an agent that implements the ImpermanentLossPolicy.

Results

You can download the results to this example below.

We offer a dashboard desktop application for visualizing your simulation results. You can download the file for the desktop application here, or just open the results in our hosted dashboard.