# lmsr_vs_cp_sim.py
# Requires: Python 3.9+, numpy, matplotlib
# Optional: seaborn (for prettier plots)

import math
import numpy as np
import matplotlib.pyplot as plt
try:
    import seaborn as sns
    sns.set_context("talk")
    sns.set_style("whitegrid")
except Exception:
    pass


# ---------------------------
# Core AMM primitives
# ---------------------------

def cp_price(X, Y):
    # Instantaneous marginal price p = dy/dx for constant product
    return Y / X

def cp_trade_y_out(X, Y, qx_in, fee=0.0):
    """
    Swap x-in (amount qx_in) for y-out on a constant-product pool with fee.
    Fee taken on input (Uniswap-style): only (1-fee)*qx_in enters the invariant.
    Returns (dy_out, X_new, Y_new, fee_value).
    """
    assert X > 0 and Y > 0 and qx_in >= 0 and 0 <= fee < 1
    k = X * Y
    effective_in = qx_in * (1 - fee)
    X_new = X + effective_in
    Y_new = k / X_new
    dy_out = Y - Y_new
    fee_value = qx_in * fee  # in x-units
    return dy_out, X_new, Y_new, fee_value

def cp_avg_y_per_x(X, Y, qx_in, fee=0.0):
    if qx_in == 0:
        return cp_price(X, Y)
    dy, *_ = cp_trade_y_out(X, Y, qx_in, fee)
    return dy / qx_in

def lmsr_calibrate_b(X, Y, mode="thin_side", factor=0.5):
    """
    Calibrate LMSR 'b' to match CP local log-price stiffness at the current state.
    - thin_side: use min(X, Y) * factor where factor≈0.5 matches ds/dq at that side
    - x_side: use X * factor
    - y_side: use Y * factor (useful if measuring q in y-space)
    For our use (q measured in x), 'x_side' or 'thin_side' with factor=0.5 are typical.
    """
    if mode == "thin_side":
        return max(1e-12, factor * min(X, Y))
    elif mode == "x_side":
        return max(1e-12, factor * X)
    elif mode == "y_side":
        return max(1e-12, factor * Y)
    else:
        raise ValueError("mode must be one of ['thin_side','x_side','y_side']")

def lmsr_y_out(qx_in, p0, b, fee=0.0):
    """
    Symmetric 2-asset LMSR approximation in x-measure:
    - We model log-price s moving linearly in q_x: ds/dq_x = 1/b
    - Then price path p(q) = p0 * exp(q/b)
    - Cumulative y received for x-in q is integral of p(q) dq: y(q) = p0 * b * (exp(q/b) - 1)
    Fee applied to input reduces effective q: q_eff = q * (1 - fee)
    Returns dy_out (in y-units) and fee_value (in x-units)
    """
    assert qx_in >= 0 and b > 0 and p0 > 0 and 0 <= fee < 1
    q_eff = qx_in * (1 - fee)
    dy = p0 * b * (math.exp(q_eff / b) - 1.0)
    fee_value = qx_in * fee
    return dy, fee_value

def lmsr_avg_y_per_x(qx_in, p0, b, fee=0.0):
    if qx_in == 0:
        return p0
    dy, _ = lmsr_y_out(qx_in, p0, b, fee)
    return dy / qx_in


# ---------------------------
# Static comparison utilities
# ---------------------------

def static_compare(X, Y, fee_cp=0.0005, fee_lmsr=0.0005, b=None, b_mode="x_side", b_factor=0.5,
                   q_grid=None):
    """
    Compute average execution (y per x), penalties vs spot, and welfare gaps over a grid of trade sizes.
    Returns dict with arrays.
    """
    p0 = cp_price(X, Y)
    if b is None:
        b = lmsr_calibrate_b(X, Y, mode=b_mode, factor=b_factor)

    if q_grid is None:
        # span from tiny to a meaningful fraction of X
        q_grid = np.geomspace(1e-9 * X, 0.3 * X, 60)

    avg_cp = np.array([cp_avg_y_per_x(X, Y, q, fee_cp) for q in q_grid])
    avg_lm = np.array([lmsr_avg_y_per_x(q, p0, b, fee_lmsr) for q in q_grid])

    # Slippage penalty vs spot p0 (in y-per-x)
    pen_cp = p0 - avg_cp
    pen_lm = p0 - avg_lm

    # Taker welfare difference: LMSR minus CP (positive means LMSR better for taker)
    welfare_gap = avg_lm - avg_cp

    return {
        "q_grid": q_grid,
        "avg_cp": avg_cp,
        "avg_lm": avg_lm,
        "pen_cp": pen_cp,
        "pen_lm": pen_lm,
        "welfare_gap": welfare_gap,
        "p0": p0,
        "b": b
    }


# ---------------------------
# Imbalance sweep
# ---------------------------

