Files
lmsr-amm/research/pool_design.py

263 lines
8.7 KiB
Python

import logging
import math
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np
log = logging.getLogger(__name__)
LMSR_FEE = 0.0025
# UNISWAP_GAS=0
# LMSR_GAS=0
UNISWAP_GAS=115_000
LMSR_GAS=150_000
ETH_PRICE=4000
UNISWAP_GAS_COST=UNISWAP_GAS*ETH_PRICE/1e9
LMSR_GAS_COST=LMSR_GAS*ETH_PRICE/1e9
print(f' LMSR gas: ${LMSR_GAS_COST:.2}')
print(f'Uniswap gas: ${UNISWAP_GAS_COST:.2}')
def lmsr_swap_amount_out(
balances,
amount_in,
token_in_index,
token_out_index,
lp_fee,
kappa,
):
"""
Compute the LMSR kernel fee-free amountOut for swapping `amount_in` of token `token_in_index`
into token `token_out_index`, applying lp_fee to the input amount (i.e., amount_in_net = amount_in * (1 - lp_fee)).
Uses native Python floats for performance and simplicity.
Notes on congruence with PartyPool / LMSRStabilized:
- The kernel formula implemented here matches the LMSR closed-form:
amountOut = b * ln(1 + r0 * (1 - exp(-a / b)))
where r0 = exp((q_i - q_j) / b) and b = kappa * S (S = sum balances).
- lp_fee is applied to the input before the kernel (fee-on-input), matching PartyPool's placement.
- This function uses continuous float arithmetic and does NOT emulate
PartyPool's integer/unit conversions or rounding (floor/ceil) and ppm fee quantization.
Parameters:
- balances: iterable of per-token balances (numbers). These represent q_i in internal units.
- amount_in: input amount supplied by swapper (before fees).
- token_in_index: index of the input token i.
- token_out_index: index of the output token j.
- lp_fee: fractional LP fee applied to the input (e.g., 0.003).
- kappa: liquidity parameter κ (must be positive, in same units as balances).
Returns:
- float: net amountOut (exclusive of fees) the swapper receives from the pool (capped at pool balance).
"""
# Normalize and validate inputs (convert to floats)
try:
q = [float(x) for x in balances]
a_in = float(amount_in)
lpf = float(lp_fee)
k = float(kappa)
except (TypeError, ValueError) as e:
raise ValueError("Invalid numeric input") from e
n = len(q)
if not (0 <= token_in_index < n and 0 <= token_out_index < n):
raise IndexError("token indices out of range")
if a_in <= 0.0:
return 0.0
if k <= 0.0:
raise ValueError("kappa must be positive")
# Size metric S = sum q_i
S = sum(q)
if S <= 0.0:
raise ValueError("size metric (sum balances) must be positive")
# b = kappa * S
b = k * S
if b <= 0.0:
raise ValueError("computed b must be positive")
# Apply LP fee on the input amount (kernel is fee-free; wrapper handles fees)
if not (0.0 <= lpf < 1.0):
raise ValueError("lp_fee must be in [0, 1)")
a_net = a_in * (1.0 - lpf)
if a_net <= 0.0:
return 0.0
qi = q[token_in_index]
qj = q[token_out_index]
# Guard: output asset must have non-zero effective reserve
if qj <= 0.0:
# No available output to withdraw
return 0.0
# Compute r0 = exp((q_j - q_i) / b) so small-trade out/in ≈ marginal price p_j/p_i
try:
r0 = math.exp((qj - qi) / b)
except OverflowError:
raise ArithmeticError("exponential overflow in r0 computation")
# Compute a/b
a_over_b = a_net / b
# exp(-a/b)
try:
exp_neg = math.exp(-a_over_b)
except OverflowError:
# If exp would underflow/overflow, treat exp(-a/b) as 0 in extreme case
exp_neg = 0.0
# inner = 1 + r0 * (1 - exp(-a/b))
inner = 1.0 + r0 * (1.0 - exp_neg)
# If inner <= 0, cap to available balance qj
if inner <= 0.0:
return float(qj)
# amountOut = b * ln(inner)
try:
ln_inner = math.log(inner)
except (ValueError, OverflowError):
# Numeric issue computing ln; be conservative and return 0
return 0.0
amount_out = b * ln_inner
# Safety: non-positive output -> return zero
if amount_out <= 0.0:
return 0.0
# Cap output to pool's current balance qj (cannot withdraw more than available)
if amount_out > qj:
return float(qj)
return float(amount_out)
def lmsr_marginal_price(balances, base_index, quote_index, kappa):
"""
Compute the LMSR marginal price ratio p_quote / p_base for the given balances state.
Formula:
b = kappa * S, where S = sum(balances)
price = exp((q_quote - q_base) / b)
Parameters:
- balances: iterable of per-token balances (q_i)
- base_index: index of the base token
- quote_index: index of the quote token
- kappa: liquidity parameter κ (must be positive)
Returns:
- float: marginal price p_quote / p_base
"""
try:
q = [float(x) for x in balances]
k = float(kappa)
except (TypeError, ValueError) as e:
raise ValueError("Invalid numeric input") from e
n = len(q)
if not (0 <= base_index < n and 0 <= quote_index < n):
raise IndexError("token indices out of range")
if k <= 0.0:
raise ValueError("kappa must be positive")
S = sum(q)
if S <= 0.0:
raise ValueError("size metric (sum balances) must be positive")
b = k * S
if b <= 0.0:
raise ValueError("computed b must be positive")
return float(math.exp((q[quote_index] - q[base_index]) / b))
def compare(file, tvl, kappa):
d = pd.read_csv(file)
d.columns = ['block', 'price0', 'price1', 'in0', 'out0', 'rate']
# Calibrate LMSR balances so that exp((q1 - q0)/b) equals the initial price
p0 = float(d.iloc[0].price0)
S = float(tvl) # choose the LMSR size metric
b = kappa * S
delta = b * math.log(p0) # q1 - q0
q0 = 0.5 * (S - delta)
q1 = 0.5 * (S + delta)
if q0 <= 0.0 or q1 <= 0.0:
raise ValueError("Invalid LMSR calibration: choose kappa such that kappa * ln(price0) < 1.")
balances = [q0, q1]
print(balances)
X = np.geomspace(1, 1_000_000, 100)
orig_price = lmsr_marginal_price(balances, 0, 1, kappa)
in_out = [(float(amount_in), lmsr_swap_amount_out(balances, float(amount_in), 0, 1, LMSR_FEE, kappa)) for amount_in in X]
print(in_out)
# Relative execution price deviation from the initial marginal price:
# slippage = |(amount_out/amount_in)/orig_price - 1|
eps = 1e-12
Y = [max(eps, abs((amount_out / amount_in) / orig_price - 1.0))
for amount_in, amount_out in in_out]
plt.plot(X, Y, label=f'LMSR {LMSR_FEE:.2%} κ={kappa:.2f}', color='cornflowerblue')
# Uniswap execution price deviation from its initial quoted price:
# slippage = |(out/in)/initial_price - 1|
uniswap_exec_price0 = d.out0 / d.in0
uniswap_slippage0 = (uniswap_exec_price0 / d.iloc[0].price0 - 1.0).abs().clip(lower=1e-12)
uniswap_fee = round(uniswap_slippage0.iloc[0], 6)
plt.plot(d.in0, uniswap_slippage0, label=f'Uniswap {uniswap_fee:.2%}', color='hotpink')
# uniswap_slippage1 = |(out1/in1)/price1 - 1|
# plt.plot(d.in1, (d.out1 / d.in1 / d.iloc[0].price1 - 1.0).abs().clip(lower=1e-12), label='CP1')
# Interpolate Uniswap slippage to match LMSR x-coordinates
interp_uniswap = np.interp(X, d.in0, uniswap_slippage0)
mask = Y < interp_uniswap
plt.fill_between(X, 0, 1, where=mask, alpha=0.2, color='green')
plt.xscale('log')
plt.yscale('log')
plt.gca().xaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: '{:g}'.format(x)))
plt.gca().yaxis.set_major_formatter(plt.FuncFormatter(lambda y, _: '{:.2%}'.format(y)))
plt.gca().set_ylim(top=.1)
plt.xlabel('Input Amount')
plt.ylabel('Slippage')
plt.title('Pool Slippages')
plt.grid(True)
plt.legend()
plt.show()
def plot_kappa():
X = np.geomspace(1, 10_000_000, 100)
for kappa in np.geomspace(0.01, 100, 8):
balance0 = 1_000_000 # estimated from the production pool
balances = [balance0, balance0]
Y = [1 -
lmsr_swap_amount_out(balances, float(amount_in), 0, 1, 0.000010, kappa)
/ amount_in
for amount_in in X]
plt.plot(X / balance0, Y, label=f'{kappa:f}')
plt.xscale('log')
plt.yscale('log')
plt.gca().xaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: '{:.2f}'.format(10000*x)))
plt.gca().yaxis.set_major_formatter(plt.FuncFormatter(lambda y, _: '{:.2%}'.format(y)))
plt.xlabel('Input Amount (basis points of initial balance)')
plt.ylabel('Slippage')
plt.title('Pool Slippages by Kappa')
plt.grid(True)
plt.legend()
plt.show()
if __name__ == '__main__':
# compare('uni4_quotes/swap_results_block_23640998.csv')
# compare('uni4_quotes/ETH-USDC-30.csv', 53_000_000, 0.1)
compare('uni4_quotes/ETH-USDC-30.csv', 1_00_000, .1)
# plot_kappa()