fix(holder-analysis): stop reporting artifacts and empty data as verdicts

The report could reach a confident rating from numbers that carried no
information. Six fixes, all of the same shape: never turn a divide-by-zero
or an empty set into a conclusion.

- Degenerate float guard (FLOAT_MIN 2%): when burn + DEX hold nearly all
  supply, every `/ float_share` inflated dust wallets to double digits. A
  wallet holding ~$2 was rated "largest wallet holds 100% — extreme
  concentration → Not Recommended". Now prints absolute token/USD figures,
  forces percentage flags to  and rates  Cannot Assess. The dev
  sock-puppet rule uses no percentage, so it still escalates to 🔴.
- Empty holder list: upstream returns {"list":[]} for dead or de-indexed
  tokens. Every percentage read 0.00%, no threshold fired, and the report
  printed "no obvious dump risk" and "Normal" — a positive verdict on zero
  data. Same treatment as above, plus "none found 🟢" lines become .
- Coverage caveat now follows actual coverage: floors below 99.5%, complete
  values when Top100 covers the whole float. It previously contradicted the
  line directly above it.
- Chip quality split into three exclusive buckets (bought-in / zero-cost
  airdrop / risk-tagged) instead of one healthy ratio that collapsed to
  "0.0% 🔴" on any airdrop-distributed token. 🔴 is reserved for
  risk-tagged >30%: unknown provenance is not proven-bad.
- Diamond hands now require buy_tx_count_cur > 0. Never-bought wallets were
  counted as diamond hands, so an airdropped token claimed both "airdrop
  79% 🔴" and "diamond hands 80% " from the same wallets. They are now a
  separate Idle airdrop line.
- ATH staleness: upstream ath_price has been seen equal to price_24h, so
  creator_ath_info.ath_mc can sit below current MC. Cannot be recomputed
  here, so it is flagged rather than presented as the dev's peak.

Also fixes a NameError in the dev created-tokens Top3 loop (mig_lcomabel
assigned, mig_label read) that fired for any dev with a non-empty tokens
array.