def sweep_imbalance(X, rho_grid, fee_cp=0.0005, fee_lmsr=0.0005, q_frac=0.01,
                    b_mode="thin_side", b_factor=0.5):
    """
    For a fixed X (x-reserve), sweep imbalance rho = Y/X and measure welfare advantage at a chosen trade size q = q_frac * X.
    Returns arrays over rho.
    """
    q = q_frac * X
    wgap = []
    pen_cp_list, pen_lm_list, p0_list, b_list = [], [], [], []
    for rho in rho_grid:
        Y = rho * X
        res = static_compare(X, Y, fee_cp, fee_lmsr, b=None, b_mode=b_mode, b_factor=b_factor, q_grid=np.array([q]))
        wgap.append(res["welfare_gap"][0])
        pen_cp_list.append(res["pen_cp"][0])
        pen_lm_list.append(res["pen_lm"][0])
        p0_list.append(res["p0"])
        b_list.append(res["b"])
    return {
        "rho_grid": rho_grid,
        "q": q,
        "welfare_gap": np.array(wgap),
        "pen_cp": np.array(pen_cp_list),
        "pen_lm": np.array(pen_lm_list),
        "p0": np.array(p0_list),
        "b": np.array(b_list)
    }


# ---------------------------
# Sequential Monte Carlo simulation
# ---------------------------

def sample_trade_sizes(n, X, dist="lognormal", mean_frac=0.005, std_frac=0.01, seed=None):
    """
    Return non-negative trade sizes q in x-units.
    - lognormal: parameters chosen so mean ≈ mean_frac*X
    - uniform: U(0, 2*mean_frac*X)
    """
    rng = np.random.default_rng(seed)
    if dist == "lognormal":
        mean = mean_frac * X
        std = std_frac * X
        # Map mean/std to lognormal parameters
        # mean = exp(mu + sigma^2/2), var = (exp(sigma^2)-1)exp(2mu+sigma^2)
        # Let CV = std/mean => sigma^2 = ln(1+CV^2), mu = ln(mean) - sigma^2/2
        cv = std / max(1e-12, mean)
        sigma2 = math.log(1 + cv * cv)
        sigma = math.sqrt(sigma2)
        mu = math.log(max(1e-12, mean)) - sigma2 / 2
        q = rng.lognormal(mean=mu, sigma=sigma, size=n)
        return q
    elif dist == "uniform":
        return rng.uniform(0, 2 * mean_frac * X, size=n)
    else:
        raise ValueError("Unknown dist")


def sequential_sim(X0, Y0, n_trades=2000, direction_bias=0.6,
                   fee_cp=0.0005, fee_lmsr=0.0005,
                   b_mode="thin_side", b_factor=0.5,
                   dist="lognormal", mean_frac=0.005, std_frac=0.01, seed=42):
    """
    Run a sequential simulation where each trade either buys y with x (prob=direction_bias)
    or buys x with y (prob=1-direction_bias). We compare CP vs LMSR per-trade and accumulate:
    - taker surplus difference (y-per-x times q, converted to y-units)
    - fee revenue for LPs (x- or y-denominated; we also track value at spot)
    - pool state evolution (for CP only; LMSR is state-less under this approximation)
    Note: We approximate LMSR as state-less with ds/dq = 1/b, b recalibrated each step to the thin side.
    """
    rng = np.random.default_rng(seed)
    X_cp, Y_cp = X0, Y0
    p_history = []
    b_history = []

    taker_y_advantage = 0.0
    lp_fee_x_cp = 0.0
    lp_fee_x_lm = 0.0

    # Track realized slippage stats at fixed snapshot prices for comparability
    for t in range(n_trades):
        p_cp = cp_price(X_cp, Y_cp)
        p_history.append(p_cp)

        # Calibrate LMSR b to thin side each step
        b = lmsr_calibrate_b(X_cp, Y_cp, mode=b_mode, factor=b_factor)
        b_history.append(b)

        # Decide direction: True => use x to buy y; False => use y to buy x
        buy_y = rng.random() < direction_bias

        if buy_y:
            # Draw trade size in x-units
            qx = float(sample_trade_sizes(1, X_cp, dist, mean_frac, std_frac, seed=rng.integers(1e9))[0])

            # CP execution
            dy_cp, X_cp_new, Y_cp_new, fee_x_cp = cp_trade_y_out(X_cp, Y_cp, qx, fee_cp)

            # LMSR execution (approximate with current spot p_cp)
            dy_lm, fee_x_lm = lmsr_y_out(qx, p_cp, b, fee_lmsr)

            # Update CP state
            X_cp, Y_cp = X_cp_new, Y_cp_new

            # Accumulate
            taker_y_advantage += (dy_lm - dy_cp)
            lp_fee_x_cp += fee_x_cp
            lp_fee_x_lm += fee_x_lm

        else:
            # Buy x with y. Mirror the model by symmetry:
            # We'll convert problem by swapping roles of (x,y) and using same formulas.
            # Draw trade size in y-units proportional to Y
            qy = float(sample_trade_sizes(1, Y_cp, dist, mean_frac, std_frac, seed=rng.integers(1e9))[0])

            # CP: y-in, x-out
            # Symmetric function by swapping X<->Y and interpreting result
            dy_dummy, Y_new, X_new, fee_y_cp = cp_trade_y_out(Y_cp, X_cp, qy, fee_cp)  # returns x-out in "dy_dummy"
            dx_cp = dy_dummy  # interpret as x-out
            X_cp, Y_cp = X_new, Y_new

            # LMSR: state-less approx using ds/dq_y = 1/b_y; we map via price and symmetry.
            # For buy-x with y-in, average x per y uses 1/p along exponential path in y-space.
            # For simplicity, we mirror by computing y-per-x with LMSR at price p, then invert locally:
            # Expected x received ≈ qy / p * (e^{(qy_eff/b_y)/?} - 1)/((qy)/?) ... too detailed.
            # Instead, use symmetry by swapping labels and reusing the function with p_inv = 1/p.
            p_inv = 1.0 / p_cp
            b_y = lmsr_calibrate_b(X_cp, Y_cp, mode="y_side", factor=b_factor)
            dx_lm, fee_y_lm = lmsr_y_out(qy, p_inv, b_y, fee_lmsr)  # gives x-out

            # Taker advantage in x-units; convert to y-units for aggregation using pre-trade spot
            taker_y_advantage += (dx_lm - dx_cp) * p_cp  # multiply by p to convert x to y at current spot
            lp_fee_x_cp += fee_y_cp * p_inv  # convert y-fee to x using p_inv
            lp_fee_x_lm += fee_y_lm * p_inv

    return {
        "taker_y_advantage": taker_y_advantage,
        "lp_fee_x_cp": lp_fee_x_cp,
        "lp_fee_x_lm": lp_fee_x_lm,
        "p_history": np.array(p_history),
        "b_history": np.array(b_history),
        "final_state_cp": (X_cp, Y_cp)
    }


