
Yield Analysis
- 193 installs
- 257 repo stars
- Updated June 24, 2026
- agiprolabs/claude-trading-skills
yield-analysis is a Claude Code skill that decomposes DeFi yield into its sources and computes real versus nominal APY net of impermanent loss, gas, and emission depreciation.
About
A Claude Code skill for evaluating DeFi yield opportunities accurately. It decomposes advertised APY into fee income, token emissions, lending interest, and staking rewards, then computes real yield net of impermanent loss, gas, and emission-token depreciation. Developers use it to compare pools and avoid high-APY yields that actually destroy capital.
- Decomposes DeFi yield into fee, emission, lending, and staking components
- Computes real vs nominal yield net of impermanent loss, gas, and depreciation
- Scores emission sustainability and flags death-spiral red flags
Yield Analysis by the numbers
- 193 all-time installs (skills.sh)
- Ranked #484 of 1,106 Finance & Trading skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
yield-analysis capabilities & compatibility
Free; the yield math runs locally in Python with pool inputs
- Capabilities
- yield analysis · defi analysis · impermanent loss estimation · apr calculation · sustainability scoring
- Use cases
- data analysis · research · trading
- Pricing
- Free
What yield-analysis says it does
DeFi yields are often misleading. A pool advertising 200% APY may deliver negative real returns once you account for impermanent loss, gas costs, and emission token depreciation.
Trading Fee Income Swap fees earned by liquidity providers. This is the most sustainable yield source because it comes from real economic activity.
The 45% nominal yield is really 23% after accounting for all factors.
npx skills add https://github.com/agiprolabs/claude-trading-skills --skill yield-analysisAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 193 |
|---|---|
| repo stars | ★ 257 |
| Last updated | June 24, 2026 |
| Repository | agiprolabs/claude-trading-skills ↗ |
What it does
Decompose a DeFi pool's advertised APY into components and compute the real yield net of all costs before providing liquidity.
Who is it for?
Comparing DeFi pools by true real yield rather than headline APY.
Skip if: Executing deposits or full CLMM position math (defers to the lp-math skill).
When should I use this skill?
You need to know a pool's real yield after impermanent loss, gas, and emission depreciation.
What you get
A decomposed real-yield estimate and sustainability assessment for a DeFi pool.
- A decomposed real-yield estimate and emission-sustainability assessment for a pool
By the numbers
- 4 yield source components (fee, emission, lending, staking)
- Worked example: 45% nominal APY resolves to 23% real yield
Files
Yield Analysis — DeFi Yield Evaluation & Comparison
DeFi yields are often misleading. A pool advertising 200% APY may deliver negative real returns once you account for impermanent loss, gas costs, and emission token depreciation. This skill provides the framework to decompose, evaluate, and compare yield opportunities accurately.
Why Yield Analysis Matters
Most DeFi yield dashboards show nominal yield — the headline number. Real yield requires decomposing that number into its components and subtracting all costs. Without this decomposition:
- LPs chase high-APY pools that destroy capital through IL
- Emission-driven yields collapse as reward tokens lose value
- Gas and rebalancing costs eat into thin margins
- Opportunity cost is ignored (you could be staking SOL at ~7%)
Yield Components
Every DeFi yield breaks down into one or more of these sources:
1. Trading Fee Income
Swap fees earned by liquidity providers. This is the most sustainable yield source because it comes from real economic activity.
fee_apr = (daily_volume * fee_rate / tvl) * 365
your_daily_fees = daily_volume * fee_rate * (your_liquidity / total_liquidity)For CLMM pools (concentrated liquidity), fee income is amplified by how tightly you concentrate your range. See the lp-math skill for CLMM mechanics.
2. Token Emissions / Incentives
Protocol reward tokens distributed to LPs. Often the largest component of advertised yields, but frequently unsustainable.
emission_apr = (daily_emission_tokens * token_price * 365) / tvlThe critical question: will the emission token hold its value? If everyone farms and dumps, the token depreciates and actual USD yield is much lower.
3. Lending Interest
Interest earned from lending protocol deposits (Marginfi, Kamino, Solend). Driven by borrowing demand — more sustainable than emissions but fluctuates with utilization.
4. Staking Rewards
Validator staking yield (~7% APR on Solana) or liquid staking token (LST) yield. The baseline risk-free rate for the Solana ecosystem.
Real vs Nominal Yield
| Metric | What It Includes | What It Ignores |
|---|---|---|
| Nominal APY | Fee APR + emission APR (compounded) | IL, gas, depreciation, risk |
| Real Yield | Everything, net of all costs | Nothing — this is the true return |
Real Yield Formula
real_yield = fee_apr
+ emission_apr × (1 - emission_depreciation)
- il_cost
- gas_cost
- rebalancing_costWhere:
fee_apr: annualized fee income as fraction of position valueemission_apr: annualized emission income at current token priceemission_depreciation: expected decline in emission token price (0.0 to 1.0)il_cost: expected impermanent loss as annualized rate (seeimpermanent-lossskill)gas_cost: transaction fees for deposits, withdrawals, claims, compoundsrebalancing_cost: for CLMM positions, cost of rebalancing out-of-range positions
Example: SOL-USDC Pool
Nominal APY displayed: 45%
Decomposition:
Fee APR: 18%
Emission APR: 30% (RAY token rewards)
Emission depreciation: 40% (RAY down 40% over 30d)
Effective emission: 18% (30% × 0.6)
IL cost (estimated): 12% (SOL volatile against USDC)
Gas + rebalance: 1%
Real yield = 18% + 18% - 12% - 1% = 23%The 45% nominal yield is really 23% after accounting for all factors.
Fee APR Calculation
Constant-Product Pools
fee_apr = fee_rate * daily_volume / tvl * 365For a pool with 0.25% fee rate, $2M daily volume, and $10M TVL:
fee_apr = 0.0025 * 2_000_000 / 10_000_000 * 365 = 18.25%Concentrated Liquidity (CLMM) Pools
CLMM fee income depends on your position range relative to trading activity:
# Simplified — see lp-math skill for full CLMM math
fee_apr = fee_rate * daily_volume_in_range / position_liquidity * 365Tighter ranges earn higher fees per dollar deployed but go out of range more frequently, requiring rebalancing.
Per-LP Share
your_share = your_liquidity / total_pool_liquidity
your_daily_fees = total_daily_fees * your_shareEmission Sustainability
The Death Spiral Pattern
1. Protocol launches with high emission rewards → attracts LPs 2. TVL grows → yield per LP drops → protocol increases emissions 3. LPs farm and dump emission tokens → token price drops 4. Lower token price → lower USD-denominated yield 5. LPs leave → TVL drops → protocol increases emissions further 6. Spiral continues until emissions stop or protocol fails
Sustainability Metrics
# Protocol P/E ratio
pe_ratio = fully_diluted_valuation / annual_protocol_revenue
# Revenue-to-emission ratio (> 1.0 is sustainable)
sustainability = annual_revenue / annual_emission_value
# Token velocity (high = lots of sell pressure)
velocity = daily_emission_selling / daily_token_volumeInterpretation:
- P/E < 20 and sustainability > 1.0: Likely sustainable yield
- P/E 20-100 and sustainability 0.3-1.0: Moderate risk
- P/E > 100 or sustainability < 0.3: Emission-dependent, high risk
Red Flags
- APY > 100% sourced primarily from emissions
- Emission token price declining consistently over 30+ days
- TVL declining while emission rate stays constant or increases
- Protocol revenue is a small fraction of emission cost
- No vesting or lockup on emission tokens
Yield Comparison Framework
When comparing yield opportunities, normalize across these dimensions:
1. Same-Asset Basis
Compare like for like. For SOL:
| Strategy | Expected APR | Risk Level | IL Exposure |
|---|---|---|---|
| Native staking | ~7% | Low | None |
| Liquid staking (mSOL) | ~7.5% | Low | Minimal |
| SOL-USDC LP (Orca) | ~15-25% | Medium | High |
| SOL lending (Marginfi) | ~3-8% | Low-Med | None |
| Leveraged yield | ~20-50% | High | Varies |
2. Risk-Adjusted Yield
risk_score = (
il_risk * 0.3 +
smart_contract_risk * 0.25 +
emission_sustainability_risk * 0.2 +
liquidity_risk * 0.15 +
protocol_risk * 0.1
)
risk_adjusted_yield = net_apr / risk_score3. Total Cost Accounting
Include all costs:
- Impermanent loss (see
impermanent-lossskill) - Gas fees for all transactions (deposit, withdraw, claim, compound)
- Opportunity cost (what you could earn risk-free)
- Smart contract risk premium
- Rebalancing costs (CLMM positions)
Solana Yield Sources
Liquidity Provision
| Protocol | Pool Types | Fee Tiers | Notes |
|---|---|---|---|
| Raydium | CPMM, CLMM | 0.01-1% | Largest Solana DEX by volume |
| Orca | CLMM (Whirlpool) | 0.01-2% | Concentrated liquidity focused |
| Meteora | DLMM, Dynamic | Variable | Dynamic fee adjustment |
Lending
| Protocol | Assets | Typical APR | Notes |
|---|---|---|---|
| Marginfi | SOL, USDC, etc. | 2-10% | Points program active |
| Kamino | SOL, USDC, etc. | 2-12% | Auto-compound vaults |
| Solend | SOL, USDC, etc. | 1-8% | Established protocol |
Staking
| Method | APR | Lock Period | Notes |
|---|---|---|---|
| Native SOL staking | ~7% | 1 epoch (~2d) | Validator selection matters |
| mSOL (Marinade) | ~7.2% | Instant | Liquid, usable in DeFi |
| jitoSOL (Jito) | ~7.5% | Instant | Includes MEV rewards |
| bSOL (BlazeStake) | ~7% | Instant | Decentralized validator set |
Data Sources
DeFiLlama Yields API (Free, No Auth)
import httpx
# All yield pools
pools = httpx.get("https://yields.llama.fi/pools").json()
# Filter for Solana
solana_pools = [p for p in pools["data"] if p["chain"] == "Solana"]
# Sort by TVL
solana_pools.sort(key=lambda p: p.get("tvlUsd", 0), reverse=True)Response fields: pool, chain, project, symbol, tvlUsd, apy, apyBase, apyReward, il7d, exposure.
Protocol-Specific APIs
- Raydium:
https://api-v3.raydium.io/pools/info/list - Orca:
https://api.mainnet.orca.so/v1/whirlpool/list - Marginfi: On-chain account data via Solana RPC
Integration with Other Skills
- `lp-math`: AMM formulas for fee calculation and position math
- `impermanent-loss`: IL estimation for real yield calculation
- `defillama-api`: Fetching yield and TVL data across protocols
- `risk-management`: Portfolio-level yield allocation decisions
- `position-sizing`: How much capital to allocate to yield strategies
Files
References
references/yield_math.md— Fee APR, APR/APY conversion, net yield formulas, break-even analysisreferences/sustainability_analysis.md— Emission sustainability metrics, death spiral patterns, real yield identification
Scripts
scripts/yield_calculator.py— Offline yield calculator with fee APR, IL estimation, net yield, break-even, and sensitivity analysisscripts/yield_comparison.py— Fetches DeFiLlama yield data and compares Solana yield opportunities with risk-adjusted ranking
Sustainability Analysis — Evaluating DeFi Yield Durability
Protocol Revenue vs Emissions
Every DeFi protocol that pays yield through token emissions faces a fundamental question: can the protocol generate enough real revenue to justify its token's value?
Revenue Sources
- Trading fees: Protocol's share of swap fees (e.g., Raydium takes a cut of LP fees)
- Lending spread: Difference between borrow and lend rates
- Liquidation fees: Earned when undercollateralized positions are liquidated
- Protocol-owned liquidity (POL): Yield from the protocol's own LP positions
- Treasury yield: Interest on treasury holdings
Emission Costs
- LP incentives: Tokens distributed to liquidity providers
- Staking rewards: Tokens for governance stakers
- Referral/growth programs: Tokens for user acquisition
- Team/investor unlocks: Vesting schedules adding supply
The Core Equation
sustainable = (annual_protocol_revenue >= annual_emission_cost_usd)When revenue exceeds emission costs, the protocol can sustain its yield without diluting token holders. When emissions exceed revenue, the protocol is paying for growth with inflation.
The Yield Farming Death Spiral
Phase 1: Launch (Weeks 1-4)
- Protocol announces high emission rewards (200%+ APY)
- Capital floods in seeking yield
- TVL grows rapidly
- Token price rises on hype and buying pressure from yield seekers
Phase 2: Peak (Weeks 4-12)
- TVL stabilizes at high level
- Yield per LP drops as more capital enters
- Early farmers begin selling emission tokens
- Token price plateaus
Phase 3: Decline (Weeks 12-26)
- Farmers sell emission tokens → price drops
- Lower token price → lower USD yield
- LPs start leaving → TVL drops
- Protocol may increase emissions to retain LPs → more sell pressure
- Remaining LPs earn less and face higher IL from declining token
Phase 4: Collapse or Stabilization
- Collapse: TVL drops 90%+, token price drops 95%+, protocol becomes ghost chain
- Stabilization: Revenue-generating protocols find equilibrium where real fees sustain modest yield
Key Observation
The difference between collapse and stabilization is whether the protocol generates real trading volume and fees independent of emission incentives. Protocols with genuine product-market fit (Raydium, Orca) stabilize. Pure yield farms collapse.
Sustainability Metrics
Protocol P/E Ratio
pe_ratio = fully_diluted_valuation / annual_protocol_revenue| P/E Range | Interpretation |
|---|---|
| < 10 | Strong value — revenue well supports valuation |
| 10-30 | Healthy — typical for established DeFi |
| 30-100 | Growth premium — market expects revenue growth |
| 100-500 | Speculative — valuation not supported by current revenue |
| > 500 | Extremely speculative — almost entirely narrative-driven |
Revenue-to-Emission Ratio
rev_emission_ratio = annual_revenue / annual_emission_value_usd| Ratio | Interpretation |
|---|---|
| > 1.0 | Sustainable — revenue covers emissions |
| 0.5-1.0 | Approaching sustainability |
| 0.1-0.5 | Emission-dependent — moderate risk |
| < 0.1 | Highly unsustainable — yield is almost entirely inflationary |
Emission Sell Pressure
daily_sell_pressure = daily_emissions_usd / daily_token_volume| Sell Pressure | Interpretation |
|---|---|
| < 1% | Minimal impact on token price |
| 1-5% | Noticeable but manageable |
| 5-15% | Significant downward pressure |
| > 15% | Severe — token likely in decline |
TVL Retention Rate
tvl_retention_30d = current_tvl / tvl_30d_agoValues below 0.8 (20% TVL loss in 30 days) signal potential death spiral.
Real Yield vs Inflationary Yield
Real Yield Protocols
Yield comes from actual economic activity:
- DEX fees: Traders pay to swap tokens — LPs earn from real demand
- Lending interest: Borrowers pay interest — lenders earn from real demand
- Staking rewards: Network inflation distributed to validators/delegators (predictable, protocol-level)
- MEV sharing: Jito shares MEV revenue with jitoSOL holders
Inflationary Yield
Yield comes from new token creation:
- Farm rewards: Protocol mints tokens and distributes them
- Points programs: Promise of future token airdrop (deferred inflation)
- Governance staking rewards: Staking rewards funded by token inflation
How to Identify Real Yield
def is_real_yield(pool: dict) -> bool:
"""Check if a pool's yield is primarily from real sources.
Args:
pool: Pool data with apyBase and apyReward fields.
Returns:
True if base (fee) APY exceeds reward (emission) APY.
"""
base_apy = pool.get("apyBase", 0) or 0
reward_apy = pool.get("apyReward", 0) or 0
total = base_apy + reward_apy
if total == 0:
return False
return base_apy / total > 0.5DeFiLlama separates apyBase (real, from fees) and apyReward (from emissions), making this check straightforward.
Red Flags Checklist
High Risk Indicators
- [ ] APY > 100% with majority from emissions
- [ ] Emission token price down > 30% in 30 days
- [ ] TVL declining while emissions stay constant
- [ ] Protocol revenue < 10% of emission cost
- [ ] No token vesting — all emissions immediately sellable
- [ ] Team allocation > 30% of total supply
- [ ] Emission schedule increases over time (not decreasing)
- [ ] No working product beyond yield farm
- [ ] Anonymous team with no track record
- [ ] Forked code with minimal changes
Moderate Risk Indicators
- [ ] APY 50-100% with mixed fee/emission sources
- [ ] Emission token price stable but flat
- [ ] TVL stable but not growing
- [ ] Protocol revenue covers 30-80% of emissions
- [ ] Some token vesting (3-12 month)
Low Risk Indicators
- [ ] APY < 30% primarily from fees
- [ ] Established protocol (6+ months, audited)
- [ ] Growing or stable TVL
- [ ] Revenue exceeds emissions
- [ ] Long vesting schedules for team tokens
- [ ] Multiple audits from reputable firms
Historical Patterns
Common Yield Farming Collapse Patterns
Pattern 1: The Vampire Attack Fade
- Protocol launches by offering higher yields than competitors
- Attracts TVL initially
- Cannot sustain yields without real product differentiation
- TVL leaves when emissions drop
Pattern 2: The Governance Token Illusion
- Protocol distributes governance token with no revenue sharing
- Token has voting power but no cash flow
- Market initially values governance, then realizes it is worthless
- Token price trends to zero, yield trends to zero
Pattern 3: The Leverage Spiral
- Protocol enables leveraged yield farming
- Users borrow to farm, multiplying apparent TVL
- Market downturn triggers cascading liquidations
- TVL collapses 80%+ in days
Solana-Specific Examples
Solana's low transaction costs enable more frequent compounding and rebalancing, which amplifies both the benefits and risks of yield strategies. Protocols that survived the 2022-2023 bear market (Raydium, Orca, Marinade) proved their revenue models. Newer protocols should be evaluated more skeptically until they demonstrate sustainable revenue.
Evaluation Framework
When assessing any yield opportunity:
1. Decompose: What percentage comes from fees vs emissions? 2. Validate: Is the fee APR consistent with actual trading volume? 3. Project: If emission token drops 50%, is the yield still attractive? 4. Compare: Does the risk-adjusted yield beat SOL staking (7%)? 5. Monitor: Set alerts for TVL changes, emission token price, volume trends
def yield_sustainability_score(
fee_pct: float,
rev_emission_ratio: float,
tvl_retention_30d: float,
token_price_change_30d: float,
) -> float:
"""Score yield sustainability from 0 (unsustainable) to 100 (sustainable).
Args:
fee_pct: Fraction of yield from fees (0-1).
rev_emission_ratio: Protocol revenue / emission cost.
tvl_retention_30d: Current TVL / TVL 30 days ago.
token_price_change_30d: Price change ratio (e.g. 0.8 = down 20%).
Returns:
Sustainability score 0-100.
"""
score = 0.0
score += min(fee_pct, 1.0) * 35 # Max 35 for 100% fee-based
score += min(rev_emission_ratio, 1.0) * 25 # Max 25 for revenue-backed
score += min(tvl_retention_30d, 1.2) / 1.2 * 20 # Max 20 for stable TVL
price_factor = max(min(token_price_change_30d, 1.5), 0) / 1.5
score += price_factor * 20 # Max 20 for stable/rising token
return round(score, 1)Yield Math — Formulas & Worked Examples
Fee APR Calculation
Constant-Product Pools (x × y = k)
All LPs share fees proportionally to their liquidity share:
fee_apr = fee_rate × daily_volume / tvl × 365Worked Example:
- Pool: SOL-USDC on Raydium
- Fee rate: 0.25% (0.0025)
- Daily volume: $3,000,000
- TVL: $12,000,000
daily_fees = 3,000,000 × 0.0025 = $7,500
fee_apr = 7,500 / 12,000,000 × 365 = 22.81%Your share with $10,000 deposited:
your_share = 10,000 / 12,000,000 = 0.000833
your_daily_fees = 7,500 × 0.000833 = $6.25
your_annual_fees = 6.25 × 365 = $2,281Concentrated Liquidity (CLMM) Pools
Fee income depends on your range width relative to active trading range:
concentration_factor = full_range_liquidity / your_range_width
effective_fee_apr = base_fee_apr × concentration_factorWorked Example:
- Base fee APR (full range): 20%
- Your range: ±10% around current price
- Full range equivalent: ±100% (roughly)
- Concentration factor: ~5x (simplified)
effective_fee_apr = 20% × 5 = 100%But you only earn when price is in your range. If price stays in range 60% of the time:
realized_fee_apr = 100% × 0.60 = 60%Minus rebalancing costs each time you go out of range (gas + potential slippage).
Volume-Weighted Fee APR
When volume varies significantly day to day, use a trailing average:
trailing_7d_volume = sum(daily_volumes[-7:]) / 7
fee_apr = fee_rate * trailing_7d_volume / current_tvl * 365Using 7-day trailing average smooths out spikes from single large trades.
APR vs APY Conversion
Definitions
- APR (Annual Percentage Rate): Simple interest, no compounding
- APY (Annual Percentage Yield): Includes compounding effect
Formulas
APY = (1 + APR / n)^n - 1
APR = n × ((1 + APY)^(1/n) - 1)Where n = number of compounding periods per year.
| Compounding | n | 20% APR → APY |
|---|---|---|
| Annual | 1 | 20.00% |
| Monthly | 12 | 21.94% |
| Weekly | 52 | 22.09% |
| Daily | 365 | 22.13% |
| Continuous | ∞ | 22.14% |
Continuous Compounding
APY = e^APR - 1
APR = ln(1 + APY)When to Use Which
- Manual LP (you claim and reinvest): Use APR — you compound when you choose
- Auto-compounding vault (Kamino, Tulip): Use APY — vault compounds for you
- Lending protocols: Usually display APY with continuous compounding
- Staking: Usually display APR
Daily Rate
daily_rate = APR / 365 # Simple
daily_rate = (1 + APY)^(1/365) - 1 # Compound-adjustedNet Yield Calculation
Full Formula
net_daily = fee_income + emission_income - il_cost - gas_cost - rebalance_cost
net_apr = (net_daily × 365) / position_valueStep-by-Step Example
Pool: SOL-USDC, $50,000 position
| Component | Daily | Annual | Source |
|---|---|---|---|
| Fee income | $20.55 | $7,500 | 15% APR |
| Emission income | $13.70 | $5,000 | 10% APR (RAY) |
| Emission adj. (-30%) | $9.59 | $3,500 | RAY declining |
| IL cost | -$8.22 | -$3,000 | ~6% annual IL |
| Gas costs | -$0.14 | -$50 | Solana tx fees |
| Rebalancing | -$0.27 | -$100 | CLMM adjustments |
net_daily = 20.55 + 9.59 - 8.22 - 0.14 - 0.27 = $21.51
net_apr = 21.51 × 365 / 50,000 = 15.70%Compare to nominal displayed APY of 25% — real yield is 15.70%.
Emission Token Yield
Basic Calculation
daily_emission_usd = daily_tokens_emitted × token_price
emission_apr = (daily_emission_usd × 365) / tvlAdjusted for Depreciation
Emission tokens face constant sell pressure from farmers. Adjust expected yield:
retention_factor = current_price / price_30d_ago
effective_emission_apr = emission_apr × retention_factorExample:
- RAY emissions: 1,000 RAY/day to pool
- RAY price today: $2.00 (was $3.00 thirty days ago)
- Pool TVL: $5,000,000
daily_emission_usd = 1,000 × 2.00 = $2,000
emission_apr = 2,000 × 365 / 5,000,000 = 14.6%
retention_factor = 2.00 / 3.00 = 0.667
effective_emission_apr = 14.6% × 0.667 = 9.74%Forward-Looking Emission Adjustment
For more conservative estimates, project further depreciation:
# If token dropped 33% last 30d, project another 33% next 30d
pessimistic_price = current_price * retention_factor
pessimistic_emission_apr = emission_apr * (retention_factor ** 2)Break-Even Analysis
Time to Break Even
Given upfront costs (gas to enter, potential IL on entry):
entry_cost = deposit_gas + initial_slippage
daily_net_income = daily_fees + daily_emissions_adj - daily_il
break_even_days = entry_cost / daily_net_incomeMinimum Fee APR for IL Break-Even
Given expected IL (from the impermanent-loss skill):
min_fee_apr = expected_il_annual + gas_cost_annual + opportunity_costExample: If expected annual IL is 8%, gas costs 0.5%, and SOL staking yields 7%:
min_fee_apr = 8% + 0.5% + 7% = 15.5%The pool must generate at least 15.5% fee APR to justify LP over staking.
Hold Duration Impact
IL accumulates over time if price keeps trending. Shorter hold periods can be better for volatile pairs:
def optimal_hold_days(fee_apr: float, volatility: float) -> int:
"""Estimate optimal holding period before IL exceeds fees.
Args:
fee_apr: Annual fee rate as decimal (e.g. 0.20 for 20%).
volatility: Annual volatility as decimal (e.g. 0.80 for 80%).
Returns:
Approximate optimal hold period in days.
"""
daily_fee = fee_apr / 365
# IL grows roughly with sqrt(time) × volatility^2
# Break-even when cumulative_fees = cumulative_il
# Simplified: days ≈ (2 × daily_fee / (vol^2 / 365))
daily_vol_sq = (volatility ** 2) / 365
if daily_vol_sq == 0:
return 365
return int(2 * daily_fee / daily_vol_sq)Common Pitfalls
1. Comparing APR to APY: Always convert to the same basis before comparing 2. Ignoring IL on volatile pairs: Fee APR looks great until you account for IL 3. Assuming emission APR is stable: Token price changes daily; recalculate regularly 4. Using spot volume for fee APR: Use 7-day trailing average, not single-day volume 5. Forgetting gas on Solana: Low but not zero — matters for small positions and frequent compounds 6. Double-counting auto-compound yield: If vault auto-compounds, don't manually compound APR → APY
#!/usr/bin/env python3
"""Offline DeFi yield calculator with fee APR, IL estimation, net yield,
break-even analysis, and sensitivity reporting.
Computes real yield from pool parameters without requiring any API calls.
Includes a --demo mode that compares three representative pool scenarios
(stablecoin, correlated, volatile).
Usage:
python scripts/yield_calculator.py
python scripts/yield_calculator.py --demo
python scripts/yield_calculator.py --tvl 5000000 --volume 1500000 --fee-rate 0.0025 --volatility 0.8
Dependencies:
uv pip install numpy
Environment Variables:
None required — this script runs entirely offline.
"""
from __future__ import annotations
import argparse
import math
import sys
from dataclasses import dataclass
from typing import Optional
try:
import numpy as np
except ImportError:
print("numpy is required. Install with: uv pip install numpy")
sys.exit(1)
# ── Data Models ─────────────────────────────────────────────────────
@dataclass
class PoolParams:
"""Parameters describing a liquidity pool and position."""
name: str
tvl: float # Total pool TVL in USD
daily_volume: float # Average daily trading volume in USD
fee_rate: float # Swap fee rate (e.g. 0.0025 for 0.25%)
annual_volatility: float # Annualised volatility of the price ratio (0-1+)
emission_apr: float # Emission-based APR as decimal (e.g. 0.10 for 10%)
emission_depreciation: float # Expected 30-day emission token decline (0-1)
position_size: float # Your deposit in USD
gas_cost_annual: float # Estimated annual gas/tx costs in USD
rebalance_cost_annual: float # CLMM rebalance costs in USD (0 for standard pools)
@dataclass
class YieldReport:
"""Computed yield metrics for a pool position."""
pool_name: str
fee_apr: float
emission_apr_raw: float
emission_apr_adjusted: float
estimated_il: float
gas_cost_pct: float
rebalance_cost_pct: float
net_apr: float
net_apy: float
daily_income: float
annual_income: float
break_even_days: Optional[float]
hold_vs_lp_advantage: float # positive = LP is better
# ── Core Calculations ───────────────────────────────────────────────
def fee_apr(daily_volume: float, fee_rate: float, tvl: float) -> float:
"""Calculate annualised fee APR for a liquidity pool.
Args:
daily_volume: Average daily trading volume in USD.
fee_rate: Swap fee rate as decimal (e.g. 0.0025).
tvl: Total pool TVL in USD.
Returns:
Fee APR as a decimal (e.g. 0.20 for 20%).
"""
if tvl <= 0:
return 0.0
return (daily_volume * fee_rate / tvl) * 365
def estimate_annual_il(annual_volatility: float) -> float:
"""Estimate annualised impermanent loss from volatility.
Uses the approximation for constant-product AMMs:
IL ≈ volatility^2 / 8
This is a second-order Taylor expansion valid for moderate price moves.
For high-volatility assets, actual IL may exceed this estimate.
Args:
annual_volatility: Annualised price-ratio volatility as decimal.
Returns:
Estimated annual IL as a positive decimal (e.g. 0.08 for 8%).
"""
return (annual_volatility ** 2) / 8
def il_from_price_ratio(price_ratio: float) -> float:
"""Exact IL for a constant-product AMM given a price ratio change.
Formula: IL = 2 * sqrt(r) / (1 + r) - 1
Args:
price_ratio: P_new / P_initial (e.g. 2.0 for a 2x price increase).
Returns:
IL as a negative decimal (e.g. -0.0566 for ~5.7% loss).
"""
if price_ratio <= 0:
return -1.0
return 2 * math.sqrt(price_ratio) / (1 + price_ratio) - 1
def apr_to_apy(apr: float, compounds_per_year: int = 365) -> float:
"""Convert APR to APY with specified compounding frequency.
Args:
apr: Annual percentage rate as decimal.
compounds_per_year: Number of compounding periods.
Returns:
APY as a decimal.
"""
if compounds_per_year <= 0:
return apr
return (1 + apr / compounds_per_year) ** compounds_per_year - 1
def compute_yield(params: PoolParams) -> YieldReport:
"""Compute full yield report for a pool position.
Args:
params: Pool and position parameters.
Returns:
Complete yield analysis report.
"""
# Fee income
f_apr = fee_apr(params.daily_volume, params.fee_rate, params.tvl)
# Emission yield adjusted for depreciation
emission_raw = params.emission_apr
emission_adj = emission_raw * (1 - params.emission_depreciation)
# IL estimate
il_annual = estimate_annual_il(params.annual_volatility)
# Cost percentages relative to position
gas_pct = params.gas_cost_annual / params.position_size if params.position_size > 0 else 0
rebalance_pct = params.rebalance_cost_annual / params.position_size if params.position_size > 0 else 0
# Net APR
net = f_apr + emission_adj - il_annual - gas_pct - rebalance_pct
# Net APY (daily compounding)
net_apy = apr_to_apy(max(net, 0), 365) if net > 0 else net
# Daily and annual income
daily = net * params.position_size / 365
annual = net * params.position_size
# Break-even: how many days until fee income covers IL + costs
daily_gross = (f_apr + emission_adj) * params.position_size / 365
daily_cost = (il_annual + gas_pct + rebalance_pct) * params.position_size / 365
if daily_gross > daily_cost and daily_cost > 0:
# Days for cumulative net to cover initial entry cost (~$1 gas on Solana)
entry_cost = 1.0 # approximate Solana deposit cost
break_even = entry_cost / (daily_gross - daily_cost)
elif daily_gross <= daily_cost:
break_even = None # Never breaks even
else:
break_even = 0.0
# LP vs hold comparison: positive means LP is better
# Hold earns 0 from fees/emissions but avoids IL
hold_return = 0.0 # holding earns nothing extra (unless staking)
lp_return = net
advantage = lp_return - hold_return
return YieldReport(
pool_name=params.name,
fee_apr=f_apr,
emission_apr_raw=emission_raw,
emission_apr_adjusted=emission_adj,
estimated_il=il_annual,
gas_cost_pct=gas_pct,
rebalance_cost_pct=rebalance_pct,
net_apr=net,
net_apy=net_apy,
daily_income=daily,
annual_income=annual,
break_even_days=break_even,
hold_vs_lp_advantage=advantage,
)
# ── Sensitivity Analysis ───────────────────────────────────────────
def sensitivity_table(
params: PoolParams,
volatilities: Optional[list[float]] = None,
) -> list[tuple[float, float, float]]:
"""Compute net yield across different volatility scenarios.
Args:
params: Base pool parameters.
volatilities: List of volatilities to test. Defaults to a range.
Returns:
List of (volatility, net_apr, estimated_il) tuples.
"""
if volatilities is None:
volatilities = [0.1, 0.2, 0.3, 0.5, 0.7, 0.9, 1.2, 1.5, 2.0]
results = []
for vol in volatilities:
p = PoolParams(
name=params.name,
tvl=params.tvl,
daily_volume=params.daily_volume,
fee_rate=params.fee_rate,
annual_volatility=vol,
emission_apr=params.emission_apr,
emission_depreciation=params.emission_depreciation,
position_size=params.position_size,
gas_cost_annual=params.gas_cost_annual,
rebalance_cost_annual=params.rebalance_cost_annual,
)
report = compute_yield(p)
results.append((vol, report.net_apr, report.estimated_il))
return results
# ── Display ─────────────────────────────────────────────────────────
def format_pct(value: float) -> str:
"""Format a decimal as a percentage string."""
return f"{value * 100:+.2f}%" if value < 0 else f"{value * 100:.2f}%"
def print_report(report: YieldReport) -> None:
"""Print a formatted yield report to stdout.
Args:
report: Computed yield report.
"""
print(f"\n{'═' * 60}")
print(f" YIELD REPORT: {report.pool_name}")
print(f"{'═' * 60}")
print(f" Fee APR: {format_pct(report.fee_apr)}")
print(f" Emission APR (raw): {format_pct(report.emission_apr_raw)}")
print(f" Emission APR (adjusted): {format_pct(report.emission_apr_adjusted)}")
print(f" Estimated IL: {format_pct(report.estimated_il)}")
print(f" Gas cost: {format_pct(report.gas_cost_pct)}")
print(f" Rebalance cost: {format_pct(report.rebalance_cost_pct)}")
print(f"{'─' * 60}")
print(f" Net APR: {format_pct(report.net_apr)}")
print(f" Net APY (daily compound): {format_pct(report.net_apy)}")
print(f" Daily income: ${report.daily_income:,.2f}")
print(f" Annual income: ${report.annual_income:,.2f}")
if report.break_even_days is not None:
print(f" Break-even: {report.break_even_days:.1f} days")
else:
print(f" Break-even: Never (costs exceed income)")
print(f" LP vs Hold advantage: {format_pct(report.hold_vs_lp_advantage)}")
print(f"{'═' * 60}\n")
def print_sensitivity(params: PoolParams) -> None:
"""Print sensitivity analysis table.
Args:
params: Base pool parameters for sensitivity sweep.
"""
results = sensitivity_table(params)
print(f"\n{'─' * 50}")
print(f" SENSITIVITY: {params.name}")
print(f" Net yield vs. annual volatility")
print(f"{'─' * 50}")
print(f" {'Volatility':>12} {'Est. IL':>10} {'Net APR':>10}")
print(f" {'─' * 12} {'─' * 10} {'─' * 10}")
for vol, net, il in results:
net_str = format_pct(net)
print(f" {format_pct(vol):>12} {format_pct(il):>10} {net_str:>10}")
print(f"{'─' * 50}\n")
def print_comparison(reports: list[YieldReport]) -> None:
"""Print a side-by-side comparison table.
Args:
reports: List of yield reports to compare.
"""
print(f"\n{'═' * 72}")
print(f" YIELD COMPARISON")
print(f"{'═' * 72}")
header = f" {'Pool':<22} {'Fee APR':>9} {'IL':>9} {'Net APR':>9} {'Net APY':>9}"
print(header)
print(f" {'─' * 22} {'─' * 9} {'─' * 9} {'─' * 9} {'─' * 9}")
for r in sorted(reports, key=lambda x: x.net_apr, reverse=True):
print(
f" {r.pool_name:<22} "
f"{format_pct(r.fee_apr):>9} "
f"{format_pct(r.estimated_il):>9} "
f"{format_pct(r.net_apr):>9} "
f"{format_pct(r.net_apy):>9}"
)
print(f"{'═' * 72}\n")
# ── Demo Scenarios ──────────────────────────────────────────────────
def demo_scenarios() -> list[PoolParams]:
"""Return three representative pool scenarios for demo mode.
Returns:
List of PoolParams for stablecoin, correlated, and volatile pools.
"""
return [
PoolParams(
name="USDC-USDT (Stable)",
tvl=50_000_000,
daily_volume=15_000_000,
fee_rate=0.0001, # 1 bps
annual_volatility=0.02,
emission_apr=0.02,
emission_depreciation=0.1,
position_size=10_000,
gas_cost_annual=10,
rebalance_cost_annual=0,
),
PoolParams(
name="SOL-mSOL (Correlated)",
tvl=20_000_000,
daily_volume=5_000_000,
fee_rate=0.0004, # 4 bps
annual_volatility=0.10,
emission_apr=0.05,
emission_depreciation=0.20,
position_size=10_000,
gas_cost_annual=20,
rebalance_cost_annual=15,
),
PoolParams(
name="SOL-USDC (Volatile)",
tvl=12_000_000,
daily_volume=3_000_000,
fee_rate=0.0025, # 25 bps
annual_volatility=0.80,
emission_apr=0.15,
emission_depreciation=0.35,
position_size=10_000,
gas_cost_annual=30,
rebalance_cost_annual=50,
),
]
# ── CLI ─────────────────────────────────────────────────────────────
def build_parser() -> argparse.ArgumentParser:
"""Build CLI argument parser.
Returns:
Configured ArgumentParser.
"""
parser = argparse.ArgumentParser(
description="DeFi yield calculator — compute real yield from pool parameters"
)
parser.add_argument(
"--demo",
action="store_true",
help="Run demo comparing stablecoin, correlated, and volatile pool scenarios",
)
parser.add_argument("--tvl", type=float, default=10_000_000, help="Pool TVL in USD")
parser.add_argument("--volume", type=float, default=2_000_000, help="Daily volume in USD")
parser.add_argument("--fee-rate", type=float, default=0.0025, help="Fee rate (e.g. 0.0025)")
parser.add_argument("--volatility", type=float, default=0.80, help="Annual volatility (e.g. 0.80)")
parser.add_argument("--emission-apr", type=float, default=0.10, help="Emission APR (e.g. 0.10)")
parser.add_argument("--emission-depreciation", type=float, default=0.30, help="Emission token depreciation (0-1)")
parser.add_argument("--position", type=float, default=10_000, help="Your position size in USD")
parser.add_argument("--name", type=str, default="Custom Pool", help="Pool name for display")
return parser
# ── Main ────────────────────────────────────────────────────────────
def main() -> None:
"""Entry point for yield calculator."""
parser = build_parser()
args = parser.parse_args()
if args.demo:
print("\n DeFi Yield Calculator — Demo Mode")
print(" Comparing three representative Solana pool scenarios\n")
scenarios = demo_scenarios()
reports = []
for params in scenarios:
report = compute_yield(params)
reports.append(report)
print_report(report)
print_comparison(reports)
# Sensitivity for the volatile pool
print_sensitivity(scenarios[2])
else:
params = PoolParams(
name=args.name,
tvl=args.tvl,
daily_volume=args.volume,
fee_rate=args.fee_rate,
annual_volatility=args.volatility,
emission_apr=args.emission_apr,
emission_depreciation=args.emission_depreciation,
position_size=args.position,
gas_cost_annual=30,
rebalance_cost_annual=0,
)
report = compute_yield(params)
print_report(report)
print_sensitivity(params)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Fetch and compare DeFi yield opportunities on Solana.
Queries the DeFiLlama yields API (free, no authentication) for Solana pools
and lending markets, then ranks them by risk-adjusted yield. Includes a
--demo mode with representative data for offline use.
Usage:
python scripts/yield_comparison.py
python scripts/yield_comparison.py --demo
python scripts/yield_comparison.py --min-tvl 1000000 --top 20
Dependencies:
uv pip install httpx
Environment Variables:
None required — DeFiLlama yields API is free and requires no API key.
"""
from __future__ import annotations
import argparse
import sys
from dataclasses import dataclass
from typing import Optional
try:
import httpx
except ImportError:
print("httpx is required. Install with: uv pip install httpx")
sys.exit(1)
# ── Configuration ───────────────────────────────────────────────────
DEFILLAMA_YIELDS_URL = "https://yields.llama.fi/pools"
REQUEST_TIMEOUT = 30
SOL_STAKING_APR = 7.0 # Baseline SOL staking APR for comparison
# ── Data Models ─────────────────────────────────────────────────────
@dataclass
class YieldOpportunity:
"""A single yield opportunity with decomposed metrics."""
pool_id: str
project: str
symbol: str
chain: str
tvl_usd: float
apy_total: float
apy_base: float # From fees / real yield
apy_reward: float # From emissions
il_7d: Optional[float] # 7-day IL percentage
exposure: str # "single" or "multi"
risk_tier: str # Computed: "low", "medium", "high"
real_yield_pct: float # Percentage of yield from fees
risk_adjusted_yield: float # yield / risk_score
# ── Risk Assessment ─────────────────────────────────────────────────
def classify_risk(
apy_total: float,
apy_base: float,
apy_reward: float,
tvl_usd: float,
exposure: str,
) -> tuple[str, float]:
"""Classify the risk tier and compute a risk score for a yield opportunity.
Risk score ranges from 1 (lowest risk) to 10 (highest risk).
Args:
apy_total: Total APY.
apy_base: Base APY from fees.
apy_reward: Reward APY from emissions.
tvl_usd: Total value locked in USD.
exposure: "single" for single-asset, "multi" for multi-asset.
Returns:
Tuple of (risk_tier, risk_score).
"""
score = 0.0
# Emission dependency: high reward APY relative to base = more risk
if apy_total > 0:
emission_ratio = apy_reward / apy_total
else:
emission_ratio = 0
score += emission_ratio * 3.0 # Max 3.0
# Absolute yield level: very high APY is suspicious
if apy_total > 100:
score += 2.5
elif apy_total > 50:
score += 1.5
elif apy_total > 20:
score += 0.5
# TVL: lower TVL = higher risk (less battle-tested, more slippage)
if tvl_usd < 100_000:
score += 2.0
elif tvl_usd < 1_000_000:
score += 1.0
elif tvl_usd < 10_000_000:
score += 0.5
# Multi-asset exposure has IL risk
if exposure == "multi":
score += 1.0
# Clamp to 1-10 range
score = max(1.0, min(10.0, score))
if score <= 3.0:
tier = "low"
elif score <= 6.0:
tier = "medium"
else:
tier = "high"
return tier, score
def compute_real_yield_pct(apy_base: float, apy_total: float) -> float:
"""Compute what percentage of total yield comes from real sources (fees).
Args:
apy_base: Base APY from fees.
apy_total: Total APY.
Returns:
Percentage of yield from fees (0-100).
"""
if apy_total <= 0:
return 0.0
return min(100.0, (apy_base / apy_total) * 100)
# ── Data Fetching ───────────────────────────────────────────────────
def fetch_solana_yields(min_tvl: float = 100_000) -> list[dict]:
"""Fetch Solana yield data from DeFiLlama.
Args:
min_tvl: Minimum TVL filter in USD.
Returns:
List of pool dictionaries from DeFiLlama.
Raises:
httpx.HTTPStatusError: On API error.
httpx.ConnectError: On network failure.
"""
try:
response = httpx.get(DEFILLAMA_YIELDS_URL, timeout=REQUEST_TIMEOUT)
response.raise_for_status()
except httpx.ConnectError:
print("Error: Could not connect to DeFiLlama API.")
print("Check your internet connection or try --demo mode.")
sys.exit(1)
except httpx.HTTPStatusError as e:
print(f"Error: DeFiLlama API returned {e.response.status_code}")
sys.exit(1)
data = response.json()
pools = data.get("data", [])
# Filter for Solana pools with minimum TVL
solana_pools = [
p for p in pools
if p.get("chain") == "Solana"
and (p.get("tvlUsd") or 0) >= min_tvl
and (p.get("apy") or 0) > 0
]
return solana_pools
def parse_pool(pool: dict) -> YieldOpportunity:
"""Parse a DeFiLlama pool dict into a YieldOpportunity.
Args:
pool: Raw pool data from DeFiLlama.
Returns:
Parsed YieldOpportunity with risk assessment.
"""
apy_total = pool.get("apy") or 0.0
apy_base = pool.get("apyBase") or 0.0
apy_reward = pool.get("apyReward") or 0.0
tvl_usd = pool.get("tvlUsd") or 0.0
exposure = pool.get("exposure") or "multi"
il_7d = pool.get("il7d")
risk_tier, risk_score = classify_risk(
apy_total, apy_base, apy_reward, tvl_usd, exposure
)
real_pct = compute_real_yield_pct(apy_base, apy_total)
# Risk-adjusted yield: higher is better
risk_adjusted = apy_total / risk_score if risk_score > 0 else 0
return YieldOpportunity(
pool_id=pool.get("pool", ""),
project=pool.get("project", "unknown"),
symbol=pool.get("symbol", "???"),
chain="Solana",
tvl_usd=tvl_usd,
apy_total=apy_total,
apy_base=apy_base,
apy_reward=apy_reward,
il_7d=il_7d,
exposure=exposure,
risk_tier=risk_tier,
real_yield_pct=real_pct,
risk_adjusted_yield=risk_adjusted,
)
# ── Demo Data ───────────────────────────────────────────────────────
def demo_data() -> list[YieldOpportunity]:
"""Generate representative Solana yield data for offline demo.
Returns:
List of YieldOpportunity objects with realistic parameters.
"""
pools = [
# LP pools
{"project": "raydium", "symbol": "SOL-USDC", "tvl": 45_000_000,
"apy": 22.5, "base": 16.8, "reward": 5.7, "exposure": "multi"},
{"project": "orca", "symbol": "SOL-USDC", "tvl": 38_000_000,
"apy": 19.2, "base": 15.1, "reward": 4.1, "exposure": "multi"},
{"project": "raydium", "symbol": "SOL-mSOL", "tvl": 12_000_000,
"apy": 11.3, "base": 8.2, "reward": 3.1, "exposure": "multi"},
{"project": "orca", "symbol": "SOL-jitoSOL", "tvl": 8_500_000,
"apy": 12.8, "base": 9.5, "reward": 3.3, "exposure": "multi"},
{"project": "raydium", "symbol": "USDC-USDT", "tvl": 55_000_000,
"apy": 5.2, "base": 4.8, "reward": 0.4, "exposure": "multi"},
{"project": "meteora", "symbol": "SOL-USDC", "tvl": 15_000_000,
"apy": 28.4, "base": 22.1, "reward": 6.3, "exposure": "multi"},
{"project": "raydium", "symbol": "RAY-USDC", "tvl": 6_200_000,
"apy": 35.6, "base": 12.3, "reward": 23.3, "exposure": "multi"},
{"project": "orca", "symbol": "BONK-SOL", "tvl": 3_800_000,
"apy": 68.2, "base": 45.1, "reward": 23.1, "exposure": "multi"},
# Lending
{"project": "marginfi", "symbol": "USDC", "tvl": 120_000_000,
"apy": 8.5, "base": 5.2, "reward": 3.3, "exposure": "single"},
{"project": "marginfi", "symbol": "SOL", "tvl": 85_000_000,
"apy": 6.1, "base": 3.8, "reward": 2.3, "exposure": "single"},
{"project": "kamino", "symbol": "USDC", "tvl": 95_000_000,
"apy": 9.2, "base": 6.1, "reward": 3.1, "exposure": "single"},
{"project": "kamino", "symbol": "SOL", "tvl": 60_000_000,
"apy": 5.8, "base": 3.5, "reward": 2.3, "exposure": "single"},
{"project": "solend", "symbol": "USDC", "tvl": 45_000_000,
"apy": 6.8, "base": 5.5, "reward": 1.3, "exposure": "single"},
# Staking / LST
{"project": "marinade", "symbol": "mSOL", "tvl": 800_000_000,
"apy": 7.2, "base": 7.2, "reward": 0.0, "exposure": "single"},
{"project": "jito", "symbol": "jitoSOL", "tvl": 600_000_000,
"apy": 7.5, "base": 7.5, "reward": 0.0, "exposure": "single"},
{"project": "blazestake", "symbol": "bSOL", "tvl": 150_000_000,
"apy": 7.0, "base": 7.0, "reward": 0.0, "exposure": "single"},
]
results = []
for p in pools:
risk_tier, risk_score = classify_risk(
p["apy"], p["base"], p["reward"], p["tvl"], p["exposure"]
)
real_pct = compute_real_yield_pct(p["base"], p["apy"])
risk_adj = p["apy"] / risk_score if risk_score > 0 else 0
results.append(YieldOpportunity(
pool_id=f"demo-{p['project']}-{p['symbol']}",
project=p["project"],
symbol=p["symbol"],
chain="Solana",
tvl_usd=p["tvl"],
apy_total=p["apy"],
apy_base=p["base"],
apy_reward=p["reward"],
il_7d=None,
exposure=p["exposure"],
risk_tier=risk_tier,
real_yield_pct=real_pct,
risk_adjusted_yield=risk_adj,
))
return results
# ── Display ─────────────────────────────────────────────────────────
def format_usd(value: float) -> str:
"""Format USD value with K/M/B suffixes."""
if value >= 1_000_000_000:
return f"${value / 1_000_000_000:.1f}B"
if value >= 1_000_000:
return f"${value / 1_000_000:.1f}M"
if value >= 1_000:
return f"${value / 1_000:.0f}K"
return f"${value:.0f}"
def print_yield_table(
opportunities: list[YieldOpportunity],
title: str,
sort_by: str = "risk_adjusted",
limit: int = 20,
) -> None:
"""Print a formatted yield comparison table.
Args:
opportunities: List of yield opportunities.
title: Table title.
sort_by: Sort key — "risk_adjusted", "apy", "real_yield", "tvl".
limit: Maximum rows to display.
"""
sort_keys = {
"risk_adjusted": lambda x: x.risk_adjusted_yield,
"apy": lambda x: x.apy_total,
"real_yield": lambda x: x.real_yield_pct,
"tvl": lambda x: x.tvl_usd,
}
key_fn = sort_keys.get(sort_by, sort_keys["risk_adjusted"])
sorted_opps = sorted(opportunities, key=key_fn, reverse=True)[:limit]
print(f"\n{'═' * 100}")
print(f" {title}")
print(f" Sorted by: {sort_by} | SOL staking baseline: {SOL_STAKING_APR:.1f}% APR")
print(f"{'═' * 100}")
header = (
f" {'Protocol':<12} {'Pool':<16} {'TVL':>9} "
f"{'APY':>7} {'Fee':>7} {'Reward':>7} "
f"{'Real%':>6} {'Risk':>6} {'Adj.':>7}"
)
print(header)
print(f" {'─' * 12} {'─' * 16} {'─' * 9} {'─' * 7} {'─' * 7} {'─' * 7} {'─' * 6} {'─' * 6} {'─' * 7}")
for opp in sorted_opps:
risk_marker = {"low": " ", "medium": "*", "high": "!"}
marker = risk_marker.get(opp.risk_tier, "?")
print(
f" {opp.project:<12} {opp.symbol:<16} {format_usd(opp.tvl_usd):>9} "
f"{opp.apy_total:>6.1f}% {opp.apy_base:>6.1f}% {opp.apy_reward:>6.1f}% "
f"{opp.real_yield_pct:>5.0f}% {opp.risk_tier:>5}{marker} "
f"{opp.risk_adjusted_yield:>6.1f}"
)
print(f"{'═' * 100}")
print(f" Risk: * = medium, ! = high | Adj. = APY / risk_score (higher is better)")
print()
def print_category_summary(opportunities: list[YieldOpportunity]) -> None:
"""Print yield summary grouped by category.
Args:
opportunities: List of yield opportunities.
"""
# Group by type
lp_pools = [o for o in opportunities if o.exposure == "multi"]
lending = [o for o in opportunities if o.exposure == "single" and o.apy_reward > 0]
staking = [o for o in opportunities if o.exposure == "single" and o.apy_reward == 0]
print(f"\n{'─' * 60}")
print(f" CATEGORY SUMMARY")
print(f"{'─' * 60}")
categories = [
("LP Pools", lp_pools),
("Lending", lending),
("Staking / LST", staking),
]
for name, group in categories:
if not group:
continue
avg_apy = sum(o.apy_total for o in group) / len(group)
avg_real = sum(o.real_yield_pct for o in group) / len(group)
total_tvl = sum(o.tvl_usd for o in group)
best = max(group, key=lambda x: x.risk_adjusted_yield)
print(f"\n {name} ({len(group)} opportunities)")
print(f" Average APY: {avg_apy:.1f}%")
print(f" Average real yield: {avg_real:.0f}% of total")
print(f" Total TVL: {format_usd(total_tvl)}")
print(f" Best risk-adjusted: {best.symbol} on {best.project} ({best.apy_total:.1f}% APY)")
print(f"\n{'─' * 60}")
# Comparison to SOL staking
above_staking = [o for o in opportunities if o.apy_base > SOL_STAKING_APR]
print(f"\n {len(above_staking)} of {len(opportunities)} opportunities have")
print(f" fee-based yield above SOL staking ({SOL_STAKING_APR:.1f}% APR).")
print()
def print_top_picks(opportunities: list[YieldOpportunity], count: int = 5) -> None:
"""Print top risk-adjusted yield picks with rationale.
Args:
opportunities: All yield opportunities.
count: Number of top picks to display.
"""
# Filter to medium or low risk only
viable = [o for o in opportunities if o.risk_tier in ("low", "medium")]
top = sorted(viable, key=lambda x: x.risk_adjusted_yield, reverse=True)[:count]
print(f"\n{'═' * 60}")
print(f" TOP {count} RISK-ADJUSTED OPPORTUNITIES")
print(f" (filtered to low/medium risk only)")
print(f"{'═' * 60}")
for i, opp in enumerate(top, 1):
real_label = "real yield" if opp.real_yield_pct > 50 else "emission-heavy"
print(f"\n {i}. {opp.symbol} on {opp.project}")
print(f" APY: {opp.apy_total:.1f}% ({opp.apy_base:.1f}% fees + {opp.apy_reward:.1f}% rewards)")
print(f" TVL: {format_usd(opp.tvl_usd)} | Risk: {opp.risk_tier} | Type: {real_label}")
print(f" Risk-adjusted score: {opp.risk_adjusted_yield:.1f}")
print(f"\n{'═' * 60}")
print(f" Note: This is analysis output, not financial advice.")
print(f" Always verify data on-chain before making decisions.")
print()
# ── CLI ─────────────────────────────────────────────────────────────
def build_parser() -> argparse.ArgumentParser:
"""Build CLI argument parser.
Returns:
Configured ArgumentParser.
"""
parser = argparse.ArgumentParser(
description="Compare Solana DeFi yield opportunities using DeFiLlama data"
)
parser.add_argument(
"--demo",
action="store_true",
help="Use representative demo data instead of live API",
)
parser.add_argument(
"--min-tvl",
type=float,
default=1_000_000,
help="Minimum TVL filter in USD (default: 1000000)",
)
parser.add_argument(
"--top",
type=int,
default=20,
help="Number of results to display (default: 20)",
)
parser.add_argument(
"--sort",
choices=["risk_adjusted", "apy", "real_yield", "tvl"],
default="risk_adjusted",
help="Sort order (default: risk_adjusted)",
)
return parser
# ── Main ────────────────────────────────────────────────────────────
def main() -> None:
"""Entry point for yield comparison tool."""
parser = build_parser()
args = parser.parse_args()
if args.demo:
print("\n Solana Yield Comparison — Demo Mode")
print(" Using representative data (not live)\n")
opportunities = demo_data()
else:
print("\n Solana Yield Comparison — Live Data")
print(f" Fetching from DeFiLlama (min TVL: {format_usd(args.min_tvl)})...\n")
raw_pools = fetch_solana_yields(min_tvl=args.min_tvl)
if not raw_pools:
print(" No Solana pools found matching criteria.")
print(" Try lowering --min-tvl or use --demo mode.")
sys.exit(0)
opportunities = [parse_pool(p) for p in raw_pools]
print(f" Found {len(opportunities)} Solana yield opportunities.\n")
# Display results
print_yield_table(opportunities, "SOLANA YIELD OPPORTUNITIES", sort_by=args.sort, limit=args.top)
print_category_summary(opportunities)
print_top_picks(opportunities)
if __name__ == "__main__":
main()
Related skills
FAQ
Why is nominal APY misleading?
It ignores impermanent loss, gas, and emission-token depreciation; a 45% nominal yield can be closer to 23% real yield after those costs.
What makes a yield sustainable?
Trading-fee income from real activity is sustainable; emission-driven yield collapses if the reward token loses value.