fix(holder-analysis): a missing buy count was read as proof of zero-cost chips

`buy_tx_count_cur == 0` was taken to mean "these chips were transferred or
airdropped in, the holder paid nothing". Measured data refutes that inference.

On musebook (robinhood) 38 wallets read `buy_tx_count_cur: 0` and
`avg_cost: null`, but 34 of them carry a finite `unrealized_pnl` ratio — +2.7%,
+76.5%, +546% — and a chip with no cost cannot produce a finite PnL ratio; a
+2.7% ratio is only possible against a cost basis near the current price. 32 of
the 38 also carry a `fomo` frontend tag and 2 a `gmgn` tag, which is the trace of
active trading, the opposite of receiving an airdrop. What is actually missing is
buy-transaction indexing on that chain, while the PnL fields come from a separate
computation and arrive populated.

The error was systematic per chain rather than per token. Never-bought share of
supply across one batch: robinhood 27.0% and 41.2%, arc 26.3%, sol 4.8%. The 20%
airdrop warn gate therefore fired on essentially every robinhood and arc token and
essentially never on a sol token — a difference in index coverage, not in chip
structure. Reporting an unpopulated field as a risk conclusion is what this skill
explicitly forbids elsewhere.

So zero cost now requires positive evidence of no cost:

    def has_cost_basis(h):
        return (h.get('avg_cost') or 0) > 0 or h.get('unrealized_pnl') is not None

Six consumers of the old predicate move to it, all the same root cause:

  - the `airdrop` cohort and its >20% warn gate, plus the chip-quality buckets and
    the exit-signal line that read it
  - `diamond`, whose own stated rationale is "a wallet that never paid has no cost
    to hold through" — a proven cost basis satisfies that, and requiring an indexed
    buy count discarded most real diamond hands (JOLLY: 34 → 75 wallets,
    21.1% → 55.7% of float)
  - `idle_airdrop`, whose "zero cost" caption was a false statement for those rows
  - Top5 sell-risk classification, which labelled a wallet sitting at +6.6x as
    "zero-cost airdrop — can dump anytime" and simultaneously dropped it from the
    trapped and in-profit counts

Wallets with no indexed buy count but a real cost basis are not dropped from the
report — they get their own neutral line, `买入未记录 / Buy count unindexed`, with
their float share. That upstream did not index their buys is true and stays
visible; it just no longer drives a zero-cost gate.

Known limit, stated in SKILL.md: a genuine airdrop wallet whose `unrealized_pnl`
is a huge ratio computed against a dust-level cost is missed here. No second ratio
threshold is added — there is no measured sample to calibrate one on.

Also in the Advice section: a ⚠️ Caution rating needs two or more warns by
definition, but only `warns[0]` was printed, so a reader could never see why the
rating was what it was. Every triggered reason is now listed.

Ratings, up/main → this branch, measured end to end:

  musebook robinhood  🔴 Not Recommended → 🟡 Light Position
  JOLLY    robinhood  🟡 Light Position  → ✅ Normal
  ARGUS    arc        ⚠️ Caution         → 🟡 Light Position
  SI       sol        🟡 Light Position  → 🟡 Light Position   (control)