# ---------------------------
# Plotting helpers
# ---------------------------

def plot_static(res):
    q = res["q_grid"]
    p0 = res["p0"]
    plt.figure(figsize=(10, 6))
    plt.plot(q, res["avg_cp"], label="CP avg y-per-x")
    plt.plot(q, res["avg_lm"], label="LMSR avg y-per-x")
    plt.axhline(p0, color="gray", linestyle="--", alpha=0.6, label="Spot p0")
    plt.xscale("log")
    plt.title("Average execution (y per x) vs trade size")
    plt.xlabel("q_x (trade size)")
    plt.ylabel("y per x")
    plt.legend()
    plt.tight_layout()
    plt.xlim(right=1)
    plt.ylim( bottom=.999,top=1)

    plt.figure(figsize=(10, 6))
    plt.plot(q, res["welfare_gap"] / res["p0"] * 1e4)
    plt.xscale("log")
    plt.axhline(0, color="gray", linestyle="--")
    plt.title("Taker welfare advantage LMSR−CP (in bp of price)")
    plt.xlabel("q_x (trade size)")
    plt.ylabel("bp")
    plt.tight_layout()

def plot_imbalance(sweep):
    rho = sweep["rho_grid"]
    plt.figure(figsize=(10, 6))
    plt.plot(rho, sweep["welfare_gap"] / sweep["p0"] * 1e4)
    plt.axhline(0, color="gray", linestyle="--")
    plt.title(f"LMSR−CP taker advantage vs imbalance (q={sweep['q']:.4g}·X)")
    plt.xlabel("rho = Y/X (imbalance)")
    plt.ylabel("bp of price")
    plt.tight_layout()


# ---------------------------
# Demo main
# ---------------------------

def demo():
    # Baseline pool
    X, Y = 10_000.0, 10_000.0
    fee_cp = 0.0005    # 5 bp
    fee_lmsr = 0.0005  # 5 bp
    b_mode = "thin_side"
    b_factor = 0.5

    # Static comparison across trade sizes
    res = static_compare(
        X, Y, fee_cp=fee_cp, fee_lmsr=fee_lmsr,
        b=None, b_mode=b_mode, b_factor=b_factor
    )
    print(f"Spot p0={res['p0']:.6f}, calibrated b={res['b']:.6f}")
    plot_static(res)

    # Imbalance sweep
    rho_grid = np.linspace(0.2, 5.0, 60)  # from thin-y to thin-x
    sw = sweep_imbalance(
        X, rho_grid, fee_cp=fee_cp, fee_lmsr=fee_lmsr,
        q_frac=0.01, b_mode=b_mode, b_factor=b_factor
    )
    plot_imbalance(sw)

    # Sequential simulation
    sim = sequential_sim(
        X0=X, Y0=Y, n_trades=2000, direction_bias=0.6,
        fee_cp=fee_cp, fee_lmsr=fee_lmsr,
        b_mode=b_mode, b_factor=b_factor,
        dist="lognormal", mean_frac=0.005, std_frac=0.01, seed=7
    )
    adv_bp_equiv = sim["taker_y_advantage"] / (X + Y / res["p0"]) * 1e4
    print(f"Sequential sim taker Y-advantage (absolute): {sim['taker_y_advantage']:.6f} y-units")
    print(f"LP fee (CP) in x-units: {sim['lp_fee_x_cp']:.6f}")
    print(f"LP fee (LMSR) in x-units: {sim['lp_fee_x_lm']:.6f}")

    plt.show()


if __name__ == "__main__":
    demo()
