Files
All-in-one-Financial-Analysis/atlas-terminal/server/routers/crypto.py
T

153 lines
5.0 KiB
Python

"""Crypto router -- live cryptocurrency prices from Bithumb (KRW) and Binance (USD)."""
import asyncio
import logging
from typing import List
import httpx
from fastapi import APIRouter, HTTPException
from server.models.schemas import CryptoPrice
router = APIRouter()
logger = logging.getLogger(__name__)
# Top 20 symbols tracked by default
TOP_SYMBOLS = [
"BTC", "ETH", "BNB", "XRP", "SOL", "ADA", "DOGE", "AVAX", "DOT", "MATIC",
"LINK", "SHIB", "TRX", "UNI", "ATOM", "LTC", "ETC", "XLM", "NEAR", "APT",
]
# Bithumb uses different ticker names for some coins
_BITHUMB_MAP = {
"MATIC": "MATIC",
"NEAR": "NEAR",
"APT": "APT",
}
async def _fetch_binance_prices(client: httpx.AsyncClient, symbols: List[str]) -> dict:
"""Fetch USD prices from Binance API for the given symbols."""
url = "https://api.binance.com/api/v3/ticker/price"
try:
resp = await client.get(url, timeout=10)
resp.raise_for_status()
data = resp.json()
except (httpx.HTTPError, ValueError) as exc:
logger.warning("Binance prices fetch failed: %s", exc)
return {}
prices = {}
lookup = {item["symbol"]: float(item["price"]) for item in data}
for sym in symbols:
key = f"{sym.upper()}USDT"
if key in lookup:
prices[sym.upper()] = lookup[key]
return prices
async def _fetch_bithumb_prices(client: httpx.AsyncClient, symbols: List[str]) -> dict:
"""Fetch KRW prices from Bithumb public API (parallel)."""
async def _one(sym: str):
bithumb_sym = _BITHUMB_MAP.get(sym.upper(), sym.upper())
url = f"https://api.bithumb.com/public/ticker/{bithumb_sym}_KRW"
try:
resp = await client.get(url, timeout=5)
resp.raise_for_status()
data = resp.json()
if data.get("status") == "0000":
closing = data.get("data", {}).get("closing_price")
if closing:
return sym.upper(), float(closing)
except (httpx.HTTPError, ValueError) as exc:
logger.debug("Bithumb %s failed: %s", sym, exc)
return None
results = await asyncio.gather(*[_one(s) for s in symbols], return_exceptions=True)
prices = {}
for r in results:
if isinstance(r, tuple):
prices[r[0]] = r[1]
return prices
async def _fetch_binance_24h_changes(client: httpx.AsyncClient, symbols: List[str]) -> dict:
"""Fetch 24h percentage changes from Binance."""
url = "https://api.binance.com/api/v3/ticker/24hr"
try:
resp = await client.get(url, timeout=10)
resp.raise_for_status()
data = resp.json()
except (httpx.HTTPError, ValueError) as exc:
logger.warning("Binance 24h changes fetch failed: %s", exc)
return {}
changes = {}
lookup = {item["symbol"]: float(item.get("priceChangePercent", 0)) for item in data}
for sym in symbols:
key = f"{sym.upper()}USDT"
if key in lookup:
changes[sym.upper()] = lookup[key]
return changes
@router.get(
"/prices",
response_model=List[CryptoPrice],
summary="Top 20 crypto prices (Bithumb KRW + Binance USD)",
)
async def crypto_prices():
"""Return current prices for the top 20 cryptocurrencies.
USD prices come from Binance; KRW prices from Bithumb.
"""
try:
async with httpx.AsyncClient() as client:
usd_prices, krw_prices, changes = await asyncio.gather(
_fetch_binance_prices(client, TOP_SYMBOLS),
_fetch_bithumb_prices(client, TOP_SYMBOLS),
_fetch_binance_24h_changes(client, TOP_SYMBOLS),
)
results: List[CryptoPrice] = []
for sym in TOP_SYMBOLS:
results.append(CryptoPrice(
symbol=sym,
name=sym,
price_usd=usd_prices.get(sym),
price_krw=krw_prices.get(sym),
change_24h_pct=changes.get(sym),
))
return results
except Exception as exc:
logger.exception("Crypto prices endpoint failed")
raise HTTPException(status_code=500, detail=f"Crypto prices failed: {exc}") from exc
@router.get(
"/price/{symbol}",
response_model=CryptoPrice,
summary="Single crypto price",
)
async def crypto_price(symbol: str):
"""Return current price for a single cryptocurrency symbol."""
try:
sym = symbol.upper()
async with httpx.AsyncClient() as client:
usd_prices, krw_prices, changes = await asyncio.gather(
_fetch_binance_prices(client, [sym]),
_fetch_bithumb_prices(client, [sym]),
_fetch_binance_24h_changes(client, [sym]),
)
return CryptoPrice(
symbol=sym,
name=sym,
price_usd=usd_prices.get(sym),
price_krw=krw_prices.get(sym),
change_24h_pct=changes.get(sym),
)
except Exception as exc:
logger.exception("Crypto price endpoint failed")
raise HTTPException(status_code=500, detail=f"Crypto price failed: {exc}") from exc