172 lines
5.9 KiB
Python
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).
|
|
|