Files
lmsr-amm/research/fees.py
2025-11-04 16:58:16 -04:00

172 lines
5.9 KiB
Python

import logging
import math
from itertools import combinations
log = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)
# Now interpret the `weights` mapping as the desired pair fee (in bps) vs USDC.
# Examples:
# - USDC: 0.0 -> trading USDC<>USDC costs 0 bps
# - USDT: 0.10 -> trading USDT<>USDC costs ~0.1 bps
# - WIF: 30.0 -> trading WIF<>USDC costs ~30 bps
#
# For arbitrary pair A<>B we compose per-asset fees multiplicatively:
# f_eff = 1 - (1 - f_A) * (1 - f_B)
# which is ≈ f_A + f_B for small fees.
STABLECOIN_RATE = 0.01 # bps
X0 = STABLECOIN_RATE / 2
targets = {
'USDT': STABLECOIN_RATE,
'USDC': STABLECOIN_RATE,
'WBTC': 3.0, # ~3 bps vs USDC
'WETH': 5.0, # ~5 bps vs USDC
'WIF': 30.0, # ~30 bps vs USDC
}
weights = {k:v-X0 for k,v in targets.items()}
# UI / safety parameters (bps)
# cap_pair_fee_bps: global cap on the composed pair fee to avoid degenerate values (set to None to disable)
cap_pair_fee_bps = None # safety cap (bps); adjust or set to None to disable
# Compute geometric mean "base" of weights (kept for diagnostics)
def geometric_mean(vals):
# Guard: all weights must be positive (allow zero for USDC)
for v in vals:
if v < 0:
raise ValueError("All weights must be non-negative")
# exclude zeros when computing geometric mean to avoid exact zero product
pos_vals = [v for v in vals if v > 0]
if not pos_vals:
return 0.0
return math.prod(pos_vals) ** (1.0 / len(pos_vals))
base = geometric_mean(list(weights.values()))
ref_asset = 'USDC'
# Convert UI bps -> fractional form for internal calculations
def bps_to_frac(bps_val):
return bps_val / 10_000.0
log.info(
f"Fee model (weights are target bps vs USDC): ref_asset={ref_asset}, base_geo={base:.6f}"
)
def clamp_nonneg(x):
return x if x >= 0.0 else 0.0
def clamp_cap_frac(x_frac):
"""Clamp a fractional fee to configured cap_pair_fee_bps if set"""
if cap_pair_fee_bps is None:
return x_frac
cap_frac = bps_to_frac(cap_pair_fee_bps)
return min(x_frac, cap_frac)
def per_asset_fee(w_bps):
"""
Interpret `w_bps` directly as the target bps vs USDC for that asset and
return the fractional per-asset fee (clamped).
So for asset A:
f_A = bps_to_frac(weights[A])
"""
return clamp_nonneg(bps_to_frac(w_bps))
def fee_for_pair(w_in_bps, w_out_bps):
"""
Compute effective fee fraction for a swap i->j using multiplicative composition
of per-asset fees:
f_i = per_asset_fee(w_in_bps)
f_j = per_asset_fee(w_out_bps)
f_eff = 1 - (1 - f_i) * (1 - f_j)
Returns tuple (f_eff_frac, f_i_frac, f_j_frac).
"""
f_i = per_asset_fee(w_in_bps)
f_j = per_asset_fee(w_out_bps)
# multiplicative composition (accurate and gas-cheap counterpart)
f_eff = 1.0 - (1.0 - f_i) * (1.0 - f_j)
# apply global cap if configured
f_eff = clamp_cap_frac(f_eff)
return clamp_nonneg(f_eff), f_i, f_j
def pct(x):
return f"{100.0 * x:.6f}%"
def bps(x):
# x expected as fraction; convert to bps (1 bps = 1/10000)
return f"{10_000.0 * x:.4f} bps"
if __name__ == "__main__":
names = list(weights.keys())
vals = list(weights.values())
# Warn if any single-asset implied fee would produce large numbers
alert_threshold_bps = 1000.0
alerts = []
max_w = max(weights.values())
min_w = min(weights.values())
max_diff = max_w - min_w
if max_w > alert_threshold_bps:
alerts.append(("single_asset_too_large_bps", max_w))
if alerts:
log.warning("Some fee configuration may produce large values (bps): %s", alerts)
# Per-asset diagnostic: show per-asset fee (interpreted vs USDC)
for n, w in weights.items():
f_token = per_asset_fee(w)
print(f"{n:>12} {bps(f_token):>12}")
print()
for (name_a, w_a), (name_b, w_b) in combinations(weights.items(), 2):
f_eff_ab, f_i_ab, f_j_ab = fee_for_pair(w_a, w_b)
print(
f"{name_a+'-'+name_b:>12} {bps(f_eff_ab):>12}"
)
# -- Solidity (ABDK Q64.64) output --
# Print derived per-asset fees and cap as Solidity int128 Q64.64 constants that can be pasted
# into a Solidity file using ABDK's 64.64 fixed-point representation (int128 constants).
print("\n// Solidity (ABDK Q64.64) constants - generated by research/fees.py")
print("// Per-asset fees (FEE_<SYMBOL>) are Q64.64 int128 literals representing the fractional fee")
print("// Example: int128 internal constant FEE_USDT = 0x012345...; // comment\n")
# Helper: order tokens consistently for array examples
token_names = list(weights.keys())
for name in token_names:
w_bps = weights[name]
t_bps = targets[name]
f_frac = per_asset_fee(w_bps)
q64_int = int(round(f_frac * (1 << 64)))
hexstr = hex(q64_int)
print(f"int128 internal constant FEE_{name} = {hexstr}; // {bps(f_frac)}")
# Cap constant if configured
if cap_pair_fee_bps is not None:
cap_frac = bps_to_frac(cap_pair_fee_bps)
cap_q64 = int(round(cap_frac * (1 << 64)))
print(f"int128 internal constant CAP_PAIR_FEE_Q64 = {hex(cap_q64)}; // cap {cap_pair_fee_bps} bps")
# Example how to assemble into an array in the same order as `token_names`.
print("\n// Example: build a per-asset fee array (same ordering as token_names list above)")
print(f"// token order: {token_names}")
print(f"int128[] memory perAssetFeesQ64 = new int128[]({len(token_names)});")
for idx, name in enumerate(token_names):
print(f"perAssetFeesQ64[{idx}] = FEE_{name};")
print("\n// End of generated Solidity constants\n")
# Notes:
# - weights[name] should be set to the desired bps (not fraction) vs USDC.
# - Per-asset fraction f_token = bps_to_frac(weights[name]) and
# composed pair fee is f_eff = 1 - (1 - f_i) * (1 - f_j).
# - This keeps USDC as the reference (weights['USDC'] == 0.0).