#!/usr/bin/env python3
"""BUY FRAMEWORK v3.1 (3 Oct 2026, rules re-tested: blind-checked filter, close-based trades, random-day baseline). Usage: python3 buyscore.py <contract> [chain]   (key: env TWAPI_KEY or /opt/common/twitterapi_key or ~/.twapi_key)
All X calls go through xsafe (the shared scam/bot filter + credit guard): http://159.223.88.168/hx-c9ois/fw/xsafe.py
Sentiment rules from the 3 Oct stage study (606 coins, ATH then 20/40/60/70/80/90% under it, mood READ by meaning on
1,514 windows, plus a 12h time grid on 189 deep coins; both time halves, both chains, every size band):
  * At every depth, coins where NO real person posted in the last 12-24h did 20 to 40 points better over 7 days
    than coins where people were talking. Euphoric, frustrated and hopeful talk after a dump = worst.
  * Nobody posts "I give up": resigned posts are under 1%. Giving up = going silent.
  * 90%+ under ATH + silent 24h + trade volume dried (<=20% of peak day): median +8% / mean +21% in 7d, 2x before -50% 71%.
  * People RETURNING after silence while price pops 15%+ = fake bounce (median -32%).
Mood is never scored with keywords: real posts are printed and the reader judges the feeling.
"""
import json, sys, time, math, re, os, urllib.request, urllib.parse, datetime as dt
UA = {"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_0) AppleWebKit/537.36 Chrome/128 Safari/537.36", "Accept": "application/json"}
BKK = dt.timezone(dt.timedelta(hours=7))


def load_xsafe():
    import importlib.util
    for p in ['/opt/common/xsafe/xsafe.py', os.path.expanduser('~/.xsafe/xsafe.py')]:
        if os.path.exists(p) and (p.startswith('/opt') or time.time() - os.path.getmtime(p) < 6 * 3600): break
    else:
        p = os.path.expanduser('~/.xsafe/xsafe.py'); os.makedirs(os.path.dirname(p), exist_ok=True)
        urllib.request.urlretrieve("http://159.223.88.168/hx-c9ois/fw/xsafe.py", p)
    sp = importlib.util.spec_from_file_location("xsafe", p); m = importlib.util.module_from_spec(sp); sp.loader.exec_module(m)
    return m


def get(u, h=UA, tries=4):
    for i in range(tries):
        try:
            with urllib.request.urlopen(urllib.request.Request(u, headers=h), timeout=40) as r: return json.load(r)
        except urllib.error.HTTPError as e:
            if e.code in (401, 402, 403, 404): return None   # no retry on auth / credits / missing
            time.sleep(4 + 6 * i)
        except Exception: time.sleep(3)
    return None

def rsi(cl, n=14):
    if len(cl) < n + 1: return None
    g = sum(max(cl[i] - cl[i - 1], 0) for i in range(-n, 0)); l = sum(max(cl[i - 1] - cl[i], 0) for i in range(-n, 0))
    return 100.0 if l == 0 else 100 - 100 / (1 + g / l)

def k(x): return f"${x/1e6:.2f}M" if x >= 1e6 else f"${x/1e3:.0f}k"

