Simple Uni V2 Tree - Infinite Index Token

  • Assumptions:
    • Uses Simple Tree
    • Uses stablecoins (ie, USDC and USDT) to control for impermanent loss
    • Infinite supply of index tokens
  • LPs include:
    • USDC-USDT
    • USDC-iUSDC
  • To run locally, download notebook from SYS-Labs repos
import os
import numpy as np
import pandas as pd
import datetime
import matplotlib.pyplot as plt
import scipy.stats as stats 
import statsmodels.api as sm
import seaborn as sns

from uniswappy import *

Script params

init_tkn_lp = 100000
tkn_delta_param = 1000
tkn_invest_amt = 100
tkn_nm = 'USDC'
itkn_nm = 'iUSDC'
usdt_nm = 'USDT'
iusdt_nm = 'iUSDT'

Simulate price data

# *************************
# *** Simulation
# *************************
n_sim_runs = 2000
seconds_year = 31536000
shape = 2000
scale = 0.0005

p_arr = np.random.gamma(shape = shape, scale = scale, size = n_sim_runs)

n_runs = len(p_arr)-1    
dt = datetime.timedelta(seconds=seconds_year/n_sim_runs)
dates = [datetime.datetime.strptime("2024-09-01", '%Y-%m-%d') + k*dt for k in range(n_sim_runs)]   

x_val = np.arange(0,len(p_arr))
fig, (USD_ax) = plt.subplots(nrows=1, sharex=False, sharey=False, figsize=(18, 5))
USD_ax.plot(dates, p_arr, color = 'r',linestyle = 'dashdot', label='initial invest') 
USD_ax.set_title(f'Price Chart ({tkn_nm}/{usdt_nm})', fontsize=20)
USD_ax.set_ylabel('Price (USD)', size=20)
USD_ax.set_xlabel('Date', size=20)
Text(0.5, 0, 'Date')

png

Initialization Params

user_nm = 'user0'
tkn_amount = init_tkn_lp 
usdt_amount = p_arr[0]*tkn_amount 

Initialize Simple DEX Tree

usdt1 = ERC20(usdt_nm, "0x111")
tkn1 = ERC20(tkn_nm, "0x09")
exchg_data = UniswapExchangeData(tkn0 = tkn1, tkn1 = usdt1, symbol="LP", address="0x011")

TKN_amt = TokenDeltaModel(tkn_delta_param)

iVault1 = IndexVault('iVault1', "0x7")
factory = UniswapFactory(f"{tkn_nm} pool factory", "0x2")
lp = factory.deploy(exchg_data)
Join().apply(lp, user_nm, tkn_amount, usdt_amount)

tkn2 = ERC20(tkn_nm, "0x09")
itkn1 = IndexERC20(itkn_nm, "0x09", tkn1, lp)
exchg_data1 = UniswapExchangeData(tkn0 = tkn2, tkn1 = itkn1, symbol="LP1", address="0x012")
lp1 = factory.deploy(exchg_data1)
JoinTree().apply(lp1, user_nm, iVault1, 10000)

# Re-balance LP price after JoinTree
SwapDeposit().apply(lp, usdt1, user_nm, lp.reserve0-lp.reserve1)

lp.summary()
lp1.summary()
Exchange USDC-USDT (LP)
Reserves: USDC = 109999.99999999997, USDT = 110000.0
Liquidity: 109985.15703628703 

Exchange USDC-iUSDC (LP1)
Reserves: USDC = 9972.071706380626, iUSDC = 4890.146199614032
Liquidity: 6983.186132220438 

Take an investment position

tkn_invest = 100
invested_user_nm = 'invested_user'

SwapIndexMint(iVault1, opposing = False).apply(lp, tkn1, invested_user_nm, tkn_invest)
mint_itkn1_deposit = iVault1.index_tokens[itkn_nm]['last_lp_deposit']
SwapDeposit().apply(lp1, itkn1, invested_user_nm, mint_itkn1_deposit)

lp.summary()
lp1.summary()

lp_invest_track  = lp.liquidity_providers[invested_user_nm]
lp1_invest_track  = lp1.liquidity_providers[invested_user_nm]

# Redeem from parent
tkn_redeem_parent = RebaseIndexToken().apply(lp, tkn1, lp_invest_track)

# Redeem from tree (child + parent)
itkn_redeem_child = RebaseIndexToken().apply(lp1, itkn1, lp1_invest_track)
tkn_redeem_tree = RebaseIndexToken().apply(lp, tkn1, itkn_redeem_child) 

print(f'{tkn_redeem_parent:.3f} USDC redeemed from {lp_invest_track:.3f} LP tokens if {tkn_invest:.1f} invested USDC immediately pulled from parent')
print(f'{tkn_redeem_tree:.3f} USDC redeemed from {lp1_invest_track:.3f} LP1 tokens if {tkn_invest:.1f} invested USDC immediately pulled from tree')
Exchange USDC-USDT (LP)
Reserves: USDC = 110099.99999999997, USDT = 110000.0
Liquidity: 110035.06384709699 