Verified on live data in zh and en: a degenerate-float token, an
empty-holder token, and a token with real holders; the full-coverage and
created-tokens branches were covered with a stub CLI.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Jiujiu Pan
2026-08-20 14:53:13 +08:00
co-authored by Claude Opus 5
parent 990fc44556
commit 48720c2f5f
2 changed files with 346 additions and 112 deletions
+39 -2
View File
@@ -31,8 +31,21 @@ After the script finishes, paste the complete stdout verbatim into your reply
All holding percentages the script prints are **share of tradeable float** (`1 - burn - DEX`), not
share of total supply. `amount_percentage` from the API is share of total supply; the script
re-bases it. Because only the top 100 holders are fetched, every float percentage is a floor
the footer reports what fraction of the float those 100 wallets cover.
re-bases it. Because only the top 100 holders are fetched, a float percentage is a floor **when
those 100 wallets do not cover the whole float**; the footer reports the actual coverage and states
which case applies — floors when coverage <99.5%, complete values when the top 100 cover all of it.
When burn + DEX leave less than 2% of supply tradeable (typically a launchpad token before
migration), the float denominator degenerates: every `/ float_share` inflates dust wallets to
double digits or 100%. The script detects this, prints a banner with absolute token/USD figures
instead, forces every percentage flag to ⚪, and sets the rating to ⚪ Cannot Assess. Do not read
those percentages as concentration findings.
The same suppression applies when `token holders` returns an empty list (token has no active
holders left, or upstream stopped indexing it). Every percentage would render 0.00% and every
threshold would pass, so the report would otherwise read "✅ Normal — no obvious dump risk". The
script prints a no-data banner instead, replaces each "none found 🟢" line with ⚪, and rates
⚪ Cannot Assess. "No data" is never reported as "no risk".
### Holder object key fields
@@ -88,6 +101,7 @@ Entry timing pressure (批次浮盈/出货) does **NOT** affect the overall rati
| Rating (ZH) | Rating (EN) | Emoji | Condition |
|-------------|-------------|-------|-----------|
| 无法评估 | Cannot Assess | ⚪ | Tradeable float <2% of supply, **or** upstream returned zero holders (all percentage rules suppressed; dev sock puppet still escalates to 🔴) |
| 不建议买 | Not Recommended | 🔴 | Any: rat traders >5% / largest wallet >10% / dev sock puppet |
| 谨慎参与 | Caution | ⚠️ | ≥2 of: Dev still holding >1% / airdrop >20% / risk wallets >35% / linked >15% |
| 可轻仓 | Light Position | 🟡 | Exactly 1 of above warns |
@@ -105,10 +119,29 @@ Entry timing pressure (批次浮盈/出货) does **NOT** affect the overall rati
| Zero-balance wallets | — | >10% | ≤10% |
Diamond hands invert (more is better) and use their own emoji set: ✅ >60% / 🟡 >35% / ⚠️ ≤35%.
Diamond hands require `buy_tx_count_cur > 0` — a wallet that never bought has no cost to hold
through, so zero-cost airdrop recipients are reported separately as "空降未动 / Idle airdrop"
rather than being credited as diamond hands.
Linked funding is escalated to at least 🟡 whenever any group was funded within 60s, regardless of
size — scripted batch funding is a structural signal, not a magnitude one.
### Chip quality (footer)
Three mutually exclusive buckets over the chips held by observed wallets (denominator is
`normal_pct`, i.e. total-supply basis, not float): **bought in with no risk tag** / **zero-cost
airdrop** / **risk-tagged**. They are reported separately rather than collapsed into one
"healthy chips" number, because a risk tag means a proven-bad address while zero-cost airdrop only
means unknown provenance.
Headline flag, first match wins: 🔴 risk-tagged >30% · 🟢 clean ≥50% · 🟡 clean ≥30% · 🟡 when
zero-cost airdrop accounts for ≥80% of the non-clean remainder · 🔴 otherwise. So an
airdrop-distributed token reads 🟡 with its composition spelled out, not "healthy chips 0.0% 🔴".
The composition is total-supply based, so it survives a degenerate float — but the headline flag is
neutralized to ⚪ (and the chips' share of supply appended) whenever the rating is ⚪ Cannot Assess,
since a 🔴/🟢 verdict over dust-level chips would contradict the rating above it.
## Supported Chains
`sol`, `bsc`, `base`, `eth`, `robinhood`, `arc`, `stable`
@@ -128,3 +161,7 @@ size — scripted batch funding is a structural signal, not a magnitude one.
- Top5 displays Twitter name when available; else `first4...last4` format
- Risk-wallet subtotals are per-category and can exceed the deduped total; the script prints how
many wallets carry more than one risk tag when that happens.
- `creator_ath_info.ath_mc` can lag behind the token's current MC after a fast pump (upstream
`ath_price` has been seen equal to `price_24h`). The script cannot recompute it, so when the
reported ATH sits more than 5% below the current MC it prints a staleness warning next to the
figure instead of presenting it as the dev's peak.
+307 -110
View File
@@ -77,6 +77,13 @@ burn = [h for h in holders if h.get('addr_type', 0) == 1]
dex = [h for h in holders if h.get('addr_type', 0) == 2]
def pct(v): return v * 100
def pct_s(v):
"""退化流通盘只有 0.0035% 这种量级,固定两位小数会打成 "0.00%",读起来就是真零 ——
"""极小但非零"在这里是两个不同结论。低于 0.01% 时切成有效数字。"""
p = pct(v)
if p <= 0: return "0%"
if p < 0.01: return f"{p:.2g}%"
return f"{p:.2f}%"
def usd(v):
if v is None: return "$0"
if abs(v) >= 1_000_000: return f"${v/1_000_000:.2f}M"
@@ -116,12 +123,41 @@ dex_pct = sum(h['amount_percentage'] for h in dex)
# 理由:报告要回答的是"能砸盘的筹码占多少",锁在 DEX 池和销毁地址里的份额
# 砸不动,把它们算进分母会把真实抛压系统性稀释(LP 占 56% 时会低估 2.3 倍)。
# burn_pct / dex_pct 本身保持总供应基准 —— 它们定义了流通盘,再用流通盘做分母会循环。
float_share = max(1.0 - burn_pct - dex_pct, 1e-9)
float_raw = 1.0 - burn_pct - dex_pct
float_share = max(float_raw, 1e-9)
# ── 流通盘退化保护 ──────────────────────────────────────────────────────
# DEX 池(或销毁地址)吃掉几乎全部供应时 float_share 趋零,于是每个 `/ float_share`
# 都把尘埃钱包放大成两位数甚至 100%:实测过一个未迁移的盘,LP 占 99.9%,一个只握着
# 约 $2 代币的钱包被判成"最大单钱包持仓 100%,筹码极度集中",评级直接给到 🔴 不建议买。
# 那个 100% 是除零噪声,不是集中度。这类币的正确结论是"此刻无法评估筹码结构"。
# 阈值取 2%:低于这个比例,Top100 里任何一个尘埃钱包都能占到"流通盘"的两位数百分比。
FLOAT_MIN = 0.02
float_degenerate = float_raw < FLOAT_MIN
# ── 空持仓表保护 ────────────────────────────────────────────────────────
# 上游 token holders 会返回 {"list":[]}(币彻底凉了、或索引里已经没有这个盘)。
# 此时每个占比都是 0dangers/warns 一条都不触发,级联会一路落到"🟢 集中度正常,
# 没发现明显砸盘风险"和"✅ 正常参与"—— 对一份零数据的报告给出正面结论,
# 比不给结论危险得多。"没有数据"必须显式说出来。
no_holders = len(normal) == 0
unassessable = float_degenerate or no_holders
# 无法评估时百分比一律不上色 —— "0.00% 🔴" 或 "0.0% 🟢" 读起来像结论,其实只是除零/空集
def pf(f): return "" if unassessable else f
# ── 流通盘换算只走这两个函数 ─────────────────────────────────────────────
# 不要再手写 `/ float_share`。漏掉一次除法就退回总供应基准,数字看上去仍然合理、
# 也不会报错 —— 正是本次迁移要修掉的那类 bug。集中成两个函数后,漏除法就是漏调用,
# 一眼能看出来。
# fs 故意用下标 h['amount_percentage'] 取字段:字段缺失时直接 KeyError 而不是静默算 0。
def fs(ws): return sum(h['amount_percentage'] for h in ws) / float_share # 一组钱包的合计占比
def f1(v): return v / float_share # 单个已取出的占比值
# 集中度只数钱包地址(addr_type==0),排除 DEX 池和销毁地址。
# holders 按 amount_percentage 降序返回,normal 保持该顺序。
top10 = sum(h['amount_percentage'] for h in normal[:10]) / float_share
top20 = sum(h['amount_percentage'] for h in normal[:20]) / float_share
top10 = fs(normal[:10])
top20 = fs(normal[:20])
airdrop = [h for h in normal if h.get('buy_tx_count_cur', 0)==0 and h.get('balance', 0)>0]
bundlers = [h for h in normal if 'bundler' in (h.get('maker_token_tags') or [])]
@@ -129,25 +165,57 @@ rats = [h for h in normal if 'rat_trader' in (h.get('maker_token_tags') or
snipers = [h for h in normal if 'sniper' in (h.get('maker_token_tags') or [])]
fresh = [h for h in normal if 'fresh_wallet' in (h.get('tags') or [])]
wash = [h for h in normal if 'wash_trader' in (h.get('tags') or [])]
risk_all = set(h['address'] for g in [bundlers, rats, snipers, fresh, wash] for h in g)
risk_pct = sum(h['amount_percentage'] for h in normal if h['address'] in risk_all) / float_share
# ── 风险标签的唯一定义处 ────────────────────────────────────────────────
# 去重合计(risk_all)、重叠计数(risk_tag_hits)、§1 的分类明细渲染,三处都从这里派生。
# 之前这五类各自写成三份字面量列表:加第六类标签要改三个地方,漏掉 risk_all 会让
# "合计"少算而明细照常打印,输出自相矛盾且不报错。列表顺序即输出顺序。
RISK_GROUPS = [
(_("老鼠仓", "Rat Trader"), rats, "🚨"),
(_("捆绑交易", "Bundler"), bundlers, "⚠️"),
(_("狙击者", "Sniper"), snipers, "⚠️"),
(_("新钱包", "Fresh"), fresh, ""),
(_("刷量", "Wash"), wash, ""),
]
risk_all = set(h['address'] for _lb, g, _fl in RISK_GROUPS for h in g)
risk_pct = fs(h for h in normal if h['address'] in risk_all)
# 分类明细各自求和会重复计算带多标签的钱包,总数是去重的 —— 输出时要说明差额来源
risk_tag_hits = sum(len(g) for g in [bundlers, rats, snipers, fresh, wash])
risk_tag_hits = sum(len(g) for _lb, g, _fl in RISK_GROUPS)
risk_overlap = risk_tag_hits - len(risk_all)
# 这两个只在 §1 小结的级联里用到,但属于计算而非渲染,放在同类指标旁边
bundler_pct_val = fs(bundlers)
sniper_pct_val = fs(snipers)
airdrop_pct = sum(h['amount_percentage'] for h in airdrop) / float_share
rats_pct = sum(h['amount_percentage'] for h in rats) / float_share
airdrop_pct = fs(airdrop)
rats_pct = fs(rats)
# normal_pct / bad_pct / healthy_pct 保持总供应基准:healthy_ratio 是"看得见的筹码里
# ── 筹码质量分解 ────────────────────────────────────────────────────────
# 三个桶都保持总供应基准,分母是观测到的钱包筹码(normal_pct):这是"看得见的筹码里
# 有多少是干净的"这一质量比值,分母就该是观测到的钱包总量。Top100 之外的长尾干净与否
# 无从得知,换成流通盘做分母会让分散型代币凭空显示成健康度下降。
normal_pct = sum(h['amount_percentage'] for h in normal)
all_bad = set(h['address'] for h in airdrop) | risk_all
bad_pct = sum(h['amount_percentage'] for h in normal if h['address'] in all_bad)
healthy_pct = max(normal_pct - bad_pct, 0)
healthy_ratio = (healthy_pct / normal_pct) if normal_pct > 0 else 0
#
# 旧版把"空降"和"风险标签"并成一个 all_bad 再取补集,压成单个 healthy_ratio。后果是
# 任何空投分发的币都塌成"健康筹码 0.0% 🔴"—— 和同一段里"钻石手持仓 80% ✅、Top10 12%"
# 直接打架,而且抹掉了结构:读者看不出这 0% 是因为有老鼠仓,还是仅仅因为筹码是转账来的。
# 拆开的依据是两者性质不同:风险标签是"这个地址有前科"(已证实的坏),零成本空降是
# "不知道这批筹码怎么来的"(来源未知)。未知不该和已证实的坏共用一个 🔴。
risk_chip_pct = sum(h['amount_percentage'] for h in normal if h['address'] in risk_all)
airdrop_only = [h for h in airdrop if h['address'] not in risk_all] # 与风险标签去重,避免重复计
airdrop_only_pct = sum(h['amount_percentage'] for h in airdrop_only)
normal_pct = sum(h['amount_percentage'] for h in normal)
clean_pct = max(normal_pct - risk_chip_pct - airdrop_only_pct, 0)
clean_ratio = (clean_pct / normal_pct) if normal_pct > 0 else 0
risk_ratio = (risk_chip_pct / normal_pct) if normal_pct > 0 else 0
airdrop_ratio = (airdrop_only_pct / normal_pct) if normal_pct > 0 else 0
# 🔴 只留给"有前科的地址占了大头"这种确定的坏。缺口主要来自零成本空降时给 🟡:
# 那是来源不透明的风险提示,不是"筹码已证实劣质"的结论。
if risk_ratio > 0.30: qf = "🔴"
elif clean_ratio >= 0.50: qf = "🟢"
elif clean_ratio >= 0.30: qf = "🟡"
elif airdrop_ratio >= (1 - clean_ratio) * 0.8: qf = "🟡" # 缺口几乎全是空降
else: qf = "🔴"
# Top100 覆盖了多少流通盘 —— 未覆盖的部分意味着所有流通盘占比都是下限
coverage = min(normal_pct / float_share, 1.0)
coverage = min(f1(normal_pct), 1.0)
# ── 关联资金 ────────────────────────────────────────────────────────────
# 文档要的是"团伙作案"。只看"共用同一个转入地址"会把交易所热钱包判成团伙 ——
@@ -176,14 +244,20 @@ for h in normal:
_cand = [(fa, ws) for fa, ws in from_map.items() if len(ws) >= 3]
same_src_groups = sorted([(fa, ws) for fa, ws in _cand if coherent(ws)], key=lambda x: -len(x[1]))
weak_src_groups = sorted([(fa, ws) for fa, ws in _cand if not coherent(ws)], key=lambda x: -len(x[1]))
same_src_wallets = sum(len(ws) for __, ws in same_src_groups)
weak_src_wallets = sum(len(ws) for __, ws in weak_src_groups)
same_src_pct = sum(h['amount_percentage'] for __, ws in same_src_groups for h in ws) / float_share
weak_src_pct = sum(h['amount_percentage'] for __, ws in weak_src_groups for h in ws) / float_share
# 丢弃项命名为 _src(来源地址)而不是 __ —— 单下划线 `_` 是本文件的 i18n 函数,
# `__` 看起来像与它相关的东西,容易误读
same_src_wallets = sum(len(ws) for _src, ws in same_src_groups)
weak_src_wallets = sum(len(ws) for _src, ws in weak_src_groups)
same_src_pct = fs(h for _src, ws in same_src_groups for h in ws)
weak_src_pct = fs(h for _src, ws in weak_src_groups for h in ws)
# 同步注资改用滑动窗口。原先的固定分桶 (ts//WINDOW)*WINDOW 漏掉跨桶边界的相邻注资:
# 两笔相隔 60 秒但落在不同桶里就检测不到。
funded = sorted([(_tr(h, 'timestamp', 0), h) for h in normal if _tr(h, 'timestamp', 0)])
# key 必须显式取 p[0]:默认的元组比较在 timestamp 相同时会继续比较第二项,
# 也就是拿两个 dict 相比 → TypeError,整个脚本崩掉、一行输出都没有。
# 而"同一秒被批量注资"正是本段要找的信号,时间戳撞车是常态而非边缘情况。
funded = sorted(((_tr(h, 'timestamp', 0), h) for h in normal if _tr(h, 'timestamp', 0)),
key=lambda p: p[0])
win_groups = []
_cur = []
for ts, h in funded:
@@ -193,7 +267,7 @@ for ts, h in funded:
_cur.append((ts, h))
if len(_cur) >= 2: win_groups.append([x[1] for x in _cur])
win_groups.sort(key=lambda v: -len(v))
win_pct = sum(h['amount_percentage'] for v in win_groups for h in v) / float_share
win_pct = fs(h for v in win_groups for h in v)
def _span(v):
ts = [t for t in (_tr(h, 'timestamp', 0) for h in v) if t]
@@ -202,24 +276,45 @@ tight_groups = [v for v in win_groups if _span(v) <= TIGHT]
tight_wallets = sum(len(v) for v in tight_groups)
related = set()
for __, ws in same_src_groups:
for _src, ws in same_src_groups:
for h in ws: related.add(h['address'])
for v in win_groups:
for h in v: related.add(h['address'])
related_pct = sum(h['amount_percentage'] for h in normal if h['address'] in related) / float_share
related_pct = fs(h for h in normal if h['address'] in related)
related_usd = sum(h.get('usd_value',0) for h in normal if h['address'] in related)
smart = [h for h in normal if any(t in (h.get('tags') or []) for t in ['smart_degen','pump_smart'])]
kol = [h for h in normal if 'kol' in (h.get('tags') or []) or 'renowned' in (h.get('tags') or [])]
whales = [h for h in normal if 'whale' in (h.get('maker_token_tags') or [])]
diamond = [h for h in normal if h.get('sell_tx_count_cur',0)==0 and h.get('balance',0)>0]
partial = [h for h in normal if 0<(h.get('sell_amount_percentage') or 0)<0.5]
heavy_sell = [h for h in normal if (h.get('sell_amount_percentage') or 0)>=0.5]
smart_pct = sum(h['amount_percentage'] for h in smart) / float_share
kol_pct = sum(h['amount_percentage'] for h in kol) / float_share
whale_pct = sum(h['amount_percentage'] for h in whales) / float_share
diamond_pct = sum(h['amount_percentage'] for h in diamond) / float_share
# ── 钻石手必须自己买过 ──────────────────────────────────────────────────
# 旧定义只要 sell_tx==0 且 balance>0,于是零成本空降钱包全被算进钻石手:空投分发的币
# 会同时打印"空降筹码 79% 🔴"和"钻石手持仓 80% ✅ 筹码稳定",同一批钱包被数了两次、
# 结论还相反。"钻石手"的含义是扛住了浮亏没卖 —— 没花钱买入的地址无所谓扛,
# 它不卖可能只是私钥在分发方手里。所以要求 buy_tx>0。
# 空降且未动的那批不丢弃,单独列成 idle_airdrop:它们仍是随时可能出货的零成本筹码。
diamond = [h for h in normal if (h.get('buy_tx_count_cur') or 0)>0
and (h.get('sell_tx_count_cur') or 0)==0
and (h.get('balance') or 0)>0]
idle_airdrop = [h for h in normal if (h.get('buy_tx_count_cur') or 0)==0
and (h.get('sell_tx_count_cur') or 0)==0
and (h.get('balance') or 0)>0]
smart_pct = fs(smart)
kol_pct = fs(kol)
whale_pct = fs(whales)
diamond_pct = fs(diamond)
idle_airdrop_pct = fs(idle_airdrop)
# §4 小结的判据。原先夹在 §4 的两条 print 之间 —— 本文件的约定是"计算全在渲染之前",
# 放回这里让那条边界重新成立
sig_count = len(smart) + len(kol) + len(whales)
kol_selling = [h for h in kol if (h.get('sell_tx_count_cur') or 0) > 0]
kol_holding = [h for h in kol if (h.get('sell_tx_count_cur') or 0) == 0 and (h.get('balance') or 0) >= 1]
smart_selling = [h for h in smart if (h.get('sell_tx_count_cur') or 0) > 0]
smart_holding = [h for h in smart if (h.get('sell_tx_count_cur') or 0) == 0 and (h.get('balance') or 0) >= 1]
top100_map = {h['address']: h for h in holders}
creator = next((d for d in devs if 'creator' in (d.get('maker_token_tags') or [])), None)
@@ -237,10 +332,10 @@ profit_w = [h for h in normal if (h.get('profit') or 0)>0]
loss_w = [h for h in normal if (h.get('profit') or 0)<0]
# 文档问"持有者大部分盈利还是亏损",钱包个数是字面答案;但抛压看的是筹码权重 ——
# 3 个握着 40% 流通盘的盈利钱包,威胁远大于 60 个尘埃钱包。两者都给出。
profit_pct = sum(h['amount_percentage'] for h in profit_w) / float_share
loss_pct = sum(h['amount_percentage'] for h in loss_w) / float_share
profit_pct = fs(profit_w)
loss_pct = fs(loss_w)
trapped = [h for h in normal if (h.get('unrealized_pnl') or 0)<-0.2 and h.get('balance',0)>0]
trapped_pct = sum(h['amount_percentage'] for h in trapped) / float_share
trapped_pct = fs(trapped)
# 只列已确认精度的链。arc / stable / robinhood 的原生精度未确认,套 1e18 会把余额
# 算成 0.000,读起来像"这些钱包没有 gas"—— 错的数字比没有数字更危险。
@@ -268,11 +363,10 @@ else:
zero_wallets = [h for h in normal if native_amt(h) <= 0]
low_wallets, mid_wallets = [], []
high_wallets = [h for h in normal if native_amt(h) > 0]
def _fs(ws): return sum(h['amount_percentage'] for h in ws) / float_share
zero_pct_val = _fs(zero_wallets)
low_pct_val = _fs(low_wallets)
mid_pct_val = _fs(mid_wallets)
high_pct_val = _fs(high_wallets)
zero_pct_val = fs(zero_wallets)
low_pct_val = fs(low_wallets)
mid_pct_val = fs(mid_wallets)
high_pct_val = fs(high_wallets)
high_total = sum(native_usd(h) for h in high_wallets)
high_native = sum(native_amt(h) for h in high_wallets)
total_buying_power = sum(native_usd(h) for h in normal)
@@ -307,6 +401,14 @@ def holding_status(h):
if sp > 0: return _("少量出货", "Light Selling")
return _("持仓未动", "Holding")
# §5 要数"这批钱包里有几个在出货"。原先的写法是把 holding_status(h) 的返回值
# 和两条本地化文案做字符串比对 —— 那是给人看的显示文本:改一个 emoji 或改一个词,
# 这里就会静默返回空列表,sell_ratio 变 0,每个批次都被降级成 🟢,而且不报任何错。
# 改成直接复用 holding_status 的同一组阈值:余额>0 且已卖出量≥30%,
# 等价于原来的 {🔴 大量出货, 🟡 出货中} 两个分支。
def is_distributing(h):
return (h.get('balance', 0) or 0) > 0 and (h.get('sell_amount_percentage', 0) or 0) >= 0.3
def wallet_behavior(h):
buy_tx = h.get('buy_tx_count_cur', 0) or 0
sell_tx = h.get('sell_tx_count_cur', 0) or 0
@@ -339,20 +441,22 @@ def trend_str(wlist):
if idle: parts.append(f"{_('无交易', 'Idle')} {len(idle)}")
return " ".join(parts) if parts else ""
def is_active(h): return (h.get('buy_tx_count_cur') or 0)+(h.get('sell_tx_count_cur') or 0)>0
def is_selling(h): return (h.get('sell_tx_count_cur') or 0)>0 and (h.get('balance') or 0)>=1
def is_buying_only(h): return (h.get('buy_tx_count_cur') or 0)>0 and (h.get('sell_tx_count_cur') or 0)==0
biggest = max(normal, key=lambda h: h['amount_percentage']) if normal else None
biggest_pct = (biggest['amount_percentage'] / float_share) if biggest else 0
biggest_pct = f1(biggest['amount_percentage']) if biggest else 0
# 以下所有阈值都已按流通盘基准重新校准(旧值是总供应基准,直接沿用会让每条都亮红)
# 流通盘退化时这些阈值判的全是除零噪声(详见 float_degenerate),一条都不能计入评级 ——
# 不依赖百分比的判据(Dev 马甲)不受影响,照常检查。
dangers = []
if rats and rats_pct > 0.05:
dangers.append(_( f"老鼠仓持仓 {pct(rats_pct):.1f}%,出货即砸盘",
f"Rat traders hold {pct(rats_pct):.1f}% — instant dump risk"))
if biggest_pct > 0.10:
dangers.append(_( f"最大单钱包持仓 {pct(biggest_pct):.1f}%,筹码极度集中",
f"Largest wallet holds {pct(biggest_pct):.1f}% — extreme concentration"))
if not float_degenerate:
if rats and rats_pct > 0.05:
dangers.append(_( f"老鼠仓持仓 {pct(rats_pct):.1f}%,出货即砸盘",
f"Rat traders hold {pct(rats_pct):.1f}% — instant dump risk"))
if biggest_pct > 0.10:
dangers.append(_( f"最大单钱包持仓 {pct(biggest_pct):.1f}%,筹码极度集中",
f"Largest wallet holds {pct(biggest_pct):.1f}% — extreme concentration"))
if creator:
to_out = creator.get('token_transfer_out') or {}
if (to_out.get('address') or '') in top100_map:
@@ -360,22 +464,36 @@ if creator:
"Dev transferred chips to internal wallet — covert control"))
warns = []
if dev_holding:
hold_pct_val = sum(d.get('amount_percentage',0) for d in dev_holding) / float_share
if hold_pct_val > 0.01:
warns.append(_( f"Dev 仍持仓 {pct(hold_pct_val):.2f}%",
f"Dev still holds {pct(hold_pct_val):.2f}%"))
if airdrop_pct > 0.2:
warns.append(_( f"空降筹码 {pct(airdrop_pct):.1f}%,来源不透明",
f"Airdrop supply {pct(airdrop_pct):.1f}% — opaque origin"))
if risk_pct > 0.35:
warns.append(_( f"风险钱包持仓 {pct(risk_pct):.1f}%,筹码质量差",
f"Risk wallets hold {pct(risk_pct):.1f}% — low chip quality"))
if related_pct > 0.15:
warns.append(_( f"关联钱包 {len(related)} 个持仓 {pct(related_pct):.1f}%",
f"Linked wallets ({len(related)}) hold {pct(related_pct):.1f}%"))
if no_holders:
warns.append(_( "上游未返回任何持仓地址,筹码结构无法评估(本报告所有占比均为空集,不是 0%",
"Upstream returned no holder addresses — chip structure not assessable (every percentage here is an empty set, not a real 0%)"))
elif float_degenerate:
# 唯一还成立的结论就是"这盘子还没放开,没法评"。给出的是绝对值而不是百分比。
warns.append(_( f"流通盘仅占总供应 {pct_s(float_raw)}DEX {pct(dex_pct):.1f}% + 销毁 {pct(burn_pct):.1f}%),筹码结构无法评估",
f"Float is only {pct_s(float_raw)} of supply (DEX {pct(dex_pct):.1f}% + burn {pct(burn_pct):.1f}%) — chip structure not assessable"))
else:
if dev_holding:
# dev 来自另一个 endpoint,字段可能缺失,所以先用 .get 求和再交给 f1 换算,
# 不走 fsfs 用下标取字段)
hold_pct_val = f1(sum(d.get('amount_percentage',0) for d in dev_holding))
if hold_pct_val > 0.01:
warns.append(_( f"Dev 仍持仓 {pct(hold_pct_val):.2f}%",
f"Dev still holds {pct(hold_pct_val):.2f}%"))
if airdrop_pct > 0.2:
warns.append(_( f"空降筹码 {pct(airdrop_pct):.1f}%,来源不透明",
f"Airdrop supply {pct(airdrop_pct):.1f}% — opaque origin"))
if risk_pct > 0.35:
warns.append(_( f"风险钱包持仓 {pct(risk_pct):.1f}%,筹码质量差",
f"Risk wallets hold {pct(risk_pct):.1f}% — low chip quality"))
if related_pct > 0.15:
warns.append(_( f"关联钱包 {len(related)} 个持仓 {pct(related_pct):.1f}%",
f"Linked wallets ({len(related)}) hold {pct(related_pct):.1f}%"))
if dangers:
if unassessable and not dangers:
# Dev 马甲那条(唯一不依赖百分比的 danger)若成立就照常给 🔴;否则不下结论。
# 退化 / 空数据时"没发现问题"和"评级正常"是两件事,不能让它落到 ✅ 正常参与。
rating_em, rating_text = "", _("无法评估", "Cannot Assess")
elif dangers:
rating_em, rating_text = "🔴", _("不建议买", "Not Recommended")
elif len(warns) >= 2:
rating_em, rating_text = "⚠️", _("谨慎参与", "Caution")
@@ -394,9 +512,10 @@ if whales:
goods.append(_( f"鲸鱼 {len(buying_w)} 个持续买入,尚未出货",
f"{len(buying_w)} whale(s) still accumulating, not sold"))
if kol:
goods.append(_( f"KOL {len(kol)} 个在场({pct(kol_pct):.2f}%",
f"{len(kol)} KOL(s) holding ({pct(kol_pct):.2f}%)"))
if diamond_pct > 0.5:
# 钱包个数是实打实的;退化时占比是噪声,就只报个数
goods.append(_( f"KOL {len(kol)} 个在场" + ("" if float_degenerate else f"{pct(kol_pct):.2f}%"),
f"{len(kol)} KOL(s) holding" + ("" if float_degenerate else f" ({pct(kol_pct):.2f}%)")))
if diamond_pct > 0.5 and not float_degenerate:
goods.append(_( f"钻石手持仓 {pct(diamond_pct):.1f}%,筹码稳定",
f"Diamond hands hold {pct(diamond_pct):.1f}% — stable chips"))
@@ -464,42 +583,72 @@ print(f"│{(' MC '+usd(cur_mc)):^56}│")
print(f"{''*56}")
print()
if no_holders:
# 空数据也必须在所有 0.00% 之前说清楚,否则整份报告读起来像"干净得没有任何风险"
print(f" ⚠️ {_('上游未返回任何持仓地址 —— 筹码结构无法评估', 'Upstream returned no holder addresses — chip structure not assessable')}")
print(_( " 下方所有占比与计数都是空集的渲染结果,不是“该项为 0”;评级已置为“无法评估”。",
" Every percentage and count below renders an empty set, not a measured zero. Rating is set to “Cannot Assess”."))
print(_( " 常见原因:代币已无活跃持仓、或上游索引里已不再收录该盘。可稍后重试确认。",
" Usual causes: the token has no active holders left, or upstream no longer indexes it. Retry later to confirm."))
print()
if float_degenerate:
# 退化时这行必须在所有百分比之前出现,否则读者会先把 100% 当成集中度结论。
# 同时给出绝对值(代币数 + 美元),这是此刻唯一有意义的量级。
_fl_tok = total_supply * max(float_raw, 0)
_fl_usd = _fl_tok * cur_price
_fl_usd_s = "<$1" if 0 < _fl_usd < 1 else usd(_fl_usd)
print(f" ⚠️ {_('流通盘退化 —— 筹码结构此刻无法评估', 'Degenerate float — chip structure not assessable right now')}")
print(_( f" DEX 池 {pct(dex_pct):.1f}% + 销毁 {pct(burn_pct):.1f}% 占掉几乎全部供应,可流通部分只剩 {pct_s(float_raw)}",
f" DEX {pct(dex_pct):.1f}% + burn {pct(burn_pct):.1f}% hold nearly all supply; only {pct_s(float_raw)} is tradeable"))
print(_( f" (通常是还没迁移的 launchpad 盘)实际可流通约 {fmt_amt(_fl_tok)} 个代币 ≈ {_fl_usd_s}",
f" (typically a launchpad token pre-migration) tradeable ≈ {fmt_amt(_fl_tok)} tokens ≈ {_fl_usd_s}"))
print(_( " 下方“占流通盘”的百分比分母趋零,会把尘埃钱包放大成两位数甚至 100%",
" Float percentages below divide by a near-zero denominator, inflating dust wallets to double digits or 100%,"))
print(_( " 不能当作集中度结论;评级已置为“无法评估”,颜色标记一律显示 ⚪。",
" so they are not concentration findings. Rating is set to “Cannot Assess” and flags show ⚪."))
print()
sec1 = _("🚨 砸盘风险", "🚨 Dump Risk")
print(f"━━ {sec1} {''*(54-len(sec1))}")
print()
c10f = "🔴" if top10>0.6 else ("🟡" if top10>0.4 else "🟢")
c20f = "🔴" if top20>0.75 else ("🟡" if top20>0.55 else "🟢")
c10f = pf("🔴" if top10>0.6 else ("🟡" if top10>0.4 else "🟢"))
c20f = pf("🔴" if top20>0.75 else ("🟡" if top20>0.55 else "🟢"))
print(f" {_('集中度(占流通盘,已剔除 LP 与销毁)', 'Concentration (of tradeable float, LP + burn excluded)')}")
print(f" Top10 {pct(top10):.1f}% {c10f} Top20 {pct(top20):.1f}% {c20f}")
print()
if burn:
print(f" 🔥 {_('销毁地址', 'Burn addr')} {pct(burn_pct):.2f}% ✅ {_('永久锁仓,无法流通', 'Permanently locked, non-circulating')}")
print()
airf = "🔴" if airdrop_pct>0.25 else ("🟡" if airdrop_pct>0.1 else "🟢")
airf = pf("🔴" if airdrop_pct>0.25 else ("🟡" if airdrop_pct>0.1 else "🟢"))
print(f" {_('空降筹码(从未买入、靠转账获得)', 'Airdrop (never bought, received via transfer)')} {len(airdrop)} {_('个钱包', 'wallets')} {_('持仓', 'hold')} {pct(airdrop_pct):.2f}% {airf}")
print()
riskf = "🔴" if risk_pct>0.35 else ("🟡" if risk_pct>0.15 else "🟢")
riskf = pf("🔴" if risk_pct>0.35 else ("🟡" if risk_pct>0.15 else "🟢"))
print(f" {_('风险钱包', 'Risk wallets')} {_('合计', 'total')} {len(risk_all)} {_('', '')} {_('持仓', 'hold')} {pct(risk_pct):.2f}% {riskf}")
risk_labels = [
(_("老鼠仓", "Rat Trader"), rats, "🚨"),
(_("捆绑交易", "Bundler"), bundlers, "⚠️"),
(_("狙击者", "Sniper"), snipers, "⚠️"),
(_("新钱包", "Fresh"), fresh, ""),
(_("刷量", "Wash"), wash, ""),
]
for label, group, flag in risk_labels:
for label, group, flag in RISK_GROUPS:
if group:
gp = pct(sum(h['amount_percentage'] for h in group) / float_share)
gp = pct(fs(group))
print(f" · {label:10s} {len(group):2d} {_('', '')} {_('持仓', 'hold')} {gp:.2f}% {flag}")
if not any([rats,bundlers,snipers,fresh,wash]):
print(f" · {_('未发现风险标签钱包', 'No risk-tagged wallets found')} 🟢")
if not any(g for _lb, g, _fl in RISK_GROUPS):
# 空持仓表时"未发现"只是没数据可查,不能打成 🟢
if no_holders:
print(f" · {_('没有钱包可供检查', 'No wallets available to check')}")
else:
print(f" · {_('未发现风险标签钱包', 'No risk-tagged wallets found')} 🟢")
if risk_overlap > 0:
# 分类明细相加会大于"合计" —— 说明差额来自多标签钱包,而不是算错了
print(f" · {_(f'合计已去重({risk_overlap} 个钱包带多个风险标签)', f'Total is deduped ({risk_overlap} wallet(s) carry multiple risk tags)')}")
print()
bundler_pct_val = sum(h['amount_percentage'] for h in bundlers) / float_share
sniper_pct_val = sum(h['amount_percentage'] for h in snipers) / float_share
if dangers:
if no_holders:
# 级联最后一档是"🟢 没发现明显砸盘风险"。空持仓表会一路落到那里,
# 把"没有数据"渲染成"没有风险" —— 必须在入口就拦掉。
sdp_summary = _( "⚪ 没有任何持仓数据,砸盘风险无法判断(不是“无风险”)",
"⚪ No holder data at all — dump risk cannot be judged (this is not \"no risk\")")
elif float_degenerate:
# 这段级联里每一条判据都是流通盘占比,退化时全是噪声 —— 只能重复"没法评"
sdp_summary = _( f"⚪ 流通盘只剩 {pct_s(float_raw)},上面的占比都是除零放大的结果,砸盘风险无法判断",
f"⚪ Only {pct_s(float_raw)} of supply is tradeable — the percentages above are divide-by-zero artifacts; dump risk cannot be judged")
elif dangers:
sdp_summary = f"🔴 {dangers[0]}"
elif rats_pct > 0.03:
sdp_summary = _( f"🔴 老鼠仓持仓 {pct(rats_pct):.1f}%,零成本拿的,随时可以无损砸盘",
@@ -532,7 +681,7 @@ print(f" {_('浮盈越高 / 建仓MC越低 → 获利了结压力越强', 'High
print()
for i, h in enumerate(top5_holders, 1):
role_str, display_id, cost_str, pnl_str, lv, note, beh_str, st = top5_pressure(h)
hp = pct(h['amount_percentage'] / float_share)
hp = pct(f1(h['amount_percentage']))
print(f" {i}. {role_str}{display_id}")
print(f" {_('持仓', 'hold')} {hp:.2f}% {cost_str} {_('盈亏', 'pnl')} {pnl_str}")
print(f" {_('抛压', 'pressure')} {lv}{note}")
@@ -560,7 +709,7 @@ if creator:
tf_out = creator.get('history_transfer_out_amount') or 0
tf_val = creator.get('history_transfer_out_income') or 0
sell_amt = creator.get('sell_amount_cur') or 0
hold_pct_c = (creator.get('amount_percentage') or 0) / float_share
hold_pct_c = f1(creator.get('amount_percentage') or 0)
c_status = (_("余额归零", "Balance zero") if (creator.get('balance') or 0)<1
else _(f"⚠️ 持仓 {pct(hold_pct_c):.2f}%", f"⚠️ Holding {pct(hold_pct_c):.2f}%"))
print(f" {_('主号 (Creator)', 'Main (Creator)')} {addr_short(creator['address'])}")
@@ -584,7 +733,7 @@ if creator:
target = top100_map[to_addr]
t_mtags = [t for t in (target.get('maker_token_tags') or []) if t not in ('top_holder','transfer_in')]
print(f"\n ⚠️ {_('转出筹码仍在 Top100(换马甲继续持有):', 'Transferred chips still in Top100 (sock puppet):')}")
print(f" {addr_short(to_addr)} {_('持仓', 'hold')} {pct((target.get('amount_percentage') or 0)/float_share):.2f}% {_('标签', 'tags')}: {' '.join(t_mtags) or _('','none')}")
print(f" {addr_short(to_addr)} {_('持仓', 'hold')} {pct(f1(target.get('amount_percentage') or 0)):.2f}% {_('标签', 'tags')}: {' '.join(t_mtags) or _('','none')}")
elif (creator.get('balance') or 0)<1 and tf_out==0:
print(f"{_('已完全卖出,无异常转账记录', 'Fully sold, no abnormal transfers')}")
print()
@@ -616,9 +765,19 @@ if creator:
print(f" {i}. {t.get('symbol','?')} {usd(float(t.get('market_cap') or 0))} [{mig_label}]")
ath_info = created_data.get('creator_ath_info') or {}
if ath_info and ath_info.get('ath_mc'):
is_curr = ath_info.get('ath_token','').lower()==TOKEN_ADDR.lower()
ath_mc = float(ath_info.get('ath_mc') or 0)
is_curr = ath_info.get('ath_token','').lower()==TOKEN_ADDR.lower()
curr_label = _('(本币)', ' (this token)') if is_curr else ''
print(f" {_('历史最高市值', 'All-time high MC')}: {ath_info.get('token_name','')}({ath_info.get('token_symbol','?')}){curr_label} ATH {usd(float(ath_info.get('ath_mc') or 0))}")
# 历史最高市值低于当前市值在数学上不可能(本币此刻的市值本身就是它的历史候选),
# 说明上游 ath 字段还没跟上这轮拉升 —— 实测 token info 的 ath_price 等于
# price_24h,一个 24 小时涨了上百倍的币,ATH 就会停在一天前的价位。
# 照原样打印会被当成真实峰值,进而拿去算"距离 ATH 还有多少空间"。留 5% 容差,
# 因为 cur_mc 自己是 median(usd_value/balance) × median(supply) 的估算值。
stale_ath = ath_mc > 0 and cur_mc > 0 and ath_mc < cur_mc * 0.95
ath_note = "" if not stale_ath else _(
f" ⚠️ 低于当前市值 {usd(cur_mc)},上游 ATH 数据滞后,不可作为峰值参考",
f" ⚠️ below current MC {usd(cur_mc)} — upstream ATH is stale, not a usable peak")
print(f" {_('历史最高市值', 'All-time high MC')}: {ath_info.get('token_name','')}({ath_info.get('token_symbol','?')}){curr_label} ATH {usd(ath_mc)}{ath_note}")
print()
sec3 = _("🔗 关联资金", "🔗 Related Funds")
@@ -627,9 +786,10 @@ print()
print(f" {_('多个钱包来自同一资金来源地址,或在极短时间内同步注资', 'Multiple wallets from same funding source or funded in tight time windows')}")
print()
if related:
relf = "🔴" if related_pct>0.25 else ("🟡" if related_pct>0.1 else "🟢")
# 秒级批量注资本身就是异常信号,不该因为占比小而在标题上显示为绿
if tight_groups and relf == "🟢": relf = "🟡"
relf = pf("🔴" if related_pct>0.25 else ("🟡" if related_pct>0.1 else "🟢"))
# 秒级批量注资本身就是异常信号,不该因为占比小而在标题上显示为绿。判据是钱包数与
# 时间跨度,不含流通盘占比,所以流通盘退化时(relf 为 ⚪)这条升级依然有效。
if tight_groups and relf in ("🟢", ""): relf = "🟡"
print(f" {_('涉及', 'Involves')} {len(related)} {_('个钱包', 'wallets')} {_('持仓', 'hold')} {pct(related_pct):.2f}% {usd(related_usd)} {relf}")
print()
print(f" ├─ {_('强关联(同一来源 + 时间集中或金额一致)', 'Strong (same source + tight timing or uniform amounts)')} {len(same_src_groups)} {_('', 'groups')} / {same_src_wallets} {_('个钱包', 'wallets')} {_('持仓', 'hold')} {pct(same_src_pct):.2f}%")
@@ -653,13 +813,16 @@ if related:
# 只有一个批次时,明细就是上面那行,不重复打印
if len(win_groups) > 1:
v = win_groups[0]
print(f" {_('最大批次', 'Largest batch')}: {len(v)} {_('个钱包', 'wallets')} {_('合计持仓', 'total hold')} {pct(sum(h['amount_percentage'] for h in v)/float_share):.3f}% {_('时间跨度', 'span')} {_span(v)}s")
print(f" {_('最大批次', 'Largest batch')}: {len(v)} {_('个钱包', 'wallets')} {_('合计持仓', 'total hold')} {pct(fs(v)):.3f}% {_('时间跨度', 'span')} {_span(v)}s")
else:
print(f" {_('时间跨度', 'Span')} {_span(win_groups[0])}s")
else:
print(f" └─ {_('未发现同步集中注资', 'No coordinated funding detected')}")
else:
print(f" {_('未发现明显关联资金', 'No significant linked funds detected')} 🟢")
if no_holders:
print(f" {_('没有钱包可供检查资金来源', 'No wallets available to trace funding')}")
else:
print(f" {_('未发现明显关联资金', 'No significant linked funds detected')} 🟢")
if weak_src_groups:
print(f" ({_(f'{weak_src_wallets} 个钱包共用转入地址,但时间和金额都不一致,判为交易所出金而非团伙', f'{weak_src_wallets} wallets share a funding address but differ in timing and amount — read as CEX withdrawals, not a gang')})")
print()
@@ -673,18 +836,16 @@ print(f" KOL {len(kol):2d} {_('持仓', 'hold')} {pct(kol_pct):.2f}%
if kol:
for h in kol:
name = h.get('twitter_name') or h.get('name') or addr_short(h['address'])
print(f" · {name} {_('持仓', 'hold')} {pct(h['amount_percentage']/float_share):.2f}% {holding_status(h)} {_('买/卖', 'buy/sell')}: {h.get('buy_tx_count_cur',0)}/{h.get('sell_tx_count_cur',0)}")
print(f" · {name} {_('持仓', 'hold')} {pct(f1(h['amount_percentage'])):.2f}% {holding_status(h)} {_('买/卖', 'buy/sell')}: {h.get('buy_tx_count_cur',0)}/{h.get('sell_tx_count_cur',0)}")
print(f" {_('鲸鱼', 'Whale')} {len(whales):2d} {_('持仓', 'hold')} {pct(whale_pct):.2f}% {'' if whales else ''}")
if whales: print(f" {_('近期动向', 'Recent')}: {trend_str(whales)}")
print()
df = "" if diamond_pct>0.6 else ("🟡" if diamond_pct>0.35 else "⚠️")
print(f" {_('钻石手(从未卖出)', 'Diamond hands (never sold)')} {len(diamond):2d} {_('持仓', 'hold')} {pct(diamond_pct):.1f}% {df}")
df = pf("" if diamond_pct>0.6 else ("🟡" if diamond_pct>0.35 else "⚠️"))
print(f" {_('钻石手(买入过且从未卖出)', 'Diamond hands (bought & never sold)')} {len(diamond):2d} {_('持仓', 'hold')} {pct(diamond_pct):.1f}% {df}")
if idle_airdrop:
# 单独一行,避免读者把这批零成本筹码当成"扛住了没卖"的钻石手
print(f" {_('空降未动(从未买入也未卖出)', 'Idle airdrop (never bought, never sold)')} {len(idle_airdrop):2d} {_('持仓', 'hold')} {pct(idle_airdrop_pct):.1f}% {_('零成本,不计入钻石手', 'zero cost — not counted as diamond hands')}")
print(f" {_('部分卖出(<50%)', 'Partial sell (<50%)')} {len(partial):2d} {_('大量卖出(≥50%)', 'Heavy sell (≥50%)')} {len(heavy_sell):2d}")
sig_count = len(smart) + len(kol) + len(whales)
kol_selling = [h for h in kol if (h.get('sell_tx_count_cur') or 0) > 0]
kol_holding = [h for h in kol if (h.get('sell_tx_count_cur') or 0) == 0 and (h.get('balance') or 0) >= 1]
smart_selling = [h for h in smart if (h.get('sell_tx_count_cur') or 0) > 0]
smart_holding = [h for h in smart if (h.get('sell_tx_count_cur') or 0) == 0 and (h.get('balance') or 0) >= 1]
if sig_count == 0:
sig_summary = _("🟡 没有聪明钱和KOL,这个币没什么外部背书",
"🟡 No smart money or KOL — no external endorsement")
@@ -722,10 +883,9 @@ sig_clusters = sorted([(age,ws) for age,ws in time_clusters.items() if len(ws)>=
for rank, (age, ws) in enumerate(sig_clusters, 1):
entry_ts = token_launch + age*86400
label = age_label(entry_ts)
total_hp = sum(h['amount_percentage'] for h in ws) / float_share
total_hp = fs(ws)
costed = [h for h in ws if (h.get('avg_cost') or 0)>0]
selling_out = [h for h in ws if holding_status(h) in (
_('🔴 大量出货','🔴 Heavy Selling'), _('🟡 出货中','🟡 Selling'))]
selling_out = [h for h in ws if is_distributing(h)]
still_hold = [h for h in ws if (h.get('balance') or 0)>=1]
if costed:
avg_entry = sum(total_supply*h['avg_cost'] for h in costed)/len(costed)
@@ -827,18 +987,55 @@ if warns:
if goods:
print(f" {_('积极因素', 'Positives')}:")
for g in goods: print(f" · {g}")
hf = "🔴" if healthy_ratio<0.3 else ("🟡" if healthy_ratio<0.5 else "🟢")
print(f"\n {_('健康筹码', 'Healthy chips')} {pct(healthy_ratio):.1f}% {hf} ({_('占观测到的钱包筹码', 'share of observed wallet chips')})")
# 三个桶互斥、相加为 100%,读者能看出缺口是"有前科"还是"来源未知"造成的
if no_holders:
# 三个桶全是 0.0% 时打印它们,等于宣称"筹码里既没有风险标签也没有空降"
print(f"\n {_('筹码质量', 'Chip quality')}"
f"{_('没有观测到任何钱包,无法计算', 'no wallets observed — cannot compute')}")
else:
# 三个桶的分母是 normal_pct(总供应基准),不受流通盘退化影响,所以构成照常打印;
# 但退化盘里被观测到的筹码本身只有尘埃级,对着尘埃下 🔴/🟢 的结论没有意义 ——
# 评级已经是"无法评估",这里再亮一个红灯只会自相矛盾。故只中和旗标,并注明筹码体量。
_qf_disp = pf(qf)
_qf_note = "" if not float_degenerate else _(
f",仅占总供应 {pct_s(normal_pct)}", f", only {pct_s(normal_pct)} of supply")
print(f"\n {_('筹码质量', 'Chip quality')} {_qf_disp} "
f"({_('占观测到的钱包筹码', 'share of observed wallet chips')}{_qf_note})")
print(f" {_('自己买入且无风险标签', 'Bought in, no risk tag')} {pct(clean_ratio):.1f}%"
f" {_('零成本空降', 'Zero-cost airdrop')} {pct(airdrop_ratio):.1f}%"
f" {_('风险标签', 'Risk-tagged')} {pct(risk_ratio):.1f}%")
if clean_ratio < 0.5 and airdrop_ratio > risk_ratio:
print(_( " (缺口主要来自零成本空降:来源未知,不等于已证实的坏筹码)",
" (Shortfall is mostly zero-cost airdrop — unknown provenance, not proven-bad chips)"))
# 退化时 .1f 会把 0.0035% 打成 0.0%,和"未发现 LP"的 0 混成一个数
_fs_disp = pct_s(float_raw) if float_degenerate else f"{pct(float_share):.1f}%"
# coverage = normal_pct / float_share,退化时它同样是除零结果(恒等于 100%),不能报
_cov_disp = "" if unassessable else _(f" Top100 覆盖流通盘 {pct(coverage):.0f}%",
f" Top100 covers {pct(coverage):.0f}% of float")
if burn_pct + dex_pct > 0.001:
print(_( f" 流通盘 {pct(float_share):.1f}%(销毁 {pct(burn_pct):.1f}% + DEX {pct(dex_pct):.1f}% 已剔除,不纳入评估) Top100 覆盖流通盘 {pct(coverage):.0f}%",
f" Float {pct(float_share):.1f}% (burn {pct(burn_pct):.1f}% + DEX {pct(dex_pct):.1f}% excluded) Top100 covers {pct(coverage):.0f}% of float"))
print(_( f" 流通盘 {_fs_disp}(销毁 {pct(burn_pct):.1f}% + DEX {pct(dex_pct):.1f}% 已剔除,不纳入评估){_cov_disp}",
f" Float {_fs_disp} (burn {pct(burn_pct):.1f}% + DEX {pct(dex_pct):.1f}% excluded){_cov_disp}"))
else:
# 没有销毁也没有 LP 行,就别写"已剔除 0.0%"
print(_( f" 流通盘 100%(未发现销毁地址与 DEX 池) Top100 覆盖流通盘 {pct(coverage):.0f}%",
f" Float 100% (no burn address or DEX pool found) Top100 covers {pct(coverage):.0f}% of float"))
print(_( " 以上持仓占比均以流通盘为分母;Top100 覆盖的部分意味着这些占比是下限",
" Percentages above are share of tradeable float; they are floors — Top100 does not cover the entire float"))
if coverage < 0.5:
print(_( f" 流通盘 100%(未发现销毁地址与 DEX 池){_cov_disp}",
f" Float 100% (no burn address or DEX pool found){_cov_disp}"))
# 这条脚注必须跟着实际覆盖率走。原先无条件打印,于是在 Top100 覆盖 100% 流通盘的币上,
# 它和上一行的"Top100 覆盖流通盘 100%"当场自相矛盾 —— 一行说覆盖满了,下一行说没覆盖满。
if no_holders:
# coverage = 0 会让下面那条打成"Top100 覆盖 0%,所以是下限"—— 下限的说法预设了有数据
print(_( " 以上占比没有任何持仓数据支撑,既不是下限也不是完整值,请勿据此比较",
" The percentages above rest on no holder data at all — they are neither floors nor complete values; do not compare on them"))
elif float_degenerate:
# 退化时 coverage 本身也是 f1() 的产物,同样是噪声,两种说法都不能给
print(_( " 以上持仓占比的分母(流通盘)趋零,数值不可用于横向比较,请以本节顶部的绝对值为准",
" The float denominator above is near zero, so these percentages are not comparable — use the absolute figures at the top of this section"))
elif coverage < 0.995:
print(_( f" 以上持仓占比均以流通盘为分母;Top100 只覆盖其中 {pct(coverage):.0f}%,未覆盖的部分意味着这些占比是下限",
f" Percentages above are share of tradeable float; Top100 covers {pct(coverage):.0f}% of it, so they are floors"))
else:
print(_( " 以上持仓占比均以流通盘为分母;Top100 已覆盖全部流通盘,占比即完整值而非下限",
" Percentages above are share of tradeable float; Top100 covers all of it, so they are complete values, not floors"))
if coverage < 0.5 and not unassessable:
# 覆盖率低时,"占比小"只说明 Top100 之外还有很多筹码,不能读成利好
print(_( f" ⚠️ Top100 只覆盖了流通盘的 {pct(coverage):.0f}%,占比偏低的指标属于未知而非利好",
f" ⚠️ Top100 covers only {pct(coverage):.0f}% of float — low percentages mean unknown, not good"))