def main(ca, chain=None):
    ds = get(f"https://api.dexscreener.com/latest/dex/tokens/{ca}")
    pairs = [p for p in (ds or {}).get('pairs') or [] if p['baseToken']['address'].lower() == ca.lower() and p.get('marketCap')]
    if chain: pairs = [p for p in pairs if p['chainId'] == chain] or pairs
    if not pairs: print("NO DATA: DexScreener has no pool for this contract."); return
    p = max(pairs, key=lambda p: (p.get('volume', {}).get('h24') or 0))
    net = {'solana': 'solana', 'robinhood': 'robinhood'}.get(p['chainId'], p['chainId'])
    mc = p['marketCap']; price = float(p['priceUsd']); sup = mc / price; pool = p['pairAddress']
    now = time.time()
    base = f"https://api.geckoterminal.com/api/v2/networks/{net}/pools/{pool}/ohlcv"
    dly = get(f"{base}/day?limit=180&currency=usd&token={ca}") or {}
    hr = get(f"{base}/hour?limit=1000&currency=usd&token={ca}") or {}
    D = sorted((dly.get('data') or {}).get('attributes', {}).get('ohlcv_list', []))
    H = sorted((hr.get('data') or {}).get('attributes', {}).get('ohlcv_list', []))
    if len(H) < 30:
        age = (now - p.get('pairCreatedAt', now * 1000) / 1000) / 3600
        print(f"{p['baseToken']['symbol']}: WAIT (TOO YOUNG)\nOnly {age:.0f} hours old. This framework buys coins 70%+ under a top that is 3+ days old, with X gone quiet. Nothing to score yet.\nMarket cap {k(mc)} | Chart: https://dexscreener.com/{p['chainId']}/{pool}\nCA: {ca}")
        return
    ath = max([r[2] for r in D] + [r[2] for r in H])
    ath_t = max((r for r in H if r[2] == max(x[2] for x in H)), key=lambda r: r[0])[0]
    if D and max(r[2] for r in D) > max(r[2] for r in H) * 1.02:
        ath_t = max(r for r in D if r[2] == max(x[2] for x in D))[0]
    cl = [r[4] for r in H]; vo = [r[5] for r in H]
    dd = 1 - price / ath; ath_age_h = (now - ath_t) / 3600
    l72 = min(r[3] for r in H[-72:]); near_low = price / l72
    r1 = rsi(cl); ret24 = price / cl[-25] - 1 if len(cl) > 25 else 0
    v24 = sum(vo[-24:]); v24pk = max(sum(vo[i - 24:i]) for i in range(24, len(vo) + 1))
    vdecay = v24 / v24pk if v24pk else 1
    sym = p['baseToken']['symbol']
    try:
        xs = load_xsafe()
        cnow = xs.search(ca, since=now - 24 * 3600, until=now, pages=10)         # contract, FULL last 24h (reads to the end, never "last 20")
        cprev = xs.search(ca, since=now - 48 * 3600, until=now - 24 * 3600)     # the day before
        chain_words = {'robinhood': '(robinhood OR rh OR long OR pons)', 'solana': '(solana OR sol OR pump)'}.get(p['chainId'], '')
        tq = f"${sym}" if len(sym) > 3 else f"(${sym} {chain_words})"
        tick = xs.search(tq, since=now - 24 * 3600, until=now)                    # people use the ticker: mood reading
    except Exception as e:
        print(f"{sym}: NO VERDICT (X DATA MISSING)\nX search refused or failed: {str(e)[:160]}. No buy call without X.\nMarket cap {k(mc)} | {dd*100:.0f}% under the top | Chart: https://dexscreener.com/{p['chainId']}/{pool}\nCA: {ca}")
        return
    TS = xs.ts
    real24 = cnow['real']; spam24 = cnow['spam']; real_prev = cprev['real']
    complete = not cnow['has_more']
    win = [sum(1 for t in real24 if now - (q + 1) * 4 * 3600 <= TS(t) < now - q * 4 * 3600) for q in range(5, -1, -1)]
    silent24 = complete and len(real24) == 0
    silent12 = complete and sum(win[-3:]) == 0
    returning = len(real_prev) == 0 and win[-1] > 0
    seen = {t.get('id') for t in real24}
    tick_real = [t for t in tick['real'] if t.get('id') not in seen]
    mood = sorted(real24 + tick_real, key=TS, reverse=True)
    spam_driven = dd >= 0.5 and len(real24) == 0 and len(spam24) >= 10
    rot = get("http://159.223.88.168/rotation.json") or {}
    # golden pocket module (tested 3 Oct: 0.618 touch alone about 0; with order block / buy shift / X quiet +18 to +29% per trade)
    hi = [r[2] for r in H]; lo = [r[3] for r in H]; op = [r[1] for r in H]
    iT = max(range(len(H)), key=lambda q: hi[q]); Ltop = min(lo[:iT + 1]); Htop = hi[iT]
    gp = None
    if Htop / Ltop >= 3 and iT < len(H) - 1:
        lvl = lambda r: Htop - r * (Htop - Ltop)
        retr = (Htop - price) / (Htop - Ltop)
        broke = any(cl[q] < lvl(0.75) for q in range(iT + 1, len(H)))
        zl, zh = lvl(0.666), lvl(0.618)
        gi = max(range(max(0, iT - 200), iT + 1), key=lambda q: (cl[q] - op[q]) if cl[q] > op[q] else -1)
        ob = next(((lo[q], op[q]) for q in range(gi - 1, -1, -1) if cl[q] < op[q]), None)
        ob_in = ob is not None and ob[0] <= zh * 1.03 and ob[1] >= zl * 0.97
        g6 = sum(vo[q] for q in range(len(H) - 6, len(H)) if cl[q] >= op[q]); r6 = sum(vo[q] for q in range(len(H) - 6, len(H)) if cl[q] < op[q])
        g24 = sum(vo[q] for q in range(len(H) - 24, len(H)) if cl[q] >= op[q]); r24 = sum(vo[q] for q in range(len(H) - 24, len(H)) if cl[q] < op[q])
        shift = g6 / (g6 + r6 + 1e-9) - g24 / (g24 + r24 + 1e-9)
        gp = dict(retr=retr, inzone=0.6 <= retr <= 0.75 and not broke, broke=broke, ob=ob_in, shift=shift, zone=(lvl(0.75) * sup, zh * sup), pump=Htop / Ltop)

    deep90 = dd >= 0.90 and ath_age_h >= 72
    deep80 = dd >= 0.80 and ath_age_h >= 72
    dried = vdecay <= 0.20
    # fresh touch: hours since the price FIRST closed 80%+ under the top (tested: entry at the touch +15 to +23%/trade, 12h later +2%)
    iA = max(range(len(cl)), key=lambda q: cl[q]); first80 = next((q for q in range(iA + 1, len(cl)) if cl[q] <= cl[iA] * 0.2), None)
    fresh_h = (len(cl) - 1 - first80) if first80 is not None else None
    fresh = fresh_h is not None and fresh_h <= 3
    chasing = ret24 > 0.38
    fake_bounce = returning and ret24 > 0.15
    talking = len(real24) > 0 or len(tick_real) >= 3

    if chasing: word, why = "NO BUY (CHASING)", f"up {ret24*100:.0f}% in 24h. Buying after a pump loses on average."
    elif fake_bounce: word, why = "NO BUY (FAKE BOUNCE)", "people came back after a silent day while price popped. In the study that bounce faded (median -32% in 7 days)."
    elif spam_driven: word, why = "NO BUY (SPAM-DRIVEN)", "real people are silent but bots and scam links keep it loud. That did worse than random."
    elif deep80 and silent12 and fresh: word, why = "STRONG BUY", "just fell 80%+ under the top in the last few hours and nobody real is posting. Tested: +23% per trade (2x target, stop -50%, 14 days, costs), people talking only +8%, random days at this depth -9%."
    elif deep80 and silent12: word, why = "BUY (small)", f"80%+ under the top and nobody real posting, but the first drop to this level was {fresh_h}h ago. The edge fades after the first hours (+7 to +9% per trade later on)."
    elif gp and gp['inzone'] and (r1 is not None and r1 < 25): word, why = "WAIT (KNIFE IN POCKET)", "in the golden pocket but RSI 1h under 25. Oversold at the pocket did worse, wait for it to settle."
    elif gp and gp['inzone'] and not talking and (gp['ob'] or gp['shift'] > 0.1): word, why = "BUY (GOLDEN POCKET)", "in the 0.618 to 0.75 zone with an order block or buyers stepping in, and nobody hyping it. Stop: 1h close under the 0.75 level."
    elif gp and gp['inzone'] and (gp['ob'] or gp['shift'] > 0.1): word, why = "BUY small (GOLDEN POCKET)", "in the 0.618 to 0.75 zone with an order block or buy shift, but people are still talking. Stop: 1h close under the 0.75 level."
    elif dd >= 0.6 and silent12: word, why = "WAIT (CLOSE)", "60 to 80% under the top and people went silent. Better than when people talk (-3% vs -15% per trade) but not profitable yet. Buy zone is 80%+."
    elif talking and dd >= 0.5: word, why = "WAIT (PEOPLE STILL TALKING)", "dipped, but real people are still posting. Read the mood below: hopeful, excited or angry = not the bottom."
    elif dd < 0.80: word, why = "WAIT", f"only {dd*100:.0f}% under the top. Silence only paid at 80%+ down."
    else: word, why = "WAIT", "deep, but the top is under 3 days old or X data is incomplete. Let it base."

    t = dt.datetime.now(BKK).strftime('%d %b %H:%M Bangkok')
    print(f"{sym}: {word}\n{why}\n")
    print(f"Time {t} | chain {p['chainId']} | market cap {k(mc)} | top {k(ath*sup)} ({dd*100:.0f}% under, top {ath_age_h/24:.1f} days ago)")
    print(f"Price vs 72h low: {near_low:.2f}x | RSI 1h: {(r1 or 0):.0f} | 24h: {ret24*100:+.0f}% | trading volume last 24h vs peak day: {vdecay*100:.0f}%")
    vol4 = [sum(vo[max(0, len(vo) - (q + 1) * 4):len(vo) - q * 4]) for q in range(5, -1, -1)]
    print("Per 4h, oldest to newest (last 24h):")
    print("  real people posting the contract: " + ' > '.join(str(x) for x in win) + ('' if complete else '  (busy: more posts than read)'))
    print("  trading volume: " + ' > '.join(k(x) for x in vol4))
    print(f"  day before: {len(real_prev)} real people | spam removed today: {len(spam24)} | ticker ${sym}: {len(tick['real'])} real people in 24h{' (short ticker, may mix other coins)' if len(sym) <= 3 else ''}")
    if mood:
        print("READ THE MOOD by meaning (not keywords): hopeful, excited or angry = not the bottom. Silence = bottom. Nobody posts that they gave up; they just stop.")
        for tw in mood[:15]:
            ts = dt.datetime.fromtimestamp(TS(tw), BKK).strftime('%d %b %H:%M')
            body = re.sub(r'\s+', ' ', tw.get('text', ''))[:170]; who = (tw.get('author') or {}).get('userName', '?')
            print(f"  {ts} @{who}: {body}")
    else:
        print("Real people posting in 24h: none (contract or ticker).")
    if rot: print(f"Market: BTC {rot.get('btc')} ({rot.get('btc_r7',0):+.1f}% 7d). Check the 5-state macro light before sizing.")
    if gp: print(f"Golden pocket: pullback {gp['retr']*100:.0f}% of the {gp['pump']:.1f}x leg | zone {k(gp['zone'][0])} to {k(gp['zone'][1])} | 1h closed under 0.75: {'yes' if gp['broke'] else 'no'} | order block in zone: {'yes' if gp['ob'] else 'no'} | buy shift {gp['shift']:+.2f}")
    print(f"Chart: https://dexscreener.com/{p['chainId']}/{pool}\nCA: {ca}")


if __name__ == '__main__':
    main(sys.argv[1], sys.argv[2] if len(sys.argv) > 2 else None)
