import json, time, math, os
from collections import Counter, defaultdict
IV=['1h','6h','24h']; now=time.time()
def sym(t):
"""Symbols are attacker-chosen text. Strip control characters, terminal escapes and the two
markdown metacharacters that survive into the report, so a crafted name cannot break the table
or smuggle instructions into it. A pipe would open an extra cell in the report's markdown table
(a token calling itself "X | buy now" would print as two columns, one of them attacker-written);
a backtick would open or close a code span. Both become ? -- the symbol is data, and a symbol
that needs either character to render is not one worth rendering."""
s=str(t.get('symbol') or '?')
s=''.join(('?' if (ord(c)<32 or ord(c)==127 or c in '|`' or 0x202a<=ord(c)<=0x202e or 0x2066<=ord(c)<=0x2069) else c) for c in s)
return s or '?'
# ---- one normalisation pass over every field this script does arithmetic on ----
# The API has been observed to send a number as a string. Read raw, one such value aborts the whole run:
# 21 calls spent and no list at all. So every numeric field is normalised once, here, before anything
# compares or divides it -- and the two kinds of field are normalised differently on purpose.
SCALEF=('liquidity','market_cap','volume','history_highest_market_cap','price_change_percent')
CNTF =('holder_count','smart_degen_count','renowned_count','visiting_count','buys','sells','swaps',
'open_timestamp','creation_timestamp')
RISKF =('bot_degen_rate','bundler_rate','rug_ratio','dev_team_hold_rate','top_10_holder_rate',
'top70_sniper_hold_rate','entrapment_ratio','bluechip_owner_percentage','insider_rate',
'rat_trader_amount_rate')
def _f(v):
"""A number, or None if it cannot be read as one. A numeric string is still a number."""
if v is None or v=='' or isinstance(v,(list,dict)): return None
if isinstance(v,bool): return float(v)
if isinstance(v,(int,float)): return None if (v!=v or v in (float('inf'),float('-inf'))) else float(v)
try: return float(str(v).strip())
except Exception: return None
def scalefix(t):
"""Normalise one row in place. A scale field (pool, market cap, volume, a count) that cannot be read
becomes 0: every one of them sits under a floor gate, so 0 fails the row rather than flattering it.
A risk field that cannot be read becomes None and is named in `t['_badrisk']` -- never 0, because a
zero risk field is indistinguishable from a clean one and would turn "cannot tell" into "safe". Both
kinds tag the row, and the tag is a rejection reason, so an unreadable row drops out saying why while
the rest of the sweep still produces a list."""
bad=[]
for k in SCALEF:
if k in t:
x=_f(t[k])
if x is None and t[k] not in (None,''): bad.append(k)
t[k]=x or 0.0
for k in CNTF:
if k in t:
x=_f(t[k])
if x is None and t[k] not in (None,''): bad.append(k)
t[k]=int(x or 0)
for k in ('market_cap','liquidity'): t.setdefault(k,0.0) # indexed directly downstream
risk=[]
for k in RISKF:
if k in t:
x=_f(t[k])
if x is None and t[k] not in (None,''): risk.append(k)
t[k]=x
if bad: t['_badnum']=bad
if risk: t['_badrisk']=risk
DATA=os.environ.get('HEAT_DATA') # no default: a fixed fallback path is a directory an attacker can plant
if not DATA: raise SystemExit('HEAT_DATA is unset. Run as: HEAT_DATA="$DATA" python3 "$DATA/heat_rank.py"')
CHAINS=['sol','bsc','base','eth','robinhood','arc','stable']
# ---- load whatever chain/interval files parsed cleanly; a chain needs 24h to be usable ----
ROWS=defaultdict(dict); missing=[]
for ch in CHAINS:
for iv in IV:
p=f'{DATA}/{ch}_{iv}.json'
try: ROWS[ch][iv]=json.load(open(p))['data']['rank']
except Exception: missing.append(f'{ch}/{iv}')
# Step 1 fetches 24h first and skips a chain's 1h/6h calls when that window comes back empty, so those
# two files are deliberately absent rather than lost. Reporting them here would turn a saving into what
# reads as two failed calls, and `missing` has to keep meaning one thing: a call that failed or returned
# JSON we could not parse. A 24h window that itself failed to load still shows up, which is the signal
# worth seeing -- the chain is unusable either way.
def _deliberate(m):
ch,iv=m.split('/')
return iv!='24h' and not ROWS[ch].get('24h')
missing=[m for m in missing if not _deliberate(m)]
for ch in ROWS:
for iv in ROWS[ch]:
for t in ROWS[ch][iv]: scalefix(t)
# Which chains carry a rug score at all? A chain whose every fetched row reads 0 has no rug model
# deployed on it, so MAX_RUG cannot fire there whatever the token is. This can only be seen chain-wide:
# one row reading 0 is indistinguishable from one clean token. Judged off every row this sweep fetched for
# the chain, which is still the age/mcap/liquidity-filtered fetch -- so a chain that returned two rows can
# be called dead on two rows. That error runs toward "no screen ran", i.e. toward strictness, which is the
# safe direction; the coverage table under `## Known limits` is the measurement to trust instead.
RUGDEAD={}
for ch in ROWS:
hi=0.0
for iv in ROWS[ch]:
for t in ROWS[ch][iv]: hi=max(hi,t.get('rug_ratio') or 0.0)
RUGDEAD[ch]=(hi==0.0)
USE=[ch for ch in CHAINS if '24h' in ROWS[ch]]
print('loaded chains:', ', '.join(f"{ch}({'/'.join(str(len(ROWS[ch][iv])) for iv in IV if iv in ROWS[ch])})" for ch in USE))
if missing: print('missing (excluded):', ', '.join(missing))
VOL ={(ch,iv):{t['address']:(t.get('volume') or 0) for t in ROWS[ch][iv]} for ch in USE for iv in IV if iv in ROWS[ch]}
U={}
for ch in USE:
# The reference row must be the 24h one. Most of what is read off it is a current snapshot and reads
# the same in every window -- market cap, pool, holders, the risk fields -- but price_change_percent is
# that window's own move, so a row taken from the 1h file prints a 1h change under a 24h heading, and
# which window a row came from varied per token. setdefault keeps the FIRST window that carried the
# token (24h, then 6h, then 1h) instead of letting the last one loaded overwrite it.
for iv in ['24h','6h','1h']:
for t in ROWS[ch].get(iv,[]): U.setdefault((ch,t['address']),{}).setdefault('ref',t)
UNI=[dict(ch=k[0],a=k[1],t=v['ref']) for k,v in U.items()]
def pctl(v):
s=sorted(v); n=len(s)
return [(sum(1 for x in s if x<q)+sum(1 for x in s if x==q)/2)/n for q in v]
def ath_pos(t): # corrupt for some tokens -> None, never "worst"
mc,hh=t['market_cap'],(t.get('history_highest_market_cap') or 0)
return None if (hh<=0 or hh>1e10 or hh>50*mc) else mc/hh
def risknum(t,k,f):
"""Read a risk field as a number. Absent is 0 -- the field simply is not sent. But a value that is
present and unreadable (a string, a container, NaN, an infinity) is refused instead of coerced: reading
it as 0 would silently turn "cannot tell" into "clean", which is the one mistake a risk gate must not
make. The rejection lands in this row's own fail list, so the row drops out and says why."""
v=t.get(k)
if v is None or v=='': return 0.0
if isinstance(v,bool): return 1.0 if v else 0.0
if isinstance(v,(int,float)):
if v!=v or v in (float('inf'),float('-inf')): f.append(f'unreadable risk field {k}'); return 0.0
return float(v)
f.append(f'unreadable risk field {k}')
return 0.0
MIN_LIQ,MIN_VOL24,MIN_TURN,MAX_TOP10,MAX_BOT=100_000,800_000,0.05,0.30,0.85
MIN_VOL1H=20_800 # pace gate: last-1h run rate must imply >=500k/day, independent of MIN_VOL24
MIN_POS,MIN_HOLDERS=0.20,500
HARD_POS = 0.10 # unconditional drawdown floor: down to 10% of its own peak is a falling knife however hot
MAX_RUG = 0.15 # platform rug score: age-independent, same on both tracks
MAX_DEV = 0.05 # how much the dev still holds: age-independent, same on both tracks
# (a) new-launch track (true age < 2d): judge the current run rate, not a 24h total it has not lived through,
# plus evidence it is not a fast rug
YOUNG_D = 2.0
MAX_AGE_D = 7.0 # local backstop for the age gate. Step 1 asks the server for --max-created 7d and the
# server has been honouring it, but 'recently hot' is the whole premise of this list and
# nothing local was checking it: one endpoint ignoring the parameter would put a
# months-old token on the list under the word 'recent'. Keep this equal to --max-created.
Y_VOL1H = 150_000 # real-volume run-rate floor: hot now, not hot once
Y_LIQ = 200_000 # absolute liquidity floor for a new launch
MIN_LMC = 0.015 # pool/mcap floor, both tracks, against shell pools; 1.5% is the low tail of the pool
Y_TOP10 = 0.25 # stricter than mature (0.30): a new launch's supply is easier to hold in few hands
Y_ATH = 0.45 # has not collapsed off its own peak yet (first sign of a fast rug)
Y_HOLD = 800 # holder base
Y_SM, Y_KOL = 20, 10 # identifiable money present (either one satisfies it)
# ---- compensating strictness where a manipulation gate is dead (option B) ----
# bot_degen_rate, bundler_rate, rug_ratio and dev_team_hold_rate read a literal 0 on some chains. That
# means "never measured", not "clean": a row no gate could judge is unverified, not verified safe. Such a
# row has to clear extra thresholds instead -- and every one of them reads a field that is reported on all
# seven chains AND carries the same meaning on each. A per-chain self-calibrated threshold is not an option
# here: the fetch is already narrowed by age / mcap / liquidity, so the sparse chains yield single-digit
# rows per run and no percentile estimated from them would mean anything.
E_HARD = 1.0 # entrapment_ratio is documented 0-1; a value outside that range is uninterpretable
U_IMBAL = 0.35 # |buys-sells|/(buys+sells): a one-sided tape is not a market
U_LIQ = 250_000 # bundling unverifiable -> the pool itself has to be able to absorb an exit
U_TOP10 = 0.25 # tighter than the mature 30%: concentration is the only holder signal left
U_SNIPER = 0.30 # top-70 sniper hold share; one-directional, only ever read when actually reported
U_SCORE_ADD = 8 # an unverified row clears a higher score floor, applied at selection
# bundler ceiling = max(60%, that chain's candidate p90): cut the extreme, not a chain's normal
def _p90(vals):
s=sorted(vals)
return s[min(len(s)-1,int(0.90*len(s)))] if s else 0.0
# leave-one-out: a token is judged against the p90 of every OTHER candidate on its chain, so a lone
# extreme value cannot open its own gate
BUND_CAP={}
BUND_LOO={}
for ch in USE:
pool=[(c['a'],(c['t'].get('bundler_rate') or 0)) for c in UNI if c['ch']==ch]
vals=[x for _,x in pool]
BUND_CAP[ch]=max(0.60,_p90(vals))
for a,_x in pool:
BUND_LOO[(ch,a)]=max(0.60,_p90([y for b,y in pool if b!=a]))
print("bundler per-chain calibrated ceiling (with self / max leave-one-out):",
{ch:(round(BUND_CAP[ch],3), round(max(BUND_LOO[(ch,c['a'])] for c in UNI if c['ch']==ch),3)) for ch in USE if any(c['ch']==ch for c in UNI)})
rej=Counter(); rej_ch=defaultdict(Counter); alive=[]
for c in UNI:
t=c['t']; ch=c['ch']; a=c['a']; f=[]
v={iv:VOL.get((ch,iv),{}).get(a) for iv in IV}
# An age we cannot read is unknown, not zero. The old fallback was `or now`, which made a row
# carrying neither timestamp read as "launched this instant": full freshness credit, and rage=0
# walked straight through MAX_AGE_D -- the one gate this entire list rests on. That is the same
# mistake as reading a missing risk field as clean, which this file refuses to make anywhere
# else. So an unreadable age is placed past the ceiling and reported as the data fault it is.
# `open_timestamp` and `creation_timestamp` are normalised as counts, so an unparseable one
# arrives here as 0 and is caught by the same test as an absent one.
_ts=t.get('open_timestamp') or t.get('creation_timestamp')
rage=(now-_ts)/86400 if _ts else MAX_AGE_D+1.0 # age in days; unknown never reads as 0
age=max(rage, 0.5) # floor on the rate denominator: a 0.6h token must not blow up holders/day
turn=(v['24h']/t['market_cap']) if (v['24h'] and t['market_cap']) else None
_ap0=ath_pos(t)
botr=t.get('bot_degen_rate')
botr=None if botr in (None,0,0.0) else botr # field absent chain-wide (eth/base) -> no discount, no penalty
disc=1.0-(botr or 0.0)
h24=None if v['24h'] is None else v['24h']*disc # real volume, bot share removed
h1h=None if v['1h'] is None else v['1h'] *disc
if t.get('_badnum'): f.append('unreadable number: '+','.join(t['_badnum']))
if t.get('_badrisk'): f.append('unreadable risk field: '+','.join(t['_badrisk']))
if not _ts: f.append('no timestamp (age unknown)')
elif rage>MAX_AGE_D: f.append(f'age>{MAX_AGE_D:g}d(local backstop)')
if (t.get('liquidity') or 0)<MIN_LIQ: f.append('liq<100k')
if (t.get('liquidity') or 0)/max(t['market_cap'] or 1,1)<MIN_LMC: f.append(f'pool/mcap<{MIN_LMC:.1%}')
young = rage < YOUNG_D
if v['24h'] is None: f.append('absent from 24h list')
elif not young:
if h24<MIN_VOL24: f.append('real volume<800k/day')
if turn is not None and turn<MIN_TURN: f.append('turnover<5%')
if v['1h'] is None: f.append('absent from 1h list')
elif h1h<(Y_VOL1H if young else MIN_VOL1H): f.append('1h real volume stalled')
if young: # the new-launch "stood up + not a fast rug" set; every one must pass
if (t.get('liquidity') or 0)<Y_LIQ: f.append('new:pool<200k')
if (t.get('top_10_holder_rate') or 0)>Y_TOP10: f.append('new:top10>25%')
if _ap0 is not None and _ap0<Y_ATH: f.append('new:collapsed off peak')
if (t.get('holder_count') or 0)<Y_HOLD: f.append('new:holders<800')
if (t.get('smart_degen_count') or 0)<Y_SM and (t.get('renowned_count') or 0)<Y_KOL:
f.append('new:no smart money/KOL')
if risknum(t,'rug_ratio',f)>MAX_RUG: f.append(f'rug score>{MAX_RUG}')
if risknum(t,'dev_team_hold_rate',f)>MAX_DEV: f.append(f'dev still holds>{MAX_DEV:.0%}')
if (t.get('holder_count') or 0)<MIN_HOLDERS: f.append('holders<500')
if risknum(t,'top_10_holder_rate',f)>MAX_TOP10: f.append('top10>30%')
_bc=BUND_LOO.get((ch,a),BUND_CAP[ch])
if risknum(t,'bundler_rate',f)>_bc: f.append(f'bundler>{_bc:.0%}(per-chain LOO)')
if botr is not None and botr>MAX_BOT: f.append('bot>85%')
if t.get('is_wash_trading'): f.append('wash trading') # the EVM filter is a no-op; this has to be caught locally
if t.get('is_honeypot') in (1,'1',True): f.append('honeypot')
_ap=_ap0
# (b) down >80% only kills when volume is also drying up: last-1h real volume under half its own daily rate
_cool=(h24 is not None and h1h is not None and h1h<0.5*(h24/24.0))
if _ap is not None and _ap<MIN_POS and _cool: f.append('down>80% and volume drying up')
# the mature track is not exempt from drawdown any more: 10% of peak is out however hot the tape
if _ap is not None and _ap<HARD_POS: f.append(f'down>{1-HARD_POS:.0%}(hard line)')
# ---- option B: which manipulation gates could actually judge this row? ----
_bund=risknum(t,'bundler_rate',f); _entr=risknum(t,'entrapment_ratio',f)
_dev =risknum(t,'dev_team_hold_rate',f); _s70=risknum(t,'top70_sniper_hold_rate',f)
no_bot_screen = (botr is None) and (_bund==0) # neither bot share nor bundling was judged at all
# rug score unmeasured, the platform says the creator is still holding, and it will not say how much:
# "holds" and "holds 0%" cannot both be true, so the overhang is unquantified rather than absent
overhang = (risknum(t,'rug_ratio',f)==0 and t.get('creator_token_status')=='creator_hold' and _dev==0)
no_rug_screen = RUGDEAD.get(ch,True) # no rug model on this chain -> MAX_RUG never fires
unverified = no_bot_screen or overhang or no_rug_screen
# entrapment_ratio is reported on all seven chains but is NOT usable as a threshold: its median runs
# 0.07 on sol against 0.88 on eth, so no absolute cut transfers, and within one chain the values sit
# close enough together that a percentile cut becomes a coin flip at the boundary. Only the one
# unambiguous reading is acted on -- an uninterpretable risk number is not a pass.
if _entr>E_HARD: f.append('entrapment out of range')
if _s70>U_SNIPER: f.append(f'snipers hold>{U_SNIPER:.0%}')
if unverified:
_b,_s=t.get('buys'),t.get('sells')
_b=_b if isinstance(_b,(int,float)) else 0; _s=_s if isinstance(_s,(int,float)) else 0
if _b+_s>0 and abs(_b-_s)/(_b+_s)>U_IMBAL: f.append('unverified:one-sided tape')
if (t.get('liquidity') or 0)<U_LIQ: f.append('unverified:pool<250k')
if (t.get('smart_degen_count') or 0)<Y_SM and (t.get('renowned_count') or 0)<Y_KOL:
f.append('unverified:no smart money/KOL')
if risknum(t,'top_10_holder_rate',f)>U_TOP10: f.append('unverified:top10>25%')
if v['6h'] is None: f.append('unverified:absent from 6h list')
f[:]=list(dict.fromkeys(f)) # a field read twice must not be reported twice
c['unverified']=unverified; c['no_bot_screen']=no_bot_screen; c['overhang']=overhang
c['no_rug_screen']=no_rug_screen
c.update(rage=rage,h24=h24,h1h=h1h,botr=botr,fail=f,v=v,age=age,turn=turn,ath=_ap)
for x in f: rej[x]+=1; rej_ch[ch][x]+=1
if not f: alive.append(c)
AVAIL={} # does this chain actually carry this field (all-zero/all-empty chain-wide = unsupported there)
for ch in USE:
pool=[c for c in UNI if c['ch']==ch]
AVAIL[ch]={fld: any((c['t'].get(fld) not in (None,0,0.0,'')) for c in pool)
for fld in ['bluechip_owner_percentage','bot_degen_rate','bundler_rate','visiting_count']}
print()
for fld in ['bluechip_owner_percentage','bot_degen_rate','bundler_rate','visiting_count']:
no=[ch for ch in USE if not AVAIL[ch][fld]]
print(f"field {fld:<28} missing on: {', '.join(no) if no else '(none)'}")
def vacc(c):
"""Volume acceleration: self-normalised, stateless, age-independent. >1 = busier now than its own daily average."""
v=c['v']; out=[]
if v['24h']:
if v['1h'] is not None: out.append((v['1h']*24)/v['24h'])
if v['6h'] is not None: out.append((v['6h']*4) /v['24h'])
return max(out) if out else None
for c in UNI:
t=c['t']; c['vacc']=vacc(c)
c['hgrow']=(t.get('holder_count') or 0)/c['age'] # holders per day
c['kgrow']=(t.get('renowned_count') or 0)/c['age'] # KOLs per day
# percentiles over the whole cross-chain pool -> scores compare across chains; the cost is that
# wallet-dense chains win the growth axes
V0=3_000_000.0 # half-weight volume for significance shrinkage: ratio metrics are noise at small size, pull toward 1.0
for c in UNI:
va=c['vacc']; vv=c['h24'] or 0
c['vacc_raw']=va
c['vacc']=None if va is None else 1.0+(va-1.0)*(vv/(vv+V0))
c['sm']=c['t'].get('smart_degen_count') or 0
c['kol']=c['t'].get('renowned_count') or 0
MIN_CH_N=5 # an in-chain percentile needs at least 5 candidates to mean anything
P=dict(
vacc =pctl([math.log1p(max(c['vacc'] or 0,0)) for c in UNI]),
size =pctl([math.log1p(c['h24'] or 0) for c in UNI]),
sm =pctl([math.log1p(c['sm']) for c in UNI]),
kol =pctl([math.log1p(c['kol']) for c in UNI]),
hgrow=None, kgrow=None, vis=None,
liq =pctl([(c['t'].get('liquidity') or 0) for c in UNI]),
turn =pctl([(c['turn'] or 0) for c in UNI]))
# platform-semantics fields: percentile within the chain (robinhood holders are app accounts, not on-chain wallets)
for key,get in [('hgrow',lambda c:c['hgrow']),('kgrow',lambda c:c['kgrow']),
('vis', lambda c:(c['t'].get('visiting_count') or 0))]:
out=[None]*len(UNI)
small=[i for i,c in enumerate(UNI) if sum(1 for x in UNI if x['ch']==c['ch'])<MIN_CH_N]
for ch in {c['ch'] for c in UNI}:
idx=[i for i,c in enumerate(UNI) if c['ch']==ch]
if len(idx)>=MIN_CH_N:
q=pctl([get(UNI[i]) for i in idx])
for j,i in enumerate(idx): out[i]=q[j]
if small:
q=pctl([get(UNI[i]) for i in small])
for j,i in enumerate(small): out[i]=q[j]
P[key]=out
for i,c in enumerate(UNI):
t=c['t']
conc=1-min(1.,(t.get('top_10_holder_rate') or 0)/MAX_TOP10)
pos = 0.5 if c['ath'] is None else min(1., c['ath']/0.8)
grow= 0.6*P['hgrow'][i]+0.4*P['kgrow'][i]
qual= 0.55*conc+0.45*P['liq'][i] # bluechip exists on sol only -> kept out of the cross-chain score
heat= 0.6*P['turn'][i]+0.4*P['vis'][i]
size= P['size'][i]
smart=0.6*P['sm'][i]+0.4*P['kol'][i]
fresh=max(0.0,min(1.0,(7.0-c['age'])/5.0)) # linear 2d->1.0, 7d->0.0; tilts inside the window only
c['score']=round(100*(0.14*P['vacc'][i]+0.22*size+0.08*pos+0.15*grow+0.13*smart+0.14*qual+0.08*heat+0.06*fresh),1)
c['p']=dict(vacc=P['vacc'][i],size=size,pos=pos,grow=grow,smart=smart,qual=qual,heat=heat,fresh=fresh)
byc=Counter(c['ch'] for c in UNI); bya=Counter(c['ch'] for c in alive)
print(f"\ncross-chain candidates = {len(UNI)} passed gates = {len(alive)}")
print(" " + " ".join(f"{ch}:{bya[ch]}/{byc[ch]}" for ch in USE))
print("rejection reasons (all chains):", rej.most_common())
for ch in USE:
if rej_ch[ch]: print(f" {ch:<10}", rej_ch[ch].most_common())
TOP_N,MIN_SCORE=10,60
ranked=sorted(alive,key=lambda x:-x['score'])
def floor_for(c): return MIN_SCORE+(U_SCORE_ADD if c['unverified'] else 0) # unverified rows earn their place at a higher bar
rows=[c for c in ranked if c['score']>=floor_for(c)][:TOP_N] # floor first, cap second: a weak market returns fewer than 10
_listed={(c['ch'],c['a']) for c in rows}
near=[c for c in ranked if (c['ch'],c['a']) not in _listed][:3]
_nu=sum(1 for c in ranked if c['unverified'])
# The floor is two-valued, so one number here is a lie that ends up in the report: a weakly screened row
# needs MIN_SCORE+U_SCORE_ADD. Printing only MIN_SCORE made the near-miss block look self-contradictory --
# a 63.4 dropped while a 63.3 was listed -- which reads as a bug in the skill rather than the rule working.
print(f"\npassed {len(alive)} -> floor {MIN_SCORE}, or {MIN_SCORE+U_SCORE_ADD} for the {_nu} of {len(ranked)} rows no manipulation gate could judge; capped at {TOP_N} = {len(rows)} listed")
print(f"\n{'#':>2} {'chain':<9} {'sym':11s} {'score':>5} | {'vacc':>5} {'size':>4} {'pos':>4} {'grow':>4} {'smart':>5} {'qual':>4} {'heat':>4} | {'mc':>12} {'liq':>9} {'vol24h':>11} {'age':>5} {'ATH':>5} {'24h%':>8}")
for i,c in enumerate(rows,1):
t=c['t']; p=c['p']; ap='n/a' if c['ath'] is None else format(c['ath'],'.2f')
# A 24h change needs 24h of history. Under one day of age the window opens before the token existed, so the
# figure is measured off the launch price and prints things like +128168.0% -- arithmetically right, useless
# as a read on momentum, and wide enough to break the column. n/a is the honest cell, and the age column
# immediately to its left already says why it is empty.
chg='n/a' if c['rage']<1.0 else format(t.get('price_change_percent') or 0,'+.1f')+'%'
print(f"{i:>2} {c['ch']:<9} {sym(t)[:11]:11s} {c['score']:>5} | {p['vacc']:>5.2f} {p['size']:>4.2f} {p['pos']:>4.2f} {p['grow']:>4.2f} {p['smart']:>5.2f} {p['qual']:>4.2f} {p['heat']:>4.2f} | ${t['market_cap']:>11,.0f} ${t['liquidity']:>8,.0f} ${c['v']['24h'] or 0:>10,.0f} {(str(round(c['rage']*24,1))+'h' if c['rage']<1 else str(round(c['rage'],1))+'d'):>5} {ap:>5} {chg:>8}")
# Addresses only, no link. Nothing in this file may point at a gmgn.ai path: the rules at the top
# forbid reaching that site, so any URL printed here is a path shape nobody was allowed to verify.
# The full address is the portable thing anyway -- it pastes into whatever front-end the reader
# already uses, and the reader searches it there.
print("\nCA (full addresses -- search one on whichever front-end you use):")
for i,c in enumerate(rows,1):
t=c['t']
print(f"{i:>2}. {c['ch']:<9} {sym(t)[:12]:12s} {c['a']} vacc={(c['vacc'] or 0):.2f} hold/d={c['hgrow']:.0f} kol/d={c['kgrow']:.1f} top10={(t.get('top_10_holder_rate') or 0)*100:.1f}%")
print("\n--- raw inputs (for hand-checking; '(no data)' = not on that window's list, NOT zero volume) ---")
fmt=lambda x: '(no data)' if x is None else format(x,',.0f')
for c in rows:
t=c['t']
print(f"{c['ch']:<9} {sym(t)[:12]:12s} vol1h={fmt(c['v']['1h']):>13} vol6h={fmt(c['v']['6h']):>13} vol24h={fmt(c['v']['24h']):>13} holders={t.get('holder_count') or 0:>7,} kol={t.get('renowned_count') or 0:>4} sm={t.get('smart_degen_count') or 0:>4} mc={t['market_cap']:>13,.0f} histhigh={t.get('history_highest_market_cap') or 0:>16,.0f}")
print("\nnear misses (so the boundary is inspectable):")
for c in near:
print(f" {c['ch']:<9} {sym(c['t'])[:11]:11s} {c['score']:>5} (needed {floor_for(c)}) {c['a']}")