per-asset fees
This commit is contained in:
172
research/fees.py
Normal file
172
research/fees.py
Normal file
@@ -0,0 +1,172 @@
|
||||
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)
|
||||
|
||||
print("\nPair fees [per-asset fee -> total]:")
|
||||
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}: per-asset_in={bps(f_i_ab)}, per-asset_out={bps(f_j_ab)}, total={bps(f_eff_ab)}"
|
||||
)
|
||||
|
||||
# Per-asset diagnostic: show per-asset fee (interpreted vs USDC)
|
||||
print("\nPer-asset diagnostics (fees interpreted vs USDC):")
|
||||
for n, w in weights.items():
|
||||
f_token = per_asset_fee(w)
|
||||
print(f"{n}: target_vs_USDC={w:.6f} bps, per_asset_fee={bps(f_token)}")
|
||||
|
||||
# -- 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).
|
||||
|
||||
Reference in New Issue
Block a user