"""EQ-FUNDNEWS-SHORT — "fondamentali/notizie NEGATIVI ma prezzo SU -> short". Screener FORWARD.

Idea utente: se i dati finanziari/notizie di un'azienda sono negativi ma la quotazione e' positiva
(sale), andare SHORT (scommessa che il prezzo scenda a riallinearsi ai fondamentali).

GATE DATI (lezione v2.0.0, PRIMA della strategia): NON backtestabile su dati certi.
  - I fondamentali da rete (Yahoo) sono SNAPSHOT CORRENTI, non point-in-time storici. Applicarli a
    prezzi passati = LOOK-AHEAD (restatement/survivorship). E' la trappola che ha creato la libreria
    fasulla v2.0.0. -> nessun backtest. Serve un DB point-in-time (Compustat PIT / news storiche), assente.
  - Quindi: come per la vol term-structure, l'unica via onesta e' uno SCREENER FORWARD che genera i
    candidati short OGGI da dati di rete e li LOGGA in avanti. L'edge resta NON PROVATO finche' non
    accumulato e validato forward. Questo script NON afferma un edge: produce candidati + li registra.

SEGNALE (tutto da rete, tokenless):
  - "dati finanziari negativi" = score strutturato da Yahoo quoteSummary (crumb flow):
      recommendationMean alto (->sell), earnings surprise recenti negative, revenueGrowth<0,
      recommendationTrend sbilanciato a sell.
  - "notizie negative" = sentiment lessicale crudo sulle headline (Yahoo news search).
  - "quotazione positiva" = momentum 1m/3m > 0 (chart API).
  -> SHORT candidate = (fond_neg alto OPPURE news_neg alto) AND momentum positivo (la DIVERGENZA).

ESEGUIBILITA' (muro): shortare richiede BORROW (locate+fee, hard-to-borrow caro/assente), perdita
illimitata, squeeze; PDT $25k per i day-trade; IB instabile qui; $600 di capitale; universo single-stock
(non i nostri ETF). Shortare la FORZA combatte il momentum (anomalia forte) -> premessa rischiosa.

    uv run python scripts/research/eq_fundnews_short.py
"""
from __future__ import annotations
import sys
import time
from pathlib import Path

ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))

import numpy as np
import pandas as pd
import requests

RAW = ROOT / "data" / "raw"
H = {"User-Agent": "Mozilla/5.0"}

# universo dimostrativo: large/mid cap liquide, vari settori
UNIVERSE = ["AAPL", "MSFT", "NVDA", "TSLA", "META", "AMZN", "INTC", "F", "GM", "BA",
            "DIS", "PYPL", "NKE", "PFE", "T", "WBA", "XOM", "KO", "CVX", "AMD"]

NEG_WORDS = {"downgrade", "miss", "missed", "cut", "cuts", "lawsuit", "probe", "fraud", "plunge",
             "plunges", "warn", "warns", "warning", "slump", "loss", "losses", "weak", "weakness",
             "decline", "declines", "fall", "falls", "drop", "sinks", "slashes", "recall", "halt",
             "bankruptcy", "default", "layoff", "layoffs", "sell-off", "bearish", "underperform"}
POS_WORDS = {"beat", "beats", "upgrade", "surge", "surges", "record", "strong", "raise", "raises",
             "soar", "soars", "rally", "jumps", "tops", "bullish", "outperform", "growth"}


def yahoo_session():
    s = requests.Session(); s.headers.update(H)
    s.get("https://fc.yahoo.com/", timeout=20)
    crumb = s.get("https://query2.finance.yahoo.com/v1/test/getcrumb", timeout=20).text.strip()
    return s, crumb


def fund_neg_score(rec_mean, surp, rev_g, sell_skew):
    """Pura: score di negativita' fondamentale [0..1] dai componenti disponibili (media)."""
    comp = []
    if rec_mean is not None:
        comp.append(float(np.clip((rec_mean - 1) / 4, 0, 1)))         # 1=buy ->0, 5=sell ->1
    if surp:
        comp.append(1.0 if np.mean(surp[:2]) < 0 else 0.0)            # ultime 2 surprise negative
    if rev_g is not None:
        comp.append(1.0 if rev_g < 0 else float(max(0.0, 1 - rev_g * 5)))
    if sell_skew is not None:
        comp.append(float(np.clip(sell_skew * 3, 0, 1)))
    return float(np.mean(comp)) if comp else None


def headline_sentiment(titles):
    """Pura: frazione di sentiment negativo sulle headline (lessico crudo). None se nessun hit."""
    neg = pos = 0
    for t in titles:
        w = set(t.lower().replace(",", " ").replace(".", " ").split())
        neg += len(w & NEG_WORDS); pos += len(w & POS_WORDS)
    tot = neg + pos
    return (neg / tot) if tot else 0.0, neg, pos


def momentum(sym):
    u = f"https://query1.finance.yahoo.com/v8/finance/chart/{sym}?range=3mo&interval=1d"
    j = requests.get(u, headers=H, timeout=25).json()["chart"]["result"][0]
    c = pd.Series(j["indicators"]["quote"][0]["close"]).dropna().values
    if len(c) < 25:
        return None
    m1 = c[-1] / c[-21] - 1.0                       # ~1 mese
    m3 = c[-1] / c[0] - 1.0                          # ~3 mesi
    return dict(last=float(c[-1]), mom_1m=float(m1), mom_3m=float(m3))