SI is the control: sol indexes buy counts normally, so it barely moves and its
🔴 risk-wallet reading of 45.36% is untouched. This is not a blanket loosening —
it only takes effect where the index is thin.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
chenjunfeng
2026-09-23 16:06:23 +08:00
co-authored by Claude Opus 5
parent 338b0efdb9
commit ff663aa3b9
2 changed files with 119 additions and 20 deletions
+70 -6
View File
@@ -1,7 +1,7 @@
---
name: gmgn-holder-analysis
description: Token holder chip analysis — deep analysis of holder structure including chip distribution, entry cost, whale/dev/KOL behavior, risk wallets (rat traders, bundlers, snipers), related wallets, smart money signals, and an AI rating based purely on token structure. Use when user asks about holder analysis, 筹码分析, 持仓分析, chip structure, who is holding, or whether a token is safe to buy based on its holder composition.
argument-hint: "--chain <sol|bsc|base|eth|arbitrum|hyperevm|robinhood|arc|stable> --address <token_address>"
argument-hint: "--chain <sol|bsc|base|eth|robinhood|arc|stable> --address <token_address>"
metadata:
cliHelp: "gmgn-cli token holders --help && gmgn-cli portfolio created-tokens --help"
---
@@ -54,6 +54,41 @@ amounts, USD values, and market caps still print — they do not pass through `f
Percentages on a **total-supply** basis also still print (`burn`, `DEX`, float share itself, and
the chip-quality buckets), because those denominators are unaffected.
### "Zero cost" must be proven, not inferred from a missing buy count
`buy_tx_count_cur == 0` does **not** mean the wallet received its chips for free. Measured on
musebook (robinhood): 38 wallets read `buy_tx_count_cur: 0` and `avg_cost: null`, but **34 of them
carry a finite `unrealized_pnl` ratio** (+2.7%, +76.5%, +546%) — a ratio that cannot be computed
without a cost basis — and 32 carry a `fomo` frontend tag, 2 a `gmgn` tag, which is the trace of
active trading, the opposite of receiving an airdrop. What is actually missing is buy-transaction
indexing on that chain, while the PnL fields come from a separate computation and arrive populated.
The error was systematic per chain, not per token. Never-bought share of supply, same batch:
**robinhood 27.0% and 41.2%, arc 26.3%, sol 4.8%.** The 20% airdrop warn gate therefore fired on
essentially every robinhood/arc token and essentially never on a sol token — a difference in index
coverage, not in chip structure. Reporting an unpopulated field as a risk conclusion is exactly what
this skill forbids.
So a wallet counts as zero-cost only when **no cost evidence exists at all**:
```python
def has_cost_basis(h):
return (h.get('avg_cost') or 0) > 0 or h.get('unrealized_pnl') is not None
```
Wallets with no indexed buy count but a real cost basis are **not dropped from the report** — they
get their own neutral line, `买入未记录 / Buy count unindexed`, with their float share. The fact that
upstream did not index their buys is true and stays visible; it just does not drive a zero-cost gate.
The same predicate now governs the `钻石手 / Diamond` cohort (its stated rationale is "a wallet that
never paid has no cost to hold through", which a proven cost basis satisfies), the
`转入筹码未动 / Idle airdrop` line (its "zero cost" caption was a false statement for those wallets),
and the Top5 sell-risk classification (which was labelling a wallet at +6.6x as "zero-cost airdrop —
can dump anytime").
Known limit: a genuine airdrop wallet whose `unrealized_pnl` is a huge ratio computed against a
dust-level cost would be missed here. No second ratio threshold is set for it — there is no measured
sample to calibrate one on.
### The token's own contract address is not a holder wallet
Upstream returns it as an ordinary wallet — measured on musebook (robinhood): `addr_type: 0`,
@@ -87,13 +122,13 @@ or gas paid to it, is not "transferred to an internal wallet".
| `address` | string | Wallet address |
| `balance` | float | Current token balance |
| `amount_percentage` | float | Fraction of total supply (0–1). Multiply by 100 for %. |
| `buy_tx_count_cur` | int | Buy transactions since creation. **Not indexed on every chain — a 0 here is not proof the wallet never bought.** See the cost-basis section above. |
| `usd_value` | float | Current USD value of holdings |
| `avg_cost` | float | Average buy price per token |
| `unrealized_pnl` | float | Unrealized PnL ratio (0.5 = +50%) |
| `unrealized_profit` | float | Unrealized PnL in USD |
| `realized_profit` | float | Realized PnL in USD |
| `profit` | float | Total PnL in USD (realized + unrealized). Also a valid `--order-by` field. |
| `buy_tx_count_cur` | int | Buy transactions since token creation |
| `sell_tx_count_cur` | int | Sell transactions since token creation |
| `sell_amount_percentage` | float | Fraction of total buys that have been sold. Drives the accumulating/distributing verdict. |
| `sell_volume_cur` | float | USD volume sold since token creation |
@@ -101,6 +136,7 @@ or gas paid to it, is not "transferred to an internal wallet".
| `history_transfer_out_amount` | float | Token amount transferred out (not sold) |
| `history_transfer_out_income` | float | USD value of transferred-out tokens |
| `token_transfer_out` | object | `{address}` — recipient of a transfer-out. Used to detect dev sock puppets when the recipient is itself in the top 100. |
| `native_transfer` | object | `{from_address, amount, timestamp}` — how the wallet was funded. Also the fallback sock-puppet probe: a top-100 holder whose gas came from the creator. |
| `name` | string | Wallet display name if known |
| `start_holding_at` | int | Unix timestamp of first buy |
| `addr_type` | int | 0=normal wallet, 1=burn/dead, 2=DEX/pool |
@@ -130,12 +166,17 @@ All thresholds below are **share of tradeable float**, matching the script. They
comparable to GMGN's own UI, which reports share of total supply — on a token whose LP holds 56%
of supply, the same wallet reads 2.4× higher here.
The Advice section prints **every** triggered reason, not just the first: a ⚠️ Caution rating means
two or more warns fired by definition, so printing one left the reader unable to see why the rating
was what it was.
Entry timing pressure (批次浮盈/出货) does **NOT** affect the overall rating — it only affects section display.
| 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% / contract self-holds >10% of supply |
| 可轻仓 | Light Position | 🟡 | Exactly 1 of above warns |
| 正常参与 | Normal | ✅ | None of the above |
@@ -152,9 +193,11 @@ 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.
Diamond hands require a **cost basis** (`has_cost_basis`, above) — a wallet that never paid has no
cost to hold through, so genuinely zero-cost recipients are reported separately as
"转入筹码未动 / Idle airdrop" rather than being credited as diamond hands. The test is the cost
basis, not `buy_tx_count_cur > 0`: on chains where buy counts are not indexed the latter discards
most real diamond hands (measured on JOLLY: 34 → 75 wallets, 21.1% → 55.7% of float).
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.
@@ -163,7 +206,7 @@ size — scripted batch funding is a structural signal, not a magnitude one.
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
airdrop** (by `has_cost_basis`, above — not by a missing buy count) / **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.
@@ -180,6 +223,27 @@ contradict the rating above it.
`sol`, `bsc`, `base`, `eth`, `robinhood`, `arc`, `stable`
## Dev lookup is dual-source
`token holders --tag dev` is the primary source, but it returns `{"list": []}` on some launchpads —
measured on all five bankr tokens in robinhood's 24h trending top 50, while pons_v2 / longxyz tokens
on the same chain return the creator normally. It can also return rows with no `creator` tag at all
(measured on a flap token on bsc). Either way the whole Dev section, **including the dev sock-puppet
danger gate**, would silently go dead.
So when no `creator`-tagged row comes back, the creator address is recovered from `token info`'s
`dev.creator_address`, and `dev.creator_token_balance` supplies its balance. That call is made only
when the primary source fails, so it costs nothing on tokens where the tag works.
`token info` does not carry `realized_profit` or `token_transfer_out`, so on the fallback path:
- the realized-profit figure is **omitted entirely** — printing `$0` would report an unmeasured
field as a measurement
- the sock-puppet gate switches judge: instead of "dev's chips went to a top-100 wallet", it asks
whether any top-100 holder's `native_transfer.from_address` **is** the creator, i.e. the dev paid
that wallet's gas. A hit is reported as the danger and names the wallets; a miss is not reported as
clean, since only 57% of holders carry that field on the measured token.
## Notes
- `balance >= 1` threshold avoids dust false positives when identifying dev holdings
+49 -14
View File
@@ -184,7 +184,29 @@ def f1(v): return v / 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]
# ── 「零成本」必须有证据,不能由 buy_tx_count_cur 缺失推出 ────────────────────
# buy_tx_count_cur == 0 曾被直接当成"这批筹码是转账/空投来的、持有人一分钱没花"。
# 实测否掉了这个推断:robinhood 的 musebook 有 38 个 buy_tx_count_cur==0 的钱包,
# 其中 34 个带着 unrealized_pnl 比值(+2.7% / +76.5% / +546% 这种有限值),而没有成本
# 的筹码算不出有限盈亏比;32/38 还带着 fomo、2 个带着 gmgn 前端标签 —— 那是主动交易
# 的痕迹,和"收到空投"正好相反。真实情况是这条链上买入笔数没被索引,而盈亏由另一路
# 计算照常返回。
#
# 后果是按链系统性误报:同一批实测里 never-bought 占供应 robinhood 27.0% / 41.2%、
# arc 26.3%、sol 4.8% —— 20% 的告警闸门在 robinhood / arc 上几乎必然触发,在 sol 上
# 几乎不可能触发,而这个差异来自索引覆盖率,不是代币的筹码结构。把字段缺失当成风险
# 结论上报,是这个技能明令不做的事。
#
# 所以改判:有成本证据的一律不算零成本,它们改走 unindexed 单独一行照常展示 ——
# "买入笔数没被索引"本身是真事实,只是不该驱动零成本闸门。
# 已知局限:若某个真空投钱包的 unrealized_pnl 是对着尘埃级成本算出来的巨大比值,
# 这里会漏判。不为此再设一道比值阈值 —— 目前没有能定档的实测样本。
def has_cost_basis(h):
return (h.get('avg_cost') or 0) > 0 or h.get('unrealized_pnl') is not None
_nobuy = [h for h in normal if (h.get('buy_tx_count_cur') or 0)==0 and (h.get('balance') or 0)>0]
airdrop = [h for h in _nobuy if not has_cost_basis(h)]
unindexed= [h for h in _nobuy if has_cost_basis(h)]
bundlers = [h for h in normal if 'bundler' in (h.get('maker_token_tags') or [])]
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 [])]
@@ -320,10 +342,13 @@ heavy_sell = [h for h in normal if (h.get('sell_amount_percentage') or 0)>=0.5]
# 结论还相反。"钻石手"的含义是扛住了浮亏没卖 —— 没花钱买入的地址无所谓扛,
# 它不卖可能只是私钥在分发方手里。所以要求 buy_tx>0。
# 空降且未动的那批不丢弃,单独列成 idle_airdrop:它们仍是随时可能出货的零成本筹码。
diamond = [h for h in normal if (h.get('buy_tx_count_cur') or 0)>0
# 判据照上面的 has_cost_basis 走,不再看 buy_tx_count_cur:本节的理由原文是"没花钱
# 买入的地址无所谓扛",那么有成本证据的钱包按这条理由本身就该算钻石手,买入笔数有没有
# 被索引与它无关。idle_airdrop 那行写着"零成本",对有成本依据的钱包是一句假话,同改。
diamond = [h for h in normal if has_cost_basis(h)
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
idle_airdrop = [h for h in normal if not has_cost_basis(h)
and (h.get('sell_tx_count_cur') or 0)==0
and (h.get('balance') or 0)>0]
@@ -656,6 +681,11 @@ airf = pf("🔴" if airdrop_pct>0.25 else ("🟡" if airdrop_pct>0.1 else "🟢
riskf = pf("🔴" if risk_pct>0.35 else ("🟡" if risk_pct>0.15 else "🟢"))
print(f" {_('转入筹码', 'Airdrop')} {len(airdrop)}{_('个', '')}({fpct(airdrop_pct)}) {airf} · {_('风险钱包', 'Risk')} {len(risk_all)}{_('个', '')}({fpct(risk_pct)}) {riskf}")
if unindexed:
# 中性信息行:这批钱包有成本依据,不是零成本筹码,所以不挂风险色
print(f" {_('买入未记录', 'Buy count unindexed')} {len(unindexed)}{_('个', '')}({fpct(fs(unindexed))})"
f" {_('有成本依据,不计入转入筹码', 'has a cost basis — not counted as airdrop')}")
if selfheld:
# 总供应基准,不经过 float_share,所以退化盘也照常打印真实数字
selff = "🔴" if self_pct > 0.30 else ("🟡" if self_pct > 0.10 else "🟢")
@@ -673,11 +703,13 @@ elif not no_holders:
print()
# Top5 出货风险
top5_airdrop_n = sum(1 for h in top5_holders if h.get('buy_tx_count_cur', 0) == 0)
# 三项都改看 has_cost_basis:原先靠 buy_tx_count_cur 分流,在买入笔数未索引的链上
# 会把有成本的大户写成"零成本转入,可随时出货",同时把它从套牢/浮盈两类里漏掉。
top5_airdrop_n = sum(1 for h in top5_holders if not has_cost_basis(h))
top5_trapped_n = sum(1 for h in top5_holders
if (h.get('unrealized_pnl') or 0) < -0.1 and (h.get('buy_tx_count_cur') or 0) > 0)
if (h.get('unrealized_pnl') or 0) < -0.1 and has_cost_basis(h))
top5_profit_n = sum(1 for h in top5_holders
if (h.get('unrealized_pnl') or 0) > 0.1 and (h.get('buy_tx_count_cur') or 0) > 0)
if (h.get('unrealized_pnl') or 0) > 0.1 and has_cost_basis(h))
top5_selling_n = sum(1 for h in top5_holders if is_selling(h))
top5_parts = []
@@ -692,13 +724,13 @@ print(f" {top5_label} {top5_summary}")
# 单个最危险钱包(优先级:零成本最大 > 高浮盈最大 > 出货最大)
danger_wallet = None
danger_reason = ""
zero_cost_top5 = [h for h in top5_holders if h.get('buy_tx_count_cur', 0) == 0]
zero_cost_top5 = [h for h in top5_holders if not has_cost_basis(h)]
if zero_cost_top5:
danger_wallet = max(zero_cost_top5, key=lambda h: h['amount_percentage'])
danger_reason = _("零成本转入,可随时出货", "Zero-cost airdrop — can dump anytime")
if not danger_wallet:
high_profit_top5 = [h for h in top5_holders
if (h.get('unrealized_pnl') or 0) > 1.0 and (h.get('buy_tx_count_cur') or 0) > 0]
if (h.get('unrealized_pnl') or 0) > 1.0 and has_cost_basis(h)]
if high_profit_top5:
danger_wallet = max(high_profit_top5, key=lambda h: h['amount_percentage'])
mult = (danger_wallet.get('unrealized_pnl') or 0) + 1
@@ -879,17 +911,20 @@ print()
print(f" {rating_em} {rating_text}")
print()
# ⚠️ 谨慎参与按定义需要 ≥2 条 warn,而这里原先只打 warns[0],所以拿到 ⚠️ 的读者
# 永远看不到第二条理由 —— 评级说"有两个问题",正文只给一个。全部列出。
if dangers:
core = _("⚠️ 高风险:", "⚠️ High risk: ") + dangers[0]
reasons = [_("⚠️ 高风险:", "⚠️ High risk: ") + d for d in dangers]
elif warns:
core = warns[0]
reasons = list(warns)
elif goods:
core = goods[0]
reasons = [goods[0]]
elif unassessable:
core = _("当前无法判断筹码质量,建议稍后重试", "Cannot assess chip quality right now — retry later")
reasons = [_("当前无法判断筹码质量,建议稍后重试", "Cannot assess chip quality right now — retry later")]
else:
core = _("筹码结构正常,未发现明显风险信号", "Chip structure normal — no obvious risk signals")
print(f" {core}")
reasons = [_("筹码结构正常,未发现明显风险信号", "Chip structure normal — no obvious risk signals")]
for r in reasons:
print(f" {r}")
print()
print(f" 💡 {_('离场信号:', 'Exit signals: ')}{' / '.join(exit_signals)}")