Exchange USDC-iUSDC (LP1)
Reserves: USDC = 9972.071706380626, iUSDC = 4940.053010423988
Liquidity: 7018.676038131248 

99.700 USDC redeemed from 49.907 LP tokens if 100.0 invested USDC immediately pulled from parent
99.403 USDC redeemed from 35.490 LP1 tokens if 100.0 invested USDC immediately pulled from tree

Simulate trading

arb = CorrectReserves(lp, x0 = 1)
arb1 = CorrectReserves(lp1, x0 = lp1.reserve1/lp1.reserve0)

TKN_amt = TokenDeltaModel(n_sim_runs)

lp_direct_invest_arr = []; lp1_direct_invest_arr = []; lp1_tree_invest_arr = []; 
pTKN_USDT_arr = []; pTKN_iTKN_arr = []
fee_lp_arr  = []; fee_lp1_arr  = [];

for k in range(n_sim_runs):

    # *****************************
    # ***** Parent Arbitrage ******
    # *****************************   
    arb.apply(p_arr[k])

    # *****************************
    # ***** Child Arbitrage ******
    # *****************************       
    p_lp1 = SettlementLPToken().apply(lp, tkn1, lp1.reserve0)/lp1.reserve0
    arb1.apply(p_lp1)

    # *****************************
    # ***** Random Swapping ******
    # *****************************       
    Swap().apply(lp, tkn1, user_nm, TKN_amt.delta()) 
    Swap().apply(lp, usdt1, user_nm, TKN_amt.delta()) 

    # conservatively assume 10% of parent trading by volume
    Swap().apply(lp1, tkn2, user_nm, 0.1*TKN_amt.delta()) 
    Swap().apply(lp1, itkn1, user_nm, SettlementLPToken().apply(lp, tkn1, 0.2*TKN_amt.delta()))

    # *****************************
    # ******* Data Capture ********
    # *****************************

    # price
    pTKN_USDT_arr.append(LPQuote().get_price(lp, tkn1)) 
    pTKN_iTKN_arr.append(LPQuote().get_price(lp1, tkn1)) 

    # investment performance
    tkn_redeem_parent = RebaseIndexToken().apply(lp, tkn1, lp_invest_track)
    itkn_redeem_child = RebaseIndexToken().apply(lp1, itkn1, lp1_invest_track)    
    tkn_redeem_tree = RebaseIndexToken().apply(lp, tkn1, itkn_redeem_child) 

    lp_direct_invest_arr.append(tkn_redeem_parent)
    lp1_direct_invest_arr.append(RebaseIndexToken().apply(lp1, tkn2, lp1_invest_track))    
    lp1_tree_invest_arr.append(tkn_redeem_tree)

    # DEX Fees     
    fee_lp_arr.append(TreeAmountQuote().get_tot_y(lp, lp.collected_fee0, lp.collected_fee1))
    fee_lp1_arr.append(TreeAmountQuote().get_tot_y(lp1, lp1.collected_fee0, lp1.collected_fee1))

lp.summary()
lp1.summary()

# Redeem from parent
tkn_redeem_parent = RebaseIndexToken().apply(lp, tkn1, lp_invest_track)

# Redeem from tree (child + parent)
itkn_redeem_child = RebaseIndexToken().apply(lp1, itkn1, lp1_invest_track)
tkn_redeem_tree = RebaseIndexToken().apply(lp, tkn1, itkn_redeem_child)

print(f'{tkn_redeem_parent:.3f} USDC redeemed from {lp_invest_track:.3f} LP tokens if {tkn_invest:.1f} invested USDC pulled from parent (lp)')
print(f'{tkn_redeem_tree:.3f} USDC redeemed from {lp1_invest_track:.3f} LP1 tokens if {tkn_invest:.1f} invested USDC pulled from tree (lp + lp1)')
Exchange USDC-USDT (LP)
Reserves: USDC = 147136.0106536532, USDT = 146900.94926218514
Liquidity: 138640.7591830568 

Exchange USDC-iUSDC (LP1)
Reserves: USDC = 12858.805522377965, iUSDC = 6120.031452710504
Liquidity: 8295.338161424965 

105.752 USDC redeemed from 49.907 LP tokens if 100.0 invested USDC pulled from parent (lp)
110.561 USDC redeemed from 35.490 LP1 tokens if 100.0 invested USDC pulled from tree (lp + lp1)
fig, (TKN_ax, USDT_ax) = plt.subplots(nrows=2, sharex=False, sharey=False, figsize=(15, 8))

strt_pt = 5

TKN_ax.plot(dates[strt_pt:], p_arr[strt_pt:], color = 'g',linestyle = 'dashed', linewidth=1, label=f'{tkn_nm} Price (Market)') 
TKN_ax.plot(dates[strt_pt:], pTKN_USDT_arr[strt_pt:], color = 'b',linestyle = '-', linewidth=0.7, label=f'{tkn_nm}/{usdt_nm} (LP)') 

