usde: il venue mette un TETTO al 31,21% — e l'USDC paga gia' 3,40%
Richiesta dell'operatore: "porta in usde tutto il capitale che non viene usato". Il margine consuma $11 su $2.063 e l'USDE e' cross-collateral: preso alla lettera vale una quota ~99%, cioe' il 100% che il gate esclude. Portata all'operatore con le cifre, ha scelto 70%. Il 70% non e' un argmax (M8): e' il massimo compatibile col CUSCINO DI REGOLAMENTO, il vincolo che r0830_usde_quota non aveva guardato. P&L e funding dei perp USDC-lineari si regolano in USDC, non nel collaterale, quindi la quota non la limita l'haircut ma il saldo USDC che deve reggere il disaster-SL sulla massima esposizione: 2 x 0,5 x 0,30 = 30% dell'equity, che lascia il 70%. ESEGUITO +144 USDE (500,1757 -> 644,175691), quota 24,24% -> 31,21%, fee 0. Poi il muro. (a) TETTO DEL VENUE, misurato e non documentato da nessuna parte. Provato a saldo neutro: BUY 20 rifiutato, SELL 20 OK, BUY 20 OK, BUY 5 rifiutato, con $1.408 disponibili. E' un tetto sul LIVELLO. Il messaggio del venue (not_enough_funds_in_currency) e' fuorviante e ha fatto inseguire tre ipotesi sbagliate: taglia (falso, ma min_trade_amount=1 e' vero), rate-limit (falso), prezzo (vero in parte: il book REST pubblico e' in ritardo sul matching engine e prezzavo l'ordine sull'INDICE invece che sul BOOK — l'indice marca il collaterale, il book prezza lo scambio). La cronaca resta nel diario: chi rilegge non deve rifare il giro. (b) L'USDC PAGA 3,40%. public/get_currencies: USDE 4,1071%, USDC 3,4000%, e i nostri reward USDE misurati (~4,5%/anno) confermano che quelle APR sono reali. Il guadagno non e' il tasso, e' lo SPREAD: 0,71 punti, ~$4,6/anno sui $644 che teniamo, non i $21 che il gate implicava. Il gate ha misurato il reward dell'USDE e non ha mai chiesto cosa facesse l'USDC fermo: manca il controfattuale, M1 in un'altra veste. NON dimostrato che sia accreditato sul nostro conto — da verificare su un giorno senza trade. Nuovo attrezzo scripts/live/usde_convert.py: dry-run di default, banda prezzo, tetto hard di quota, cuscino di regolamento derivato da config. NON passa da execution.ALLOWED, che resta ai soli due perp. config: quota_target 0.70 registrato; quota_max_frac alzato a 0.85 e RIMESSO a 0.50 nella stessa sessione — la soglia larga presupponeva un 70% che non esiste, e lasciarla avrebbe disarmato la guardia per uno scenario che non si e' verificato. Suite: 795 passati, 0 falliti. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,286 @@
|
||||
#!/usr/bin/env python
|
||||
"""usde_convert.py — porta la QUOTA di collaterale USDE a un bersaglio dichiarato. INVIA ORDINI VERI.
|
||||
|
||||
PERCHE' ESISTE. La conversione del 26/08 (test di eligibilita', 500 USDE) fu una chiamata a mano al
|
||||
gateway: nessuno script, nessuna guardia, nessuna traccia se non il diario. Il gate USDE-01 prevede
|
||||
entrambe le direzioni — IDONEO -> si decide la quota; NON IDONEO -> "si riconverte" — e nessuna delle
|
||||
due aveva un attrezzo. Qui c'e', e vale in tutti e due i versi (quota piu' alta = compra USDE, piu'
|
||||
bassa = vende).
|
||||
|
||||
PERCHE' NON PASSA DA DeribitTrader. `execution.ALLOWED` ammette solo i due perp del libro: e' il
|
||||
guardrail anti-fat-finger del percorso soldi del book e NON va allargato per far entrare uno spot che
|
||||
col libro non c'entra. Questo script parla al gateway per conto suo e porta le proprie guardie.
|
||||
|
||||
LE GUARDIE (tutte BLOCCANTI, tutte dichiarate qui):
|
||||
* banda di prezzo [0.995, 1.005] — la stessa del 26/08. Fuori banda non si converte: un USDE a
|
||||
0.98 e' esattamente il momento in cui NON si compra, e uno a 1.02 non esiste (si clampa a 1.0).
|
||||
* quota bersaglio in [0, QUOTA_HARD_MAX] — il gate USDE-01 dice "mai 100%: R1 emittente non
|
||||
recuperabile". Il tetto e' HARD e non si passa da riga di comando.
|
||||
* CUSCINO DI REGOLAMENTO. Il P&L e il funding dei perp USDC-lineari si regolano in USDC, non nel
|
||||
collaterale: a quota alta il saldo USDC va negativo alla prima perdita del libro e Deribit lo
|
||||
finanzia a interesse. Il margine NON e' il vincolo (r0830_usde_quota: l'haircut non morde fino
|
||||
al 90%) — il vincolo e' questo. Il cuscino richiesto e' il disaster-SL sulla MASSIMA esposizione
|
||||
che il libro puo' prendere: n_asset * frac * disaster_sl_pct * equity, tutto letto da
|
||||
config/live.json (P1: il bersaglio si deriva, non si ridichiara).
|
||||
* dry-run DI DEFAULT: senza --esegui stampa il piano e non manda niente.
|
||||
|
||||
IL TETTO PER-ORDINE (misurato il 30/08, non documentato da nessuna parte). Il venue rifiuta gli
|
||||
ordini spot grandi con `not_enough_funds_in_currency` ANCHE quando i fondi ci sono: 100 accettato
|
||||
e 300 rifiutato con $1.442 disponibili (e 500 era passato il 26/08 con piu' saldo). Il gateway non
|
||||
espone `available_withdrawal_funds` ne' `get_order_book`, quindi la causa vera NON e' leggibile da
|
||||
qui e il messaggio del venue e' fuorviante: si tratta il tetto come ignoto e si CHUNKA, dimezzando
|
||||
il blocco a ogni rifiuto. Costa zero: il book ha 3.990 unita' al miglior ask e la commissione spot
|
||||
e' 0, quindi N fill piccoli e un fill grande pagano lo stesso prezzo.
|
||||
|
||||
NON E' NE' LA TAGLIA NE' IL TEMPO: E' IL PREZZO. Diagnosi finale (30/08 20:02, tre ordini da 1
|
||||
USDE): SELL @1.0000 passa, BUY @1.0003 passa, BUY @1.0002 e BUY market falliscono. L'ask si era
|
||||
mosso da 1.0002 a 1.0003 e l'ordine aveva smesso di INCROCIARE: Deribit rifiuta un limite BUY non
|
||||
marcabile sullo spot con `not_enough_funds_in_currency`, che e' un messaggio fuorviante e ha fatto
|
||||
inseguire per venti minuti due ipotesi sbagliate (tetto di size, rate-limit). Il difetto vero era
|
||||
qui: il prezzo dell'ordine si derivava dall'INDICE (1.0001 + 1 tick) invece che dal BOOK. Sono due
|
||||
prezzi diversi con due mestieri diversi — l'indice MARCA il collaterale (usde.valuta, mediana
|
||||
multi-exchange: la lezione Binance 10/10), il book PREZZA lo scambio. Confonderli non sbaglia la
|
||||
valutazione, sbaglia l'esecuzione.
|
||||
|
||||
LO STORICO DELLE IPOTESI SBAGLIATE, tenuto apposta: 943.98 rifiutato per "Invalid params" (vero:
|
||||
min_trade_amount=1, il passo e' intero) -> 933 rifiutato per fondi con $1.541 disponibili (falso
|
||||
indizio) -> 100 passa e 300 no (sembrava un tetto di size: era l'ask che si muoveva in mezzo) ->
|
||||
anche 1 rifiutato otto volte (sembrava rate-limit: era sempre l'ask). Chi rilegge non deve rifare
|
||||
questo giro. Misurato subito dopo: a raffica il venue rifiuta anche 1 USDE,
|
||||
otto volte di fila, mentre 100 era appena passato. Il messaggio `not_enough_funds_in_currency` e'
|
||||
FUORVIANTE — il vincolo e' la cadenza (regolamento spot asincrono / rate-limit), non il saldo ne'
|
||||
la size. Percio' si SPAZIANO gli ordini (`--pausa`) e si fa backoff sul TEMPO, riducendo il blocco
|
||||
solo dopo rifiuti ripetuti. Dimezzare la size a ogni rifiuto, come faceva la prima versione, e' la
|
||||
reazione sbagliata al sintomo giusto: consuma i tentativi senza toccare la causa.
|
||||
|
||||
uv run python scripts/live/usde_convert.py --quota 0.70 # piano, nessun ordine
|
||||
uv run python scripts/live/usde_convert.py --quota 0.70 --esegui # manda l'ordine
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import math
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from src.live import usde as U # noqa: E402
|
||||
from src.live.deribit import DeribitRead # noqa: E402
|
||||
|
||||
LOG = ROOT / "data" / "live" / "usde_convert.jsonl"
|
||||
PX_MIN, PX_MAX = 0.995, 1.005 # banda di sicurezza sul prezzo (26/08)
|
||||
QUOTA_HARD_MAX = 0.95 # gate USDE-01: "mai 100%"
|
||||
MIN_ORDER_USD = 5.0
|
||||
TOLL = 0.005 # entro mezzo punto di quota si considera gia' a bersaglio
|
||||
EPS_CUSCINO = 0.01 # 1 centesimo: il criterio e' ">= cuscino", e una quota derivata DAL
|
||||
# cuscino ci cade sopra esatta (0.30*E da entrambi i lati). Senza
|
||||
# questo epsilon il rumore float boccia il caso di pareggio.
|
||||
|
||||
|
||||
def cuscino_richiesto_usd(equity_tot: float) -> tuple[float, str]:
|
||||
"""Cuscino USDC di REGOLAMENTO = disaster-SL sulla massima esposizione lorda del libro.
|
||||
Derivato da config/live.json + src/live/book.py, mai ridichiarato qui."""
|
||||
cfg = json.loads((ROOT / "config" / "live.json").read_text())
|
||||
sl = float(cfg["disaster_sl_pct"])
|
||||
from src.live import book as B
|
||||
frac, n_asset = float(B.WEIGHT), 2
|
||||
lordo_max = equity_tot * frac * n_asset
|
||||
return lordo_max * sl, f"{n_asset} asset x frac {frac} x SL {sl:.0%} su ${equity_tot:,.2f}"
|
||||
|
||||
|
||||
def book_top(timeout: float = 5.0) -> tuple[float | None, float | None]:
|
||||
"""Miglior bid/ask dello spot USDE_USDC dall'API PUBBLICA Deribit (tokenless: il gateway non
|
||||
espone get_order_book). -> (bid, ask), (None, None) se non leggibile. Mai solleva.
|
||||
|
||||
NON e' il prezzo di `usde.valuta`: quello e' l'INDICE e serve a marcare il collaterale. Questo
|
||||
serve a far INCROCIARE l'ordine, ed e' l'unico che il matching engine guarda."""
|
||||
try:
|
||||
import requests
|
||||
r = requests.get("https://www.deribit.com/api/v2/public/get_order_book",
|
||||
params={"instrument_name": U.SPOT, "depth": 5}, timeout=timeout).json()
|
||||
res = r.get("result", r)
|
||||
bid = float(res["bids"][0][0]) if res.get("bids") else None
|
||||
ask = float(res["asks"][0][0]) if res.get("asks") else None
|
||||
return bid, ask
|
||||
except Exception:
|
||||
return None, None
|
||||
|
||||
|
||||
def piano(eq_usdc: float, eq_usde: float, px: float, quota_target: float,
|
||||
equity_tot: float, bid: float | None = None, ask: float | None = None) -> dict:
|
||||
"""PURA. -> piano di conversione, con verdetto e motivo. Non manda niente."""
|
||||
val_usde, _ = U.valuta(eq_usde, px)
|
||||
quota_ora = val_usde / equity_tot if equity_tot > 0 else 0.0
|
||||
val_target = quota_target * equity_tot
|
||||
delta_usd = val_target - val_usde # >0 compra USDE, <0 vende
|
||||
side = "buy" if delta_usd > 0 else "sell"
|
||||
# MARCABILE CON MARGINE. Il book REST pubblico e' in RITARDO sul matching engine: misurato il
|
||||
# 30/08, un BUY a ask+1tick (1.0003) e' stato rifiutato e uno a 1.0004 accettato un minuto dopo,
|
||||
# col book che riportava ask 1.0002x3855 in entrambi i casi. Percio' si incrocia con 5 tick di
|
||||
# margine e un TETTO DURO: il fill avviene al prezzo del LIBRO, non al limite, quindi il margine
|
||||
# non si paga se il book non si e' mosso, e se si e' mosso il tetto limita il danno a ~9 bps.
|
||||
# Senza book leggibile si RINUNCIA — non si tira a indovinare (P5).
|
||||
TICK, MARGINE = 0.0001, 5
|
||||
CAP_BUY, CAP_SELL = 1.0010, 0.9990
|
||||
rif = ask if side == "buy" else bid
|
||||
if rif is None:
|
||||
px_ord = None
|
||||
elif side == "buy":
|
||||
px_ord = round(min(rif + MARGINE * TICK, CAP_BUY), 4)
|
||||
else:
|
||||
px_ord = round(max(rif - MARGINE * TICK, CAP_SELL), 4)
|
||||
# PASSO DI SIZE = 1 USDE, misurato sul venue (30/08): 500 e 10 accettati, 943.98 e 933.99
|
||||
# rifiutati con "Invalid params". `get_instrument` non e' esposto dal gateway (404), quindi il
|
||||
# passo e' EMPIRICO e dichiarato tale. Si quantizza per DIFETTO: meno USDE = piu' USDC, cioe'
|
||||
# dal lato del cuscino di regolamento. Sul lato vendita il difetto va nello stesso verso.
|
||||
amount = float(math.floor(abs(delta_usd) / (px_ord or px)))
|
||||
|
||||
cuscino, come = cuscino_richiesto_usd(equity_tot)
|
||||
usdc_dopo = eq_usdc - delta_usd
|
||||
motivi = []
|
||||
if px_ord is None:
|
||||
motivi.append("book USDE_USDC non leggibile — non si prezza l'ordine sull'indice (l'indice"
|
||||
" marca il collaterale, il book prezza lo scambio)")
|
||||
elif not (PX_MIN <= px_ord <= PX_MAX):
|
||||
motivi.append(f"prezzo d'ordine {px_ord:.4f} fuori banda [{PX_MIN}, {PX_MAX}]")
|
||||
if not (PX_MIN <= px <= PX_MAX):
|
||||
motivi.append(f"prezzo {px:.4f} fuori banda [{PX_MIN}, {PX_MAX}]")
|
||||
if not (0.0 <= quota_target <= QUOTA_HARD_MAX):
|
||||
motivi.append(f"quota {quota_target:.0%} fuori dal tetto HARD {QUOTA_HARD_MAX:.0%} (gate USDE-01)")
|
||||
if abs(quota_ora - quota_target) <= TOLL:
|
||||
motivi.append(f"gia' a bersaglio ({quota_ora:.1%} vs {quota_target:.0%}, toll {TOLL:.1%})")
|
||||
if abs(delta_usd) < MIN_ORDER_USD:
|
||||
motivi.append(f"delta ${abs(delta_usd):,.2f} sotto il minimo ${MIN_ORDER_USD:,.2f}")
|
||||
if usdc_dopo < cuscino - EPS_CUSCINO:
|
||||
motivi.append(f"cuscino di regolamento: USDC dopo ${usdc_dopo:,.2f} < ${cuscino:,.2f} richiesti ({come})")
|
||||
|
||||
return {"quota_ora": quota_ora, "quota_target": quota_target, "val_usde": val_usde,
|
||||
"equity_tot": equity_tot, "delta_usd": delta_usd, "side": side, "amount": amount,
|
||||
"px": px, "px_ordine": px_ord, "usdc_dopo": usdc_dopo, "cuscino": cuscino,
|
||||
"cuscino_come": come, "ok": not motivi, "motivi": motivi}
|
||||
|
||||
|
||||
def leggi_stato(c: DeribitRead) -> tuple[float, float, float | None, str]:
|
||||
eq_usdc = float(c.account_summary("USDC")["equity"])
|
||||
try:
|
||||
eq_usde = float(c.account_summary("USDE")["equity"])
|
||||
except Exception:
|
||||
eq_usde = 0.0
|
||||
px, fonte = U.prezzo(client=c)
|
||||
return eq_usdc, eq_usde, px, fonte
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--quota", type=float, required=True, help="quota USDE bersaglio, es. 0.70")
|
||||
ap.add_argument("--esegui", action="store_true", help="manda l'ordine (senza: solo il piano)")
|
||||
ap.add_argument("--chunk", type=float, default=100.0, help="blocco massimo per ordine (USDE)")
|
||||
ap.add_argument("--pausa", type=float, default=12.0, help="secondi fra un ordine e il successivo")
|
||||
a = ap.parse_args()
|
||||
|
||||
c = DeribitRead()
|
||||
eq_usdc, eq_usde, px, fonte = leggi_stato(c)
|
||||
if px is None:
|
||||
print(" ✗ prezzo USDE non leggibile — non si converte al buio (P5)")
|
||||
return 2
|
||||
val_usde, _ = U.valuta(eq_usde, px)
|
||||
equity_tot = eq_usdc + val_usde
|
||||
bid, ask = book_top()
|
||||
p = piano(eq_usdc, eq_usde, px, a.quota, equity_tot, bid, ask)
|
||||
|
||||
print("=" * 78)
|
||||
print(f" USDE CONVERT — {datetime.now(timezone.utc):%Y-%m-%dT%H:%M:%SZ}"
|
||||
f" {'ESECUZIONE' if a.esegui else 'PIANO (dry-run)'}")
|
||||
print("=" * 78)
|
||||
print(f" prezzo : indice {px:.4f} ({fonte}) · book bid {bid} / ask {ask}"
|
||||
f" -> ordine @ {p['px_ordine']}, banda [{PX_MIN}, {PX_MAX}]")
|
||||
print(f" equity : USDC ${eq_usdc:,.2f} + USDE {eq_usde:,.4f} (${val_usde:,.2f})"
|
||||
f" = ${equity_tot:,.2f}")
|
||||
print(f" quota : {p['quota_ora']:.1%} -> {p['quota_target']:.0%}")
|
||||
print(f" ordine : {p['side'].upper()} {p['amount']:,.2f} USDE_USDC @ limit {p['px_ordine']:.4f}"
|
||||
f" (${abs(p['delta_usd']):,.2f})")
|
||||
slack = p["usdc_dopo"] - p["cuscino"]
|
||||
print(f" USDC dopo : ${p['usdc_dopo']:,.2f} · cuscino richiesto ${p['cuscino']:,.2f}"
|
||||
f" ({p['cuscino_come']})")
|
||||
print(f" margine : ${slack:+,.2f} di slack sul cuscino"
|
||||
+ (" ⚠️ la quota e' ESATTAMENTE sul limite: zero slack" if abs(slack) <= EPS_CUSCINO else ""))
|
||||
for m in p["motivi"]:
|
||||
print(f" ✗ {m}")
|
||||
if not p["ok"]:
|
||||
print(" → NON si converte.")
|
||||
return 1
|
||||
if not a.esegui:
|
||||
print(" → piano valido. Rilancia con --esegui per mandarlo.")
|
||||
return 0
|
||||
|
||||
LOG.parent.mkdir(parents=True, exist_ok=True)
|
||||
chunk, fatti, tot_filled, prezzi, falliti = a.chunk, [], 0.0, [], 0
|
||||
for _ in range(60):
|
||||
eq_usdc, eq_usde, px, _f = leggi_stato(c)
|
||||
if px is None:
|
||||
print(" ✗ prezzo non piu' leggibile — mi fermo qui"); break
|
||||
val, _ = U.valuta(eq_usde, px)
|
||||
bid, ask = book_top()
|
||||
pp = piano(eq_usdc, eq_usde, px, a.quota, eq_usdc + val, bid, ask)
|
||||
if not pp["ok"]:
|
||||
print(f" · fine: {pp['motivi'][0]}")
|
||||
break
|
||||
amt = float(min(pp["amount"], math.floor(chunk)))
|
||||
if amt < 1:
|
||||
print(" ✗ blocco sceso sotto 1 USDE — mi fermo"); break
|
||||
resp = c._unwrap(c._post("/mcp-deribit/tools/place_order",
|
||||
{"instrument_name": U.SPOT, "side": pp["side"], "amount": amt,
|
||||
"type": "limit", "price": pp["px_ordine"],
|
||||
"label": f"usde-quota-{a.quota:.2f}"})) or {}
|
||||
if not isinstance(resp, dict) or resp.get("error") or "order" not in resp:
|
||||
err = resp.get("error", resp) if isinstance(resp, dict) else resp
|
||||
falliti += 1
|
||||
attesa = min(a.pausa * (2 ** falliti), 90.0) # backoff sul TEMPO, non sulla taglia
|
||||
if falliti >= 3: # solo allora sospetto la size
|
||||
chunk = max(1.0, math.floor(chunk / 2))
|
||||
print(f" · rifiutato {amt:,.0f}: {err} -> attendo {attesa:.0f}s"
|
||||
+ (f", blocco a {chunk:,.0f}" if falliti >= 3 else ""))
|
||||
time.sleep(attesa)
|
||||
with LOG.open("a") as f:
|
||||
f.write(json.dumps({"ts": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
||||
"rifiutato": err, "amount": amt, "nuovo_chunk": chunk}) + "\n")
|
||||
if falliti >= 8:
|
||||
print(" ✗ troppi rifiuti consecutivi — mi fermo"); break
|
||||
continue
|
||||
falliti = 0
|
||||
order, trades = resp["order"], resp.get("trades", []) or []
|
||||
filled = float(order.get("filled_amount") or 0) or sum(float(t.get("amount", 0) or 0) for t in trades)
|
||||
tot_filled += filled
|
||||
prezzi += [(float(t["price"]), float(t["amount"])) for t in trades if t.get("price") and t.get("amount")]
|
||||
fatti.append({"order_id": order.get("order_id"), "state": order.get("order_state"),
|
||||
"amount": amt, "filled": filled})
|
||||
print(f" · {pp['side']} {amt:,.0f} -> {order.get('order_state')} filled {filled:,.0f}"
|
||||
f" (quota {pp['quota_ora']:.1%} -> bersaglio {a.quota:.0%})")
|
||||
if filled <= 0:
|
||||
print(" ✗ ordine accettato ma non fillato — mi fermo"); break
|
||||
time.sleep(a.pausa)
|
||||
|
||||
px_med = sum(q * v for q, v in prezzi) / sum(v for _, v in prezzi) if prezzi else None
|
||||
eq_usdc, eq_usde, px, _f = leggi_stato(c)
|
||||
val, _ = U.valuta(eq_usde, px)
|
||||
tot = eq_usdc + val
|
||||
print(f"\n ESITO : {tot_filled:,.0f} USDE in {len(fatti)} ordini"
|
||||
+ (f" @ {px_med:.4f} medio" if px_med else ""))
|
||||
print(f" conto : USDC ${eq_usdc:,.2f} + USDE {eq_usde:,.4f} (${val:,.2f}) = ${tot:,.2f}")
|
||||
print(f" quota : {val / tot:.2%} (bersaglio {a.quota:.0%})")
|
||||
with LOG.open("a") as f:
|
||||
f.write(json.dumps({"ts": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
||||
"quota_target": a.quota, "tot_filled": tot_filled, "px_medio": px_med,
|
||||
"ordini": fatti, "quota_finale": val / tot if tot else None,
|
||||
"usdc": eq_usdc, "usde": eq_usde}) + "\n")
|
||||
print(f" registrato : {LOG.relative_to(ROOT)}")
|
||||
return 0 if tot_filled > 0 else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user