|
| 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