TKN_ax.set_title('Price comparison: parent vs child LPs', fontsize=20)
TKN_ax.set_ylabel('Price (USD)', size=20)
TKN_ax.legend(fontsize=12)
TKN_ax.grid()

USDT_ax.plot(dates[strt_pt:], pTKN_iTKN_arr[strt_pt:], color = 'b',linestyle = 'dashed', label=f'{tkn_nm}/{itkn_nm} (LP1)') 
USDT_ax.set_ylabel('prices', size=20)
USDT_ax.set_ylabel('Price (USD)', size=20)
USDT_ax.legend(fontsize=12)
USDT_ax.grid()

png

fig, ax = plt.subplots(1, 2, figsize=(15,5))

sns.distplot(pTKN_USDT_arr, hist=True, kde=True, bins=int(30), color = 'darkblue',
             hist_kws={'edgecolor':'black'}, kde_kws={'linewidth': 2}, ax=ax[0])

sns.distplot(pTKN_iTKN_arr, hist=True, kde=True, bins=int(30), color = 'darkblue',
             hist_kws={'edgecolor':'black'}, kde_kws={'linewidth': 2}, ax=ax[1])

ax[0].set_title(f'Distribution: {tkn_nm}/{usdt_nm} LP price (parent)')
ax[0].set_xlabel('Price')
ax[0].set_ylabel('Frequency')

ax[1].set_title(f'Distribution: {tkn_nm}/{itkn_nm} LP1 price (child)')
ax[1].set_xlabel('Price')
ax[1].set_ylabel('Frequency')
Text(0, 0.5, 'Frequency')

png

lowess = sm.nonparametric.lowess
x = range(0,n_sim_runs)
res = lowess(lp_direct_invest_arr, x, frac=1/15); sm_lp_direct = res[:,1]
res = lowess(lp1_direct_invest_arr, x, frac=1/15); sm_lp1_direct = res[:,1]
res = lowess(lp1_tree_invest_arr, x, frac=1/15); sm_lp1_tree= res[:,1]

strt_ind = 3

fig, (p_ax) = plt.subplots(nrows=1, sharex=True, sharey=False, figsize=(15, 8))
fig.suptitle('Simple Tree (USDC / USDT) performance ', fontsize=20)
p_ax.plot(dates[strt_ind:], lp_direct_invest_arr[strt_ind:], linestyle='dashed', linewidth=0.5, color = 'g') 
p_ax.plot(dates[strt_ind:], sm_lp_direct[strt_ind:], color = 'g', label = 'Expected return from parent (LP)') 
p_ax.plot(dates[strt_ind:], lp1_direct_invest_arr[strt_ind:], linestyle='dashed', linewidth=0.5, color = 'b')
p_ax.plot(dates[strt_ind:], sm_lp1_direct[strt_ind:], color = 'b', label = 'Expected return from child (LP1)') 
p_ax.plot(dates[strt_ind:], lp1_tree_invest_arr[strt_ind:], linestyle='dashed', linewidth=0.5, color = 'r')
p_ax.plot(dates[strt_ind:], sm_lp1_tree[strt_ind:], color = 'r', label = 'Expected return from tree (LP+LP1)') 
p_ax.legend( fontsize=12)
p_ax.set_ylim(80, 140) 
p_ax.set_ylabel("$100 USD Investment", fontsize=14)
Text(0, 0.5, '$100 USD Investment')

png

print(f'{tkn_invest:.3f} TKN before is worth {sm_lp_direct[-1]:.3f} TKN after direct investment into parent (lp)') 
print(f'{tkn_invest:.3f} TKN before is worth {sm_lp1_direct[-1]:.3f} TKN after direct investment into child (lp1)') 
print(f'{tkn_invest:.3f} TKN before is worth {sm_lp1_tree[-1]:.3f} TKN after investment into simple tree (lp + lp1)')
100.000 TKN before is worth 106.113 TKN after direct investment into parent (lp)
100.000 TKN before is worth 109.111 TKN after direct investment into child (lp1)
100.000 TKN before is worth 111.629 TKN after investment into simple tree (lp + lp1)
t = np.arange(0,len(fee_lp_arr))

fee_lpB = np.array(fee_lp1_arr)
fee_lpA = fee_lpB+np.array(fee_lp_arr)

fig = plt.figure(figsize=(15, 5))

plt.plot(dates, fee_lpA, color = 'r', label = f'Parent LP ({tkn_nm}/{usdt_nm})')
plt.fill_between(dates, fee_lpB, fee_lpA, alpha=0.3, color='r')

plt.plot(dates, fee_lpB, color = 'b', label = f'Child LP1 ({tkn_nm}/{itkn_nm})') 
plt.fill_between(dates, np.repeat(0,len(fee_lp_arr)), fee_lpB, alpha=0.3, color='b')

plt.title('Cumulative Arbitrage Fees (Stablecoin Simple Tree, Uni V2)', fontsize = 20)
plt.ylabel("Collected Fees (USD)", fontsize=14) 

plt.legend(fontsize=12)
<matplotlib.legend.Legend at 0x282f48eb0>

png