Files
openbb-invest-api/defi_service.py
Yaojia Wang 37c46e76ae feat: add DeFi data via DefiLlama API (TDD)
7 new endpoints under /api/v1/defi/ (all free, no API key):
- GET /defi/tvl/protocols - top DeFi protocols by TVL
- GET /defi/tvl/chains - chain TVL rankings
- GET /defi/tvl/{protocol} - single protocol TVL
- GET /defi/yields - top yield pools (filter by chain/project)
- GET /defi/stablecoins - stablecoin market data
- GET /defi/volumes/dexs - DEX volume overview
- GET /defi/fees - protocol fees/revenue overview

Data source: DefiLlama API (free, no key needed)
58 new tests (33 service + 25 route). All 561 tests passing.
2026-03-19 23:03:01 +01:00

196 lines
6.6 KiB
Python

"""DeFi data service via DefiLlama API (no API key required)."""
import logging
from typing import Any
import httpx
logger = logging.getLogger(__name__)
LLAMA_BASE = "https://api.llama.fi"
STABLES_BASE = "https://stablecoins.llama.fi"
YIELDS_BASE = "https://yields.llama.fi"
TIMEOUT = 15.0
async def get_top_protocols(limit: int = 20) -> list[dict[str, Any]]:
"""Fetch top DeFi protocols ranked by TVL from DefiLlama."""
try:
async with httpx.AsyncClient(timeout=TIMEOUT) as client:
resp = await client.get(f"{LLAMA_BASE}/protocols")
resp.raise_for_status()
data = resp.json()
return [
{
"name": p.get("name"),
"symbol": p.get("symbol"),
"tvl": p.get("tvl"),
"chain": p.get("chain"),
"chains": p.get("chains", []),
"category": p.get("category"),
"change_1d": p.get("change_1d"),
"change_7d": p.get("change_7d"),
}
for p in data[:limit]
]
except Exception:
logger.exception("Failed to fetch top protocols from DefiLlama")
return []
async def get_chain_tvls() -> list[dict[str, Any]]:
"""Fetch TVL rankings for all chains from DefiLlama."""
try:
async with httpx.AsyncClient(timeout=TIMEOUT) as client:
resp = await client.get(f"{LLAMA_BASE}/v2/chains")
resp.raise_for_status()
data = resp.json()
return [
{
"name": c.get("name"),
"tvl": c.get("tvl"),
"tokenSymbol": c.get("tokenSymbol"),
}
for c in data
]
except Exception:
logger.exception("Failed to fetch chain TVLs from DefiLlama")
return []
async def get_protocol_tvl(protocol: str) -> float | None:
"""Fetch current TVL for a specific protocol slug."""
try:
async with httpx.AsyncClient(timeout=TIMEOUT) as client:
resp = await client.get(f"{LLAMA_BASE}/tvl/{protocol}")
resp.raise_for_status()
return resp.json()
except Exception:
logger.exception("Failed to fetch TVL for protocol %s", protocol)
return None
async def get_yield_pools(
chain: str | None = None,
project: str | None = None,
) -> list[dict[str, Any]]:
"""Fetch yield pools from DefiLlama, optionally filtered by chain and/or project.
Returns top 20 by TVL descending.
"""
try:
async with httpx.AsyncClient(timeout=TIMEOUT) as client:
resp = await client.get(f"{YIELDS_BASE}/pools")
resp.raise_for_status()
payload = resp.json()
pools: list[dict[str, Any]] = payload.get("data", [])
if chain is not None:
pools = [p for p in pools if p.get("chain") == chain]
if project is not None:
pools = [p for p in pools if p.get("project") == project]
pools = sorted(pools, key=lambda p: p.get("tvlUsd") or 0, reverse=True)[:20]
return [
{
"pool": p.get("pool"),
"chain": p.get("chain"),
"project": p.get("project"),
"symbol": p.get("symbol"),
"tvlUsd": p.get("tvlUsd"),
"apy": p.get("apy"),
"apyBase": p.get("apyBase"),
"apyReward": p.get("apyReward"),
}
for p in pools
]
except Exception:
logger.exception("Failed to fetch yield pools from DefiLlama")
return []
def _extract_circulating(asset: dict[str, Any]) -> float | None:
"""Extract the primary circulating supply value from a stablecoin asset dict."""
raw = asset.get("circulating")
if raw is None:
return None
if isinstance(raw, (int, float)):
return float(raw)
if isinstance(raw, dict):
# DefiLlama returns {"peggedUSD": <amount>, ...}
values = [v for v in raw.values() if isinstance(v, (int, float))]
return values[0] if values else None
return None
async def get_stablecoins(limit: int = 20) -> list[dict[str, Any]]:
"""Fetch top stablecoins by circulating supply from DefiLlama."""
try:
async with httpx.AsyncClient(timeout=TIMEOUT) as client:
resp = await client.get(f"{STABLES_BASE}/stablecoins")
resp.raise_for_status()
payload = resp.json()
assets: list[dict[str, Any]] = payload.get("peggedAssets", [])
return [
{
"name": a.get("name"),
"symbol": a.get("symbol"),
"pegType": a.get("pegType"),
"circulating": _extract_circulating(a),
"price": a.get("price"),
}
for a in assets[:limit]
]
except Exception:
logger.exception("Failed to fetch stablecoins from DefiLlama")
return []
async def get_dex_volumes() -> dict[str, Any] | None:
"""Fetch DEX volume overview from DefiLlama."""
try:
async with httpx.AsyncClient(timeout=TIMEOUT) as client:
resp = await client.get(f"{LLAMA_BASE}/overview/dexs")
resp.raise_for_status()
payload = resp.json()
protocols = [
{
"name": p.get("name"),
"volume24h": p.get("total24h"),
}
for p in payload.get("protocols", [])
]
return {
"totalVolume24h": payload.get("total24h"),
"totalVolume7d": payload.get("total7d"),
"protocols": protocols,
}
except Exception:
logger.exception("Failed to fetch DEX volumes from DefiLlama")
return None
async def get_protocol_fees() -> list[dict[str, Any]]:
"""Fetch protocol fees and revenue overview from DefiLlama."""
try:
async with httpx.AsyncClient(timeout=TIMEOUT) as client:
resp = await client.get(f"{LLAMA_BASE}/overview/fees")
resp.raise_for_status()
payload = resp.json()
return [
{
"name": p.get("name"),
"fees24h": p.get("total24h"),
"revenue24h": p.get("revenue24h"),
}
for p in payload.get("protocols", [])
]
except Exception:
logger.exception("Failed to fetch protocol fees from DefiLlama")
return []