"""M13 教学图：换手拖累，以及平方根冲击随参与率上升。

两条曲线都是公式。c = 1 是教学系数，不是某篇论文估出来的常数。
"""

from __future__ import annotations

import math
from decimal import Decimal
from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np

from quant.backtest.fees import FeeSchedule
from quant.backtest.fills import participation_cap

plt.rcParams["font.sans-serif"] = ["Microsoft YaHei", "SimHei", "DejaVu Sans"]
plt.rcParams["axes.unicode_minus"] = False

OUT = Path(__file__).resolve().parent / "turnover_and_impact.png"


def drag(one_way: float, side_cost: float) -> float:
    # 单边换手已经把成交额折半。费用打在每一元成交上，所以要乘回 2。
    return 2.0 * one_way * side_cost


def square_root_impact(daily_vol: float, participation: float, coef: float = 1.0) -> float:
    return coef * daily_vol * math.sqrt(participation)


def implementation_shortfall() -> None:
    decision = 100.0
    end = 101.0
    filled = 80.0
    order = 100.0
    fill_price = 100.40
    fee = 1.0
    paper = order * (end - decision)
    execution = filled * (fill_price - decision)
    opportunity = (order - filled) * (end - decision)
    actual = filled * (end - fill_price) - fee
    print("paper", f"{paper:.2f}", "actual", f"{actual:.2f}", "shortfall", f"{paper - actual:.2f}")
    print("execution", f"{execution:.2f}", "fee", f"{fee:.2f}", "opportunity", f"{opportunity:.2f}")
    print("pieces", f"{execution + fee + opportunity:.2f}")


def fee_examples() -> None:
    # 教学价目，不是富途或盈透的价目表。最低佣金会让小单的 bps 升高。
    schedule = FeeSchedule(
        commission_bps=Decimal("1"),
        min_commission=Decimal("1"),
        platform_fee=Decimal("0"),
        label="teaching_not_a_broker_card",
    )
    for notional in (Decimal("2000"), Decimal("20000")):
        fee = schedule.fee_for(notional)
        bps = fee / notional * Decimal(10000)
        print("fee", notional, fee, f"{float(bps):.2f}")


def two_slice_objective(first: float) -> tuple[float, float, float]:
    """两段都付 0.001*n^2。方差来自还没买到的股数。系数是为了手算，不是论文估计。"""
    second = 100.0 - first
    cost = 0.001 * first * first + 0.001 * second * second
    variance = second * second
    return cost, variance, cost + 0.001 * variance


def buffer_turnover() -> None:
    # 持有前 2 名。缓冲：排名落到 3 名以外才卖。满仓且没人被卖时，不为边界名额换手。
    held = {"A", "B"}
    ranks = {"A": 3, "B": 1, "C": 2, "D": 4, "E": 5}
    sell = {name for name in held if ranks[name] > 3}
    print("buffer_sell", sorted(sell), "one_way", f"{0.5 * len(sell):.1f}")
    ranks["A"] = 4
    sell = {name for name in held if ranks[name] > 3}
    print("buffer_sell_later", sorted(sell), "one_way", f"{0.5 * len(sell):.1f}")


def main() -> None:
    daily_vol = 0.40 / math.sqrt(252.0)
    impact = square_root_impact(daily_vol, 0.01)
    print("daily_vol", f"{daily_vol:.6f}", "impact_1pct", f"{impact:.6f}")
    for one_way, label in ((2.0, "traded_4x"), (7.2, "monthly_0.60"), (3.6, "monthly_0.30")):
        for side, name in ((0.0020, "c20"), (0.0015, "c15"), (0.0025, "c25"), (0.0010, "stamp")):
            print(label, name, f"{drag(one_way, side):.4f}")
    print("stamp_old_traded4", f"{4.0 * 0.0013:.4f}", "stamp_new_traded4", f"{4.0 * 0.0010:.4f}")
    print("rate_slip", f"{4.0 * 0.013:.4f}")
    implementation_shortfall()
    fee_examples()
    buffer_turnover()
    for first in (100.0, 80.0, 200.0 / 3.0, 50.0):
        cost, variance, utility = two_slice_objective(first)
        print(
            "slice", f"{first:.6f}",
            "cost", f"{cost:.6f}",
            "var", f"{variance:.6f}",
            "utility", f"{utility:.6f}",
        )
    short = [Decimal(1000)] * 19
    full = [Decimal(1000)] * 20
    print("adv_short", participation_cap(short, Decimal("0.01"), Decimal(1)))
    print("adv_full", participation_cap(full, Decimal("0.01"), Decimal(1)))
    partial = 0.05
    print("partial_drag_15bps", f"{partial * 0.0015:.6f}")

    fig, axes = plt.subplots(1, 2, figsize=(11.2, 4.8))
    ax = axes[0]
    one_ways = np.linspace(0.0, 8.0, 81)
    ax.plot(one_ways, drag(one_ways, 0.0015) * 100.0, color="#1f4e79", label="每侧 15 bps")
    ax.plot(one_ways, drag(one_ways, 0.0025) * 100.0, color="#a33b3b", label="每侧 25 bps")
    ax.axvline(2.0, color="#888888", linewidth=0.8, linestyle="--")
    ax.set_xlabel("年化单边换手")
    ax.set_ylabel("成本拖累（%）")
    ax.set_title("费用打在全部成交额上")
    ax.legend(frameon=False)
    ax = axes[1]
    participation = np.linspace(0.001, 0.10, 80)
    impacts = [square_root_impact(daily_vol, float(q)) * 10000.0 for q in participation]
    ax.plot(participation * 100.0, impacts, color="#1f4e79")
    ax.scatter([1.0], [impact * 10000.0], color="#a33b3b", zorder=3)
    ax.set_xlabel("订单占日成交额（%）")
    ax.set_ylabel("冲击（bps）")
    ax.set_title("c = 1，日波动 40% / √252")
    fig.suptitle("M13 成本是公式，不是成交回报")
    fig.tight_layout()
    fig.savefig(OUT, dpi=140)
    print("wrote", OUT)


if __name__ == "__main__":
    main()
