Simple Uni V2 Tree - Finite Index Token

  • Assumptions:
    • Uses Simple Tree
    • Uses stablecoins (ie, USDC and USDT) to control for impermanent loss
    • Includes state machine to handle finite 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'
usd_nm = 'USDT'
iusd_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}/{usd_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 
dai_amount = p_arr[0]*tkn_amount 

Initialize Left DEX Tree

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

TKN_amt = TokenDeltaModel(tkn_delta_param)
TKN_amt_arb = TokenDeltaModel(100)

lp1_state = MarkovState(stochastic = True)
iVault1 = IndexVault('iVault1', "0x7")

factory = UniswapFactory(f"{tkn_nm} pool factory", "0x2")
lp = factory.deploy(exchg_data)
Join().apply(lp, user_nm, tkn_amount, dai_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, dai1, user_nm, lp.reserve0-lp.reserve1)

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

Exchange USDC-iUSDC (LP1)
Reserves: USDC = 9972.071706380626, iUSDC = 4836.2900332872905
Liquidity: 6944.62605219279 

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']
lp1_state.next_state(mint_itkn1_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]

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) 

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 = 109999.99999999996
Liquidity: 110033.32218331363 

Exchange USDC-iUSDC (LP1)
Reserves: USDC = 9972.071706380626, iUSDC = 4886.196054159184
Liquidity: 6980.31144644773 

99.700 USDC redeemed from 49.906 LP tokens if 100.0 invested USDC immediately pulled from parent
99.403 USDC redeemed from 35.685 LP1 tokens if 100.0 invested USDC immediately pulled from tree

Simulate trading

arb = CorrectReserves(lp, x0 = 1)
arb1 = Arbitrage(lp1, lp1_state) 

TKN_amt = TokenDeltaModel(tkn_delta_param)

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

for k in range(n_sim_runs):

    #if(k % 100 == 0 and k != 0): print(f'Processing event {k}')
    
    # *****************************
    # ***** Parent Arbitrage ******
    # *****************************   
    arb.apply(p_arr[k])

    # *****************************
    # ***** Child Arbitrage ******
    # *****************************       
    amt_arb1 = TKN_amt_arb.delta()   
    arb1.apply(1, user_nm, amt_arb1)
    arb1.update_state(itkn1)    

    mint_tkn1_amt = 0.5*TKN_amt.delta()
    SwapIndexMint(iVault1, opposing = False).apply(lp, tkn1, user_nm, mint_tkn1_amt)
    mint_itkn1_deposit = iVault1.index_tokens[itkn_nm]['last_lp_deposit']
    lp1_state.next_state(mint_itkn1_deposit)   
    vault_lp1_amt = lp1_state.get_current_state('dVault')  
    burned_itkn1_amt = lp1_state.get_current_state('dBurned') 

    ## WithdrawSwap burned token from parent LP
    if(burned_itkn1_amt > 0):
        total_tkn_w_swap = LPQuote(False).get_amount_from_lp(lp, tkn1, burned_itkn1_amt)
        amt_out = RemoveLiquidity().apply(lp, tkn1, user_nm, total_tkn_w_swap/2)    

    ## Balance LP1: TKN/iTKN
    if(vault_lp1_amt > 0):
        # A portion of aquired token is coming from newly minted, while the remainder is coming from held 
        amt_tkn = LPQuote(False).get_amount_from_lp(lp, tkn1, vault_lp1_amt) 
        price_tkn = amt_tkn/vault_lp1_amt 
        AddLiquidity(price_tkn).apply(lp1, itkn1, user_nm, vault_lp1_amt)        
    elif(vault_lp1_amt < 0):
        # A portion of removed token is getting held, while the remainder is getting burned
        RemoveLiquidity().apply(lp1, itkn1, user_nm, abs(vault_lp1_amt))  

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

    # conservatively assume 10% of tokens held outside vault are traded
    held_tokens = lp1_state.get_current_state('Held')
    if(held_tokens > 0):
        tradable_itkn1_amt = 0.1*held_tokens
        Swap().apply(lp1, tkn2, user_nm, LPQuote(False).get_amount_from_lp(lp, tkn1, tradable_itkn1_amt))  
        Swap().apply(lp1, itkn1, user_nm, tradable_itkn1_amt)

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

    # price
    pTKN_DAI_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()

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) 

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

Exchange USDC-iUSDC (LP1)
Reserves: USDC = 30172.363323917827, iUSDC = 14919.845854947607
Liquidity: 18018.420437590765 

103.708 USDC redeemed from 49.906 LP tokens if 100.0 invested USDC pulled from parent
122.500 USDC redeemed from 35.685 LP1 tokens if 100.0 invested USDC pulled from tree
lp1_state.check_states()
lp1_state.inspect_states(tail = True, num_states = 5)
Amount of tokens retained across states: FAIL
Mint Held Vault Burned dHeld dVault dBurned
1996 20.464916 3907.214028 16139.020590 73695.645149 -171.987708 165.276973 29.088187
1997 20.407510 4461.748224 15553.598456 73746.998003 554.534196 -585.422134 51.352855
1998 9.126091 4496.482451 15483.465970 73802.803772 34.734228 -70.132486 55.805768
1999 1.355033 4197.025621 15782.904086 73811.948577 -299.456830 299.438116 9.144805
2000 10.243511 3703.699982 16246.715137 73842.818198 -493.325640 463.811051 30.869621
fig, (TKN_ax, DAI_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_DAI_arr[strt_pt:], color = 'b',linestyle = '-', linewidth=0.7, label=f'{tkn_nm}/{usd_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()

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

png

y1_samp = stats.gamma.rvs(a=2000, scale=0.0005, size=10000)

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

sns.distplot(pTKN_DAI_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}/{usd_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_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 104.377 TKN after direct investment into parent (lp)
100.000 TKN before is worth 119.011 TKN after direct investment into child (lp1)
100.000 TKN before is worth 123.596 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}/{usd_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 (Direct Investment, Simple Tree, Uni V2)', fontsize = 20)
plt.xlabel("Time unit", fontsize=12)
plt.ylabel("Collected Fees (USD)", fontsize=14) 

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

png