Skip to content

Commit 5a7984c

Browse files
committed
initial import
0 parents  commit 5a7984c

11 files changed

+3007
-0
lines changed

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2023 Atis Elsts
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Code for the Uniswap LP Articles
2+
3+
This repository contains Python code for the artice series ["Liquidity Provider Strategies for Uniswap v3"](https://atise.medium.com/liquidity-provider-strategies-for-uniswap-v3-table-of-contents-64725c6c0b10).
4+
5+
The code is used to produce the graphs in the published articles.
6+
7+
For visualization, it uses Matplotlib in combination with the [ING theme](https://pypi.org/project/ing-theme-matplotlib/).
8+
9+
# Contents
10+
11+
* v2_math.py - math for full-range positions
12+
* v3_math.py - math for concentrated liquidity positions
13+
* plot_article_1.py - plots for the article "An Introduction to Uniswap LP Strategies and Hedging"
14+
* plot_article_2.py - plots for the article "Dynamic Hedging"
15+
* plot_article_3.py - plots for the article "Loss Versus Rebalancing (LVR)"
16+
* plot_article_4.py - plots for the article "Power Perpetuals"
17+
* plot_article_6.py - plots for the article "Liquidity Relocation (Rebalancing)"
18+
* fit_power_perpetual_coefficients.py - code from the power perpetual article exported to a standalone module
19+
20+
# Simulations
21+
22+
The current implementation of price path simulations use significant RAM
23+
and require several minutes to complete. They could be improved by using the JIT
24+
decorator, or by rewriting the code to a faster programming language.

fit_power_perpetual_coefficients.py

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
#!/usr/bin/env python
2+
3+
#
4+
# This script computes the power perpetual coefficients required to hedge
5+
# a v3 concentrated liquidity coeffcient.
6+
# The inspiration comes from the article "Spanning with Power Perpetuals" by J. Clarke.
7+
#
8+
9+
import matplotlib.pyplot as pl
10+
import numpy as np
11+
from ing_theme_matplotlib import mpl_style
12+
import v2_math
13+
import v3_math
14+
15+
# max order to use for the power perp
16+
MAX_ORDER = 2
17+
18+
# the higher, the more accurate the fit
19+
NUM_STEPS = 1000
20+
21+
INITIAL_PRICE = 100
22+
INITIAL_VALUE = 200
23+
24+
#
25+
# This uses hedging formula from the article "Spanning with Power Perpetuals" by Joseph Clark,
26+
# where the AMM is approximated using the Taylor expansion around r=0.
27+
#
28+
# This assumes zero funding rate! (as time is not given as a parameter to this function)
29+
#
30+
def power_perp_v2(initial_value, initial_price, price, order, include_first_order=True):
31+
r = price / initial_price - 1.0 # r is the price return
32+
perp_return = 0
33+
if order >= 1 and include_first_order:
34+
perp_return += -0.5 * r
35+
if order >= 2:
36+
perp_return += 0.125 * (r ** 2)
37+
if order >= 3:
38+
perp_return += -3 / 48 * (r ** 3)
39+
if order >= 4:
40+
perp_return += 15 / 384 * (r ** 4)
41+
perp_value = initial_value * (perp_return + 1.0)
42+
# print("power perp value at price", order, include_first_order, price, perp_value)
43+
return perp_value
44+
45+
#
46+
# This uses numerically found coefficient array for a narrow-range v3 positions.
47+
#
48+
def power_perp_v3(initial_value, initial_price, price, price_a, price_b, coefficients, order, include_first_order=True):
49+
# assume that when the price crosses the LP range boundaries, the owner sells the power perp,
50+
# effectively implying that the price can never go out of boundaries.
51+
price = min(price, price_b)
52+
price = max(price, price_a)
53+
54+
r = price / initial_price - 1.0 # r is the price return
55+
perp_return = 0
56+
if order >= 1 and include_first_order:
57+
perp_return += coefficients[1] * r
58+
if order >= 2:
59+
perp_return += coefficients[2] * (r ** 2)
60+
if order >= 3:
61+
perp_return += coefficients[3] * (r ** 3)
62+
if order >= 4:
63+
perp_return += coefficients[4] * (r ** 4)
64+
65+
perp_value = initial_value * (perp_return + 1.0)
66+
# print("power perp value at price", order, include_first_order, price, perp_value)
67+
if perp_value < 0:
68+
perp_value = 0
69+
return perp_value
70+
71+
72+
#
73+
# Returns coefficients of the power perpetual needed to hedge a given position.
74+
#
75+
# Negative coefficient means that the perp should be short, positive: long.
76+
# The coefficient are in terms of the initial value of the LP position.
77+
#
78+
# For instance, if the function returns [0, -0.5, 0.125, -0.0625, 0.0390625],
79+
# (these are the coefficients for a full-range position)
80+
# the ETH/USD LP should:
81+
# 1. Buy short ETH perp worth 50% of the position.
82+
# 2. Buy long ETH^2 perp worth 12.5% of the position.
83+
# 3. (Optionally) Buy short ETH^3 perp worth 6.25% of the position.
84+
# 4. (Optionally) Buy long ETH^4 perp worth ~3.9% of the position.
85+
#
86+
#
87+
# Example n-th order reconstructions from the result:
88+
#
89+
# coefficients = fit_power_hedging_coefficients(L, p_a, p_b)
90+
# reconstruction1 = [power_perp_v3(price, coefficients, 1) for price in x]
91+
# reconstruction2 = [power_perp_v3(price, coefficients, 2) for price in x]
92+
# reconstruction3 = [power_perp_v3(price, coefficients, 3) for price in x]
93+
#
94+
def fit_power_hedging_coefficients(liquidity, price_a, price_b, max_order):
95+
# the max value is bounded by the amount of tokens `y` at price `price_b`
96+
v_max = liquidity * (price_b ** 0.5 - price_a ** 0.5)
97+
98+
step = (price_b - price_a) / NUM_STEPS
99+
100+
prices = np.arange(price_a, price_b + step, step)
101+
values = [v3_math.position_value_from_max_value(v_max, price, price_a, price_b) for price in prices]
102+
103+
# convert from f(x) to f(r) before doing the fit
104+
price_returns = [price / INITIAL_PRICE - 1.0 for price in prices]
105+
value_returns = [v / INITIAL_VALUE - 1.0 for v in values]
106+
107+
# do a polynomial fit, with a high-order polynomial
108+
assert max_order < 20
109+
polyfit_many = np.polyfit(price_returns, value_returns, 20)
110+
111+
# use just the first N coefficients from the result
112+
coefficients = [0] * (max_order + 1)
113+
coefficients[0] = 0 # always zero for the 0-th order hedge
114+
for i in range(1, max_order + 1):
115+
coefficients[i] = -polyfit_many[-(i + 1)] # i-th order power perp
116+
117+
return coefficients
118+
119+
120+
def main():
121+
mpl_style(True)
122+
123+
# max order of the power perp
124+
order = MAX_ORDER
125+
126+
# Full range positions
127+
min_price = INITIAL_PRICE / 4
128+
max_price = INITIAL_PRICE * 4
129+
step = (max_price - min_price) / NUM_STEPS
130+
prices = np.arange(min_price, max_price + step, step)
131+
liquidity = v2_math.get_liquidity(INITIAL_VALUE / INITIAL_PRICE / 2, INITIAL_VALUE / 2)
132+
pl.figure()
133+
profit_pos = [v2_math.position_value_from_liquidity(liquidity, price) - INITIAL_VALUE for price in prices]
134+
pl.plot(prices, profit_pos, label="LP position")
135+
profit_perp = [power_perp_v2(INITIAL_VALUE, INITIAL_PRICE, price, order) - INITIAL_VALUE for price in prices]
136+
pl.plot(prices, profit_perp, label=f"Power perperpetual, order={order}")
137+
pl.plot(prices, [a+b for a,b in zip(profit_perp, profit_pos)], label="Hedged position")
138+
139+
pl.xlabel("Volatile asset price, $")
140+
pl.ylabel("Profit, $")
141+
pl.title("Full range")
142+
pl.legend()
143+
pl.show()
144+
pl.close()
145+
146+
147+
# Concentrated liquidity positions with different range r
148+
for r in [1.01, 1.1, 1.5, 2.0]:
149+
for skew in [-1, 0, +1]:
150+
r_low = r_high = r
151+
if skew < 0:
152+
r_low = r_low ** 2
153+
elif skew > 0:
154+
r_high = r_high ** 2
155+
price_a = INITIAL_PRICE / r_low
156+
price_b = INITIAL_PRICE * r_high
157+
liquidity = v3_math.position_liquidity_from_value(
158+
INITIAL_VALUE, INITIAL_PRICE, price_a, price_b)
159+
160+
# check that the liquidity/value math is correct
161+
v_test = v3_math.position_value_from_liquidity(
162+
liquidity, INITIAL_PRICE, price_a, price_b)
163+
assert v_test - 1e-8 < INITIAL_VALUE < v_test + 1e-8
164+
165+
# get the hedging power perp coefficients
166+
coefficients = fit_power_hedging_coefficients(liquidity, price_a, price_b, order)
167+
168+
min_price = price_a / (r ** 0.5)
169+
max_price = price_b * (r ** 0.5)
170+
step = (max_price - min_price) / NUM_STEPS
171+
prices = np.arange(min_price, max_price + step, step)
172+
pl.figure()
173+
174+
profit_pos = [v3_math.position_value_from_liquidity(
175+
liquidity, price, price_a, price_b) - INITIAL_VALUE for price in prices]
176+
pl.plot(prices, profit_pos, label="LP position")
177+
profit_perp = [power_perp_v3(INITIAL_VALUE, INITIAL_PRICE, price, price_a, price_b,
178+
coefficients, order) - INITIAL_VALUE for price in prices]
179+
pl.plot(prices, profit_perp, label=f"Power perp, order={order}")
180+
pl.plot(prices, [a+b for a,b in zip(profit_perp, profit_pos)], label="Hedged position")
181+
182+
pl.title(f"range=[{price_a:.1f} {price_b:.1f}]")
183+
pl.legend()
184+
pl.show()
185+
pl.close()
186+
187+
if __name__ == '__main__':
188+
main()
189+
print("all done!")

0 commit comments

Comments
 (0)