#!/usr/bin/env python3
"""
Simulazione di una seduta SPX 0DTE: evoluzione oraria del campo gamma.

Produce:
  - seduta_tabella.txt      tabella dei valori usati nel capitolo 11
  - seduta_gex.dat          GEX 0DTE netto e zero gamma ora per ora
  - seduta_campo_A.dat      GEX per strike alle 10:00  (spot 6500)
  - seduta_campo_B.dat      GEX per strike alle 15:00  (spot 6520)
  - seduta_profilo_A.dat    profilo GEX(x) alle 10:00
  - seduta_profilo_B.dat    profilo GEX(x) alle 15:00

Ipotesi dichiarate:
  sigma = 12% costante su tutta la catena (nessuno skew), r = q = 0,
  M = 100, anno = 252 x 6.5 = 1638 ore di borsa, ipotesi di segno H0.
"""

import math
import os

HERE = os.path.dirname(os.path.abspath(__file__))
SIG = 0.12
M = 100
HRS_YR = 252 * 6.5
SQRT2PI = math.sqrt(2.0 * math.pi)


def phi(x):
    return math.exp(-0.5 * x * x) / SQRT2PI


def d12(S, K, T):
    a = (math.log(S / K) + 0.5 * SIG * SIG * T) / (SIG * math.sqrt(T))
    return a, a - SIG * math.sqrt(T)


def gamma(S, K, T):
    a, _ = d12(S, K, T)
    return phi(a) / (S * SIG * math.sqrt(T))


def charm(S, K, T):
    a, b = d12(S, K, T)
    return phi(a) * b / (2.0 * T)


# ---------------------------------------------------------------------------
# Open interest di apertura (ereditato dalle sedute precedenti) e flusso
# intraday cumulato. Il flusso e' concentrato intorno allo spot del momento:
# e' la caratteristica distintiva delle 0DTE.
# ---------------------------------------------------------------------------
OI_APERTURA = {
    # strike: (call, put)
    6400: (600, 7000), 6425: (900, 9000), 6450: (1500, 14000),
    6475: (2600, 19000), 6500: (14000, 11000), 6525: (19000, 4000),
    6550: (26000, 2000), 6575: (8000, 1200), 6600: (12000, 800),
}

# flusso intraday cumulato per snapshot: {ora: {strike: (dcall, dput)}}
FLUSSO = {
    "09:30": {},
    "11:00": {6500: (7000, 4000), 6510: (5000, 2500), 6520: (6000, 1500)},
    "12:30": {6500: (11000, 7000), 6510: (9000, 5000), 6520: (10000, 2500),
              6490: (2500, 4000)},
    "14:00": {6500: (12000, 11000), 6510: (10000, 7000), 6520: (11000, 3500),
              6490: (4000, 8000), 6480: (2000, 6000)},
    "15:00": {6500: (12000, 14000), 6510: (10000, 8000), 6520: (11000, 4000),
              6490: (5000, 11000), 6480: (3000, 9000), 6470: (1800, 6000),
              6460: (900, 4000)},
    "15:45": {6500: (12000, 15000), 6510: (10000, 8000), 6520: (11000, 4000),
              6490: (5000, 12000), 6480: (3200, 10000), 6470: (2400, 9000),
              6460: (1800, 7000), 6450: (1200, 5000)},
}

SNAPSHOT = [
    # (ora, spot, ore residue)
    ("09:30", 6500.0, 6.50),
    ("11:00", 6512.0, 5.00),
    ("12:30", 6506.0, 3.50),
    ("14:00", 6496.0, 2.00),
    ("15:00", 6478.0, 1.00),
    ("15:45", 6462.0, 0.25),
]


def catena(ora):
    """Open interest complessivo allo snapshot indicato."""
    out = {}
    for K, (c, p) in OI_APERTURA.items():
        out[K] = [c, p]
    for K, (dc, dp) in FLUSSO[ora].items():
        if K not in out:
            out[K] = [0, 0]
        out[K][0] += dc
        out[K][1] += dp
    return out


def gex(S, T, ch):
    """GEX netto in miliardi di dollari per 1%."""
    return sum(gamma(S, K, T) * (c - p) * M * S * S * 0.01
               for K, (c, p) in ch.items()) / 1e9


def cex(S, T, ch):
    """Charm exposure: flusso in miliardi per ora di borsa."""
    return sum(charm(S, K, T) * (c - p) * M * S * (1.0 / HRS_YR)
               for K, (c, p) in ch.items()) / 1e9


