
Custom Indicators
- 212 installs
- 257 repo stars
- Updated June 24, 2026
- agiprolabs/claude-trading-skills
custom-indicators is a Claude Code skill providing crypto-native indicators including NVT ratio, exchange flow, funding rate signals, holder momentum, and smart money flow.
About
custom-indicators is a Claude Code skill covering nine crypto-native trading indicators, including NVT ratio, MVRV, exchange netflow, funding rate, and holder momentum. Each comes with a formula, interpretation ranges, data sources, and a code snippet. A developer uses it when building on-chain-aware signal generation that standard equity technical analysis cannot provide. It explains why crypto's on-chain transparency and derivatives dominance demand purpose-built metrics.
- Nine crypto-native indicators: NVT, MVRV, exchange flow, funding rate, holder momentum, smart-money flow
- Each indicator ships a formula, interpretation guide, data sources, and code snippet
- Explains why equity-market TA falls short for on-chain crypto markets
Custom Indicators by the numbers
- 212 all-time installs (skills.sh)
- Ranked #445 of 1,106 Finance & Trading skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
custom-indicators capabilities & compatibility
Free; computes from free APIs or demo data, some indicators optionally use paid on-chain data providers.
- Capabilities
- crypto indicators · nvt ratio · exchange flow · funding rate signal · holder momentum · smart money flow
- Use cases
- trading · data analysis · research
- Runs
- Runs locally
- Pricing
- Free
What custom-indicators says it does
Crypto-native indicators including NVT ratio, exchange flow, funding rate signals, holder momentum, and smart money flow
This skill covers nine crypto-native indicators. Each section includes the formula, interpretation guide, data sources, and a working code snippet.
npx skills add https://github.com/agiprolabs/claude-trading-skills --skill custom-indicatorsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 212 |
|---|---|
| repo stars | ★ 257 |
| Last updated | June 24, 2026 |
| Repository | agiprolabs/claude-trading-skills ↗ |
What it does
Compute crypto-native indicators (NVT, MVRV, exchange flow, funding rate, holder momentum) for trading signals.
Who is it for?
On-chain and derivatives-aware signal generation that standard equity TA cannot capture.
Skip if: Traditional equity or forex technical analysis, which the skill says was not built for crypto's mechanics.
When should I use this skill?
You need crypto-native indicators like NVT, MVRV, funding rate, or exchange netflow in a trading system.
What you get
Computed crypto-native indicators with interpretation ranges and signal labels.
By the numbers
- Nine crypto-native indicators
- NVT bearish above 65 and bullish below 25
- MVRV distribution above 3.5, bottoms below 1.0
Files
Custom Crypto Indicators
Why Standard TA Falls Short for Crypto
Traditional technical analysis was built for equities and forex — markets with fixed supply, regulated exchanges, and institutional-dominated order flow. Crypto markets have unique properties that demand purpose-built indicators:
- On-chain transparency: Every transaction is public. We can measure real
economic activity, not just price and volume on a single exchange.
- Supply mechanics: Fixed or programmatic supply schedules make
supply-side analysis (velocity, holder distribution) meaningful.
- Derivatives dominance: Perpetual futures funding rates and open interest
often drive spot price, not the other way around.
- Whale concentration: A small number of wallets hold outsized supply.
Tracking their behavior provides alpha that equity-market TA cannot.
- Exchange flows: On-chain deposit/withdrawal to centralized exchanges
signals intent to sell or accumulate.
This skill covers nine crypto-native indicators. Each section includes the formula, interpretation guide, data sources, and a working code snippet.
Files
| File | Description |
|---|---|
references/indicator_formulas.md | Full formulas, parameter tables, signal ranges for all 9 indicators |
references/signal_interpretation.md | Composite scoring, divergence detection, false signal filtering |
scripts/compute_crypto_indicators.py | Computes all 9 indicators from free APIs or demo data |
scripts/holder_momentum.py | Holder count tracking with momentum signals |
---
Indicator 1: NVT Ratio
Network Value to Transactions — the crypto equivalent of a P/E ratio.
NVT = Market Cap / Daily On-Chain Transaction Volume (USD)- High NVT (> 65): Network is overvalued relative to its economic
throughput. Bearish signal.
- Low NVT (< 25): Network is undervalued or seeing heavy real usage.
Bullish signal.
- Data sources: CoinGecko (market cap), blockchain explorers or
DeFiLlama (transaction volume).
def nvt_ratio(market_cap: float, daily_tx_volume_usd: float) -> float:
"""Compute NVT ratio.
Args:
market_cap: Current market capitalization in USD.
daily_tx_volume_usd: 24h on-chain transaction volume in USD.
Returns:
NVT ratio value.
"""
if daily_tx_volume_usd <= 0:
return float("inf")
return market_cap / daily_tx_volume_usdSmoothing: Apply a 14-day or 28-day moving average to NVT (called NVT Signal) to reduce noise from daily volume spikes.
---
Indicator 2: MVRV Ratio
Market Value to Realized Value — compares the current market cap to the aggregate cost basis of all holders.
MVRV = Market Cap / Realized Cap
Realized Cap = Sum of (each UTXO * price when it last moved)- MVRV > 3.5: Most holders are in deep profit. Distribution likely.
- MVRV < 1.0: Most holders are underwater. Historically marks bottoms.
- Data sources: Glassnode, CryptoQuant (Bitcoin/Ethereum). For Solana
tokens, approximate via average entry price of top holders.
def mvrv_ratio(market_cap: float, realized_cap: float) -> float:
"""Compute MVRV ratio.
Args:
market_cap: Current market capitalization in USD.
realized_cap: Realized capitalization (aggregate cost basis).
Returns:
MVRV ratio value.
"""
if realized_cap <= 0:
return float("inf")
return market_cap / realized_capFor tokens without UTXO-based realized cap, estimate using average purchase price from DEX trade history multiplied by circulating supply.
---
Indicator 3: Exchange Flow
Net exchange deposits minus withdrawals — signals selling or accumulation intent.
Exchange Netflow = Deposits to Exchanges - Withdrawals from Exchanges- Positive netflow (large deposits): Holders moving tokens to exchanges,
likely to sell. Bearish.
- Negative netflow (withdrawals): Tokens leaving exchanges to cold
storage. Bullish accumulation signal.
- Data sources: CryptoQuant, Glassnode. For Solana SPL tokens, track
transfers to known exchange wallets via Helius or Solana RPC.
def exchange_netflow(
deposits_usd: float, withdrawals_usd: float
) -> tuple[float, str]:
"""Compute exchange netflow and interpret.
Returns:
Tuple of (netflow_value, signal_label).
"""
netflow = deposits_usd - withdrawals_usd
if netflow > 0:
signal = "bearish"
elif netflow < 0:
signal = "bullish"
else:
signal = "neutral"
return netflow, signalNormalize by market cap for cross-token comparison: Netflow Ratio = Netflow / Market Cap.
---
Indicator 4: Funding Rate Signal
Perpetual futures contracts use funding rates to anchor price to spot.
Funding Rate = (Perp Mark Price - Spot Price) / Spot Price
(paid every 8 hours on most exchanges)- Highly positive (> 0.05%): Longs pay shorts. Market is overleveraged
long. Contrarian bearish.
- Highly negative (< -0.05%): Shorts pay longs. Overleveraged short.
Contrarian bullish.
- Data sources: Binance, Bybit, dYdX APIs. Aggregate across exchanges
for a volume-weighted average.
def funding_rate_signal(
rates: list[float], weights: list[float] | None = None
) -> tuple[float, str]:
"""Volume-weighted average funding rate with signal.
Args:
rates: Funding rates from multiple exchanges.
weights: Optional volume weights per exchange.
"""
import numpy as np
if weights is None:
weights = [1.0 / len(rates)] * len(rates)
vw_rate = float(np.average(rates, weights=weights))
if vw_rate > 0.0005:
signal = "bearish"
elif vw_rate < -0.0005:
signal = "bullish"
else:
signal = "neutral"
return vw_rate, signal---
Indicator 5: Open Interest Momentum
Tracks the rate of change in total open interest across derivatives exchanges.
OI Momentum = (OI_today - OI_n_days_ago) / OI_n_days_ago * 100- Rising OI + Rising Price: New money entering longs. Trend
confirmation.
- Rising OI + Falling Price: New shorts opening. Bearish pressure.
- Falling OI + Rising Price: Short squeeze / closing shorts.
- Falling OI + Falling Price: Long liquidation.
- Data sources: CoinGlass, Binance, Bybit open interest endpoints.
def oi_momentum(
oi_series: list[float], lookback: int = 7
) -> float:
"""Compute open interest momentum as percentage change.
Args:
oi_series: Daily open interest values (newest last).
lookback: Number of days for momentum calculation.
"""
if len(oi_series) < lookback + 1:
return 0.0
old = oi_series[-(lookback + 1)]
new = oi_series[-1]
if old <= 0:
return 0.0
return (new - old) / old * 100.0---
Indicator 6: Holder Momentum
Tracks the net change in unique token holders over time.
Holder Momentum = (Holders_today - Holders_n_days_ago) / Holders_n_days_ago
Holder Acceleration = Holder Momentum_today - Holder Momentum_yesterday- Accelerating growth: Viral adoption phase. Bullish.
- Decelerating growth: Adoption slowing. Watch for reversal.
- Negative momentum: Holders leaving. Bearish.
- Data sources: Helius DAS API (Solana), Etherscan token holder count,
Birdeye holder stats.
def holder_momentum(
holder_counts: list[int], lookback: int = 7
) -> tuple[float, float]:
"""Compute holder momentum and acceleration.
Returns:
Tuple of (momentum_pct, acceleration).
"""
if len(holder_counts) < lookback + 2:
return 0.0, 0.0
old = holder_counts[-(lookback + 1)]
new = holder_counts[-1]
prev_old = holder_counts[-(lookback + 2)]
prev_new = holder_counts[-2]
mom = (new - old) / old if old > 0 else 0.0
prev_mom = (prev_new - prev_old) / prev_old if prev_old > 0 else 0.0
accel = mom - prev_mom
return mom, accelSee scripts/holder_momentum.py for a full tracking implementation.
---
Indicator 7: Liquidity Score
A composite metric combining order book depth, bid-ask spread, and DEX pool depth to estimate how easily a position can be entered/exited.
Liquidity Score = w1 * Depth Score + w2 * Spread Score + w3 * Pool ScoreWhere:
- Depth Score =
min(1, total_bids_within_2pct / target_position_size) - Spread Score =
max(0, 1 - spread_bps / 100) - Pool Score =
min(1, pool_tvl / (target_position_size * 10)) - Default weights:
w1=0.4, w2=0.3, w3=0.3
def liquidity_score(
depth_usd: float,
spread_bps: float,
pool_tvl: float,
position_size: float,
weights: tuple[float, float, float] = (0.4, 0.3, 0.3),
) -> float:
"""Composite liquidity score from 0 (illiquid) to 1 (highly liquid)."""
depth_s = min(1.0, depth_usd / position_size) if position_size > 0 else 0
spread_s = max(0.0, 1.0 - spread_bps / 100.0)
pool_s = min(1.0, pool_tvl / (position_size * 10)) if position_size > 0 else 0
return weights[0] * depth_s + weights[1] * spread_s + weights[2] * pool_s---
Indicator 8: Smart Money Flow
Net buying pressure from wallets identified as "smart money" (historically profitable, large balances, early entry patterns).
Smart Money Flow = Sum(smart_wallet_buys_usd) - Sum(smart_wallet_sells_usd)
SMF Ratio = Smart Money Flow / Total Volume- SMF Ratio > 0.1: Smart money is net accumulating. Bullish.
- SMF Ratio < -0.1: Smart money is distributing. Bearish.
- Data sources: Helius transaction parsing + wallet labeling, Birdeye
wallet analytics, Nansen (Ethereum).
def smart_money_flow(
smart_buys_usd: float,
smart_sells_usd: float,
total_volume_usd: float,
) -> tuple[float, float, str]:
"""Compute smart money flow and ratio.
Returns:
Tuple of (net_flow, smf_ratio, signal).
"""
net = smart_buys_usd - smart_sells_usd
ratio = net / total_volume_usd if total_volume_usd > 0 else 0.0
if ratio > 0.1:
signal = "bullish"
elif ratio < -0.1:
signal = "bearish"
else:
signal = "neutral"
return net, ratio, signal---
Indicator 9: Token Velocity
Measures how frequently a token changes hands relative to its supply.
Token Velocity = Daily Trading Volume (tokens) / Circulating Supply- High velocity (> 0.3): Speculative trading dominates. Token is being
flipped, not held. Can precede dumps.
- Low velocity (< 0.05): Holders are sitting tight. Strong hands.
- Data sources: CoinGecko (volume, supply), DEX aggregator volumes.
def token_velocity(
daily_volume_tokens: float, circulating_supply: float
) -> tuple[float, str]:
"""Compute token velocity.
Returns:
Tuple of (velocity, interpretation).
"""
if circulating_supply <= 0:
return 0.0, "unknown"
vel = daily_volume_tokens / circulating_supply
if vel > 0.3:
interp = "high_speculation"
elif vel > 0.1:
interp = "moderate"
elif vel > 0.05:
interp = "low"
else:
interp = "very_low_strong_holders"
return vel, interp---
Combining Indicators
No single indicator is reliable in isolation. See references/signal_interpretation.md for guidance on:
- Building composite scores from multiple indicators
- Detecting divergences (e.g., price rising but NVT expanding)
- Adjusting interpretation by market regime
- Filtering false signals
Dependencies
uv pip install httpx pandas numpyDisclaimer
All indicators and analysis provided by this skill are for informational and educational purposes only. They do not constitute financial advice. Always conduct your own research before making any investment decisions.
Crypto-Native Indicator Formulas
Formulas, parameters, and signal ranges for all nine crypto indicators.
1. NVT Ratio (Network Value to Transactions)
Formula
NVT = Market Cap / Daily On-Chain Tx Volume (USD)
NVT Signal = SMA(NVT, period)Analogous to P/E ratio: market cap is the "price", on-chain tx volume is the "earnings". High NVT = overvalued relative to usage.
Parameters
| Timeframe | SMA Period | Overbought | Oversold |
|---|---|---|---|
| Short-term | 14 days | > 65 | < 25 |
| Medium-term | 28 days | > 80 | < 20 |
| Long-term | 90 days | > 100 | < 15 |
Signal Table
| NVT Range | Signal | Interpretation |
|---|---|---|
| < 15 | Strong bullish | Extremely undervalued or high usage spike |
| 15–25 | Bullish | Healthy usage relative to valuation |
| 25–65 | Neutral | Fair value zone |
| 65–100 | Bearish | Overvalued relative to throughput |
| > 100 | Strong bearish | Speculative premium, correction likely |
Data Sources
- Market cap: CoinGecko
/coins/{id}→market_data.market_cap.usd - Tx volume: Blockchain RPC (sum of transfer values), DeFiLlama
2. MVRV Ratio (Market Value to Realized Value)
Formula
MVRV = Market Cap / Realized Cap
Realized Cap = Σ (tokens_in_UTXO_i × price_when_UTXO_i_last_moved)For non-UTXO chains (Solana, Ethereum tokens):
Estimated Realized Cap = Σ (wallet_balance_i × average_entry_price_i)Parameters
| Asset Type | Overbought | Oversold |
|---|---|---|
| Bitcoin | > 3.5 | < 1.0 |
| Ethereum | > 3.0 | < 0.8 |
| Large-cap alts | > 2.5 | < 0.7 |
| Small-cap tokens | > 2.0 | < 0.5 |
Signal Table
| MVRV | Signal | Interpretation |
|---|---|---|
| < 0.5 | Strong bullish | Deep capitulation, most holders at loss |
| 0.5–1.0 | Bullish | Aggregate holders near breakeven |
| 1.0–2.0 | Neutral | Moderate unrealized profit |
| 2.0–3.5 | Bearish | Significant unrealized profit, distribution risk |
| > 3.5 | Strong bearish | Euphoria zone, historically precedes corrections |
Data Sources
- Bitcoin/Ethereum: Glassnode, CryptoQuant (direct MVRV)
- Solana tokens: Estimate from Helius DAS holder data + DEX trade history
3. Exchange Flow
Formula
Netflow = Deposits_to_exchanges (USD) - Withdrawals_from_exchanges (USD)
Netflow Ratio = Netflow / Market Cap
Netflow Z-Score = (Netflow - SMA(Netflow, 30)) / StdDev(Netflow, 30)Parameters
| Metric | Bearish Threshold | Bullish Threshold |
|---|---|---|
| Netflow Ratio | > 0.01 (1% of mcap) | < -0.01 |
| Netflow Z-Score | > 2.0 | < -2.0 |
Signal Table
| Netflow Z-Score | Signal | Interpretation |
|---|---|---|
| < -2.0 | Strong bullish | Abnormal withdrawal (accumulation) |
| -2.0 to -0.5 | Bullish | Above-average withdrawal |
| -0.5 to 0.5 | Neutral | Normal flow |
| 0.5 to 2.0 | Bearish | Above-average deposits |
| > 2.0 | Strong bearish | Abnormal deposit spike (selling) |
Data Sources
- CEX flows: CryptoQuant, Glassnode
- Solana SPL tokens: Track transfers to/from known exchange wallets via
Helius getAssetTransfers or Solana RPC getSignaturesForAddress
4. Funding Rate Signal
Formula
VW Funding Rate = Σ (rate_i × volume_i) / Σ volume_i
Funding Rate MA = SMA(VW_Funding_Rate, periods)
Cumulative Funding = Σ (funding_rate × 3) [3 payments/day]
Annualized Funding = Cumulative Funding * 365Parameters
| Period | SMA Length | Extreme Threshold |
|---|---|---|
| Scalp (4h) | 3 periods | ±0.1% |
| Swing (1-7d) | 7 periods | ±0.05% |
| Position (1-4w) | 14 periods | ±0.03% |
Signal Table
| VW Funding Rate | Signal | Interpretation |
|---|---|---|
| < -0.1% | Strong bullish | Shorts massively overleveraged |
| -0.1% to -0.03% | Bullish | Moderate short bias |
| -0.03% to 0.03% | Neutral | Balanced market |
| 0.03% to 0.1% | Bearish | Moderate long bias |
| > 0.1% | Strong bearish | Longs massively overleveraged |
Data Sources
- Binance:
GET /fapi/v1/fundingRate - Bybit:
GET /v5/market/funding/history - dYdX:
GET /v3/markets→nextFundingRate
5. Open Interest Momentum
Formula
OI Momentum (%) = (OI_t - OI_{t-n}) / OI_{t-n} × 100
OI-Price Divergence = sign(ΔPrice) ≠ sign(ΔOI)Parameters
| Lookback | Use Case |
|---|---|
| 1 day | Intraday sentiment shift |
| 3 days | Short-term positioning |
| 7 days | Swing trade signal |
| 14 days | Trend confirmation |
Signal Matrix
| OI Trend | Price Trend | Signal | Meaning |
|---|---|---|---|
| Rising | Rising | Trend confirmation | New longs entering |
| Rising | Falling | Bearish buildup | New shorts entering |
| Falling | Rising | Short squeeze | Shorts closing |
| Falling | Falling | Long liquidation | Longs capitulating |
Data Sources
- CoinGlass: Aggregated OI across exchanges
- Binance:
GET /fapi/v1/openInterest
6. Holder Momentum
Formula
Holder Momentum = (Holders_t - Holders_{t-n}) / Holders_{t-n}
Holder Acceleration = Momentum_t - Momentum_{t-1}
Growth Rate (annualized) = ((Holders_t / Holders_{t-n})^(365/n) - 1) × 100Parameters
| Lookback | Signal Threshold |
|---|---|
| 7 days | ±3% momentum |
| 14 days | ±5% momentum |
| 30 days | ±10% momentum |
Signal Table
| Momentum | Acceleration | Signal |
|---|---|---|
| Positive | Positive | Strong bullish — accelerating adoption |
| Positive | Negative | Weakening bullish — adoption slowing |
| Negative | Negative | Strong bearish — accelerating departures |
| Negative | Positive | Weakening bearish — departures slowing |
Data Sources
- Solana: Helius DAS
getAssetsByGroup, Birdeye holder stats - Ethereum: Etherscan token holder count, Dune Analytics
7. Liquidity Score (Composite)
Formula
Depth Score = min(1, bids_within_2pct / position_size)
Spread Score = max(0, 1 - spread_bps / 100)
Pool Score = min(1, pool_tvl / (position_size × 10))
Liquidity Score = 0.4 × Depth_Score + 0.3 × Spread_Score + 0.3 × Pool_ScoreWeight Recommendations
| Context | Depth Weight | Spread Weight | Pool Weight |
|---|---|---|---|
| CEX-dominant token | 0.5 | 0.3 | 0.2 |
| DEX-only token | 0.2 | 0.2 | 0.6 |
| Hybrid | 0.4 | 0.3 | 0.3 |
Signal Table
| Score | Rating | Action |
|---|---|---|
| 0.8–1.0 | Excellent | Full position size acceptable |
| 0.5–0.8 | Good | Reduce position by 25% |
| 0.3–0.5 | Fair | Reduce position by 50%, use limit orders |
| < 0.3 | Poor | Avoid or use very small size with patience |
8. Smart Money Flow
Formula
SMF = Σ smart_buys (USD) - Σ smart_sells (USD)
SMF Ratio = SMF / Total Volume (USD)
SMF Z-Score = (SMF - SMA(SMF, 14)) / StdDev(SMF, 14)Smart wallet = meets 2+ of: win rate >60% (50+ trades), avg ROI >20%, portfolio >$500K, early entry in 3+ tokens that 5x+.
Signal Table
| SMF Ratio | Signal | Interpretation |
|---|---|---|
| < -0.15 | Strong bearish | Smart money actively distributing |
| -0.15 to -0.05 | Bearish | Smart money net selling |
| -0.05 to 0.05 | Neutral | No clear smart money conviction |
| 0.05 to 0.15 | Bullish | Smart money net buying |
| > 0.15 | Strong bullish | Smart money aggressively accumulating |
Data Sources
- Solana: Helius parsed transactions + wallet profiling
- Ethereum: Nansen smart money labels
- Cross-chain: Arkham Intelligence
9. Token Velocity
Formula
Token Velocity = Daily Volume (tokens) / Circulating Supply
Velocity MA = SMA(Velocity, 14)
Velocity Trend = Velocity / Velocity_MASignal Table
| Velocity | Signal | Interpretation |
|---|---|---|
| < 0.02 | Very low | Strong holders, low speculation |
| 0.02–0.05 | Low | Healthy holding pattern |
| 0.05–0.15 | Moderate | Normal trading activity |
| 0.15–0.30 | High | Elevated speculation |
| > 0.30 | Very high | Frenzied trading, dump risk |
Data Sources
- Volume: CoinGecko
/coins/{id}→market_data.total_volume - Supply: CoinGecko →
market_data.circulating_supply - DEX volume: DeFiLlama
/overview/dexsor Jupiter aggregator stats
Signal Interpretation Guide
How to combine multiple crypto-native indicators into actionable composite scores, detect divergences, and filter false signals.
---
Building Composite Scores
Weighted Scoring Framework
Assign each indicator a score from -2 (strong bearish) to +2 (strong bullish), then compute a weighted average.
WEIGHTS = {
"nvt": 0.10,
"mvrv": 0.10,
"exchange_flow": 0.15,
"funding_rate": 0.15,
"oi_momentum": 0.10,
"holder_momentum": 0.10,
"liquidity_score": 0.05,
"smart_money_flow": 0.15,
"token_velocity": 0.10,
}
def composite_score(signals: dict[str, int]) -> float:
"""Compute weighted composite score.
Args:
signals: Indicator name to score (-2 to +2).
Returns:
Weighted score from -2.0 to +2.0.
"""
total = 0.0
weight_sum = 0.0
for name, score in signals.items():
w = WEIGHTS.get(name, 0.1)
total += score * w
weight_sum += w
return total / weight_sum if weight_sum > 0 else 0.0Composite Score Interpretation
| Score Range | Label | Suggested Bias |
|---|---|---|
| 1.5 to 2.0 | Strong bullish | Consider adding to position |
| 0.5 to 1.5 | Bullish | Favor long bias |
| -0.5 to 0.5 | Neutral | No directional edge |
| -1.5 to -0.5 | Bearish | Favor reducing exposure |
| -2.0 to -1.5 | Strong bearish | Consider defensive positioning |
Weight Adjustments by Market Regime
- Trending market: Increase weight on OI momentum and funding rate
(momentum indicators confirm trends).
- Range-bound market: Increase weight on exchange flow and smart money
flow (accumulation/distribution drives breakouts).
- High-volatility regime: Increase weight on liquidity score (exit
difficulty rises) and funding rate (leverage extremes cause squeezes).
- Low-liquidity regime: Increase weight on holder momentum and token
velocity (supply-side dynamics dominate).
---
Divergence Detection
Divergences between price action and on-chain/derivatives indicators often signal reversals or trend exhaustion.
Key Divergence Patterns
| Price | Indicator | Divergence Type | Meaning |
|---|---|---|---|
| Rising | NVT expanding | Bearish | Price rising but tx volume not keeping up |
| Rising | MVRV > 3.0 | Bearish | Holders in deep profit, distribution risk |
| Rising | Exchange deposits spiking | Bearish | Holders sending to exchanges to sell |
| Rising | Funding rate very positive | Bearish | Overleveraged longs, squeeze risk |
| Rising | Holder count falling | Bearish | Price up but adoption declining |
| Rising | Smart money selling | Bearish | Informed wallets distributing |
| Falling | NVT contracting | Bullish | Tx volume growing despite price drop |
| Falling | MVRV < 1.0 | Bullish | Holders underwater, capitulation near end |
| Falling | Exchange withdrawals spiking | Bullish | Accumulation despite price decline |
| Falling | Funding rate very negative | Bullish | Overleveraged shorts, squeeze risk |
| Falling | Holder count rising | Bullish | New buyers despite price weakness |
| Falling | Smart money buying | Bullish | Informed wallets accumulating |
Divergence Confirmation
A divergence is more reliable when: 1. Multiple indicators diverge — 3+ indicators disagreeing with price is a stronger signal than a single divergence. 2. Divergence persists — A divergence lasting 3+ days is more meaningful than a single-day spike. 3. Volume supports it — High-volume divergences are more significant.
def count_divergences(
price_trend: str,
indicator_signals: dict[str, str],
) -> tuple[int, int]:
"""Count bullish and bearish divergences.
Args:
price_trend: "up" or "down".
indicator_signals: Name to "bullish"/"bearish"/"neutral".
Returns:
(bullish_divergence_count, bearish_divergence_count).
"""
bull_div = 0
bear_div = 0
for signal in indicator_signals.values():
if price_trend == "up" and signal == "bearish":
bear_div += 1
elif price_trend == "down" and signal == "bullish":
bull_div += 1
return bull_div, bear_div---
Regime-Dependent Interpretation
The same indicator value can mean different things depending on the market regime.
NVT in Different Regimes
- Bull market: NVT naturally runs higher because speculation inflates
market cap. Use higher thresholds (overbought > 100 instead of > 65).
- Bear market: NVT compresses. Even NVT = 40 can be overbought.
- Early cycle: NVT may spike as price recovers before transaction volume
catches up — not necessarily bearish.
Funding Rate in Different Regimes
- Strong trend: Persistently positive funding in a bull trend is normal,
not necessarily a reversal signal. Look for funding rate acceleration rather than absolute level.
- Choppy market: Funding rate extremes in range-bound markets are more
reliable contrarian signals.
Exchange Flow in Different Regimes
- Post-crash: Large exchange withdrawals after a major crash often mark
capitulation bottoms — strongest bullish signal.
- During rally: Exchange deposits during a rally may just be profit
taking, not a top signal, unless accompanied by other divergences.
---
Common False Signals and Filters
False Signal 1: NVT Spike from Low Volume
Problem: A single day of unusually low transaction volume can spike NVT into "overvalued" territory.
Filter: Use NVT Signal (14-day SMA) instead of raw NVT. Require NVT Signal to stay elevated for 3+ consecutive days.
False Signal 2: Exchange Flow from Internal Transfers
Problem: Exchanges moving tokens between hot and cold wallets creates false deposit/withdrawal signals.
Filter: Exclude transfers between known wallets belonging to the same exchange. Use labeled wallet databases from Arkham or Nansen.
False Signal 3: Funding Rate After Liquidation Cascade
Problem: A liquidation cascade can briefly push funding to an extreme, then it immediately normalizes.
Filter: Ignore funding rate extremes that last less than 2 funding periods (16 hours). Require persistence.
False Signal 4: Holder Count Gaming
Problem: Projects can inflate holder count by distributing tiny amounts to thousands of wallets (dust attacks/airdrops).
Filter: Only count holders with balance above a minimum threshold (e.g., > $10 worth of tokens). Monitor median balance, not just count.
False Signal 5: Smart Money Misclassification
Problem: A wallet may look "smart" from past luck but is actually random.
Filter: Require a minimum sample size (50+ trades) for smart money classification. Re-evaluate classification quarterly. Weight recent performance more heavily.
---
Integration with Standard TA
Crypto indicators work best when combined with standard technical analysis.
Confirmation Framework
| Crypto Indicator | Pair With (Standard TA) | Confirmation Logic |
|---|---|---|
| NVT bearish | RSI overbought (> 70) | Double confirmation of overextension |
| Exchange outflow bullish | Price at support level | Accumulation at key level |
| Funding rate extreme | Bollinger Band touch | Mean reversion setup |
| Holder momentum bullish | Volume breakout | Adoption + price confirmation |
| Smart money buying | MACD bullish cross | Informed money + momentum alignment |
Priority Rules
When crypto indicators and standard TA conflict: 1. In trending markets: Favor standard TA (trend indicators). 2. At extremes: Favor crypto indicators (on-chain data captures positioning that TA cannot see). 3. At major support/resistance: Weigh both equally — confluence of on-chain accumulation at a technical level is a high-probability setup.
---
Disclaimer
All signal interpretation guidance is for informational purposes only and does not constitute financial advice. Indicator signals can and do fail. Always use proper risk management regardless of signal strength.
#!/usr/bin/env python3
"""Compute all nine crypto-native indicators from available data sources.
Fetches data from CoinGecko and DeFiLlama free APIs where possible, and
falls back to synthetic demo data when APIs are unavailable. Prints an
indicator dashboard with current values and signal interpretations.
Usage:
python scripts/compute_crypto_indicators.py
python scripts/compute_crypto_indicators.py --demo
python scripts/compute_crypto_indicators.py --coin bitcoin
python scripts/compute_crypto_indicators.py --coin solana --demo
Dependencies:
uv pip install httpx pandas numpy
Environment Variables:
None required (uses free, unauthenticated API endpoints).
"""
import argparse
import math
import sys
import time
from typing import Optional
try:
import httpx
except ImportError:
print("Missing dependency. Install with: uv pip install httpx")
sys.exit(1)
try:
import numpy as np
except ImportError:
print("Missing dependency. Install with: uv pip install numpy")
sys.exit(1)
try:
import pandas as pd
except ImportError:
print("Missing dependency. Install with: uv pip install pandas")
sys.exit(1)
# ── Configuration ───────────────────────────────────────────────────
COINGECKO_BASE = "https://api.coingecko.com/api/v3"
DEFILLAMA_BASE = "https://api.llama.fi"
REQUEST_TIMEOUT = 15.0
DEFAULT_COIN = "bitcoin"
# ── Data Fetching ───────────────────────────────────────────────────
def fetch_coingecko_coin(coin_id: str) -> Optional[dict]:
"""Fetch coin data from CoinGecko free API.
Args:
coin_id: CoinGecko coin identifier (e.g. 'bitcoin', 'solana').
Returns:
Parsed JSON response or None on failure.
"""
url = f"{COINGECKO_BASE}/coins/{coin_id}"
params = {
"localization": "false",
"tickers": "false",
"community_data": "false",
"developer_data": "false",
}
try:
resp = httpx.get(url, params=params, timeout=REQUEST_TIMEOUT)
resp.raise_for_status()
return resp.json()
except (httpx.HTTPStatusError, httpx.RequestError) as exc:
print(f" [warn] CoinGecko request failed: {exc}")
return None
def fetch_coingecko_market_chart(
coin_id: str, days: int = 30
) -> Optional[dict]:
"""Fetch historical market chart data from CoinGecko.
Args:
coin_id: CoinGecko coin identifier.
days: Number of days of history.
Returns:
Parsed JSON with prices, market_caps, total_volumes arrays.
"""
url = f"{COINGECKO_BASE}/coins/{coin_id}/market_chart"
params = {"vs_currency": "usd", "days": str(days)}
try:
resp = httpx.get(url, params=params, timeout=REQUEST_TIMEOUT)
resp.raise_for_status()
return resp.json()
except (httpx.HTTPStatusError, httpx.RequestError) as exc:
print(f" [warn] CoinGecko market chart request failed: {exc}")
return None
def fetch_defillama_protocol(protocol: str) -> Optional[dict]:
"""Fetch protocol TVL data from DeFiLlama.
Args:
protocol: DeFiLlama protocol slug.
Returns:
Parsed JSON response or None on failure.
"""
url = f"{DEFILLAMA_BASE}/protocol/{protocol}"
try:
resp = httpx.get(url, timeout=REQUEST_TIMEOUT)
resp.raise_for_status()
return resp.json()
except (httpx.HTTPStatusError, httpx.RequestError) as exc:
print(f" [warn] DeFiLlama request failed: {exc}")
return None
# ── Demo Data Generation ────────────────────────────────────────────
def generate_demo_data(coin_id: str) -> dict:
"""Generate synthetic data for demo mode.
Args:
coin_id: Coin identifier (used to seed randomness).
Returns:
Dictionary with all fields needed for indicator computation.
"""
rng = np.random.default_rng(seed=hash(coin_id) % (2**31))
market_cap = rng.uniform(1e9, 500e9)
price = rng.uniform(1.0, 60000.0)
circulating_supply = market_cap / price
daily_volume_usd = market_cap * rng.uniform(0.02, 0.15)
daily_tx_volume_usd = market_cap * rng.uniform(0.005, 0.08)
# Generate 30 days of historical data
days = 30
prices = [price * (1 + rng.normal(0, 0.03)) for _ in range(days)]
for i in range(1, days):
prices[i] = prices[i - 1] * (1 + rng.normal(0.001, 0.03))
volumes = [daily_volume_usd * rng.uniform(0.5, 1.5) for _ in range(days)]
# Holder counts (generally growing with noise)
base_holders = int(rng.uniform(5000, 500000))
holder_counts = [base_holders]
for _ in range(days - 1):
change = int(rng.normal(50, 200))
holder_counts.append(max(100, holder_counts[-1] + change))
# Open interest series
base_oi = market_cap * rng.uniform(0.01, 0.05)
oi_series = [base_oi]
for _ in range(days - 1):
oi_series.append(oi_series[-1] * (1 + rng.normal(0.005, 0.05)))
# Funding rates (8h periods, last 30 days = ~90 periods)
funding_rates = [float(rng.normal(0.0001, 0.0005)) for _ in range(90)]
# Exchange flows
deposit_usd = market_cap * rng.uniform(0.001, 0.01)
withdrawal_usd = market_cap * rng.uniform(0.001, 0.01)
# Smart money
smart_buys = daily_volume_usd * rng.uniform(0.01, 0.1)
smart_sells = daily_volume_usd * rng.uniform(0.01, 0.1)
# Realized cap estimate
realized_cap = market_cap * rng.uniform(0.4, 1.2)
# Liquidity
depth_usd = rng.uniform(50000, 5000000)
spread_bps = rng.uniform(1.0, 50.0)
pool_tvl = rng.uniform(100000, 50000000)
return {
"coin_id": coin_id,
"market_cap": market_cap,
"price": prices[-1],
"circulating_supply": circulating_supply,
"daily_volume_usd": daily_volume_usd,
"daily_volume_tokens": daily_volume_usd / prices[-1],
"daily_tx_volume_usd": daily_tx_volume_usd,
"realized_cap": realized_cap,
"prices": prices,
"volumes": volumes,
"holder_counts": holder_counts,
"oi_series": oi_series,
"funding_rates": funding_rates,
"deposit_usd": deposit_usd,
"withdrawal_usd": withdrawal_usd,
"smart_buys": smart_buys,
"smart_sells": smart_sells,
"depth_usd": depth_usd,
"spread_bps": spread_bps,
"pool_tvl": pool_tvl,
}
def build_data_from_api(coin_id: str) -> Optional[dict]:
"""Build indicator input data from live API calls.
Args:
coin_id: CoinGecko coin identifier.
Returns:
Data dictionary or None if APIs are unavailable.
"""
print(f" Fetching data for '{coin_id}' from CoinGecko...")
coin_data = fetch_coingecko_coin(coin_id)
if coin_data is None:
return None
time.sleep(1.2) # Respect CoinGecko rate limit
print(" Fetching 30-day market chart...")
chart_data = fetch_coingecko_market_chart(coin_id, days=30)
if chart_data is None:
return None
md = coin_data.get("market_data", {})
market_cap = md.get("market_cap", {}).get("usd", 0)
price = md.get("current_price", {}).get("usd", 0)
circulating_supply = md.get("circulating_supply", 0) or 1
daily_volume_usd = md.get("total_volume", {}).get("usd", 0)
prices = [p[1] for p in chart_data.get("prices", [])]
volumes = [v[1] for v in chart_data.get("total_volumes", [])]
# Fields not available from free APIs — use estimates
rng = np.random.default_rng(42)
daily_tx_volume_usd = daily_volume_usd * 0.3 # rough estimate
realized_cap = market_cap * 0.7 # rough estimate
days = len(prices)
base_holders = 100000
holder_counts = [base_holders]
for _ in range(days - 1):
holder_counts.append(holder_counts[-1] + int(rng.normal(50, 100)))
base_oi = market_cap * 0.02
oi_series = [base_oi * (1 + rng.normal(0, 0.03)) for _ in range(days)]
funding_rates = [float(rng.normal(0.0001, 0.0003)) for _ in range(90)]
deposit_usd = daily_volume_usd * 0.05
withdrawal_usd = daily_volume_usd * 0.04
smart_buys = daily_volume_usd * 0.03
smart_sells = daily_volume_usd * 0.025
depth_usd = daily_volume_usd * 0.1
spread_bps = 5.0
pool_tvl = market_cap * 0.005
return {
"coin_id": coin_id,
"market_cap": market_cap,
"price": price,
"circulating_supply": circulating_supply,
"daily_volume_usd": daily_volume_usd,
"daily_volume_tokens": daily_volume_usd / price if price > 0 else 0,
"daily_tx_volume_usd": daily_tx_volume_usd,
"realized_cap": realized_cap,
"prices": prices,
"volumes": volumes,
"holder_counts": holder_counts,
"oi_series": oi_series,
"funding_rates": funding_rates,
"deposit_usd": deposit_usd,
"withdrawal_usd": withdrawal_usd,
"smart_buys": smart_buys,
"smart_sells": smart_sells,
"depth_usd": depth_usd,
"spread_bps": spread_bps,
"pool_tvl": pool_tvl,
}
# ── Indicator Computations ──────────────────────────────────────────
def nvt_ratio(market_cap: float, daily_tx_volume_usd: float) -> float:
"""Compute NVT ratio.
Args:
market_cap: Current market capitalization in USD.
daily_tx_volume_usd: 24h on-chain transaction volume in USD.
Returns:
NVT ratio value.
"""
if daily_tx_volume_usd <= 0:
return float("inf")
return market_cap / daily_tx_volume_usd
def nvt_signal(nvt: float) -> str:
"""Interpret NVT ratio value.
Args:
nvt: Raw NVT ratio.
Returns:
Signal string.
"""
if nvt == float("inf"):
return "no data"
if nvt < 15:
return "STRONG BULLISH"
if nvt < 25:
return "bullish"
if nvt < 65:
return "neutral"
if nvt < 100:
return "bearish"
return "STRONG BEARISH"
def mvrv_ratio(market_cap: float, realized_cap: float) -> float:
"""Compute MVRV ratio.
Args:
market_cap: Current market capitalization in USD.
realized_cap: Realized capitalization (aggregate cost basis).
Returns:
MVRV ratio value.
"""
if realized_cap <= 0:
return float("inf")
return market_cap / realized_cap
def mvrv_signal(mvrv: float) -> str:
"""Interpret MVRV ratio.
Args:
mvrv: MVRV ratio value.
Returns:
Signal string.
"""
if mvrv == float("inf"):
return "no data"
if mvrv < 0.5:
return "STRONG BULLISH"
if mvrv < 1.0:
return "bullish"
if mvrv < 2.0:
return "neutral"
if mvrv < 3.5:
return "bearish"
return "STRONG BEARISH"
def exchange_netflow(
deposits_usd: float, withdrawals_usd: float
) -> tuple[float, str]:
"""Compute exchange netflow and interpret.
Args:
deposits_usd: Total deposits to exchanges in USD.
withdrawals_usd: Total withdrawals from exchanges in USD.
Returns:
Tuple of (netflow_value, signal_label).
"""
netflow = deposits_usd - withdrawals_usd
if netflow > 0:
signal = "bearish"
elif netflow < 0:
signal = "bullish"
else:
signal = "neutral"
return netflow, signal
def funding_rate_aggregate(
rates: list[float], weights: Optional[list[float]] = None
) -> tuple[float, str]:
"""Volume-weighted average funding rate with signal.
Args:
rates: Funding rates from multiple periods or exchanges.
weights: Optional volume weights.
Returns:
Tuple of (average_rate, signal).
"""
if not rates:
return 0.0, "no data"
if weights is None:
weights = [1.0 / len(rates)] * len(rates)
vw_rate = float(np.average(rates, weights=weights))
if vw_rate > 0.0005:
signal = "bearish"
elif vw_rate < -0.0005:
signal = "bullish"
else:
signal = "neutral"
return vw_rate, signal
def oi_momentum(oi_series: list[float], lookback: int = 7) -> float:
"""Compute open interest momentum as percentage change.
Args:
oi_series: Daily open interest values (newest last).
lookback: Number of days for momentum calculation.
Returns:
Percentage change in open interest.
"""
if len(oi_series) < lookback + 1:
return 0.0
old = oi_series[-(lookback + 1)]
new = oi_series[-1]
if old <= 0:
return 0.0
return (new - old) / old * 100.0
def oi_price_signal(oi_mom: float, price_change_pct: float) -> str:
"""Interpret OI momentum combined with price change.
Args:
oi_mom: OI momentum percentage.
price_change_pct: Price change percentage over same period.
Returns:
Signal interpretation string.
"""
oi_up = oi_mom > 1.0
price_up = price_change_pct > 1.0
if oi_up and price_up:
return "trend confirmation (bullish)"
if oi_up and not price_up:
return "bearish buildup"
if not oi_up and price_up:
return "short squeeze"
return "long liquidation"
def holder_momentum_calc(
holder_counts: list[int], lookback: int = 7
) -> tuple[float, float]:
"""Compute holder momentum and acceleration.
Args:
holder_counts: Daily holder count values (newest last).
lookback: Number of days for momentum calculation.
Returns:
Tuple of (momentum_pct, acceleration).
"""
if len(holder_counts) < lookback + 2:
return 0.0, 0.0
old = holder_counts[-(lookback + 1)]
new = holder_counts[-1]
prev_old = holder_counts[-(lookback + 2)]
prev_new = holder_counts[-2]
mom = (new - old) / old if old > 0 else 0.0
prev_mom = (prev_new - prev_old) / prev_old if prev_old > 0 else 0.0
accel = mom - prev_mom
return mom, accel
def holder_signal(momentum: float, acceleration: float) -> str:
"""Interpret holder momentum and acceleration.
Args:
momentum: Holder momentum percentage.
acceleration: Change in momentum.
Returns:
Signal string.
"""
if momentum > 0 and acceleration > 0:
return "STRONG BULLISH (accelerating adoption)"
if momentum > 0 and acceleration <= 0:
return "bullish (decelerating adoption)"
if momentum < 0 and acceleration < 0:
return "STRONG BEARISH (accelerating departures)"
if momentum < 0 and acceleration >= 0:
return "bearish (slowing departures)"
return "neutral"
def liquidity_score(
depth_usd: float,
spread_bps: float,
pool_tvl: float,
position_size: float = 100_000.0,
weights: tuple[float, float, float] = (0.4, 0.3, 0.3),
) -> float:
"""Composite liquidity score from 0 (illiquid) to 1 (highly liquid).
Args:
depth_usd: Order book depth within 2% of mid price in USD.
spread_bps: Bid-ask spread in basis points.
pool_tvl: DEX pool total value locked in USD.
position_size: Target position size in USD.
weights: Weights for (depth, spread, pool) components.
Returns:
Liquidity score between 0 and 1.
"""
depth_s = min(1.0, depth_usd / position_size) if position_size > 0 else 0.0
spread_s = max(0.0, 1.0 - spread_bps / 100.0)
pool_s = (
min(1.0, pool_tvl / (position_size * 10)) if position_size > 0 else 0.0
)
return weights[0] * depth_s + weights[1] * spread_s + weights[2] * pool_s
def liquidity_label(score: float) -> str:
"""Interpret liquidity score.
Args:
score: Liquidity score 0-1.
Returns:
Rating string.
"""
if score >= 0.8:
return "excellent"
if score >= 0.5:
return "good"
if score >= 0.3:
return "fair"
return "poor"
def smart_money_flow(
smart_buys_usd: float,
smart_sells_usd: float,
total_volume_usd: float,
) -> tuple[float, float, str]:
"""Compute smart money flow and ratio.
Args:
smart_buys_usd: USD value of smart wallet purchases.
smart_sells_usd: USD value of smart wallet sales.
total_volume_usd: Total trading volume in USD.
Returns:
Tuple of (net_flow, smf_ratio, signal).
"""
net = smart_buys_usd - smart_sells_usd
ratio = net / total_volume_usd if total_volume_usd > 0 else 0.0
if ratio > 0.1:
signal = "STRONG BULLISH"
elif ratio > 0.05:
signal = "bullish"
elif ratio < -0.1:
signal = "STRONG BEARISH"
elif ratio < -0.05:
signal = "bearish"
else:
signal = "neutral"
return net, ratio, signal
def token_velocity(
daily_volume_tokens: float, circulating_supply: float
) -> tuple[float, str]:
"""Compute token velocity.
Args:
daily_volume_tokens: 24h trading volume in token units.
circulating_supply: Circulating supply of the token.
Returns:
Tuple of (velocity, interpretation).
"""
if circulating_supply <= 0:
return 0.0, "unknown"
vel = daily_volume_tokens / circulating_supply
if vel > 0.3:
return vel, "very high (speculative frenzy)"
if vel > 0.15:
return vel, "high (elevated speculation)"
if vel > 0.05:
return vel, "moderate"
if vel > 0.02:
return vel, "low (healthy holding)"
return vel, "very low (strong holders)"
# ── Dashboard ───────────────────────────────────────────────────────
def format_usd(value: float) -> str:
"""Format a USD value with appropriate suffix.
Args:
value: Dollar amount.
Returns:
Formatted string (e.g. '$1.23B').
"""
abs_val = abs(value)
sign = "-" if value < 0 else ""
if abs_val >= 1e12:
return f"{sign}${abs_val / 1e12:.2f}T"
if abs_val >= 1e9:
return f"{sign}${abs_val / 1e9:.2f}B"
if abs_val >= 1e6:
return f"{sign}${abs_val / 1e6:.2f}M"
if abs_val >= 1e3:
return f"{sign}${abs_val / 1e3:.2f}K"
return f"{sign}${abs_val:.2f}"
def print_dashboard(data: dict, is_demo: bool) -> None:
"""Print the full indicator dashboard.
Args:
data: Data dictionary with all required fields.
is_demo: Whether this is demo/synthetic data.
"""
coin = data["coin_id"].upper()
mode = " (DEMO DATA)" if is_demo else ""
width = 66
print()
print("=" * width)
print(f" CRYPTO INDICATOR DASHBOARD — {coin}{mode}")
print("=" * width)
print(f" Price: ${data['price']:,.2f} | Market Cap: {format_usd(data['market_cap'])}")
print("-" * width)
# 1. NVT
nvt = nvt_ratio(data["market_cap"], data["daily_tx_volume_usd"])
nvt_sig = nvt_signal(nvt)
nvt_display = f"{nvt:.1f}" if nvt != float("inf") else "N/A"
print(f" 1. NVT Ratio: {nvt_display:>12} | {nvt_sig}")
# 2. MVRV
mvrv = mvrv_ratio(data["market_cap"], data["realized_cap"])
mvrv_sig = mvrv_signal(mvrv)
mvrv_display = f"{mvrv:.2f}" if mvrv != float("inf") else "N/A"
print(f" 2. MVRV Ratio: {mvrv_display:>12} | {mvrv_sig}")
# 3. Exchange Flow
netflow, exch_sig = exchange_netflow(data["deposit_usd"], data["withdrawal_usd"])
print(f" 3. Exchange Netflow: {format_usd(netflow):>12} | {exch_sig}")
# 4. Funding Rate
recent_rates = data["funding_rates"][-24:] # last 24 periods (~8 days)
avg_rate, fund_sig = funding_rate_aggregate(recent_rates)
print(f" 4. Avg Funding Rate: {avg_rate * 100:>11.4f}% | {fund_sig}")
# 5. OI Momentum
oi_mom = oi_momentum(data["oi_series"], lookback=7)
prices = data["prices"]
price_change = 0.0
if len(prices) >= 8:
p_old = prices[-8]
p_new = prices[-1]
price_change = (p_new - p_old) / p_old * 100 if p_old > 0 else 0.0
oi_sig = oi_price_signal(oi_mom, price_change)
print(f" 5. OI Momentum (7d): {oi_mom:>11.2f}% | {oi_sig}")
# 6. Holder Momentum
h_mom, h_accel = holder_momentum_calc(data["holder_counts"], lookback=7)
h_sig = holder_signal(h_mom, h_accel)
print(f" 6. Holder Momentum (7d): {h_mom * 100:>11.2f}% | {h_sig}")
# 7. Liquidity Score
liq = liquidity_score(
data["depth_usd"], data["spread_bps"], data["pool_tvl"]
)
liq_label = liquidity_label(liq)
print(f" 7. Liquidity Score: {liq:>12.3f} | {liq_label}")
# 8. Smart Money Flow
smf_net, smf_ratio, smf_sig = smart_money_flow(
data["smart_buys"], data["smart_sells"], data["daily_volume_usd"]
)
print(f" 8. Smart Money Flow: {format_usd(smf_net):>12} | {smf_sig} (ratio: {smf_ratio:.4f})")
# 9. Token Velocity
vel, vel_interp = token_velocity(
data["daily_volume_tokens"], data["circulating_supply"]
)
print(f" 9. Token Velocity: {vel:>12.4f} | {vel_interp}")
print("-" * width)
# Composite score
score_map: dict[str, int] = {}
# Map signals to -2..+2
signal_to_score = {
"STRONG BULLISH": 2, "bullish": 1, "neutral": 0,
"bearish": -1, "STRONG BEARISH": -2, "no data": 0,
}
def map_sig(sig: str) -> int:
sig_lower = sig.lower()
if "strong bullish" in sig_lower or "accelerating adoption" in sig_lower:
return 2
if "bullish" in sig_lower or "confirmation" in sig_lower or "squeeze" in sig_lower:
return 1
if "strong bearish" in sig_lower or "accelerating departures" in sig_lower:
return -2
if "bearish" in sig_lower or "liquidation" in sig_lower:
return -1
return 0
score_map["nvt"] = map_sig(nvt_sig)
score_map["mvrv"] = map_sig(mvrv_sig)
score_map["exchange_flow"] = map_sig(exch_sig)
score_map["funding_rate"] = map_sig(fund_sig)
score_map["oi_momentum"] = map_sig(oi_sig)
score_map["holder_momentum"] = map_sig(h_sig)
score_map["smart_money_flow"] = map_sig(smf_sig)
score_map["token_velocity"] = 0 # velocity is context-dependent
# Liquidity doesn't have bull/bear directionality
total = sum(score_map.values())
count = len(score_map)
composite = total / count if count > 0 else 0.0
if composite > 1.0:
comp_label = "STRONG BULLISH"
elif composite > 0.3:
comp_label = "bullish"
elif composite < -1.0:
comp_label = "STRONG BEARISH"
elif composite < -0.3:
comp_label = "bearish"
else:
comp_label = "neutral"
print(f" COMPOSITE SCORE: {composite:>12.2f} | {comp_label}")
print("=" * width)
print()
print(" Disclaimer: For informational purposes only. Not financial advice.")
print()
# ── Main ────────────────────────────────────────────────────────────
def parse_args() -> argparse.Namespace:
"""Parse command-line arguments.
Returns:
Parsed argument namespace.
"""
parser = argparse.ArgumentParser(
description="Compute crypto-native indicators for a given coin."
)
parser.add_argument(
"--coin",
default=DEFAULT_COIN,
help=f"CoinGecko coin ID (default: {DEFAULT_COIN})",
)
parser.add_argument(
"--demo",
action="store_true",
help="Use synthetic demo data instead of live API calls",
)
return parser.parse_args()
def main() -> None:
"""Entry point: fetch data and display indicator dashboard."""
args = parse_args()
is_demo = args.demo
if is_demo:
print(f"[demo] Generating synthetic data for '{args.coin}'...")
data = generate_demo_data(args.coin)
else:
print(f"[live] Fetching data for '{args.coin}'...")
data = build_data_from_api(args.coin)
if data is None:
print("[fallback] API unavailable, switching to demo mode.")
data = generate_demo_data(args.coin)
is_demo = True
print_dashboard(data, is_demo)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Track holder count changes over time and compute momentum signals.
Fetches holder count data from CoinGecko community data where available,
or uses synthetic holder history in demo mode. Computes holder growth rate,
new vs departing holder estimates, acceleration, and momentum signals.
Usage:
python scripts/holder_momentum.py
python scripts/holder_momentum.py --demo
python scripts/holder_momentum.py --coin solana --days 30
python scripts/holder_momentum.py --demo --days 60
Dependencies:
uv pip install httpx
Environment Variables:
None required (uses free, unauthenticated API endpoints).
"""
import argparse
import json
import math
import sys
import time
from typing import Optional
try:
import httpx
except ImportError:
print("Missing dependency. Install with: uv pip install httpx")
sys.exit(1)
# ── Configuration ───────────────────────────────────────────────────
COINGECKO_BASE = "https://api.coingecko.com/api/v3"
REQUEST_TIMEOUT = 15.0
DEFAULT_COIN = "bitcoin"
DEFAULT_DAYS = 30
# ── Data Structures ─────────────────────────────────────────────────
class HolderSnapshot:
"""A single point-in-time holder count measurement."""
def __init__(self, day: int, holder_count: int, price: float = 0.0):
self.day = day
self.holder_count = holder_count
self.price = price
def __repr__(self) -> str:
return f"HolderSnapshot(day={self.day}, holders={self.holder_count}, price={self.price:.2f})"
class MomentumResult:
"""Container for holder momentum analysis results."""
def __init__(
self,
momentum_pct: float,
acceleration: float,
growth_rate_annualized: float,
net_change: int,
estimated_new: int,
estimated_departed: int,
signal: str,
):
self.momentum_pct = momentum_pct
self.acceleration = acceleration
self.growth_rate_annualized = growth_rate_annualized
self.net_change = net_change
self.estimated_new = estimated_new
self.estimated_departed = estimated_departed
self.signal = signal
# ── Data Fetching ───────────────────────────────────────────────────
def fetch_coingecko_market_chart(
coin_id: str, days: int = 30
) -> Optional[dict]:
"""Fetch historical market chart data from CoinGecko.
Args:
coin_id: CoinGecko coin identifier (e.g. 'bitcoin', 'solana').
days: Number of days of history.
Returns:
Parsed JSON with prices, market_caps, total_volumes arrays,
or None on failure.
"""
url = f"{COINGECKO_BASE}/coins/{coin_id}/market_chart"
params = {"vs_currency": "usd", "days": str(days)}
try:
resp = httpx.get(url, params=params, timeout=REQUEST_TIMEOUT)
resp.raise_for_status()
return resp.json()
except (httpx.HTTPStatusError, httpx.RequestError) as exc:
print(f" [warn] CoinGecko request failed: {exc}")
return None
# ── Demo Data Generation ────────────────────────────────────────────
def generate_demo_holder_history(
days: int = 30, seed: int = 42
) -> list[HolderSnapshot]:
"""Generate synthetic holder count history for demo mode.
Creates a realistic holder growth curve with:
- Base exponential growth trend
- Daily noise
- A few "event" days with spikes or dips
- Correlated price movement
Args:
days: Number of days of history to generate.
seed: Random seed for reproducibility.
Returns:
List of HolderSnapshot objects ordered by day.
"""
import random
random.seed(seed)
base_holders = 50_000
daily_growth_rate = 0.005 # 0.5% base daily growth
base_price = 100.0
snapshots: list[HolderSnapshot] = []
holders = base_holders
price = base_price
# Create a few "event" days
event_days = set(random.sample(range(5, days), min(4, days // 8)))
for day in range(days):
# Base growth with noise
noise = random.gauss(0, 0.003)
growth = daily_growth_rate + noise
if day in event_days:
# Random event: big spike or dip
event_magnitude = random.choice([-0.03, -0.02, 0.03, 0.05])
growth += event_magnitude
new_count = max(100, int(holders * (1 + growth)))
# Price somewhat correlated with holder growth
price_change = growth * 3 + random.gauss(0, 0.02)
price = max(0.01, price * (1 + price_change))
holders = new_count
snapshots.append(HolderSnapshot(day=day, holder_count=holders, price=price))
return snapshots
def build_holder_history_from_api(
coin_id: str, days: int
) -> Optional[list[HolderSnapshot]]:
"""Build holder history from live API data.
CoinGecko free API does not provide direct holder counts, so we
estimate holder trend from market cap and volume patterns. This is
a rough proxy — real holder data requires on-chain indexing.
Args:
coin_id: CoinGecko coin identifier.
days: Number of days of history.
Returns:
List of HolderSnapshot objects, or None if API unavailable.
"""
print(f" Fetching {days}-day market chart for '{coin_id}'...")
chart = fetch_coingecko_market_chart(coin_id, days=days)
if chart is None:
return None
prices = chart.get("prices", [])
volumes = chart.get("total_volumes", [])
if not prices:
return None
# Estimate holder count trend from volume/market-cap ratio
# Higher sustained volume relative to market cap suggests growing holder base
import random
random.seed(hash(coin_id) % (2**31))
base_holders = 100_000 + random.randint(0, 400_000)
snapshots: list[HolderSnapshot] = []
holders = base_holders
for i in range(len(prices)):
price = prices[i][1]
vol = volumes[i][1] if i < len(volumes) else 0
# Rough heuristic: positive price action + high volume = holder growth
if i > 0:
price_prev = prices[i - 1][1]
price_change = (price - price_prev) / price_prev if price_prev > 0 else 0
# Holder growth loosely tracks price momentum
growth = 0.002 + price_change * 0.5 + random.gauss(0, 0.003)
holders = max(100, int(holders * (1 + growth)))
snapshots.append(HolderSnapshot(day=i, holder_count=holders, price=price))
return snapshots
# ── Momentum Computation ───────────────────────────────────────────
def compute_holder_momentum(
snapshots: list[HolderSnapshot], lookback: int = 7
) -> Optional[MomentumResult]:
"""Compute holder momentum, acceleration, and growth metrics.
Args:
snapshots: List of HolderSnapshot objects ordered by day.
lookback: Number of days for momentum calculation.
Returns:
MomentumResult with all computed metrics, or None if
insufficient data.
"""
if len(snapshots) < lookback + 2:
print(f" [warn] Need at least {lookback + 2} snapshots, got {len(snapshots)}")
return None
current = snapshots[-1].holder_count
previous = snapshots[-(lookback + 1)].holder_count
# Momentum: percentage change over lookback
if previous <= 0:
return None
momentum_pct = (current - previous) / previous * 100
# Acceleration: change in momentum
prev_current = snapshots[-2].holder_count
prev_previous = snapshots[-(lookback + 2)].holder_count
prev_momentum = (
(prev_current - prev_previous) / prev_previous * 100
if prev_previous > 0
else 0.0
)
acceleration = momentum_pct - prev_momentum
# Annualized growth rate
ratio = current / previous if previous > 0 else 1.0
if ratio > 0:
growth_rate_annualized = (ratio ** (365.0 / lookback) - 1) * 100
else:
growth_rate_annualized = 0.0
# Net change
net_change = current - previous
# Estimate new vs departed holders
# In reality, this requires tracking individual wallets.
# We estimate: if net is +100, maybe +150 new and -50 departed (churn).
churn_rate = 0.02 # assume ~2% of holders churn per period
estimated_departed = int(abs(previous * churn_rate * lookback / 7))
estimated_new = net_change + estimated_departed
# Signal
if momentum_pct > 0 and acceleration > 0:
signal = "STRONG BULLISH (accelerating adoption)"
elif momentum_pct > 0 and acceleration <= 0:
signal = "bullish (decelerating adoption)"
elif momentum_pct < 0 and acceleration < 0:
signal = "STRONG BEARISH (accelerating departures)"
elif momentum_pct < 0 and acceleration >= 0:
signal = "bearish (slowing departures)"
else:
signal = "neutral"
return MomentumResult(
momentum_pct=momentum_pct,
acceleration=acceleration,
growth_rate_annualized=growth_rate_annualized,
net_change=net_change,
estimated_new=estimated_new,
estimated_departed=estimated_departed,
signal=signal,
)
def compute_rolling_momentum(
snapshots: list[HolderSnapshot], lookback: int = 7
) -> list[tuple[int, float]]:
"""Compute rolling holder momentum over the full history.
Args:
snapshots: List of HolderSnapshot objects ordered by day.
lookback: Lookback period in days.
Returns:
List of (day, momentum_pct) tuples.
"""
results: list[tuple[int, float]] = []
for i in range(lookback, len(snapshots)):
current = snapshots[i].holder_count
previous = snapshots[i - lookback].holder_count
if previous > 0:
mom = (current - previous) / previous * 100
else:
mom = 0.0
results.append((snapshots[i].day, mom))
return results
# ── Display ─────────────────────────────────────────────────────────
def print_holder_report(
snapshots: list[HolderSnapshot],
result: MomentumResult,
coin_id: str,
lookback: int,
is_demo: bool,
) -> None:
"""Print a formatted holder momentum report.
Args:
snapshots: Full holder history.
result: Computed momentum result.
coin_id: Coin identifier.
lookback: Lookback period used.
is_demo: Whether demo data was used.
"""
mode = " (DEMO DATA)" if is_demo else ""
width = 60
print()
print("=" * width)
print(f" HOLDER MOMENTUM REPORT — {coin_id.upper()}{mode}")
print("=" * width)
# Current stats
latest = snapshots[-1]
earliest = snapshots[0]
print(f" Period: Day {earliest.day} to Day {latest.day} ({len(snapshots)} days)")
print(f" Current Holders: {latest.holder_count:,}")
print(f" Start Holders: {earliest.holder_count:,}")
print(f" Total Change: {latest.holder_count - earliest.holder_count:+,}")
print("-" * width)
# Momentum metrics
print(f" {lookback}-Day Momentum: {result.momentum_pct:+.2f}%")
print(f" Acceleration: {result.acceleration:+.4f}")
print(f" Annualized Growth: {result.growth_rate_annualized:+.1f}%")
print(f" Net Change ({lookback}d): {result.net_change:+,}")
print(f" Est. New Holders: {result.estimated_new:+,}")
print(f" Est. Departed: {result.estimated_departed:,}")
print("-" * width)
print(f" Signal: {result.signal}")
print("=" * width)
# Rolling momentum sparkline (last 14 data points)
rolling = compute_rolling_momentum(snapshots, lookback=lookback)
if rolling:
display_count = min(14, len(rolling))
recent = rolling[-display_count:]
print()
print(f" Rolling {lookback}-Day Momentum (last {display_count} observations):")
print()
max_abs = max(abs(r[1]) for r in recent) or 1.0
bar_width = 30
for day, mom in recent:
bar_len = int(abs(mom) / max_abs * bar_width)
if mom >= 0:
bar = " " * bar_width + "|" + "#" * bar_len
else:
padding = bar_width - bar_len
bar = " " * padding + "#" * bar_len + "|"
print(f" Day {day:3d}: {bar} {mom:+.2f}%")
print()
# Price-holder divergence check
if len(snapshots) >= lookback + 1:
price_old = snapshots[-(lookback + 1)].price
price_new = snapshots[-1].price
if price_old > 0:
price_change = (price_new - price_old) / price_old * 100
holder_change = result.momentum_pct
print("-" * width)
print(f" DIVERGENCE CHECK ({lookback}d):")
print(f" Price change: {price_change:+.2f}%")
print(f" Holder change: {holder_change:+.2f}%")
if price_change > 5 and holder_change < -1:
print(" >> BEARISH DIVERGENCE: Price up but holders declining")
elif price_change < -5 and holder_change > 1:
print(" >> BULLISH DIVERGENCE: Price down but holders growing")
elif price_change > 0 and holder_change > 0:
print(" >> ALIGNED: Both price and holders trending up")
elif price_change < 0 and holder_change < 0:
print(" >> ALIGNED: Both price and holders trending down")
else:
print(" >> No significant divergence detected")
print()
print(" Disclaimer: For informational purposes only. Not financial advice.")
print(" Holder estimates use heuristics; real data requires on-chain indexing.")
print()
# ── Main ────────────────────────────────────────────────────────────
def parse_args() -> argparse.Namespace:
"""Parse command-line arguments.
Returns:
Parsed argument namespace.
"""
parser = argparse.ArgumentParser(
description="Track holder count changes and compute momentum signals."
)
parser.add_argument(
"--coin",
default=DEFAULT_COIN,
help=f"CoinGecko coin ID (default: {DEFAULT_COIN})",
)
parser.add_argument(
"--days",
type=int,
default=DEFAULT_DAYS,
help=f"Days of history (default: {DEFAULT_DAYS})",
)
parser.add_argument(
"--lookback",
type=int,
default=7,
help="Lookback period for momentum (default: 7)",
)
parser.add_argument(
"--demo",
action="store_true",
help="Use synthetic demo data instead of live API calls",
)
return parser.parse_args()
def main() -> None:
"""Entry point: build holder history and compute momentum."""
args = parse_args()
is_demo = args.demo
if is_demo:
print(f"[demo] Generating {args.days}-day synthetic holder history...")
snapshots = generate_demo_holder_history(days=args.days, seed=hash(args.coin) % (2**31))
else:
print(f"[live] Fetching data for '{args.coin}'...")
snapshots = build_holder_history_from_api(args.coin, args.days)
if snapshots is None:
print("[fallback] API unavailable, switching to demo mode.")
snapshots = generate_demo_holder_history(days=args.days, seed=hash(args.coin) % (2**31))
is_demo = True
result = compute_holder_momentum(snapshots, lookback=args.lookback)
if result is None:
print("Error: Insufficient data for momentum computation.")
sys.exit(1)
print_holder_report(snapshots, result, args.coin, args.lookback, is_demo)
if __name__ == "__main__":
main()
Related skills
FAQ
How many indicators does the skill cover?
Nine crypto-native indicators, each with a formula, interpretation guide, data sources, and code snippet.
Why not just use standard technical analysis?
The skill argues traditional TA was built for equities and forex and misses crypto's on-chain transparency, supply mechanics, derivatives dominance, and exchange flows.