Merge branch '2-news-api' into 3-market-api

This commit is contained in:
2025-09-30 17:30:01 +02:00
9 changed files with 226 additions and 755 deletions

View File

@@ -1,87 +1,30 @@
from .base import BaseWrapper
from app.markets.coinbase import CoinBaseWrapper
from app.markets.cryptocompare import CryptoCompareWrapper
from app.markets.binance import BinanceWrapper
from app.markets.binance_public import PublicBinanceAgent
from app.markets.error_handler import ProviderFallback, MarketAPIError, safe_execute
from .coinbase import CoinBaseWrapper
from .cryptocompare import CryptoCompareWrapper
from app.utils.wrapper_handler import WrapperHandler
from agno.utils.log import log_warning
import logging
__all__ = [ "MarketAPIs", "BaseWrapper", "CoinBaseWrapper", "CryptoCompareWrapper" ]
logger = logging.getLogger(__name__)
# TODO se si vuole usare un aggregatore di dati di mercato, si può aggiungere qui facendo una classe extra (simile a questa) che per ogni chiamata chiama tutti i wrapper e aggrega i risultati
class MarketAPIs(BaseWrapper):
"""
Classe per gestire le API di mercato disponibili.
Permette di ottenere un'istanza della prima API disponibile in base alla priorità specificata.
Supporta operazioni come ottenere informazioni su singoli prodotti, liste di prodotti e dati storici.
Usa un WrapperHandler per gestire più wrapper e tentare chiamate in modo resiliente.
"""
@staticmethod
def get_list_available_market_apis(currency: str = "USD") -> list[BaseWrapper]:
"""
Restituisce una lista di istanze delle API di mercato disponibili.
La priorità è data dall'ordine delle API nella lista wrappers.
1. CoinBase
2. CryptoCompare
:param currency: Valuta di riferimento (default "USD")
:return: Lista di istanze delle API di mercato disponibili
"""
wrapper_builders = [
CoinBaseWrapper,
CryptoCompareWrapper,
]
result = []
for wrapper in wrapper_builders:
try:
result.append(wrapper(currency=currency))
except Exception as e:
log_warning(f"{wrapper} cannot be initialized: {e}")
assert result, "No market API keys set in environment variables."
return result
def __init__(self, currency: str = "USD"):
"""
Inizializza la classe con la valuta di riferimento e la priorità dei provider.
Args:
currency: Valuta di riferimento (default "USD")
"""
self.currency = currency
self.wrappers = MarketAPIs.get_list_available_market_apis(currency=currency)
self.fallback_manager = ProviderFallback(self.wrappers)
# Metodi con fallback robusto tra provider multipli
def get_product(self, asset_id: str):
"""Ottiene informazioni su un prodotto con fallback automatico tra provider."""
try:
return self.fallback_manager.execute_with_fallback("get_product", asset_id)
except MarketAPIError as e:
logger.error(f"Failed to get product {asset_id}: {str(e)}")
raise
wrappers = [ CoinBaseWrapper, CryptoCompareWrapper ]
self.wrappers: WrapperHandler[BaseWrapper] = WrapperHandler.build_wrappers(wrappers)
def get_product(self, asset_id):
return self.wrappers.try_call(lambda w: w.get_product(asset_id))
def get_products(self, asset_ids: list):
"""Ottiene informazioni su più prodotti con fallback automatico tra provider."""
try:
return self.fallback_manager.execute_with_fallback("get_products", asset_ids)
except MarketAPIError as e:
logger.error(f"Failed to get products {asset_ids}: {str(e)}")
raise
return self.wrappers.try_call(lambda w: w.get_products(asset_ids))
def get_all_products(self):
"""Ottiene tutti i prodotti con fallback automatico tra provider."""
try:
return self.fallback_manager.execute_with_fallback("get_all_products")
except MarketAPIError as e:
logger.error(f"Failed to get all products: {str(e)}")
raise
def get_historical_prices(self, asset_id: str = "BTC"):
"""Ottiene prezzi storici con fallback automatico tra provider."""
try:
return self.fallback_manager.execute_with_fallback("get_historical_prices", asset_id)
except MarketAPIError as e:
logger.error(f"Failed to get historical prices for {asset_id}: {str(e)}")
raise
return self.wrappers.try_call(lambda w: w.get_all_products())
def get_historical_prices(self, asset_id = "BTC", limit: int = 100):
return self.wrappers.try_call(lambda w: w.get_historical_prices(asset_id, limit))

