feat: cockpit
This commit is contained in:
@@ -109,7 +109,14 @@ def get_quote(symbol: str) -> Optional[Dict[str, Any]]:
|
||||
if hist.empty:
|
||||
continue
|
||||
price = float(hist["Close"].iloc[-1])
|
||||
prev = float(hist["Close"].iloc[-2]) if len(hist) > 1 else price
|
||||
# Explicit D-1 close: the latest row whose calendar date differs from the
|
||||
# most recent row's date, not just "the row before last" — near-24h
|
||||
# instruments (FX, futures) can otherwise return two rows for the same
|
||||
# session, silently comparing "today vs today" and making change_pct swing
|
||||
# around against a moving reference instead of a fixed prior close.
|
||||
last_date = hist.index[-1].date()
|
||||
prior_rows = hist[hist.index.date < last_date]
|
||||
prev = float(prior_rows["Close"].iloc[-1]) if not prior_rows.empty else price
|
||||
change = price - prev
|
||||
change_pct = (change / prev * 100) if prev else 0
|
||||
return {
|
||||
@@ -447,13 +454,13 @@ MACRO_GAUGE_CONFIG = [
|
||||
|
||||
SCENARIO_META = {
|
||||
"goldilocks": {"label": "Goldilocks", "color": "#10b981", "emoji": "🟢"},
|
||||
"desinflation": {"label": "Désinflation / Baisse taux","color": "#3b82f6", "emoji": "🔵"},
|
||||
"desinflation": {"label": "Disinflation / Rate Cuts", "color": "#3b82f6", "emoji": "🔵"},
|
||||
"soft_landing": {"label": "Soft Landing", "color": "#06b6d4", "emoji": "🔷"},
|
||||
"reflation": {"label": "Reflation", "color": "#f97316", "emoji": "🟠"},
|
||||
"stagflation": {"label": "Stagflation", "color": "#f59e0b", "emoji": "🟡"},
|
||||
"inflation_shock": {"label": "Choc Inflationniste", "color": "#dc2626", "emoji": "🔥"},
|
||||
"recession": {"label": "Récession", "color": "#ef4444", "emoji": "🔴"},
|
||||
"crise_liquidite": {"label": "Crise de liquidité", "color": "#7c3aed", "emoji": "🟣"},
|
||||
"inflation_shock": {"label": "Inflation Shock", "color": "#dc2626", "emoji": "🔥"},
|
||||
"recession": {"label": "Recession", "color": "#ef4444", "emoji": "🔴"},
|
||||
"crise_liquidite": {"label": "Liquidity Crisis", "color": "#7c3aed", "emoji": "🟣"},
|
||||
}
|
||||
|
||||
SCENARIO_ASSET_BIAS = {
|
||||
@@ -807,165 +814,165 @@ def _score_raw(gauges: Dict[str, Any]) -> tuple:
|
||||
elif vix < 18: s += 20; r.append("VIX<18")
|
||||
elif vix < 22: s += 10
|
||||
if slope is not None:
|
||||
if slope > 1.0: s += 20; r.append("Courbe +1%pt")
|
||||
elif slope > 0.3: s += 10; r.append("Courbe légèrement positive")
|
||||
if slope > 1.0: s += 20; r.append("Curve +1%pt")
|
||||
elif slope > 0.3: s += 10; r.append("Curve slightly positive")
|
||||
if gcr is not None:
|
||||
if gcr < 500: s += 20; r.append(f"Or/Cu {gcr} (croissance)")
|
||||
if gcr < 500: s += 20; r.append(f"Gold/Cu {gcr} (growth)")
|
||||
elif gcr < 600: s += 10
|
||||
if hyg_c > 0.2: s += 15; r.append("HYG↑ (crédit OK)")
|
||||
if hyg_c > 0.2: s += 15; r.append("HYG↑ (credit OK)")
|
||||
elif hyg_c > 0: s += 5
|
||||
if vs200 is not None:
|
||||
if vs200 > 5: s += 15; r.append(f"S&P+{vs200}% vs 200j")
|
||||
if vs200 > 5: s += 15; r.append(f"S&P+{vs200}% vs 200d")
|
||||
elif vs200 > 0: s += 7
|
||||
if copper_c > 0.5: s += 10; r.append("Cuivre↑")
|
||||
if copper_c > 0.5: s += 10; r.append("Copper↑")
|
||||
if skew_v < 115: s += 6; r.append(f"SKEW {skew_v:.0f} (no tail hedge)")
|
||||
if vvix_v < 85: s += 5; r.append(f"VVIX {vvix_v:.0f} (vol stable)")
|
||||
if tech_vs_staples > 0.5: s += 7; r.append("Tech > Défensifs (risk-on sectoriel)")
|
||||
if eem_c > 0.3: s += 6; r.append("EM↑ (croissance globale)")
|
||||
if usdjpy_c > 0.2: s += 4; r.append("JPY↓ (carry actif = risk-on)")
|
||||
if tech_vs_staples > 0.5: s += 7; r.append("Tech > Defensives (sector risk-on)")
|
||||
if eem_c > 0.3: s += 6; r.append("EM↑ (global growth)")
|
||||
if usdjpy_c > 0.2: s += 4; r.append("JPY↓ (active carry = risk-on)")
|
||||
scores["goldilocks"] = min(100, s); reasons["goldilocks"] = r
|
||||
|
||||
# DÉSINFLATION / BAISSE DE TAUX
|
||||
# DISINFLATION / RATE CUTS
|
||||
s = 0; r = []
|
||||
if brent_c < -1.0: s += 25; r.append("Brent↓↓ (désinflationniste)")
|
||||
if brent_c < -1.0: s += 25; r.append("Brent↓↓ (disinflationary)")
|
||||
elif brent_c < 0: s += 10
|
||||
if ng_c < -1.0: s += 10; r.append("Gaz↓")
|
||||
if ief_c > 0.2: s += 20; r.append("IEF↑ (taux longs baissent)")
|
||||
if ng_c < -1.0: s += 10; r.append("Gas↓")
|
||||
if ief_c > 0.2: s += 20; r.append("IEF↑ (long rates falling)")
|
||||
elif ief_c > 0: s += 10
|
||||
if vix < 20: s += 15; r.append("VIX<20")
|
||||
if vs200 is not None and vs200 > 0: s += 20; r.append("S&P au-dessus 200j")
|
||||
if vs200 is not None and vs200 > 0: s += 20; r.append("S&P above 200d")
|
||||
if hyg_c > 0: s += 10; r.append("HYG↑")
|
||||
if gold_c > 0 and brent_c < 0: s += 10; r.append("Or↑+Brent↓ (taux réels ↓)")
|
||||
if tlt_c > 0.5: s += 12; r.append("TLT↑↑ (désinflation confirmée)")
|
||||
elif tlt_c > 0.2: s += 6; r.append("TLT↑ (bonds longs soutiennent)")
|
||||
if xlf_c > 0: s += 5; r.append("XLF↑ (anticipent baisse taux)")
|
||||
if gold_c > 0 and brent_c < 0: s += 10; r.append("Gold↑+Brent↓ (real rates ↓)")
|
||||
if tlt_c > 0.5: s += 12; r.append("TLT↑↑ (disinflation confirmed)")
|
||||
elif tlt_c > 0.2: s += 6; r.append("TLT↑ (long bonds supportive)")
|
||||
if xlf_c > 0: s += 5; r.append("XLF↑ (pricing in rate cuts)")
|
||||
scores["desinflation"] = min(100, s); reasons["desinflation"] = r
|
||||
|
||||
# STAGFLATION
|
||||
s = 0; r = []
|
||||
if brent_c > 2.0: s += 30; r.append("Brent↑↑")
|
||||
elif brent_c > 0.5: s += 15; r.append("Brent↑")
|
||||
if ng_c > 2.0: s += 15; r.append("Gaz↑↑")
|
||||
if ng_c > 2.0: s += 15; r.append("Gas↑↑")
|
||||
elif ng_c > 0.5: s += 7
|
||||
if slope is not None:
|
||||
if slope < 0: s += 20; r.append("Courbe inversée")
|
||||
elif slope < 0.3: s += 10; r.append("Courbe plate")
|
||||
if gold_c > 0.5: s += 15; r.append("Or↑ (protection inflation)")
|
||||
if copper_c < 0: s += 15; r.append("Cuivre↓ (demande faible)")
|
||||
if vix > 18: s += 10; r.append("VIX élevé")
|
||||
if xlp_c > xlk_c + 0.5: s += 8; r.append("Défensifs > Tech (rotation stagflationniste)")
|
||||
if xlu_c > 0.4: s += 6; r.append("Utilities↑ (revenus stables)")
|
||||
if skew_v > 130: s += 5; r.append(f"SKEW {skew_v:.0f} (tail risk croissant)")
|
||||
if tlt_c < -0.3: s += 5; r.append("TLT↓ (inflation persistante)")
|
||||
if slope < 0: s += 20; r.append("Curve inverted")
|
||||
elif slope < 0.3: s += 10; r.append("Curve flat")
|
||||
if gold_c > 0.5: s += 15; r.append("Gold↑ (inflation hedge)")
|
||||
if copper_c < 0: s += 15; r.append("Copper↓ (weak demand)")
|
||||
if vix > 18: s += 10; r.append("VIX elevated")
|
||||
if xlp_c > xlk_c + 0.5: s += 8; r.append("Defensives > Tech (stagflationary rotation)")
|
||||
if xlu_c > 0.4: s += 6; r.append("Utilities↑ (stable income)")
|
||||
if skew_v > 130: s += 5; r.append(f"SKEW {skew_v:.0f} (rising tail risk)")
|
||||
if tlt_c < -0.3: s += 5; r.append("TLT↓ (persistent inflation)")
|
||||
scores["stagflation"] = min(100, s); reasons["stagflation"] = r
|
||||
|
||||
# RÉCESSION
|
||||
# RECESSION
|
||||
s = 0; r = []
|
||||
if slope is not None:
|
||||
if slope < -0.5: s += 30; r.append("Courbe fortement inversée")
|
||||
elif slope < 0: s += 15; r.append("Courbe inversée")
|
||||
if slope < -0.5: s += 30; r.append("Curve deeply inverted")
|
||||
elif slope < 0: s += 15; r.append("Curve inverted")
|
||||
if gcr is not None:
|
||||
if gcr > 750: s += 25; r.append(f"Or/Cu {gcr} (peur)")
|
||||
if gcr > 750: s += 25; r.append(f"Gold/Cu {gcr} (fear)")
|
||||
elif gcr > 650: s += 10
|
||||
if vix > 28: s += 25; r.append("VIX>28")
|
||||
elif vix > 22: s += 12
|
||||
if copper_c < -1.5: s += 20; r.append("Cuivre↓↓")
|
||||
if copper_c < -1.5: s += 20; r.append("Copper↓↓")
|
||||
elif copper_c < -0.5: s += 8
|
||||
if hyg_c < -0.5: s += 15; r.append("HYG↓ (spreads s'écartent)")
|
||||
if hyg_c < -0.5: s += 15; r.append("HYG↓ (spreads widening)")
|
||||
elif hyg_c < 0: s += 5
|
||||
if gold_c > 0.3: s += 10; r.append("Or↑ (refuge)")
|
||||
if tlt_c > 0.5: s += 15; r.append("TLT↑↑ (signal recessionnaire fort)")
|
||||
elif tlt_c > 0.2: s += 7; r.append("TLT↑ (obligations soutenues)")
|
||||
if xlf_c < -1.0: s += 12; r.append("Financières↓↓ (leading indicator récession)")
|
||||
if gold_c > 0.3: s += 10; r.append("Gold↑ (safe haven)")
|
||||
if tlt_c > 0.5: s += 15; r.append("TLT↑↑ (strong recession signal)")
|
||||
elif tlt_c > 0.2: s += 7; r.append("TLT↑ (bonds supported)")
|
||||
if xlf_c < -1.0: s += 12; r.append("Financials↓↓ (recession leading indicator)")
|
||||
elif xlf_c < -0.3: s += 5
|
||||
if eem_c < -1.0: s += 8; r.append("EM↓ (global slowdown)")
|
||||
if usdjpy_c < -1.0: s += 10; r.append("JPY↑↑ (carry unwind = risk-off global)")
|
||||
if usdjpy_c < -1.0: s += 10; r.append("JPY↑↑ (carry unwind = global risk-off)")
|
||||
elif usdjpy_c < -0.5: s += 5
|
||||
if skew_v > 135: s += 8; r.append(f"SKEW {skew_v:.0f} (tail risk extrême)")
|
||||
if skew_v > 135: s += 8; r.append(f"SKEW {skew_v:.0f} (extreme tail risk)")
|
||||
scores["recession"] = min(100, s); reasons["recession"] = r
|
||||
|
||||
# CRISE DE LIQUIDITÉ
|
||||
# LIQUIDITY CRISIS
|
||||
s = 0; r = []
|
||||
if vix > 35: s += 35; r.append("VIX>35 (panique)")
|
||||
if vix > 35: s += 35; r.append("VIX>35 (panic)")
|
||||
elif vix > 28: s += 20; r.append("VIX>28")
|
||||
elif vix > 22: s += 8
|
||||
if hyg_c < -1.5: s += 35; r.append("HYG↓↓ (crise crédit)")
|
||||
if hyg_c < -1.5: s += 35; r.append("HYG↓↓ (credit crisis)")
|
||||
elif hyg_c < -0.5: s += 15
|
||||
if lqd_c < -0.5: s += 10; r.append("IG↓ (spreads s'écartent)")
|
||||
if lqd_c < -0.5: s += 10; r.append("IG↓ (spreads widening)")
|
||||
if vs200 is not None:
|
||||
if vs200 < -10: s += 25; r.append("S&P<200j -10%")
|
||||
if vs200 < -10: s += 25; r.append("S&P<200d -10%")
|
||||
elif vs200 < -3: s += 10
|
||||
if gold_c > 1.0 and copper_c < -1.0: s += 20; r.append("Or↑+Cuivre↓ (fuite sécurité)")
|
||||
if gold_c > 1.0 and copper_c < -1.0: s += 20; r.append("Gold↑+Copper↓ (flight to safety)")
|
||||
if dxy_c > 1.0: s += 15; r.append("Dollar↑↑")
|
||||
if ief_c > 0.5: s += 10; r.append("Obligations souveraines↑↑")
|
||||
if skew_v > 145: s += 15; r.append(f"SKEW {skew_v:.0f} — tail risk extrême")
|
||||
if ief_c > 0.5: s += 10; r.append("Sovereign bonds↑↑")
|
||||
if skew_v > 145: s += 15; r.append(f"SKEW {skew_v:.0f} — extreme tail risk")
|
||||
elif skew_v > 135: s += 8
|
||||
if vvix_v > 115: s += 15; r.append(f"VVIX {vvix_v:.0f} — vol-of-vol panique")
|
||||
elif vvix_v > 100: s += 8; r.append(f"VVIX {vvix_v:.0f} — vol élevée")
|
||||
if usdjpy_c < -1.5: s += 15; r.append("JPY↑↑↑ (carry unwind = panique globale)")
|
||||
if vvix_v > 115: s += 15; r.append(f"VVIX {vvix_v:.0f} — vol-of-vol panic")
|
||||
elif vvix_v > 100: s += 8; r.append(f"VVIX {vvix_v:.0f} — elevated vol")
|
||||
if usdjpy_c < -1.5: s += 15; r.append("JPY↑↑↑ (carry unwind = global panic)")
|
||||
elif usdjpy_c < -0.8: s += 7; r.append("JPY↑ (risk-off carry)")
|
||||
if xlf_c < -2.0: s += 15; r.append("Banques↓↓ (stress bancaire systémique)")
|
||||
if xlf_c < -2.0: s += 15; r.append("Banks↓↓ (systemic banking stress)")
|
||||
elif xlf_c < -1.0: s += 7
|
||||
if emb_c < -1.0: s += 10; r.append("EM Bonds↓ (fuite liquidité EM)")
|
||||
if emb_c < -1.0: s += 10; r.append("EM Bonds↓ (EM liquidity flight)")
|
||||
scores["crise_liquidite"] = min(100, s); reasons["crise_liquidite"] = r
|
||||
|
||||
# REFLATION
|
||||
s = 0; r = []
|
||||
if copper_c > 1.5: s += 25; r.append("Cuivre↑↑ (Dr Copper = croissance)")
|
||||
elif copper_c > 0.5: s += 12; r.append("Cuivre↑")
|
||||
if xli_c > 0.8: s += 20; r.append("Industriels↑↑ (activité mfg forte)")
|
||||
elif xli_c > 0.2: s += 10; r.append("Industriels↑")
|
||||
if brent_c > 1.5: s += 15; r.append("Brent↑ (reflation énergie)")
|
||||
if copper_c > 1.5: s += 25; r.append("Copper↑↑ (Dr Copper = growth)")
|
||||
elif copper_c > 0.5: s += 12; r.append("Copper↑")
|
||||
if xli_c > 0.8: s += 20; r.append("Industrials↑↑ (strong mfg activity)")
|
||||
elif xli_c > 0.2: s += 10; r.append("Industrials↑")
|
||||
if brent_c > 1.5: s += 15; r.append("Brent↑ (energy reflation)")
|
||||
elif brent_c > 0.3: s += 6
|
||||
if vs200 is not None and vs200 > 8: s += 20; r.append(f"S&P+{vs200}% vs 200j (bull fort)")
|
||||
if vs200 is not None and vs200 > 8: s += 20; r.append(f"S&P+{vs200}% vs 200d (strong bull)")
|
||||
elif vs200 is not None and vs200 > 3: s += 10
|
||||
if slope is not None and slope > 1.0: s += 15; r.append("Courbe pentue (croissance)")
|
||||
if slope is not None and slope > 1.0: s += 15; r.append("Curve steep (growth)")
|
||||
elif slope is not None and slope > 0.3: s += 6
|
||||
if rel_perf > 0.3: s += 10; r.append("Small caps > large (risk-on large)")
|
||||
if rel_perf > 0.3: s += 10; r.append("Small caps > large (broad risk-on)")
|
||||
elif rel_perf > 0: s += 4
|
||||
if vix < 18: s += 5
|
||||
if eem_c > 1.0: s += 10; r.append("EM↑↑ (reflation globale)")
|
||||
if eem_c > 1.0: s += 10; r.append("EM↑↑ (global reflation)")
|
||||
elif eem_c > 0.3: s += 5; r.append("EM↑ (global risk-on)")
|
||||
if xlk_c > 1.0: s += 8; r.append("Tech↑↑ (croissance+momentum)")
|
||||
if silver_c > 1.5: s += 8; r.append("Argent↑↑ (reflation industrielle)")
|
||||
if usdjpy_c > 0.5: s += 6; r.append("JPY↓ (carry trades actifs)")
|
||||
if xlk_c > 1.0: s += 8; r.append("Tech↑↑ (growth+momentum)")
|
||||
if silver_c > 1.5: s += 8; r.append("Silver↑↑ (industrial reflation)")
|
||||
if usdjpy_c > 0.5: s += 6; r.append("JPY↓ (active carry trades)")
|
||||
scores["reflation"] = min(100, s); reasons["reflation"] = r
|
||||
|
||||
# SOFT LANDING
|
||||
s = 0; r = []
|
||||
if vs200 is not None and vs200 > 0: s += 20; r.append("S&P > MA200 (croissance intacte)")
|
||||
if brent_c < -0.5 and brent_c > -3: s += 20; r.append("Brent légèrement ↓ (désinflation graduelle)")
|
||||
if vs200 is not None and vs200 > 0: s += 20; r.append("S&P > MA200 (growth intact)")
|
||||
if brent_c < -0.5 and brent_c > -3: s += 20; r.append("Brent slightly ↓ (gradual disinflation)")
|
||||
elif brent_c < 0: s += 8
|
||||
if vix < 20: s += 15; r.append("VIX<20 (pas de stress)")
|
||||
if hyg_c > 0: s += 12; r.append("HYG↑ (crédit solide)")
|
||||
if lqd_c > 0: s += 8; r.append("IG↑ (spreads IG calmes)")
|
||||
if slope is not None and slope > 0: s += 10; r.append("Courbe non-inversée")
|
||||
if xli_c > 0: s += 8; r.append("Industriels positifs")
|
||||
if copper_c > 0: s += 5; r.append("Cuivre stable")
|
||||
if ief_c > 0 and brent_c < 0: s += 7; r.append("Taux baissent + énergie recule")
|
||||
if xlf_c > 0: s += 8; r.append("Financières↑ (économie saine)")
|
||||
if eem_c > 0: s += 5; r.append("EM stable (croissance globale intacte)")
|
||||
if skew_v < 130: s += 4; r.append(f"SKEW {skew_v:.0f} (tail risk non-extrême)")
|
||||
if vix < 20: s += 15; r.append("VIX<20 (no stress)")
|
||||
if hyg_c > 0: s += 12; r.append("HYG↑ (solid credit)")
|
||||
if lqd_c > 0: s += 8; r.append("IG↑ (calm IG spreads)")
|
||||
if slope is not None and slope > 0: s += 10; r.append("Curve not inverted")
|
||||
if xli_c > 0: s += 8; r.append("Industrials positive")
|
||||
if copper_c > 0: s += 5; r.append("Copper stable")
|
||||
if ief_c > 0 and brent_c < 0: s += 7; r.append("Rates falling + energy retreating")
|
||||
if xlf_c > 0: s += 8; r.append("Financials↑ (healthy economy)")
|
||||
if eem_c > 0: s += 5; r.append("EM stable (global growth intact)")
|
||||
if skew_v < 130: s += 4; r.append(f"SKEW {skew_v:.0f} (non-extreme tail risk)")
|
||||
scores["soft_landing"] = min(100, s); reasons["soft_landing"] = r
|
||||
|
||||
# CHOC INFLATIONNISTE
|
||||
# INFLATION SHOCK
|
||||
s = 0; r = []
|
||||
if brent_c > 4.0: s += 40; r.append("Brent↑↑↑ (choc énergie majeur)")
|
||||
if brent_c > 4.0: s += 40; r.append("Brent↑↑↑ (major energy shock)")
|
||||
elif brent_c > 2.0: s += 25; r.append("Brent↑↑")
|
||||
elif brent_c > 0.8: s += 10
|
||||
if ng_c > 4.0: s += 20; r.append("Gaz↑↑↑ (choc supply gaz)")
|
||||
elif ng_c > 2.0: s += 12; r.append("Gaz↑↑")
|
||||
if gold_c > 1.0: s += 20; r.append("Or↑↑ (refuge inflation/géo)")
|
||||
elif gold_c > 0.3: s += 8; r.append("Or↑")
|
||||
if vix > 22: s += 15; r.append("VIX↑ (stress montant)")
|
||||
if ng_c > 4.0: s += 20; r.append("Gas↑↑↑ (gas supply shock)")
|
||||
elif ng_c > 2.0: s += 12; r.append("Gas↑↑")
|
||||
if gold_c > 1.0: s += 20; r.append("Gold↑↑ (inflation/geo hedge)")
|
||||
elif gold_c > 0.3: s += 8; r.append("Gold↑")
|
||||
if vix > 22: s += 15; r.append("VIX↑ (rising stress)")
|
||||
elif vix > 18: s += 5
|
||||
if copper_c < -0.5: s += 8; r.append("Cuivre↓ (demand destruction)")
|
||||
if ief_c < -0.2: s += 8; r.append("Trésor↓ (taux longs remontent)")
|
||||
if ovx_v > 45: s += 15; r.append(f"OVX {ovx_v:.0f} — vol pétrole extrême")
|
||||
elif ovx_v > 35: s += 8; r.append(f"OVX {ovx_v:.0f} — vol pétrole élevée")
|
||||
if gvz_v > 22: s += 8; r.append(f"GVZ {gvz_v:.0f} — vol or élevée")
|
||||
if xlp_c > 0.5: s += 6; r.append("Défensifs↑ (rotation anti-inflation)")
|
||||
if tlt_c < -0.5: s += 8; r.append("TLT↓↓ (anticipation inflation)")
|
||||
if copper_c < -0.5: s += 8; r.append("Copper↓ (demand destruction)")
|
||||
if ief_c < -0.2: s += 8; r.append("Treasuries↓ (long rates rising)")
|
||||
if ovx_v > 45: s += 15; r.append(f"OVX {ovx_v:.0f} — extreme oil vol")
|
||||
elif ovx_v > 35: s += 8; r.append(f"OVX {ovx_v:.0f} — elevated oil vol")
|
||||
if gvz_v > 22: s += 8; r.append(f"GVZ {gvz_v:.0f} — elevated gold vol")
|
||||
if xlp_c > 0.5: s += 6; r.append("Defensives↑ (anti-inflation rotation)")
|
||||
if tlt_c < -0.5: s += 8; r.append("TLT↓↓ (inflation expectations)")
|
||||
scores["inflation_shock"] = min(100, s); reasons["inflation_shock"] = r
|
||||
|
||||
return scores, reasons
|
||||
|
||||
Reference in New Issue
Block a user