def fundamentals(s, crumb, sym):
    mods = "financialData,recommendationTrend,earningsHistory"
    u = f"https://query2.finance.yahoo.com/v10/finance/quoteSummary/{sym}?modules={mods}&crumb={crumb}"
    res = s.get(u, timeout=25).json()["quoteSummary"]["result"][0]
    fd = res.get("financialData", {})
    rec_mean = fd.get("recommendationMean", {}).get("raw")
    rev_g = fd.get("revenueGrowth", {}).get("raw")
    eh = res.get("earningsHistory", {}).get("history", [])
    surp = [h.get("surprisePercent", {}).get("raw") for h in eh if h.get("surprisePercent")]
    rt = res.get("recommendationTrend", {}).get("trend", [])
    sell_skew = None
    if rt:
        t = rt[0]; tot = sum(t.get(k, 0) for k in ("strongBuy", "buy", "hold", "sell", "strongSell"))
        sell_skew = (t.get("sell", 0) + t.get("strongSell", 0)) / tot if tot else None
    return dict(fund_neg=fund_neg_score(rec_mean, surp, rev_g, sell_skew),
                rec_mean=rec_mean, rev_growth=rev_g, last_surprise=surp[0] if surp else None,
                sell_skew=sell_skew)


def news_sentiment(sym):
    u = f"https://query1.finance.yahoo.com/v1/finance/search?q={sym}&newsCount=8&quotesCount=0"
    news = requests.get(u, headers=H, timeout=25).json().get("news", [])
    if not news:
        return dict(news_neg=None, n=0)
    nn, neg, pos = headline_sentiment([n.get("title", "") for n in news])
    return dict(news_neg=nn, n=len(news), neg_hits=neg, pos_hits=pos)


def screen(universe):
    s, crumb = yahoo_session()
    rows = []
    for sym in universe:
        try:
            mom = momentum(sym)
            if mom is None:
                continue
            fun = fundamentals(s, crumb, sym)
            nw = news_sentiment(sym)
            fneg = fun["fund_neg"]
            nneg = nw["news_neg"]
            rising = mom["mom_1m"] > 0.02                              # quotazione positiva
            fund_bad = fneg is not None and fneg >= 0.5
            news_bad = nneg is not None and nneg >= 0.5
            short_cand = rising and (fund_bad or news_bad)
            rows.append(dict(sym=sym, mom_1m=mom["mom_1m"], mom_3m=mom["mom_3m"],
                             fund_neg=fneg, news_neg=nneg, rec_mean=fun["rec_mean"],
                             rev_growth=fun["rev_growth"], last_surprise=fun["last_surprise"],
                             short_cand=short_cand))
            time.sleep(0.3)
        except Exception as e:
            rows.append(dict(sym=sym, error=repr(e)[:60]))
    return pd.DataFrame(rows)


def main():
    print("=" * 100)
    print("  EQ-FUNDNEWS-SHORT — divergenza fondamentali/notizie NEG vs prezzo SU -> short candidate")
    print("  (SCREENER FORWARD da dati di rete — NON un backtest: edge non provato, vedi header)")
    print("=" * 100)
    df = screen(UNIVERSE)
    ok = df[df.get("error").isna()] if "error" in df else df
    ok = ok.sort_values("fund_neg", ascending=False, na_position="last")
    print(f"  {'sym':5} {'mom1m':>7} {'mom3m':>7} {'fund_neg':>8} {'news_neg':>8} "
          f"{'recMean':>7} {'revGr':>7} {'surp':>7}  SHORT?")
    for _, r in ok.iterrows():
        fn = f"{r['fund_neg']:.2f}" if pd.notna(r['fund_neg']) else "  n/a"
        nn = f"{r['news_neg']:.2f}" if pd.notna(r['news_neg']) else "  n/a"
        rm = f"{r['rec_mean']:.2f}" if pd.notna(r['rec_mean']) else "  n/a"
        rg = f"{r['rev_growth']*100:+.0f}%" if pd.notna(r['rev_growth']) else "  n/a"
        sp = f"{r['last_surprise']*100:+.0f}%" if pd.notna(r['last_surprise']) else "  n/a"
        flag = "  <<< SHORT" if r["short_cand"] else ""
        print(f"  {r['sym']:5} {r['mom_1m']*100:>+6.1f}% {r['mom_3m']*100:>+6.1f}% {fn:>8} {nn:>8} "
              f"{rm:>7} {rg:>7} {sp:>7}{flag}")
    cands = ok[ok["short_cand"]]["sym"].tolist()
    print(f"\n  CANDIDATI SHORT oggi (fond/news neg + prezzo su): {cands or 'nessuno'}")

    # log forward (idempotente per giorno)
    today = pd.Timestamp.now("UTC").normalize()
    ok2 = ok.copy(); ok2.insert(0, "date", today)
    fp = RAW / "fundnews_short_screen.parquet"
    hist = pd.read_parquet(fp) if fp.exists() else pd.DataFrame()
    if len(hist):
        hist = hist[hist["date"] != today]
    pd.concat([hist, ok2], ignore_index=True).to_parquet(fp, index=False)
    print(f"  -> snapshot loggato in {fp.name} (forward dataset; serve accumulo+validazione)")

    print("\n" + "=" * 100)
    print("  ONESTA' / ESEGUIBILITA'")
    print("=" * 100)
    print("  - NON backtestabile: fondamentali = snapshot correnti, non point-in-time -> look-ahead (v2.0.0).")
    print("  - Premessa RISCHIOSA: shortare un prezzo che SALE combatte il momentum (anomalia forte);")
    print("    il rialzo 'malgrado' notizie cattive spesso PREZZA info che i fondamentali trailing non hanno.")
    print("  - Eseguibilita': borrow (locate/fee, hard-to-borrow), perdita illimitata, squeeze, PDT $25k,")
    print("    IB instabile, $600. -> NON deployabile. Deliverable = screener forward + log, edge da provare.")


if __name__ == "__main__":
    main()