View File

@@ -12,7 +12,7 @@ class BaseWrapper:
raise NotImplementedError
def get_all_products(self) -> list['ProductInfo']:
raise NotImplementedError
def get_historical_prices(self, asset_id: str = "BTC") -> list['Price']:
def get_historical_prices(self, asset_id: str = "BTC", limit: int = 100) -> list['Price']:
raise NotImplementedError
class ProductInfo(BaseModel):

View File

@@ -1,27 +1,36 @@
import os
from typing import Optional
from enum import Enum
from datetime import datetime, timedelta
from coinbase.rest import RESTClient
from .base import ProductInfo, BaseWrapper, Price
from .error_handler import retry_on_failure, handle_api_errors, MarketAPIError, RateLimitError
class Granularity(Enum):
UNKNOWN_GRANULARITY = 0
ONE_MINUTE = 60
FIVE_MINUTE = 300
FIFTEEN_MINUTE = 900
THIRTY_MINUTE = 1800
ONE_HOUR = 3600
TWO_HOUR = 7200
FOUR_HOUR = 14400
SIX_HOUR = 21600
ONE_DAY = 86400
class CoinBaseWrapper(BaseWrapper):
"""
Wrapper per le API di Coinbase Advanced Trade.
Wrapper per le API di Coinbase Advanced Trade.\n
Implementa l'interfaccia BaseWrapper per fornire accesso unificato
ai dati di mercato di Coinbase tramite le API REST.
La documentazione delle API è disponibile qui:
ai dati di mercato di Coinbase tramite le API REST.\n
https://docs.cdp.coinbase.com/api-reference/advanced-trade-api/rest-api/introduction
"""
def __init__(self, api_key: Optional[str] = None, api_private_key: Optional[str] = None, currency: str = "USD"):
if api_key is None:
api_key = os.getenv("COINBASE_API_KEY")
def __init__(self, currency: str = "USD"):
api_key = os.getenv("COINBASE_API_KEY")
assert api_key is not None, "API key is required"
if api_private_key is None:
api_private_key = os.getenv("COINBASE_API_SECRET")
api_private_key = os.getenv("COINBASE_API_SECRET")
assert api_private_key is not None, "API private key is required"
self.currency = currency
@@ -33,49 +42,30 @@ class CoinBaseWrapper(BaseWrapper):
def __format(self, asset_id: str) -> str:
return asset_id if '-' in asset_id else f"{asset_id}-{self.currency}"
@retry_on_failure(max_retries=3, delay=1.0)
@handle_api_errors
def get_product(self, asset_id: str) -> ProductInfo:
asset_id = self.__format(asset_id)
asset = self.client.get_product(asset_id)
return ProductInfo.from_coinbase(asset)
@retry_on_failure(max_retries=3, delay=1.0)
@handle_api_errors
def get_products(self, asset_ids: list[str]) -> list[ProductInfo]:
all_asset_ids = [self.__format(asset_id) for asset_id in asset_ids]
assets = self.client.get_products(product_ids=all_asset_ids)
if assets.products:
return [ProductInfo.from_coinbase_product(asset) for asset in assets.products]
return []
return [ProductInfo.from_coinbase(asset) for asset in assets.products]
@retry_on_failure(max_retries=3, delay=1.0)
@handle_api_errors
def get_all_products(self) -> list[ProductInfo]:
assets = self.client.get_products()
if assets.products:
return [ProductInfo.from_coinbase_product(asset) for asset in assets.products]
return []
return [ProductInfo.from_coinbase_product(asset) for asset in assets.products]
@retry_on_failure(max_retries=3, delay=1.0)
@handle_api_errors
def get_historical_prices(self, asset_id: str = "BTC") -> list[Price]:
def get_historical_prices(self, asset_id: str = "BTC", limit: int = 100) -> list[Price]:
asset_id = self.__format(asset_id)
# Get last 14 days of hourly data (14*24 = 336 candles, within 350 limit)
end_time = datetime.now()
start_time = end_time - timedelta(days=14)
# Convert to UNIX timestamps as strings (required by Coinbase API)
start_timestamp = str(int(start_time.timestamp()))
end_timestamp = str(int(end_time.timestamp()))
data = self.client.get_candles(
product_id=asset_id,
start=start_timestamp,
end=end_timestamp,
granularity="ONE_HOUR",
limit=350 # Explicitly set the limit
granularity=Granularity.ONE_HOUR.name,
start=str(int(start_time.timestamp())),
end=str(int(end_time.timestamp())),
limit=limit
)
if data.candles:
return [Price.from_coinbase(candle) for candle in data.candles]
return []
return [Price.from_coinbase(candle) for candle in data.candles]

