research(capitale): il lump-sum vale 2.45x, e la protezione dalla rovina non passa da GTAA01
Quattro filoni chiesti dall'operatore ("proposte"). Book, pesi, config: INVARIATI.
1. LUMP-SUM + VENUE (r0727_lumpsum_split.py). Tutte le traiettorie del 25-26/07 avevano
START=600 cablato: mai misurato un versamento iniziale, mentre ~10k EUR stanno fermi
altrove. Macchineria validata: con lump 0 riproduce IDENTICI i numeri del 26/07.
- 10k EUR oggi e mai piu' nulla -> traguardo 17.2a, P 62%, rendita 61.58 EUR/g
- equivalenza onesta: +154 EUR/mese per 13 anni = 24.523 EUR, cioe' 2.45x
(la prima stesura misurava i versamenti risparmiati: numero giusto, domanda sbagliata)
- col rischio venue: a 11.500$ lo split e' possibile (quota IB 26%, non 25%) e taglia
P(perso tutto) da 18.4% a 3.5% a p=1%, costando 1.9-2.6pp di P(arrivare)
- SPLIT-CASSA: seconda gamba ferma costa altri 0.6-0.8pp e protegge IDENTICO
-> la protezione non e' bloccata dal PRIIPs: serve un CONTO, non uno sleeve
2. FEE WATCH (scripts/live/fee_watch.py). Nuovo schema Deribit dal 1 agosto senza numeri
pubblicati -> sorvegliante invece di promemoria. Legge il tier base dall'endpoint
pubblico (oggi taker 5.00 bps), applica la regola congelata e allerta sui cambiamenti.
3. MONITOR HEALTH (src/live/monitor_health.py). Tre gate pre-registrati si decidono su
serie forward di cui una sola era sorvegliata. Misura coda E buchi interni: una serie
bucata ma fresca passa qualunque guardia di freschezza.
4. BANDA GTAA01 25% VALIDATA (r0727_gtaa_band_gate.py). 30 celle, 29.9 anni, dpy=252.
Non e' selection-on-holdout (4/30 IS, 5/30 OOS), DSR 0.999, tracking OK ma AL BORDO.
Il modo di fallire non e' il de-levering (la vol non scende) ma la perdita di tracking.
Impatto sul book: zero -> REBAL_BAND_USD non toccato, si applica al deploy.
Aggiunto anche il bullet edge_watch, cablato il 26/07 e mai finito in CLAUDE.md.
Test: 56 nuovi, 504/504 verdi.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,291 @@
|
||||
#!/usr/bin/env python
|
||||
"""r0727_lumpsum_split.py — il capitale che e' GIA' fermo, e cosa fa alla decisione di venue.
|
||||
|
||||
LA DOMANDA. Sul conto che gira ci sono **$600**. Fermi altrove ce ne sono ~**€10.000** (Revolut:
|
||||
€6.043 in XEON, che rende ~0% reale netto dopo inflazione e tasse, piu' VWCE/SPYM/PPFB) e €329 su
|
||||
Degiro. Tutte le traiettorie pubblicate (25/07 e 26/07) partono da `START = 600.0` **cablato**: il
|
||||
progetto ha misurato con cura il CALENDARIO dei versamenti (fattore 6) e non ha mai misurato un
|
||||
**versamento iniziale**.
|
||||
|
||||
PERCHE' LE DUE DOMANDE SI MISURANO INSIEME. Un lump-sum e' il front-loading estremo, e il 26/07
|
||||
aveva gia' misurato che il front-loading *mette piu' capitale sull'exchange prima* — cioe' proprio
|
||||
dove vive il rischio di venue. E la decisione del 26/07 ("100% Deribit fino a $20k") e' stata
|
||||
presa con **$600 sul conto, dove lo split e' impossibile per costruzione**. CLAUDE.md registra
|
||||
l'impegno esplicito: *"Se cambia il piano (orizzonte, importo dei versamenti) si riapre PRIMA."*
|
||||
Un lump-sum da €5-10k e' esattamente quel cambiamento. Misurare il lump-sum senza il rischio di
|
||||
venue risponderebbe alla meta' comoda della domanda.
|
||||
|
||||
COSA NON DECIDE QUESTO SCRIPT. Quanto di quei €6.043 sia un vero fondo d'emergenza e quanto
|
||||
capitale in attesa: e' una decisione dell'operatore, e un fondo d'emergenza NON e' capitale
|
||||
disponibile. Qui si misura **cosa compra ogni euro che entra**, per ogni taglia, cosi' che la
|
||||
decisione si prenda sul prezzo giusto.
|
||||
|
||||
ASSUNZIONI DICHIARATE
|
||||
* Il costo-opportunita' del lump-sum e' ~0% reale (XEON: ~2% lordo, ~0 netto dopo inflazione e
|
||||
26%). Tutti i numeri sono NOMINALI in USD, come ovunque nel progetto: il confronto e' quindi
|
||||
fra "fermo a ~0 reale" e "dentro il book", non fra due investimenti rischiosi.
|
||||
* Book = quello LIVE (TP01+SKH01 su Deribit), de-luckato ×0.89 (fattore MISURATO,
|
||||
r0726_deluck_factor). E' lo stesso book di ogni traiettoria pubblicata -> i numeri sono
|
||||
confrontabili con la tabella dei versamenti del 26/07.
|
||||
* Bersaglio = $272.061, il muro del 26/07 (rendita perpetua a €50/g netti). Identico per CONC e
|
||||
SPLIT: e' CONSERVATIVO contro lo split, che avrebbe un muro leggermente diverso.
|
||||
* `p` (probabilita' annua di fallimento di un venue) NON e' stimata: e' una sensibilita'.
|
||||
La sceglie l'operatore.
|
||||
|
||||
uv run python scripts/research/r0727_lumpsum_split.py
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
sys.path.insert(0, str(ROOT / "scripts" / "research"))
|
||||
|
||||
import r0725_capcurve as CC # noqa: E402
|
||||
import r0726_capwall_refresh as WR # noqa: E402
|
||||
import r0726_venue_risk as VR # noqa: E402
|
||||
from src.portfolio.gtaa import GTAA_MIN_CAPITAL # noqa: E402
|
||||
|
||||
DELUCK = 0.89
|
||||
START = 600.0 # il conto Deribit di oggi
|
||||
YEARS = 20
|
||||
N_PATHS = 4000
|
||||
BLOCK = 20
|
||||
SEED = 726 # lo stesso di r0726_venue_risk: consente la replica esatta
|
||||
TARGET = VR.TARGET # $272.061
|
||||
LUMPS_EUR = (0.0, 2_000.0, 5_000.0, 10_000.0)
|
||||
P_GRID = (0.0, 0.005, 0.01, 0.02, 0.05)
|
||||
|
||||
# Split: la quota su IB parte dal 25% raccomandato da r0725_ib10k (oltre, la rendita cala:
|
||||
# 0% -> €2.16/g, 50% -> €1.54/g su $11.5k). Ma la quota MINIMA e' imposta dall'eseguibilita'
|
||||
# della gamba equity: sotto GTAA_MIN_CAPITAL non c'e' uno sleeve, c'e' cash su un secondo conto.
|
||||
# Si accetta di salire fino a IB_SHARE_MAX pur di rendere lo split possibile; oltre, no.
|
||||
CONC = {"Deribit": 1.0}
|
||||
IB_SHARE = 0.25
|
||||
IB_SHARE_MAX = 0.35
|
||||
|
||||
# Haircut sulla gamba IB nella variante realistica: costi a taglia piccola (CAGR 3.8% -> 3.0% a
|
||||
# $3k, misurato il 25/07) + drag del veicolo UCITS (+0.10%/anno, misurato il 26/07).
|
||||
IB_HAIRCUT_YR = 0.008 + 0.001
|
||||
|
||||
|
||||
def simulate(cfg: dict, series: dict, p_annual: float, lump_usd: float = 0.0,
|
||||
dep_eur: float = 250.0, n_paths: int = N_PATHS, years: int = YEARS,
|
||||
seed: int = SEED) -> dict:
|
||||
"""Accumulo con capitale iniziale `START + lump_usd` e jump di venue.
|
||||
|
||||
Generalizza `r0726_venue_risk.simulate` sul solo capitale iniziale e sul deposito: la
|
||||
struttura (ordine di consumo dell'RNG compreso) e' identica, cosi' che con lump=0 e
|
||||
dep=250 i numeri debbano coincidere ESATTAMENTE con quelli pubblicati il 26/07.
|
||||
"""
|
||||
rng = np.random.default_rng(seed)
|
||||
n_days = years * 365
|
||||
names = list(cfg)
|
||||
J = pd.concat({k: series[k] for k in names}, axis=1, join="inner").fillna(0.0)
|
||||
R = J.values.astype(float)
|
||||
idx = CC._boot_paths(np.arange(len(R), dtype=float), n_paths, n_days, BLOCK, rng).astype(int)
|
||||
dep = dep_eur * CC.EURUSD
|
||||
|
||||
w0 = np.array([cfg[k] for k in names], float)
|
||||
cap = np.outer(np.full(n_paths, START + lump_usd), w0)
|
||||
alive = np.ones((n_paths, len(names)), bool)
|
||||
haz = 1.0 - (1.0 - p_annual) ** (1.0 / 365.0)
|
||||
hit = np.zeros(n_paths, bool)
|
||||
hit_day = np.full(n_paths, n_days + 1, int)
|
||||
for t in range(n_days):
|
||||
cap *= (1.0 + R[idx[:, t], :])
|
||||
if haz > 0:
|
||||
dead = (rng.random((n_paths, len(names))) < haz) & alive
|
||||
cap = np.where(dead, 0.0, cap)
|
||||
alive &= ~dead
|
||||
if t % 30 == 0 and t > 0 and dep > 0:
|
||||
wa = alive * w0
|
||||
tot = wa.sum(axis=1, keepdims=True)
|
||||
share = np.divide(wa, tot, out=np.zeros_like(wa), where=tot > 0)
|
||||
cap += dep * share
|
||||
tot_cap = cap.sum(axis=1)
|
||||
newly = (~hit) & (tot_cap >= TARGET)
|
||||
hit_day[newly] = t
|
||||
hit |= newly
|
||||
yrs = np.where(hit, hit_day / 365.0, np.nan)
|
||||
fin = yrs[~np.isnan(yrs)]
|
||||
return dict(p_reach=float(hit.mean()),
|
||||
median_yrs=float(np.median(fin)) if len(fin) > 50 else float("nan"),
|
||||
p_all_dead=float((~alive.any(axis=1)).mean()),
|
||||
median_end=float(np.median(cap.sum(axis=1))),
|
||||
paid=START + lump_usd + dep * (n_days // 30))
|
||||
|
||||
|
||||
def haircut(s: pd.Series, per_year: float) -> pd.Series:
|
||||
"""Abbassa il drift di una serie giornaliera di `per_year` all'anno (costi non modellati)."""
|
||||
return s - per_year / 365.0
|
||||
|
||||
|
||||
def split_config(total_usd: float) -> dict | None:
|
||||
"""Configurazione di split possibile a quel capitale, o None.
|
||||
|
||||
La quota IB non e' libera: dal basso la vincola l'ESEGUIBILITA' (sotto GTAA_MIN_CAPITAL la
|
||||
gamba equity non esiste, e lo 'split' sarebbe cash fermo su un secondo conto), dall'alto la
|
||||
vincola la RENDITA (r0725_ib10k: oltre il 25% si paga, oltre il 50% si dimezza). Fra i due
|
||||
vincoli si prende la quota minima che rende la gamba eseguibile — mai meno del 25%
|
||||
raccomandato, mai piu' di IB_SHARE_MAX.
|
||||
"""
|
||||
need = GTAA_MIN_CAPITAL / total_usd if total_usd > 0 else float("inf")
|
||||
share = max(IB_SHARE, need)
|
||||
if share > IB_SHARE_MAX:
|
||||
return None
|
||||
return {"Deribit": 1.0 - share, "IB": share}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
print("=" * 104)
|
||||
print(" r0727 — IL CAPITALE GIA' FERMO: cosa compra un versamento iniziale, e cosa fa al venue")
|
||||
print("=" * 104)
|
||||
|
||||
series = VR.venue_series()
|
||||
r_deribit = series["Deribit"].values.astype(float)
|
||||
gross = CC.TARGET_EUR_DAY * 365 * CC.EURUSD / (1 - CC.TAX_RATE)
|
||||
_, perp, wall = WR.perp_and_wall(r_deribit, 1.0, gross)
|
||||
print(f" book live de-luckato ×{DELUCK} | rendita perpetua {perp:.2%} | bersaglio ${TARGET:,.0f}")
|
||||
print(f" {N_PATHS} path, blocchi {BLOCK}g, orizzonte {YEARS}a, conto di partenza ${START:.0f}, "
|
||||
f"EURUSD {CC.EURUSD}")
|
||||
|
||||
# ---------------------------------------------------------------- validazione
|
||||
print("\n" + "-" * 104)
|
||||
print(" VALIDAZIONE — con lump 0 e €250/mese si devono riprodurre i numeri del 26/07")
|
||||
print("-" * 104)
|
||||
for p in (0.0, 0.01):
|
||||
mine = simulate(CONC, series, p, lump_usd=0.0, dep_eur=250.0)
|
||||
theirs = VR.simulate({"Deribit": 1.0}, series, p, n_paths=N_PATHS, years=YEARS, seed=SEED)
|
||||
ok = (abs(mine["p_reach"] - theirs["p_reach"]) < 1e-12
|
||||
and abs(mine["p_all_dead"] - theirs["p_all_dead"]) < 1e-12)
|
||||
print(f" p={p:.1%} P(arrivare) {mine['p_reach']:.4f} vs {theirs['p_reach']:.4f} · "
|
||||
f"P(perso tutto) {mine['p_all_dead']:.4f} vs {theirs['p_all_dead']:.4f} "
|
||||
f"-> {'IDENTICO' if ok else 'DIVERGE (la generalizzazione ha cambiato qualcosa)'}")
|
||||
|
||||
# ---------------------------------------------------------------- (1) cosa compra il lump
|
||||
print("\n" + "=" * 104)
|
||||
print(" (1) COSA COMPRA UN VERSAMENTO INIZIALE — senza rischio di venue (p=0), 100% Deribit")
|
||||
print("=" * 104)
|
||||
print(f"\n {'lump':>8}{'dep/mese':>10}{'tot versato':>14}{'anni al muro':>14}"
|
||||
f"{'P(entro 20a)':>14}{'cap. mediano':>15}{'rendita a 20a':>16}")
|
||||
grid = {}
|
||||
for dep in (0.0, 250.0):
|
||||
for L in LUMPS_EUR:
|
||||
r = simulate(CONC, series, 0.0, lump_usd=L * CC.EURUSD, dep_eur=dep)
|
||||
grid[(L, dep)] = r
|
||||
rent = r["median_end"] * perp * (1 - CC.TAX_RATE) / 365.0 / CC.EURUSD
|
||||
yrs = f"{r['median_yrs']:.1f}a" if not np.isnan(r["median_yrs"]) else "mai"
|
||||
print(f" €{L:>7,.0f}{dep:>10,.0f}{r['paid']:>13,.0f}${yrs:>14}"
|
||||
f"{r['p_reach']:>13.1%}{r['median_end']:>14,.0f}${rent:>13.2f} €/g")
|
||||
|
||||
# equivalenza: a quale versamento mensile equivale il lump, a parita' di traguardo mediano.
|
||||
# NB: NON si confronta il lump col totale dei versamenti risparmiati — sarebbe la domanda
|
||||
# sbagliata (il valore e' arrivare prima, non versare meno), e darebbe un rapporto <1 che
|
||||
# invita alla conclusione opposta.
|
||||
print("\n EQUIVALENZA — a quanti €/mese IN PIU', per sempre, equivale il lump-sum?")
|
||||
base = grid[(0.0, 250.0)]["median_yrs"]
|
||||
for L in LUMPS_EUR[1:]:
|
||||
got = grid[(L, 250.0)]["median_yrs"]
|
||||
lo, hi = 250.0, 3_000.0
|
||||
for _ in range(12): # bisezione sul deposito a pari traguardo
|
||||
mid = 0.5 * (lo + hi)
|
||||
y = simulate(CONC, series, 0.0, lump_usd=0.0, dep_eur=mid)["median_yrs"]
|
||||
if np.isnan(y) or y > got:
|
||||
lo = mid
|
||||
else:
|
||||
hi = mid
|
||||
eq = 0.5 * (lo + hi)
|
||||
print(f" €{L:>6,.0f} oggi -> traguardo {got:.1f}a (invece di {base:.1f}a) "
|
||||
f"= come versare €{eq:,.0f}/mese invece di €250, cioe' €{eq - 250:,.0f}/mese in piu' "
|
||||
f"per {got:.0f} anni (€{(eq - 250) * 12 * got:,.0f})")
|
||||
|
||||
# ---------------------------------------------------------------- (2) venue
|
||||
print("\n" + "=" * 104)
|
||||
print(" (2) LO STESSO LUMP, COL RISCHIO DI VENUE DENTRO — concentrato vs split")
|
||||
print(f" SPLIT = Deribit / IB, quota IB scelta dal CAPITALE (minimo eseguibile "
|
||||
f"${GTAA_MIN_CAPITAL:,.0f}, tetto {IB_SHARE_MAX:.0%})")
|
||||
print("=" * 104)
|
||||
ib_real = {**series, "IB": haircut(series["IB"], IB_HAIRCUT_YR)}
|
||||
for L in LUMPS_EUR:
|
||||
tot = START + L * CC.EURUSD
|
||||
cfg = split_config(tot)
|
||||
if cfg:
|
||||
why = (f"POSSIBILE a quota IB {cfg['IB']:.0%} (${tot * cfg['IB']:,.0f} sulla gamba equity; "
|
||||
f"il minimo eseguibile e' ${GTAA_MIN_CAPITAL:,.0f})")
|
||||
else:
|
||||
why = (f"IMPOSSIBILE: servirebbe una quota IB del {GTAA_MIN_CAPITAL / tot:.0%} "
|
||||
f"per fare ${GTAA_MIN_CAPITAL:,.0f}, oltre il tetto {IB_SHARE_MAX:.0%}")
|
||||
print(f"\n lump €{L:,.0f} -> capitale iniziale ${tot:,.0f} · split {why}")
|
||||
print(f" {'p annua':>9}{'P(arrivare) CONC':>19}{'SPLIT':>9}{'SPLIT+costi':>13}"
|
||||
f"{'P(perso tutto) CONC':>22}{'SPLIT':>9}")
|
||||
for p in P_GRID:
|
||||
c = simulate(CONC, series, p, lump_usd=L * CC.EURUSD)
|
||||
if cfg:
|
||||
s = simulate(cfg, series, p, lump_usd=L * CC.EURUSD)
|
||||
sr = simulate(cfg, ib_real, p, lump_usd=L * CC.EURUSD)
|
||||
print(f" {p:>8.1%}{c['p_reach']:>18.1%}{s['p_reach']:>9.1%}{sr['p_reach']:>13.1%}"
|
||||
f"{c['p_all_dead']:>21.1%}{s['p_all_dead']:>9.1%}")
|
||||
else:
|
||||
print(f" {p:>8.1%}{c['p_reach']:>18.1%}{'-':>9}{'-':>13}"
|
||||
f"{c['p_all_dead']:>21.1%}{'-':>9}")
|
||||
# --- lo split che si puo' fare OGGI: la gamba equity e' bloccata dal PRIIPs -------------
|
||||
print("\n" + "-" * 104)
|
||||
print(" (2-bis) E SE LA SECONDA GAMBA FOSSE SOLO LIQUIDITA'?")
|
||||
print(" GTAA01 oggi NON e' deployabile (blocco PRIIPs sugli ETF USA; la via UCITS su Degiro")
|
||||
print(" e' verificata ma in preparazione). Lo split disponibile SUBITO e' Deribit + un conto")
|
||||
print(" fermo (XEON o simile, ~0% reale). Quanta protezione viene dalla DIVERSIFICAZIONE DI")
|
||||
print(" VENUE e quanta dai ritorni della gamba equity?")
|
||||
print("-" * 104)
|
||||
L = 10_000.0
|
||||
tot = START + L * CC.EURUSD
|
||||
cfg = split_config(tot)
|
||||
zero = {**series, "IB": series["IB"] * 0.0} # stessa griglia temporale, drift nullo
|
||||
print(f"\n lump €{L:,.0f} (${tot:,.0f}), quota seconda gamba {cfg['IB']:.0%}")
|
||||
print(f" {'p annua':>9}{'P(arrivare) CONC':>19}{'SPLIT-GTAA':>13}{'SPLIT-CASSA':>13}"
|
||||
f"{'P(perso tutto) CONC':>22}{'SPLIT':>9}")
|
||||
for p in P_GRID:
|
||||
c = simulate(CONC, series, p, lump_usd=L * CC.EURUSD)
|
||||
g = simulate(cfg, series, p, lump_usd=L * CC.EURUSD)
|
||||
z = simulate(cfg, zero, p, lump_usd=L * CC.EURUSD)
|
||||
print(f" {p:>8.1%}{c['p_reach']:>18.1%}{g['p_reach']:>13.1%}{z['p_reach']:>13.1%}"
|
||||
f"{c['p_all_dead']:>21.1%}{g['p_all_dead']:>9.1%}")
|
||||
print("\n (P(perso tutto) e' identica fra SPLIT-GTAA e SPLIT-CASSA: dipende da QUANTI conti")
|
||||
print(" falliscono, non da cosa ci sta sopra. La differenza fra le due colonne di")
|
||||
print(" P(arrivare) e' il prezzo di tenere ferma la seconda gamba invece di investirla.)")
|
||||
|
||||
print("\n ⚠️ La colonna P(perso tutto) sotto CONC non dipende dal lump: con UN conto solo,")
|
||||
print(" 'almeno un fallimento' COINCIDE con 'perso tutto', e quella probabilita' e' una")
|
||||
print(" proprieta' del tempo di esposizione, non del capitale. Il lump non la peggiora —")
|
||||
print(" ma moltiplica cio' che quella probabilita' porta via.")
|
||||
|
||||
# ---------------------------------------------------------------- (3) la soglia
|
||||
print("\n" + "=" * 104)
|
||||
print(" (3) DA QUALE CAPITALE LO SPLIT E' POSSIBILE SENZA DISTORCERE I PESI")
|
||||
print("=" * 104)
|
||||
ideale = GTAA_MIN_CAPITAL / IB_SHARE
|
||||
minimo = GTAA_MIN_CAPITAL / IB_SHARE_MAX
|
||||
tot10 = START + 10_000 * CC.EURUSD
|
||||
print(f"\n soglia di eseguibilita' della gamba equity (GTAA_MIN_CAPITAL): ${GTAA_MIN_CAPITAL:,.0f}")
|
||||
print(f" quota IB raccomandata da r0725_ib10k (oltre, cala la rendita): {IB_SHARE:.0%}")
|
||||
print(f" -> split a quota RACCOMANDATA possibile da : ${ideale:,.0f}")
|
||||
print(f" -> split possibile forzando la quota fino al {IB_SHARE_MAX:.0%} da : ${minimo:,.0f}")
|
||||
print(f"\n Con un lump da €10.000 il conto sarebbe ${tot10:,.0f}: "
|
||||
f"{'SOPRA' if tot10 >= ideale else 'SOTTO'} la soglia a quota")
|
||||
print(f" raccomandata (${ideale:,.0f}) e {'sopra' if tot10 >= minimo else 'sotto'} "
|
||||
f"quella a quota forzata (${minimo:,.0f}) -> lo split e' possibile, ma a")
|
||||
print(f" quota IB {max(IB_SHARE, GTAA_MIN_CAPITAL / tot10):.0%}, non {IB_SHARE:.0%}: "
|
||||
"e' il capitale a scegliere la quota, non la preferenza.")
|
||||
print(f"\n La riapertura della decisione di venue e' fissata a ${20_000:,.0f}. Un lump da")
|
||||
print(f" €10k porta il conto a ${tot10:,.0f} — sotto quella data ma sopra la fattibilita'")
|
||||
print(" tecnica, ed e' un cambiamento del piano: CLAUDE.md dice di riaprire PRIMA in")
|
||||
print(" questo caso, e questa tabella e' il materiale per farlo.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user