def zero_gamma(T, ch, lo=6380.0, hi=6660.0):
    """Radici del profilo nell'intervallo; restituisce quella piu' vicina
    al centro dell'intervallo di scansione, o None."""
    radici = []
    x, f = lo, gex(lo, T, ch)
    while x < hi:
        y = x + 1.0
        g = gex(y, T, ch)
        if f * g < 0.0:
            a, b = x, y
            for _ in range(60):
                m = 0.5 * (a + b)
                if gex(a, T, ch) * gex(m, T, ch) <= 0.0:
                    b = m
                else:
                    a = m
            radici.append(0.5 * (a + b))
        x, f = y, g
    return radici


def muri(S, T, ch):
    """Call wall e put wall definiti sul GEX netto per strike."""
    netti = {K: gamma(S, K, T) * (c - p) * M * S * S * 0.01 / 1e9
             for K, (c, p) in ch.items()}
    cw = max(netti, key=netti.get)
    pw = min(netti, key=netti.get)
    return cw, netti[cw], pw, netti[pw]


righe = []
for ora, S, h in SNAPSHOT:
    T = h / HRS_YR
    ch = catena(ora)
    g = gex(S, T, ch)
    c = cex(S, T, ch)
    rad = zero_gamma(T, ch)
    zg = min(rad, key=lambda r: abs(r - S)) if rad else None
    cw, cwv, pw, pwv = muri(S, T, ch)
    gatm = gamma(S, S, T)
    em = S * SIG * math.sqrt(T)
    # parametro di feedback (cap. 7): beta_d = (sigma_d/1%) * GEX/V$
    #   sigma_d = 0.75% ; V$ = 300 miliardi ; beta(5') = beta_d * sqrt(78)
    beta_d = 0.75 * abs(g) / 300.0 * (1 if g >= 0 else -1)
    beta_5 = beta_d * math.sqrt(390.0 / 5.0)
    righe.append(dict(beta_d=beta_d, beta_5=beta_5,
                      ora=ora, S=S, h=h, T=T, gex=g, cex=c, zg=zg,
                      n_radici=len(rad), cw=cw, cwv=cwv, pw=pw, pwv=pwv,
                      gatm=gatm, em=em,
                      oi_tot=sum(c_ + p_ for c_, p_ in ch.values())))

# ------------------------------------------------------------------ output
out = []
out.append(f"{'ora':>6} {'spot':>7} {'ore':>5} {'OI tot':>9} {'GEX':>8} "
           f"{'CEX/h':>8} {'zero g.':>9} {'#rad':>5} {'callW':>7} {'putW':>7} "
           f"{'G_ATM':>9} {'EM 1sd':>7} {'beta5m':>8} {'molt5m':>8}")
for r in righe:
    zg = f"{r['zg']:.0f}" if r['zg'] else "n.d."
    out.append(f"{r['ora']:>6} {r['S']:>7.0f} {r['h']:>5.2f} {r['oi_tot']:>9d} "
               f"{r['gex']:>8.2f} {r['cex']:>8.2f} {zg:>9} {r['n_radici']:>5d} "
               f"{r['cw']:>7.0f} {r['pw']:>7.0f} {r['gatm']:>9.5f} "
               f"{r['em']:>7.1f} {r['beta_5']:>8.4f} "
               f"{1.0/(1.0+r['beta_5']):>8.3f}")
testo = "\n".join(out)
print(testo)
with open(os.path.join(HERE, "seduta_tabella.txt"), "w") as f:
    f.write(testo + "\n")


def scrivi(nome, header, righe_dat):
    with open(os.path.join(HERE, nome), "w") as f:
        f.write("# " + header + "\n")
        for rr in righe_dat:
            f.write(" ".join(f"{v:.8g}" for v in rr) + "\n")
    print(f"scritto {nome}")


# serie oraria
scrivi("seduta_gex.dat", "indice_snapshot  spot  GEX  CEX  zero_gamma",
       [[i, r["S"], r["gex"], r["cex"], r["zg"] if r["zg"] else 0]
        for i, r in enumerate(righe)])

# campo per strike in due momenti
for etichetta, ora, S, h in [("A", "09:30", 6500.0, 6.50),
                             ("B", "15:00", 6478.0, 1.00)]:
    T = h / HRS_YR
    ch = catena(ora)
    dati = sorted([K, gamma(S, K, T) * c * M * S * S * 0.01 / 1e9,
                   -gamma(S, K, T) * p * M * S * S * 0.01 / 1e9]
                  for K, (c, p) in ch.items())
    scrivi(f"seduta_campo_{etichetta}.dat", "K  GEX_call  GEX_put", dati)

    prof = []
    x = 6400.0
    while x <= 6640.0001:
        prof.append([x, gex(x, T, ch)])
        x += 1.0
    scrivi(f"seduta_profilo_{etichetta}.dat", "x  GEX(x)", prof)