View File

@@ -12,9 +12,8 @@ class CryptoCompareWrapper(BaseWrapper):
La documentazione delle API è disponibile qui: https://developers.coindesk.com/documentation/legacy/Price/SingleSymbolPriceEndpoint
!!ATTENZIONE!! sembra essere una API legacy e potrebbe essere deprecata in futuro.
"""
def __init__(self, api_key: Optional[str] = None, currency: str = 'USD'):
if api_key is None:
api_key = os.getenv("CRYPTOCOMPARE_API_KEY")
def __init__(self, currency:str='USD'):
api_key = os.getenv("CRYPTOCOMPARE_API_KEY")
assert api_key is not None, "API key is required"
self.api_key = api_key
@@ -28,8 +27,6 @@ class CryptoCompareWrapper(BaseWrapper):
response = requests.get(f"{BASE_URL}{endpoint}", params=params)
return response.json()
@retry_on_failure(max_retries=3, delay=1.0)
@handle_api_errors
def get_product(self, asset_id: str) -> ProductInfo:
response = self.__request("/data/pricemultifull", params = {
"fsyms": asset_id,
@@ -38,8 +35,6 @@ class CryptoCompareWrapper(BaseWrapper):
data = response.get('RAW', {}).get(asset_id, {}).get(self.currency, {})
return ProductInfo.from_cryptocompare(data)
@retry_on_failure(max_retries=3, delay=1.0)
@handle_api_errors
def get_products(self, asset_ids: list[str]) -> list[ProductInfo]:
response = self.__request("/data/pricemultifull", params = {
"fsyms": ",".join(asset_ids),
@@ -52,42 +47,14 @@ class CryptoCompareWrapper(BaseWrapper):
assets.append(ProductInfo.from_cryptocompare(asset_data))
return assets
@retry_on_failure(max_retries=3, delay=1.0)
@handle_api_errors
def get_all_products(self) -> list[ProductInfo]:
"""
Workaround per CryptoCompare: utilizza una lista predefinita di asset popolari
poiché l'API non fornisce un endpoint per recuperare tutti i prodotti.
"""
# Lista di asset popolari supportati da CryptoCompare
popular_assets = [
"BTC", "ETH", "ADA", "DOT", "LINK", "LTC", "XRP", "BCH", "BNB", "SOL",
"MATIC", "AVAX", "ATOM", "UNI", "DOGE", "SHIB", "TRX", "ETC", "FIL", "XLM"
]
try:
# Utilizza get_products per recuperare i dati di tutti gli asset popolari
return self.get_products(popular_assets)
except Exception as e:
# Fallback: prova con un set ridotto di asset principali
main_assets = ["BTC", "ETH", "ADA", "DOT", "LINK"]
try:
return self.get_products(main_assets)
except Exception as fallback_error:
# Se anche il fallback fallisce, solleva l'errore originale con informazioni aggiuntive
raise NotImplementedError(
f"CryptoCompare get_all_products() workaround failed. "
f"Original error: {str(e)}, Fallback error: {str(fallback_error)}"
)
raise NotImplementedError("get_all_products is not supported by CryptoCompare API")
@retry_on_failure(max_retries=3, delay=1.0)
@handle_api_errors
def get_historical_prices(self, asset_id: str = "BTC", day_back: int = 10) -> list[Price]:
assert day_back <= 30, "day_back should be less than or equal to 30"
def get_historical_prices(self, asset_id: str, limit: int = 100) -> list[dict]:
response = self.__request("/data/v2/histohour", params = {
"fsym": asset_id,
"tsym": self.currency,
"limit": day_back * 24
"limit": limit-1 # because the API returns limit+1 items (limit + current)
})
data = response.get('Data', {}).get('Data', [])