9 enhancement con financialdatasettool e yfinance #11
19
.env.example
19
.env.example
@@ -6,19 +6,30 @@
|
|||||||
# Vedi https://docs.agno.com/examples/models per vedere tutti i modelli supportati
|
# Vedi https://docs.agno.com/examples/models per vedere tutti i modelli supportati
|
||||||
GOOGLE_API_KEY=
|
GOOGLE_API_KEY=
|
||||||
|
|
||||||
|
# Inserire il percorso di installazione di ollama (es. /usr/share/ollama/.ollama)
|
||||||
|
# attenzione che fra Linux nativo e WSL il percorso è diverso
|
||||||
|
OLLAMA_MODELS_PATH=
|
||||||
###############################################################################
|
###############################################################################
|
||||||
# Configurazioni per gli agenti di mercato
|
# Configurazioni per gli agenti di mercato
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
# Coinbase CDP API per Market Agent
|
# Coinbase CDP API per Market Agent
|
||||||
# Ottenibili da: https://portal.cdp.coinbase.com/access/api
|
# Ottenibili da: https://portal.cdp.coinbase.com/access/api
|
||||||
CDP_API_KEY_NAME=
|
# IMPORTANTE: Usare le credenziali CDP (NON Exchange legacy)
|
||||||
CDP_API_PRIVATE_KEY=
|
# - COINBASE_API_KEY: organizations/{org_id}/apiKeys/{key_id}
|
||||||
|
# - COINBASE_API_SECRET: La private key completa (inclusi BEGIN/END)
|
||||||
|
# - NON serve COINBASE_PASSPHRASE (solo per Exchange legacy)
|
||||||
|
COINBASE_API_KEY=
|
||||||
|
COINBASE_API_SECRET=
|
||||||
|
|
||||||
# CryptoCompare API per Market Agent (alternativa)
|
# CryptoCompare API per Market Agent
|
||||||
# Ottenibile da: https://www.cryptocompare.com/cryptopian/api-keys
|
# Ottenibile da: https://www.cryptocompare.com/cryptopian/api-keys
|
||||||
|
# NOTA: API legacy, potrebbe essere deprecata in futuro
|
||||||
|
# Funzionalità limitata: get_all_products() non supportato
|
||||||
CRYPTOCOMPARE_API_KEY=
|
CRYPTOCOMPARE_API_KEY=
|
||||||
|
|
||||||
# Binance API per Market Agent (alternativa)
|
# Binance API per Market Agent
|
||||||
|
# Ottenibili da: https://www.binance.com/en/my/settings/api-management
|
||||||
|
# Supporta sia API autenticate che pubbliche (PublicBinance)
|
||||||
BINANCE_API_KEY=
|
BINANCE_API_KEY=
|
||||||
BINANCE_API_SECRET=
|
BINANCE_API_SECRET=
|
||||||
|
|||||||
@@ -1,116 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Demo script per testare il MarketAgent aggiornato con Coinbase CDP
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
|
|
||||||
|
|
||||||
from src.app.agents.market_agent import MarketAgent
|
|
||||||
import logging
|
|
||||||
|
|
||||||
# Setup logging
|
|
||||||
logging.basicConfig(
|
|
||||||
level=logging.INFO,
|
|
||||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
||||||
)
|
|
||||||
|
|
||||||
def main():
|
|
||||||
print("🚀 Test MarketAgent con Coinbase CDP")
|
|
||||||
print("=" * 50)
|
|
||||||
|
|
||||||
# Inizializza l'agent
|
|
||||||
agent = MarketAgent()
|
|
||||||
|
|
||||||
# Verifica provider disponibili
|
|
||||||
providers = agent.get_available_providers()
|
|
||||||
print(f"📡 Provider disponibili: {providers}")
|
|
||||||
|
|
||||||
if not providers:
|
|
||||||
print("⚠️ Nessun provider configurato. Verifica il file .env")
|
|
||||||
print("\nPer Coinbase CDP, serve:")
|
|
||||||
print("CDP_API_KEY_NAME=your_key_name")
|
|
||||||
print("CDP_API_PRIVATE_KEY=your_private_key")
|
|
||||||
print("\nPer CryptoCompare, serve:")
|
|
||||||
print("CRYPTOCOMPARE_API_KEY=your_api_key")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Mostra capabilities di ogni provider
|
|
||||||
for provider in providers:
|
|
||||||
capabilities = agent.get_provider_capabilities(provider)
|
|
||||||
print(f"🔧 {provider.upper()}: {capabilities}")
|
|
||||||
|
|
||||||
print("\n" + "=" * 50)
|
|
||||||
|
|
||||||
# Test ottenimento prezzo singolo
|
|
||||||
test_symbols = ["BTC", "ETH", "ADA"]
|
|
||||||
|
|
||||||
for symbol in test_symbols:
|
|
||||||
print(f"\n💰 Prezzo {symbol}:")
|
|
||||||
|
|
||||||
# Prova ogni provider
|
|
||||||
for provider in providers:
|
|
||||||
try:
|
|
||||||
price = agent.get_asset_price(symbol, provider)
|
|
||||||
if price:
|
|
||||||
print(f" {provider}: ${price:,.2f}")
|
|
||||||
else:
|
|
||||||
print(f" {provider}: N/A")
|
|
||||||
except Exception as e:
|
|
||||||
print(f" {provider}: Errore - {e}")
|
|
||||||
|
|
||||||
print("\n" + "=" * 50)
|
|
||||||
|
|
||||||
# Test market overview
|
|
||||||
print("\n📊 Market Overview:")
|
|
||||||
try:
|
|
||||||
overview = agent.get_market_overview(["BTC", "ETH", "ADA", "DOT"])
|
|
||||||
|
|
||||||
if overview["data"]:
|
|
||||||
print(f"📡 Fonte: {overview['source']}")
|
|
||||||
|
|
||||||
for crypto, prices in overview["data"].items():
|
|
||||||
if isinstance(prices, dict):
|
|
||||||
usd_price = prices.get("USD", "N/A")
|
|
||||||
eur_price = prices.get("EUR", "N/A")
|
|
||||||
|
|
||||||
if eur_price != "N/A":
|
|
||||||
print(f" {crypto}: ${usd_price} (€{eur_price})")
|
|
||||||
else:
|
|
||||||
print(f" {crypto}: ${usd_price}")
|
|
||||||
else:
|
|
||||||
print("⚠️ Nessun dato disponibile")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ Errore nel market overview: {e}")
|
|
||||||
|
|
||||||
print("\n" + "=" * 50)
|
|
||||||
|
|
||||||
# Test funzione analyze
|
|
||||||
print("\n🔍 Analisi mercato:")
|
|
||||||
try:
|
|
||||||
analysis = agent.analyze("Market overview")
|
|
||||||
print(analysis)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ Errore nell'analisi: {e}")
|
|
||||||
|
|
||||||
# Test specifico Coinbase CDP se disponibile
|
|
||||||
if 'coinbase' in providers:
|
|
||||||
print("\n" + "=" * 50)
|
|
||||||
print("\n🏦 Test specifico Coinbase CDP:")
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Test asset singolo
|
|
||||||
btc_info = agent.get_coinbase_asset_info("BTC")
|
|
||||||
print(f"📈 BTC Info: {btc_info}")
|
|
||||||
|
|
||||||
# Test asset multipli
|
|
||||||
multi_assets = agent.get_coinbase_multiple_assets(["BTC", "ETH"])
|
|
||||||
print(f"📊 Multi Assets: {multi_assets}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ Errore nel test Coinbase CDP: {e}")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Esempio di utilizzo del MarketAgent unificato.
|
|
||||||
Questo script mostra come utilizzare il nuovo MarketAgent che supporta
|
|
||||||
multiple fonti di dati (Coinbase e CryptoCompare).
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
# Aggiungi il path src al PYTHONPATH
|
|
||||||
src_path = Path(__file__).parent / "src"
|
|
||||||
sys.path.insert(0, str(src_path))
|
|
||||||
|
|
||||||
from dotenv import load_dotenv
|
|
||||||
from app.agents.market_agent import MarketAgent
|
|
||||||
|
|
||||||
# Carica variabili d'ambiente
|
|
||||||
load_dotenv()
|
|
||||||
|
|
||||||
def main():
|
|
||||||
print("🚀 Market Agent Demo\n")
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Inizializza il market agent (auto-configura i provider disponibili)
|
|
||||||
agent = MarketAgent()
|
|
||||||
|
|
||||||
# Mostra provider disponibili
|
|
||||||
providers = agent.get_available_providers()
|
|
||||||
print(f"📡 Available providers: {providers}")
|
|
||||||
|
|
||||||
if not providers:
|
|
||||||
print("❌ No providers configured. Please check your .env file.")
|
|
||||||
print("Required variables:")
|
|
||||||
print(" For Coinbase: COINBASE_API_KEY, COINBASE_SECRET, COINBASE_PASSPHRASE")
|
|
||||||
print(" For CryptoCompare: CRYPTOCOMPARE_API_KEY")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Mostra le capacità di ogni provider
|
|
||||||
print("\n🔧 Provider capabilities:")
|
|
||||||
for provider in providers:
|
|
||||||
capabilities = agent.get_provider_capabilities(provider)
|
|
||||||
print(f" {provider}: {capabilities}")
|
|
||||||
|
|
||||||
# Ottieni panoramica del mercato
|
|
||||||
print("\n📊 Market Overview:")
|
|
||||||
overview = agent.get_market_overview(["BTC", "ETH", "ADA"])
|
|
||||||
print(f"Data source: {overview.get('source', 'Unknown')}")
|
|
||||||
|
|
||||||
for crypto, prices in overview.get('data', {}).items():
|
|
||||||
if isinstance(prices, dict):
|
|
||||||
usd = prices.get('USD', 'N/A')
|
|
||||||
eur = prices.get('EUR', 'N/A')
|
|
||||||
if eur != 'N/A':
|
|
||||||
print(f" {crypto}: ${usd} (€{eur})")
|
|
||||||
else:
|
|
||||||
print(f" {crypto}: ${usd}")
|
|
||||||
|
|
||||||
# Analisi completa del mercato
|
|
||||||
print("\n📈 Market Analysis:")
|
|
||||||
analysis = agent.analyze("comprehensive market analysis")
|
|
||||||
print(analysis)
|
|
||||||
|
|
||||||
# Test specifici per provider (se disponibili)
|
|
||||||
if 'cryptocompare' in providers:
|
|
||||||
print("\n🔸 CryptoCompare specific test:")
|
|
||||||
try:
|
|
||||||
btc_price = agent.get_single_crypto_price("BTC", "USD")
|
|
||||||
print(f" BTC price: ${btc_price}")
|
|
||||||
|
|
||||||
top_coins = agent.get_top_cryptocurrencies(5)
|
|
||||||
if top_coins.get('Data'):
|
|
||||||
print(" Top 5 cryptocurrencies by market cap:")
|
|
||||||
for coin in top_coins['Data'][:3]: # Show top 3
|
|
||||||
coin_info = coin.get('CoinInfo', {})
|
|
||||||
display = coin.get('DISPLAY', {}).get('USD', {})
|
|
||||||
name = coin_info.get('FullName', 'Unknown')
|
|
||||||
price = display.get('PRICE', 'N/A')
|
|
||||||
print(f" {name}: {price}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f" CryptoCompare test failed: {e}")
|
|
||||||
|
|
||||||
if 'coinbase' in providers:
|
|
||||||
print("\n🔸 Coinbase specific test:")
|
|
||||||
try:
|
|
||||||
ticker = agent.get_coinbase_ticker("BTC-USD")
|
|
||||||
price = ticker.get('price', 'N/A')
|
|
||||||
volume = ticker.get('volume_24h', 'N/A')
|
|
||||||
print(f" BTC-USD: ${price} (24h volume: {volume})")
|
|
||||||
except Exception as e:
|
|
||||||
print(f" Coinbase test failed: {e}")
|
|
||||||
|
|
||||||
print("\n✅ Demo completed successfully!")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ Demo failed: {e}")
|
|
||||||
print("Make sure you have configured at least one provider in your .env file.")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
374
demos/market_providers_api_demo.py
Normal file
374
demos/market_providers_api_demo.py
Normal file
@@ -0,0 +1,374 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Demo Completo per Market Data Providers
|
||||||
|
========================================
|
||||||
|
|
||||||
|
Questo script dimostra l'utilizzo di tutti i wrapper che implementano BaseWrapper:
|
||||||
|
- CoinBaseWrapper (richiede credenziali)
|
||||||
|
- CryptoCompareWrapper (richiede API key)
|
||||||
|
- BinanceWrapper (richiede credenziali)
|
||||||
|
- PublicBinanceAgent (accesso pubblico)
|
||||||
|
|
||||||
|
Lo script effettua chiamate GET a diversi provider e visualizza i dati
|
||||||
|
in modo strutturato con informazioni dettagliate su timestamp, stato
|
||||||
|
delle richieste e formattazione tabellare.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Dict, List, Optional, Any
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
# Aggiungi il path src al PYTHONPATH
|
||||||
|
project_root = Path(__file__).parent.parent
|
||||||
|
sys.path.insert(0, str(project_root / "src"))
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from app.markets import (
|
||||||
|
CoinBaseWrapper,
|
||||||
|
CryptoCompareWrapper,
|
||||||
|
BinanceWrapper,
|
||||||
|
PublicBinanceAgent,
|
||||||
|
BaseWrapper
|
||||||
|
)
|
||||||
|
|
||||||
|
# Carica variabili d'ambiente
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
class DemoFormatter:
|
||||||
|
"""Classe per formattare l'output del demo in modo strutturato."""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def print_header(title: str, char: str = "=", width: int = 80):
|
||||||
|
"""Stampa un'intestazione formattata."""
|
||||||
|
print(f"\n{char * width}")
|
||||||
|
print(f"{title:^{width}}")
|
||||||
|
print(f"{char * width}")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def print_subheader(title: str, char: str = "-", width: int = 60):
|
||||||
|
"""Stampa una sotto-intestazione formattata."""
|
||||||
|
print(f"\n{char * width}")
|
||||||
|
print(f" {title}")
|
||||||
|
print(f"{char * width}")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def print_request_info(provider_name: str, method: str, timestamp: datetime,
|
||||||
|
status: str, error: Optional[str] = None):
|
||||||
|
"""Stampa informazioni sulla richiesta."""
|
||||||
|
print(f"🕒 Timestamp: {timestamp.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||||
|
print(f"🏷️ Provider: {provider_name}")
|
||||||
|
print(f"🔧 Method: {method}")
|
||||||
|
print(f"📊 Status: {status}")
|
||||||
|
if error:
|
||||||
|
print(f"❌ Error: {error}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def print_product_table(products: List[Any], title: str = "Products"):
|
||||||
|
"""Stampa una tabella di prodotti."""
|
||||||
|
if not products:
|
||||||
|
print(f"📋 {title}: Nessun prodotto trovato")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"📋 {title} ({len(products)} items):")
|
||||||
|
print(f"{'Symbol':<15} {'ID':<20} {'Price':<12} {'Quote':<10} {'Status':<10}")
|
||||||
|
print("-" * 67)
|
||||||
|
|
||||||
|
for product in products[:10]: # Mostra solo i primi 10
|
||||||
|
symbol = getattr(product, 'symbol', 'N/A')
|
||||||
|
product_id = getattr(product, 'id', 'N/A')
|
||||||
|
price = getattr(product, 'price', 0.0)
|
||||||
|
quote = getattr(product, 'quote_currency', 'N/A')
|
||||||
|
status = getattr(product, 'status', 'N/A')
|
||||||
|
|
||||||
|
# Tronca l'ID se troppo lungo
|
||||||
|
if len(product_id) > 18:
|
||||||
|
product_id = product_id[:15] + "..."
|
||||||
|
|
||||||
|
price_str = f"${price:.2f}" if price > 0 else "N/A"
|
||||||
|
|
||||||
|
print(f"{symbol:<15} {product_id:<20} {price_str:<12} {quote:<10} {status:<10}")
|
||||||
|
|
||||||
|
if len(products) > 10:
|
||||||
|
print(f"... e altri {len(products) - 10} prodotti")
|
||||||
|
print()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def print_prices_table(prices: List[Any], title: str = "Historical Prices"):
|
||||||
|
"""Stampa una tabella di prezzi storici."""
|
||||||
|
if not prices:
|
||||||
|
print(f"💰 {title}: Nessun prezzo trovato")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"💰 {title} ({len(prices)} entries):")
|
||||||
|
print(f"{'Time':<12} {'Open':<12} {'High':<12} {'Low':<12} {'Close':<12} {'Volume':<15}")
|
||||||
|
print("-" * 75)
|
||||||
|
|
||||||
|
for price in prices[:5]: # Mostra solo i primi 5
|
||||||
|
time_str = getattr(price, 'time', 'N/A')
|
||||||
|
# Il time è già una stringa, non serve strftime
|
||||||
|
if len(time_str) > 10:
|
||||||
|
time_str = time_str[:10] # Tronca se troppo lungo
|
||||||
|
|
||||||
|
open_price = f"${getattr(price, 'open', 0):.2f}"
|
||||||
|
high_price = f"${getattr(price, 'high', 0):.2f}"
|
||||||
|
low_price = f"${getattr(price, 'low', 0):.2f}"
|
||||||
|
close_price = f"${getattr(price, 'close', 0):.2f}"
|
||||||
|
volume = f"{getattr(price, 'volume', 0):,.0f}"
|
||||||
|
|
||||||
|
print(f"{time_str:<12} {open_price:<12} {high_price:<12} {low_price:<12} {close_price:<12} {volume:<15}")
|
||||||
|
|
||||||
|
if len(prices) > 5:
|
||||||
|
print(f"... e altri {len(prices) - 5} prezzi")
|
||||||
|
print()
|
||||||
|
|
||||||
|
class ProviderTester:
|
||||||
|
"""Classe per testare i provider di market data."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.formatter = DemoFormatter()
|
||||||
|
self.test_symbols = ["BTC", "ETH", "ADA"]
|
||||||
|
|
||||||
|
def test_provider(self, wrapper: BaseWrapper, provider_name: str) -> Dict[str, Any]:
|
||||||
|
"""Testa un provider specifico con tutti i metodi disponibili."""
|
||||||
|
results = {
|
||||||
|
"provider_name": provider_name,
|
||||||
|
"tests": {},
|
||||||
|
"overall_status": "SUCCESS"
|
||||||
|
}
|
||||||
|
|
||||||
|
self.formatter.print_subheader(f"🔍 Testing {provider_name}")
|
||||||
|
|
||||||
|
# Test get_product
|
||||||
|
for symbol in self.test_symbols:
|
||||||
|
timestamp = datetime.now()
|
||||||
|
try:
|
||||||
|
product = wrapper.get_product(symbol)
|
||||||
|
self.formatter.print_request_info(
|
||||||
|
provider_name, f"get_product({symbol})", timestamp, "✅ SUCCESS"
|
||||||
|
)
|
||||||
|
if product:
|
||||||
|
print(f"📦 Product: {product.symbol} (ID: {product.id})")
|
||||||
|
print(f" Price: ${product.price:.2f}, Quote: {product.quote_currency}")
|
||||||
|
print(f" Status: {product.status}, Volume 24h: {product.volume_24h:,.2f}")
|
||||||
|
else:
|
||||||
|
print(f"📦 Product: Nessun prodotto trovato per {symbol}")
|
||||||
|
|
||||||
|
results["tests"][f"get_product_{symbol}"] = "SUCCESS"
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = str(e)
|
||||||
|
self.formatter.print_request_info(
|
||||||
|
provider_name, f"get_product({symbol})", timestamp, "❌ ERROR", error_msg
|
||||||
|
)
|
||||||
|
results["tests"][f"get_product_{symbol}"] = f"ERROR: {error_msg}"
|
||||||
|
results["overall_status"] = "PARTIAL"
|
||||||
|
|
||||||
|
# Test get_products
|
||||||
|
timestamp = datetime.now()
|
||||||
|
try:
|
||||||
|
products = wrapper.get_products(self.test_symbols)
|
||||||
|
self.formatter.print_request_info(
|
||||||
|
provider_name, f"get_products({self.test_symbols})", timestamp, "✅ SUCCESS"
|
||||||
|
)
|
||||||
|
self.formatter.print_product_table(products, f"{provider_name} Products")
|
||||||
|
results["tests"]["get_products"] = "SUCCESS"
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = str(e)
|
||||||
|
self.formatter.print_request_info(
|
||||||
|
provider_name, f"get_products({self.test_symbols})", timestamp, "❌ ERROR", error_msg
|
||||||
|
)
|
||||||
|
results["tests"]["get_products"] = f"ERROR: {error_msg}"
|
||||||
|
results["overall_status"] = "PARTIAL"
|
||||||
|
|
||||||
|
# Test get_all_products
|
||||||
|
timestamp = datetime.now()
|
||||||
|
try:
|
||||||
|
all_products = wrapper.get_all_products()
|
||||||
|
self.formatter.print_request_info(
|
||||||
|
provider_name, "get_all_products()", timestamp, "✅ SUCCESS"
|
||||||
|
)
|
||||||
|
self.formatter.print_product_table(all_products, f"{provider_name} All Products")
|
||||||
|
results["tests"]["get_all_products"] = "SUCCESS"
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = str(e)
|
||||||
|
self.formatter.print_request_info(
|
||||||
|
provider_name, "get_all_products()", timestamp, "❌ ERROR", error_msg
|
||||||
|
)
|
||||||
|
results["tests"]["get_all_products"] = f"ERROR: {error_msg}"
|
||||||
|
results["overall_status"] = "PARTIAL"
|
||||||
|
|
||||||
|
# Test get_historical_prices
|
||||||
|
timestamp = datetime.now()
|
||||||
|
try:
|
||||||
|
prices = wrapper.get_historical_prices("BTC")
|
||||||
|
self.formatter.print_request_info(
|
||||||
|
provider_name, "get_historical_prices(BTC)", timestamp, "✅ SUCCESS"
|
||||||
|
)
|
||||||
|
self.formatter.print_prices_table(prices, f"{provider_name} BTC Historical Prices")
|
||||||
|
results["tests"]["get_historical_prices"] = "SUCCESS"
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = str(e)
|
||||||
|
self.formatter.print_request_info(
|
||||||
|
provider_name, "get_historical_prices(BTC)", timestamp, "❌ ERROR", error_msg
|
||||||
|
)
|
||||||
|
results["tests"]["get_historical_prices"] = f"ERROR: {error_msg}"
|
||||||
|
results["overall_status"] = "PARTIAL"
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
def check_environment_variables() -> Dict[str, bool]:
|
||||||
|
"""Verifica la presenza delle variabili d'ambiente necessarie."""
|
||||||
|
env_vars = {
|
||||||
|
"COINBASE_API_KEY": bool(os.getenv("COINBASE_API_KEY")),
|
||||||
|
"COINBASE_API_SECRET": bool(os.getenv("COINBASE_API_SECRET")),
|
||||||
|
"CRYPTOCOMPARE_API_KEY": bool(os.getenv("CRYPTOCOMPARE_API_KEY")),
|
||||||
|
"BINANCE_API_KEY": bool(os.getenv("BINANCE_API_KEY")),
|
||||||
|
"BINANCE_API_SECRET": bool(os.getenv("BINANCE_API_SECRET")),
|
||||||
|
}
|
||||||
|
return env_vars
|
||||||
|
|
||||||
|
def initialize_providers() -> Dict[str, BaseWrapper]:
|
||||||
|
"""Inizializza tutti i provider disponibili."""
|
||||||
|
providers = {}
|
||||||
|
env_vars = check_environment_variables()
|
||||||
|
|
||||||
|
# PublicBinanceAgent (sempre disponibile)
|
||||||
|
try:
|
||||||
|
providers["PublicBinance"] = PublicBinanceAgent()
|
||||||
|
print("✅ PublicBinanceAgent inizializzato con successo")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Errore nell'inizializzazione di PublicBinanceAgent: {e}")
|
||||||
|
|
||||||
|
# CryptoCompareWrapper
|
||||||
|
if env_vars["CRYPTOCOMPARE_API_KEY"]:
|
||||||
|
try:
|
||||||
|
providers["CryptoCompare"] = CryptoCompareWrapper()
|
||||||
|
print("✅ CryptoCompareWrapper inizializzato con successo")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Errore nell'inizializzazione di CryptoCompareWrapper: {e}")
|
||||||
|
else:
|
||||||
|
print("⚠️ CryptoCompareWrapper saltato: CRYPTOCOMPARE_API_KEY non trovata")
|
||||||
|
|
||||||
|
# CoinBaseWrapper
|
||||||
|
if env_vars["COINBASE_API_KEY"] and env_vars["COINBASE_API_SECRET"]:
|
||||||
|
try:
|
||||||
|
providers["CoinBase"] = CoinBaseWrapper()
|
||||||
|
print("✅ CoinBaseWrapper inizializzato con successo")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Errore nell'inizializzazione di CoinBaseWrapper: {e}")
|
||||||
|
else:
|
||||||
|
print("⚠️ CoinBaseWrapper saltato: credenziali Coinbase non complete")
|
||||||
|
|
||||||
|
# BinanceWrapper
|
||||||
|
if env_vars["BINANCE_API_KEY"] and env_vars["BINANCE_API_SECRET"]:
|
||||||
|
try:
|
||||||
|
providers["Binance"] = BinanceWrapper()
|
||||||
|
print("✅ BinanceWrapper inizializzato con successo")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Errore nell'inizializzazione di BinanceWrapper: {e}")
|
||||||
|
else:
|
||||||
|
print("⚠️ BinanceWrapper saltato: credenziali Binance non complete")
|
||||||
|
|
||||||
|
return providers
|
||||||
|
|
||||||
|
def print_summary(results: List[Dict[str, Any]]):
|
||||||
|
"""Stampa un riassunto finale dei risultati."""
|
||||||
|
formatter = DemoFormatter()
|
||||||
|
formatter.print_header("📊 RIASSUNTO FINALE", "=", 80)
|
||||||
|
|
||||||
|
total_providers = len(results)
|
||||||
|
successful_providers = sum(1 for r in results if r["overall_status"] == "SUCCESS")
|
||||||
|
partial_providers = sum(1 for r in results if r["overall_status"] == "PARTIAL")
|
||||||
|
|
||||||
|
print(f"🔢 Provider testati: {total_providers}")
|
||||||
|
print(f"✅ Provider completamente funzionanti: {successful_providers}")
|
||||||
|
print(f"⚠️ Provider parzialmente funzionanti: {partial_providers}")
|
||||||
|
print(f"❌ Provider non funzionanti: {total_providers - successful_providers - partial_providers}")
|
||||||
|
|
||||||
|
print("\n📋 Dettaglio per provider:")
|
||||||
|
for result in results:
|
||||||
|
provider_name = result["provider_name"]
|
||||||
|
status = result["overall_status"]
|
||||||
|
status_icon = "✅" if status == "SUCCESS" else "⚠️" if status == "PARTIAL" else "❌"
|
||||||
|
|
||||||
|
print(f"\n{status_icon} {provider_name}:")
|
||||||
|
for test_name, test_result in result["tests"].items():
|
||||||
|
test_icon = "✅" if test_result == "SUCCESS" else "❌"
|
||||||
|
print(f" {test_icon} {test_name}: {test_result}")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Funzione principale del demo."""
|
||||||
|
formatter = DemoFormatter()
|
||||||
|
|
||||||
|
# Intestazione principale
|
||||||
|
formatter.print_header("🚀 DEMO COMPLETO MARKET DATA PROVIDERS", "=", 80)
|
||||||
|
|
||||||
|
print(f"🕒 Avvio demo: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||||
|
print("📝 Questo demo testa tutti i wrapper BaseWrapper disponibili")
|
||||||
|
print("🔍 Ogni test include timestamp, stato della richiesta e dati formattati")
|
||||||
|
|
||||||
|
# Verifica variabili d'ambiente
|
||||||
|
formatter.print_subheader("🔐 Verifica Configurazione")
|
||||||
|
env_vars = check_environment_variables()
|
||||||
|
|
||||||
|
print("Variabili d'ambiente:")
|
||||||
|
for var_name, is_present in env_vars.items():
|
||||||
|
status = "✅ Presente" if is_present else "❌ Mancante"
|
||||||
|
print(f" {var_name}: {status}")
|
||||||
|
|
||||||
|
# Inizializza provider
|
||||||
|
formatter.print_subheader("🏗️ Inizializzazione Provider")
|
||||||
|
providers = initialize_providers()
|
||||||
|
|
||||||
|
if not providers:
|
||||||
|
print("❌ Nessun provider disponibile. Verifica la configurazione.")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"\n🎯 Provider disponibili per il test: {list(providers.keys())}")
|
||||||
|
|
||||||
|
# Testa ogni provider
|
||||||
|
formatter.print_header("🧪 ESECUZIONE TEST PROVIDER", "=", 80)
|
||||||
|
|
||||||
|
tester = ProviderTester()
|
||||||
|
all_results = []
|
||||||
|
|
||||||
|
for provider_name, wrapper in providers.items():
|
||||||
|
try:
|
||||||
|
result = tester.test_provider(wrapper, provider_name)
|
||||||
|
all_results.append(result)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Errore critico nel test di {provider_name}: {e}")
|
||||||
|
traceback.print_exc()
|
||||||
|
all_results.append({
|
||||||
|
"provider_name": provider_name,
|
||||||
|
"tests": {},
|
||||||
|
"overall_status": "CRITICAL_ERROR",
|
||||||
|
"error": str(e)
|
||||||
|
})
|
||||||
|
|
||||||
|
# Stampa riassunto finale
|
||||||
|
print_summary(all_results)
|
||||||
|
|
||||||
|
# Informazioni aggiuntive
|
||||||
|
formatter.print_header("ℹ️ INFORMAZIONI AGGIUNTIVE", "=", 80)
|
||||||
|
print("📚 Documentazione:")
|
||||||
|
print(" - BaseWrapper: src/app/markets/base.py")
|
||||||
|
print(" - Test completi: tests/agents/test_market.py")
|
||||||
|
print(" - Configurazione: .env")
|
||||||
|
|
||||||
|
print("\n🔧 Per abilitare tutti i provider:")
|
||||||
|
print(" 1. Configura le credenziali nel file .env")
|
||||||
|
print(" 2. Segui la documentazione di ogni provider")
|
||||||
|
print(" 3. Riavvia il demo")
|
||||||
|
|
||||||
|
print(f"\n🏁 Demo completato: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -1,8 +1,14 @@
|
|||||||
from app.markets.base import BaseWrapper
|
from .base import BaseWrapper
|
||||||
from app.markets.coinbase import CoinBaseWrapper
|
from .coinbase import CoinBaseWrapper
|
||||||
from app.markets.cryptocompare import CryptoCompareWrapper
|
from .cryptocompare import CryptoCompareWrapper
|
||||||
|
from .binance import BinanceWrapper
|
||||||
|
from .binance_public import PublicBinanceAgent
|
||||||
|
from .error_handler import ProviderFallback, MarketAPIError, safe_execute
|
||||||
|
|
||||||
from agno.utils.log import log_warning
|
from agno.utils.log import log_warning
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
class MarketAPIs(BaseWrapper):
|
class MarketAPIs(BaseWrapper):
|
||||||
"""
|
"""
|
||||||
@@ -43,15 +49,37 @@ class MarketAPIs(BaseWrapper):
|
|||||||
"""
|
"""
|
||||||
self.currency = currency
|
self.currency = currency
|
||||||
self.wrappers = MarketAPIs.get_list_available_market_apis(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
|
||||||
|
|
||||||
# Metodi che semplicemente chiamano il metodo corrispondente del primo wrapper disponibile
|
|
||||||
# TODO magari fare in modo che se il primo fallisce, prova con il secondo, ecc.
|
|
||||||
# oppure fare un round-robin tra i vari wrapper oppure usarli tutti e fare una media dei risultati
|
|
||||||
def get_product(self, asset_id):
|
|
||||||
return self.wrappers[0].get_product(asset_id)
|
|
||||||
def get_products(self, asset_ids: list):
|
def get_products(self, asset_ids: list):
|
||||||
return self.wrappers[0].get_products(asset_ids)
|
"""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
|
||||||
|
|
||||||
def get_all_products(self):
|
def get_all_products(self):
|
||||||
return self.wrappers[0].get_all_products()
|
"""Ottiene tutti i prodotti con fallback automatico tra provider."""
|
||||||
def get_historical_prices(self, asset_id = "BTC"):
|
try:
|
||||||
return self.wrappers[0].get_historical_prices(asset_id)
|
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
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from coinbase.rest.types.product_types import Candle, GetProductResponse
|
from coinbase.rest.types.product_types import Candle, GetProductResponse, Product
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
class BaseWrapper:
|
class BaseWrapper:
|
||||||
@@ -27,16 +27,28 @@ class ProductInfo(BaseModel):
|
|||||||
status: str = ""
|
status: str = ""
|
||||||
quote_currency: str = ""
|
quote_currency: str = ""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
def from_coinbase(product_data: GetProductResponse) -> 'ProductInfo':
|
def from_coinbase(product_data: GetProductResponse) -> 'ProductInfo':
|
||||||
product = ProductInfo()
|
product = ProductInfo()
|
||||||
product.id = product_data.product_id
|
product.id = product_data.product_id or ""
|
||||||
product.symbol = product_data.base_currency_id
|
product.symbol = product_data.base_currency_id or ""
|
||||||
product.price = float(product_data.price)
|
product.price = float(product_data.price) if product_data.price else 0.0
|
||||||
product.volume_24h = float(product_data.volume_24h) if product_data.volume_24h else 0
|
product.volume_24h = float(product_data.volume_24h) if product_data.volume_24h else 0.0
|
||||||
# TODO Check what status means in Coinbase
|
# TODO Check what status means in Coinbase
|
||||||
product.status = product_data.status
|
product.status = product_data.status or ""
|
||||||
return product
|
return product
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_coinbase_product(product_data: Product) -> 'ProductInfo':
|
||||||
|
product = ProductInfo()
|
||||||
|
product.id = product_data.product_id or ""
|
||||||
|
product.symbol = product_data.base_currency_id or ""
|
||||||
|
product.price = float(product_data.price) if product_data.price else 0.0
|
||||||
|
product.volume_24h = float(product_data.volume_24h) if product_data.volume_24h else 0.0
|
||||||
|
product.status = product_data.status or ""
|
||||||
|
return product
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
def from_cryptocompare(asset_data: dict) -> 'ProductInfo':
|
def from_cryptocompare(asset_data: dict) -> 'ProductInfo':
|
||||||
product = ProductInfo()
|
product = ProductInfo()
|
||||||
product.id = asset_data['FROMSYMBOL'] + '-' + asset_data['TOSYMBOL']
|
product.id = asset_data['FROMSYMBOL'] + '-' + asset_data['TOSYMBOL']
|
||||||
@@ -46,6 +58,27 @@ class ProductInfo(BaseModel):
|
|||||||
product.status = "" # Cryptocompare does not provide status
|
product.status = "" # Cryptocompare does not provide status
|
||||||
return product
|
return product
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_binance(ticker_data: dict, ticker_24h_data: dict) -> 'ProductInfo':
|
||||||
|
"""
|
||||||
|
Crea un oggetto ProductInfo da dati Binance.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ticker_data: Dati del ticker di prezzo
|
||||||
|
ticker_24h_data: Dati del ticker 24h
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Oggetto ProductInfo
|
||||||
|
"""
|
||||||
|
product = ProductInfo()
|
||||||
|
product.id = ticker_data['symbol']
|
||||||
|
product.symbol = ticker_data['symbol'].replace('USDT', '').replace('BUSD', '')
|
||||||
|
product.price = float(ticker_data['price'])
|
||||||
|
product.volume_24h = float(ticker_24h_data['volume'])
|
||||||
|
product.status = "TRADING" # Binance non fornisce status esplicito
|
||||||
|
product.quote_currency = "USDT" # Assumiamo USDT come default
|
||||||
|
return product
|
||||||
|
|
||||||
class Price(BaseModel):
|
class Price(BaseModel):
|
||||||
"""
|
"""
|
||||||
Rappresenta i dati di prezzo per un asset, come ottenuti dalle API di mercato.
|
Rappresenta i dati di prezzo per un asset, come ottenuti dalle API di mercato.
|
||||||
@@ -58,16 +91,18 @@ class Price(BaseModel):
|
|||||||
volume: float = 0.0
|
volume: float = 0.0
|
||||||
time: str = ""
|
time: str = ""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
def from_coinbase(candle_data: Candle) -> 'Price':
|
def from_coinbase(candle_data: Candle) -> 'Price':
|
||||||
price = Price()
|
price = Price()
|
||||||
price.high = float(candle_data.high)
|
price.high = float(candle_data.high) if candle_data.high else 0.0
|
||||||
price.low = float(candle_data.low)
|
price.low = float(candle_data.low) if candle_data.low else 0.0
|
||||||
price.open = float(candle_data.open)
|
price.open = float(candle_data.open) if candle_data.open else 0.0
|
||||||
price.close = float(candle_data.close)
|
price.close = float(candle_data.close) if candle_data.close else 0.0
|
||||||
price.volume = float(candle_data.volume)
|
price.volume = float(candle_data.volume) if candle_data.volume else 0.0
|
||||||
price.time = str(candle_data.start)
|
price.time = str(candle_data.start) if candle_data.start else ""
|
||||||
return price
|
return price
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
def from_cryptocompare(price_data: dict) -> 'Price':
|
def from_cryptocompare(price_data: dict) -> 'Price':
|
||||||
price = Price()
|
price = Price()
|
||||||
price.high = float(price_data['high'])
|
price.high = float(price_data['high'])
|
||||||
|
|||||||
@@ -1,30 +1,169 @@
|
|||||||
# Versione pubblica senza autenticazione
|
import os
|
||||||
|
from typing import Optional
|
||||||
|
from datetime import datetime, timedelta
|
||||||
from binance.client import Client
|
from binance.client import Client
|
||||||
|
from .base import ProductInfo, BaseWrapper, Price
|
||||||
|
from .error_handler import retry_on_failure, handle_api_errors, MarketAPIError
|
||||||
|
|
||||||
# TODO fare l'aggancio con API in modo da poterlo usare come wrapper di mercato
|
|
||||||
# TODO implementare i metodi di BaseWrapper
|
|
||||||
|
|
||||||
class PublicBinanceAgent:
|
class BinanceWrapper(BaseWrapper):
|
||||||
def __init__(self):
|
"""
|
||||||
# Client pubblico (senza credenziali)
|
Wrapper per le API autenticate di Binance.
|
||||||
self.client = Client()
|
|
||||||
|
|
||||||
def get_public_prices(self):
|
Implementa l'interfaccia BaseWrapper per fornire accesso unificato
|
||||||
"""Ottiene prezzi pubblici"""
|
ai dati di mercato di Binance tramite le API REST con autenticazione.
|
||||||
|
|
||||||
|
La documentazione delle API è disponibile qui:
|
||||||
|
https://binance-docs.github.io/apidocs/spot/en/
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, api_key: Optional[str] = None, api_secret: Optional[str] = None, currency: str = "USDT"):
|
||||||
|
"""
|
||||||
|
Inizializza il wrapper con le credenziali API.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
api_key: Chiave API di Binance (se None, usa variabile d'ambiente)
|
||||||
|
api_secret: Secret API di Binance (se None, usa variabile d'ambiente)
|
||||||
|
currency: Valuta di quotazione di default (default: USDT)
|
||||||
|
"""
|
||||||
|
if api_key is None:
|
||||||
|
api_key = os.getenv("BINANCE_API_KEY")
|
||||||
|
assert api_key is not None, "API key is required"
|
||||||
|
|
||||||
|
if api_secret is None:
|
||||||
|
api_secret = os.getenv("BINANCE_API_SECRET")
|
||||||
|
assert api_secret is not None, "API secret is required"
|
||||||
|
|
||||||
|
self.currency = currency
|
||||||
|
self.client = Client(api_key=api_key, api_secret=api_secret)
|
||||||
|
|
||||||
|
def __format_symbol(self, asset_id: str) -> str:
|
||||||
|
"""
|
||||||
|
Formatta l'asset_id nel formato richiesto da Binance.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
asset_id: ID dell'asset (es. "BTC" o "BTC-USDT")
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Simbolo formattato per Binance (es. "BTCUSDT")
|
||||||
|
"""
|
||||||
|
if '-' in asset_id:
|
||||||
|
# Se già nel formato "BTC-USDT", converte in "BTCUSDT"
|
||||||
|
return asset_id.replace('-', '')
|
||||||
|
else:
|
||||||
|
# Se solo "BTC", aggiunge la valuta di default
|
||||||
|
return 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:
|
||||||
|
"""
|
||||||
|
Ottiene informazioni su un singolo prodotto.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
asset_id: ID dell'asset da recuperare
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Oggetto ProductInfo con le informazioni del prodotto
|
||||||
|
"""
|
||||||
|
symbol = self.__format_symbol(asset_id)
|
||||||
|
ticker = self.client.get_symbol_ticker(symbol=symbol)
|
||||||
|
ticker_24h = self.client.get_ticker(symbol=symbol)
|
||||||
|
|
||||||
|
return ProductInfo.from_binance(ticker, ticker_24h)
|
||||||
|
|
||||||
|
@retry_on_failure(max_retries=3, delay=1.0)
|
||||||
|
@handle_api_errors
|
||||||
|
def get_products(self, asset_ids: list[str]) -> list[ProductInfo]:
|
||||||
|
"""
|
||||||
|
Ottiene informazioni su più prodotti.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
asset_ids: Lista di ID degli asset da recuperare
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Lista di oggetti ProductInfo
|
||||||
|
"""
|
||||||
|
products = []
|
||||||
|
for asset_id in asset_ids:
|
||||||
|
try:
|
||||||
|
product = self.get_product(asset_id)
|
||||||
|
products.append(product)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Errore nel recupero di {asset_id}: {e}")
|
||||||
|
continue
|
||||||
|
return products
|
||||||
|
|
||||||
|
@retry_on_failure(max_retries=3, delay=1.0)
|
||||||
|
@handle_api_errors
|
||||||
|
def get_all_products(self) -> list[ProductInfo]:
|
||||||
|
"""
|
||||||
|
Ottiene informazioni su tutti i prodotti disponibili.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Lista di oggetti ProductInfo per tutti i prodotti
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
btc_price = self.client.get_symbol_ticker(symbol="BTCUSDT")
|
# Ottiene tutti i ticker 24h che contengono le informazioni necessarie
|
||||||
eth_price = self.client.get_symbol_ticker(symbol="ETHUSDT")
|
all_tickers = self.client.get_ticker()
|
||||||
|
products = []
|
||||||
|
|
||||||
return {
|
for ticker in all_tickers:
|
||||||
'BTC_USD': float(btc_price['price']),
|
# Filtra solo i simboli che terminano con la valuta di default
|
||||||
'ETH_USD': float(eth_price['price']),
|
if ticker['symbol'].endswith(self.currency):
|
||||||
'source': 'binance_public'
|
try:
|
||||||
}
|
# Crea ProductInfo direttamente dal ticker 24h
|
||||||
|
product = ProductInfo()
|
||||||
|
product.id = ticker['symbol']
|
||||||
|
product.symbol = ticker['symbol'].replace(self.currency, '')
|
||||||
|
product.price = float(ticker['lastPrice'])
|
||||||
|
product.volume_24h = float(ticker['volume'])
|
||||||
|
product.status = "TRADING" # Binance non fornisce status esplicito
|
||||||
|
product.quote_currency = self.currency
|
||||||
|
products.append(product)
|
||||||
|
except (ValueError, KeyError) as e:
|
||||||
|
print(f"Errore nel parsing di {ticker['symbol']}: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
return products
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Errore: {e}")
|
print(f"Errore nel recupero di tutti i prodotti: {e}")
|
||||||
return None
|
return []
|
||||||
|
|
||||||
# Uso senza credenziali
|
@retry_on_failure(max_retries=3, delay=1.0)
|
||||||
public_agent = PublicBinanceAgent()
|
@handle_api_errors
|
||||||
public_prices = public_agent.get_public_prices()
|
def get_historical_prices(self, asset_id: str = "BTC") -> list[Price]:
|
||||||
print(public_prices)
|
"""
|
||||||
|
Ottiene i prezzi storici per un asset.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
asset_id: ID dell'asset (default: "BTC")
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Lista di oggetti Price con i dati storici
|
||||||
|
"""
|
||||||
|
symbol = self.__format_symbol(asset_id)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Ottiene candele orarie degli ultimi 30 giorni
|
||||||
|
klines = self.client.get_historical_klines(
|
||||||
|
symbol=symbol,
|
||||||
|
interval=Client.KLINE_INTERVAL_1HOUR,
|
||||||
|
start_str="30 days ago UTC"
|
||||||
|
)
|
||||||
|
|
||||||
|
prices = []
|
||||||
|
for kline in klines:
|
||||||
|
price = Price()
|
||||||
|
price.open = float(kline[1])
|
||||||
|
price.high = float(kline[2])
|
||||||
|
price.low = float(kline[3])
|
||||||
|
price.close = float(kline[4])
|
||||||
|
price.volume = float(kline[5])
|
||||||
|
price.time = str(datetime.fromtimestamp(kline[0] / 1000))
|
||||||
|
prices.append(price)
|
||||||
|
|
||||||
|
return prices
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Errore nel recupero dei prezzi storici per {symbol}: {e}")
|
||||||
|
return []
|
||||||
|
|||||||
227
src/app/markets/binance_public.py
Normal file
227
src/app/markets/binance_public.py
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
"""
|
||||||
|
Versione pubblica di Binance per accesso ai dati pubblici senza autenticazione.
|
||||||
|
|
||||||
|
Questa implementazione estende BaseWrapper per mantenere coerenza
|
||||||
|
con l'architettura del modulo markets.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Optional, Dict, Any
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from binance.client import Client
|
||||||
|
from .base import BaseWrapper, ProductInfo, Price
|
||||||
|
from .error_handler import retry_on_failure, handle_api_errors, MarketAPIError
|
||||||
|
|
||||||
|
|
||||||
|
class PublicBinanceAgent(BaseWrapper):
|
||||||
|
"""
|
||||||
|
Agent per l'accesso ai dati pubblici di Binance.
|
||||||
|
|
||||||
|
Utilizza l'API pubblica di Binance per ottenere informazioni
|
||||||
|
sui prezzi e sui mercati senza richiedere autenticazione.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""Inizializza il client pubblico senza credenziali."""
|
||||||
|
self.client = Client()
|
||||||
|
|
||||||
|
def __format_symbol(self, asset_id: str) -> str:
|
||||||
|
"""
|
||||||
|
Formatta l'asset_id per Binance (es. BTC -> BTCUSDT).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
asset_id: ID dell'asset (es. "BTC", "ETH")
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Simbolo formattato per Binance
|
||||||
|
"""
|
||||||
|
if asset_id.endswith("USDT") or asset_id.endswith("BUSD"):
|
||||||
|
return asset_id
|
||||||
|
return f"{asset_id}USDT"
|
||||||
|
|
||||||
|
@retry_on_failure(max_retries=3, delay=1.0)
|
||||||
|
@handle_api_errors
|
||||||
|
def get_product(self, asset_id: str) -> ProductInfo:
|
||||||
|
"""
|
||||||
|
Ottiene informazioni su un singolo prodotto.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
asset_id: ID dell'asset (es. "BTC")
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Oggetto ProductInfo con le informazioni del prodotto
|
||||||
|
"""
|
||||||
|
symbol = self.__format_symbol(asset_id)
|
||||||
|
try:
|
||||||
|
ticker = self.client.get_symbol_ticker(symbol=symbol)
|
||||||
|
ticker_24h = self.client.get_ticker(symbol=symbol)
|
||||||
|
return ProductInfo.from_binance(ticker, ticker_24h)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Errore nel recupero del prodotto {asset_id}: {e}")
|
||||||
|
return ProductInfo(id=asset_id, symbol=asset_id)
|
||||||
|
|
||||||
|
@retry_on_failure(max_retries=3, delay=1.0)
|
||||||
|
@handle_api_errors
|
||||||
|
def get_products(self, asset_ids: list[str]) -> list[ProductInfo]:
|
||||||
|
"""
|
||||||
|
Ottiene informazioni su più prodotti.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
asset_ids: Lista di ID degli asset
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Lista di oggetti ProductInfo
|
||||||
|
"""
|
||||||
|
products = []
|
||||||
|
for asset_id in asset_ids:
|
||||||
|
product = self.get_product(asset_id)
|
||||||
|
products.append(product)
|
||||||
|
return products
|
||||||
|
|
||||||
|
@retry_on_failure(max_retries=3, delay=1.0)
|
||||||
|
@handle_api_errors
|
||||||
|
def get_all_products(self) -> list[ProductInfo]:
|
||||||
|
"""
|
||||||
|
Ottiene informazioni su tutti i prodotti disponibili.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Lista di oggetti ProductInfo per i principali asset
|
||||||
|
"""
|
||||||
|
# Per la versione pubblica, restituiamo solo i principali asset
|
||||||
|
major_assets = ["BTC", "ETH", "BNB", "ADA", "DOT", "LINK", "LTC", "XRP"]
|
||||||
|
return self.get_products(major_assets)
|
||||||
|
|
||||||
|
@retry_on_failure(max_retries=3, delay=1.0)
|
||||||
|
@handle_api_errors
|
||||||
|
def get_historical_prices(self, asset_id: str = "BTC") -> list[Price]:
|
||||||
|
"""
|
||||||
|
Ottiene i prezzi storici per un asset.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
asset_id: ID dell'asset (default: "BTC")
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Lista di oggetti Price con i dati storici
|
||||||
|
"""
|
||||||
|
symbol = self.__format_symbol(asset_id)
|
||||||
|
try:
|
||||||
|
# Ottieni candele degli ultimi 30 giorni
|
||||||
|
end_time = datetime.now()
|
||||||
|
start_time = end_time - timedelta(days=30)
|
||||||
|
|
||||||
|
klines = self.client.get_historical_klines(
|
||||||
|
symbol,
|
||||||
|
Client.KLINE_INTERVAL_1DAY,
|
||||||
|
start_time.strftime("%d %b %Y %H:%M:%S"),
|
||||||
|
end_time.strftime("%d %b %Y %H:%M:%S")
|
||||||
|
)
|
||||||
|
|
||||||
|
prices = []
|
||||||
|
for kline in klines:
|
||||||
|
price = Price(
|
||||||
|
open=float(kline[1]),
|
||||||
|
high=float(kline[2]),
|
||||||
|
low=float(kline[3]),
|
||||||
|
close=float(kline[4]),
|
||||||
|
volume=float(kline[5]),
|
||||||
|
time=str(datetime.fromtimestamp(kline[0] / 1000))
|
||||||
|
)
|
||||||
|
prices.append(price)
|
||||||
|
|
||||||
|
return prices
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Errore nel recupero dei prezzi storici per {asset_id}: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
def get_public_prices(self, symbols: Optional[list[str]] = None) -> Optional[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Ottiene i prezzi pubblici per i simboli specificati.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
symbols: Lista di simboli da recuperare (es. ["BTCUSDT", "ETHUSDT"]).
|
||||||
|
Se None, recupera BTC e ETH di default.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dizionario con i prezzi e informazioni sulla fonte, o None in caso di errore.
|
||||||
|
"""
|
||||||
|
if symbols is None:
|
||||||
|
symbols = ["BTCUSDT", "ETHUSDT"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
prices = {}
|
||||||
|
for symbol in symbols:
|
||||||
|
ticker = self.client.get_symbol_ticker(symbol=symbol)
|
||||||
|
# Converte BTCUSDT -> BTC_USD per consistenza
|
||||||
|
clean_symbol = symbol.replace("USDT", "_USD").replace("BUSD", "_USD")
|
||||||
|
prices[clean_symbol] = float(ticker['price'])
|
||||||
|
|
||||||
|
return {
|
||||||
|
**prices,
|
||||||
|
'source': 'binance_public',
|
||||||
|
'timestamp': self.client.get_server_time()['serverTime']
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Errore nel recupero dei prezzi pubblici: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_24hr_ticker(self, symbol: str) -> Optional[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Ottiene le statistiche 24h per un simbolo specifico.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
symbol: Simbolo del trading pair (es. "BTCUSDT")
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dizionario con le statistiche 24h o None in caso di errore.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
ticker = self.client.get_ticker(symbol=symbol)
|
||||||
|
return {
|
||||||
|
'symbol': ticker['symbol'],
|
||||||
|
'price': float(ticker['lastPrice']),
|
||||||
|
'price_change': float(ticker['priceChange']),
|
||||||
|
'price_change_percent': float(ticker['priceChangePercent']),
|
||||||
|
'high_24h': float(ticker['highPrice']),
|
||||||
|
'low_24h': float(ticker['lowPrice']),
|
||||||
|
'volume_24h': float(ticker['volume']),
|
||||||
|
'source': 'binance_public'
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Errore nel recupero del ticker 24h per {symbol}: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_exchange_info(self) -> Optional[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Ottiene informazioni generali sull'exchange.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dizionario con informazioni sull'exchange o None in caso di errore.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
info = self.client.get_exchange_info()
|
||||||
|
return {
|
||||||
|
'timezone': info['timezone'],
|
||||||
|
'server_time': info['serverTime'],
|
||||||
|
'symbols_count': len(info['symbols']),
|
||||||
|
'source': 'binance_public'
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Errore nel recupero delle informazioni exchange: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# Esempio di utilizzo
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Uso senza credenziali
|
||||||
|
public_agent = PublicBinanceAgent()
|
||||||
|
|
||||||
|
# Ottieni prezzi di default (BTC e ETH)
|
||||||
|
public_prices = public_agent.get_public_prices()
|
||||||
|
print("Prezzi pubblici:", public_prices)
|
||||||
|
|
||||||
|
# Ottieni statistiche 24h per BTC
|
||||||
|
btc_stats = public_agent.get_24hr_ticker("BTCUSDT")
|
||||||
|
print("Statistiche BTC 24h:", btc_stats)
|
||||||
|
|
||||||
|
# Ottieni informazioni exchange
|
||||||
|
exchange_info = public_agent.get_exchange_info()
|
||||||
|
print("Info exchange:", exchange_info)
|
||||||
@@ -1,13 +1,21 @@
|
|||||||
import os
|
import os
|
||||||
|
from typing import Optional
|
||||||
|
from datetime import datetime, timedelta
|
||||||
from coinbase.rest import RESTClient
|
from coinbase.rest import RESTClient
|
||||||
from app.markets.base import ProductInfo, BaseWrapper, Price
|
from .base import ProductInfo, BaseWrapper, Price
|
||||||
|
from .error_handler import retry_on_failure, handle_api_errors, MarketAPIError, RateLimitError
|
||||||
|
|
||||||
class CoinBaseWrapper(BaseWrapper):
|
class CoinBaseWrapper(BaseWrapper):
|
||||||
"""
|
"""
|
||||||
Wrapper per le API di Coinbase.
|
Wrapper per le API di Coinbase Advanced Trade.
|
||||||
La documentazione delle API è disponibile qui: https://docs.cdp.coinbase.com/api-reference/advanced-trade-api/rest-api/introduction
|
|
||||||
|
Implementa l'interfaccia BaseWrapper per fornire accesso unificato
|
||||||
|
ai dati di mercato di Coinbase tramite le API REST.
|
||||||
|
|
||||||
|
La documentazione delle API è disponibile qui:
|
||||||
|
https://docs.cdp.coinbase.com/api-reference/advanced-trade-api/rest-api/introduction
|
||||||
"""
|
"""
|
||||||
def __init__(self, api_key:str = None, api_private_key:str = None, currency: str = "USD"):
|
def __init__(self, api_key: Optional[str] = None, api_private_key: Optional[str] = None, currency: str = "USD"):
|
||||||
if api_key is None:
|
if api_key is None:
|
||||||
api_key = os.getenv("COINBASE_API_KEY")
|
api_key = os.getenv("COINBASE_API_KEY")
|
||||||
assert api_key is not None, "API key is required"
|
assert api_key is not None, "API key is required"
|
||||||
@@ -25,21 +33,49 @@ class CoinBaseWrapper(BaseWrapper):
|
|||||||
def __format(self, asset_id: str) -> str:
|
def __format(self, asset_id: str) -> str:
|
||||||
return asset_id if '-' in asset_id else f"{asset_id}-{self.currency}"
|
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:
|
def get_product(self, asset_id: str) -> ProductInfo:
|
||||||
asset_id = self.__format(asset_id)
|
asset_id = self.__format(asset_id)
|
||||||
asset = self.client.get_product(asset_id)
|
asset = self.client.get_product(asset_id)
|
||||||
return ProductInfo.from_coinbase(asset)
|
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]:
|
def get_products(self, asset_ids: list[str]) -> list[ProductInfo]:
|
||||||
all_asset_ids = [self.__format(asset_id) for asset_id in asset_ids]
|
all_asset_ids = [self.__format(asset_id) for asset_id in asset_ids]
|
||||||
assets = self.client.get_products(all_asset_ids)
|
assets = self.client.get_products(product_ids=all_asset_ids)
|
||||||
return [ProductInfo.from_coinbase(asset) for asset in assets.products]
|
if assets.products:
|
||||||
|
return [ProductInfo.from_coinbase_product(asset) for asset in assets.products]
|
||||||
|
return []
|
||||||
|
|
||||||
|
@retry_on_failure(max_retries=3, delay=1.0)
|
||||||
|
@handle_api_errors
|
||||||
def get_all_products(self) -> list[ProductInfo]:
|
def get_all_products(self) -> list[ProductInfo]:
|
||||||
assets = self.client.get_products()
|
assets = self.client.get_products()
|
||||||
return [ProductInfo.from_coinbase(asset) for asset in assets.products]
|
if assets.products:
|
||||||
|
return [ProductInfo.from_coinbase_product(asset) for asset in assets.products]
|
||||||
|
return []
|
||||||
|
|
||||||
|
@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") -> list[Price]:
|
||||||
asset_id = self.__format(asset_id)
|
asset_id = self.__format(asset_id)
|
||||||
data = self.client.get_candles(product_id=asset_id)
|
# Get last 14 days of hourly data (14*24 = 336 candles, within 350 limit)
|
||||||
return [Price.from_coinbase(candle) for candle in data.candles]
|
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
|
||||||
|
)
|
||||||
|
if data.candles:
|
||||||
|
return [Price.from_coinbase(candle) for candle in data.candles]
|
||||||
|
return []
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import os
|
import os
|
||||||
import requests
|
import requests
|
||||||
from app.markets.base import ProductInfo, BaseWrapper, Price
|
from typing import Optional, Dict, Any
|
||||||
|
from .base import ProductInfo, BaseWrapper, Price
|
||||||
|
from .error_handler import retry_on_failure, handle_api_errors, MarketAPIError
|
||||||
|
|
||||||
BASE_URL = "https://min-api.cryptocompare.com"
|
BASE_URL = "https://min-api.cryptocompare.com"
|
||||||
|
|
||||||
@@ -10,7 +12,7 @@ class CryptoCompareWrapper(BaseWrapper):
|
|||||||
La documentazione delle API è disponibile qui: https://developers.coindesk.com/documentation/legacy/Price/SingleSymbolPriceEndpoint
|
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.
|
!!ATTENZIONE!! sembra essere una API legacy e potrebbe essere deprecata in futuro.
|
||||||
"""
|
"""
|
||||||
def __init__(self, api_key:str = None, currency:str='USD'):
|
def __init__(self, api_key: Optional[str] = None, currency: str = 'USD'):
|
||||||
if api_key is None:
|
if api_key is None:
|
||||||
api_key = os.getenv("CRYPTOCOMPARE_API_KEY")
|
api_key = os.getenv("CRYPTOCOMPARE_API_KEY")
|
||||||
assert api_key is not None, "API key is required"
|
assert api_key is not None, "API key is required"
|
||||||
@@ -18,7 +20,7 @@ class CryptoCompareWrapper(BaseWrapper):
|
|||||||
self.api_key = api_key
|
self.api_key = api_key
|
||||||
self.currency = currency
|
self.currency = currency
|
||||||
|
|
||||||
def __request(self, endpoint: str, params: dict = None) -> dict:
|
def __request(self, endpoint: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||||
if params is None:
|
if params is None:
|
||||||
params = {}
|
params = {}
|
||||||
params['api_key'] = self.api_key
|
params['api_key'] = self.api_key
|
||||||
@@ -26,6 +28,8 @@ class CryptoCompareWrapper(BaseWrapper):
|
|||||||
response = requests.get(f"{BASE_URL}{endpoint}", params=params)
|
response = requests.get(f"{BASE_URL}{endpoint}", params=params)
|
||||||
return response.json()
|
return response.json()
|
||||||
|
|
||||||
|
@retry_on_failure(max_retries=3, delay=1.0)
|
||||||
|
@handle_api_errors
|
||||||
def get_product(self, asset_id: str) -> ProductInfo:
|
def get_product(self, asset_id: str) -> ProductInfo:
|
||||||
response = self.__request("/data/pricemultifull", params = {
|
response = self.__request("/data/pricemultifull", params = {
|
||||||
"fsyms": asset_id,
|
"fsyms": asset_id,
|
||||||
@@ -34,6 +38,8 @@ class CryptoCompareWrapper(BaseWrapper):
|
|||||||
data = response.get('RAW', {}).get(asset_id, {}).get(self.currency, {})
|
data = response.get('RAW', {}).get(asset_id, {}).get(self.currency, {})
|
||||||
return ProductInfo.from_cryptocompare(data)
|
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]:
|
def get_products(self, asset_ids: list[str]) -> list[ProductInfo]:
|
||||||
response = self.__request("/data/pricemultifull", params = {
|
response = self.__request("/data/pricemultifull", params = {
|
||||||
"fsyms": ",".join(asset_ids),
|
"fsyms": ",".join(asset_ids),
|
||||||
@@ -46,10 +52,37 @@ class CryptoCompareWrapper(BaseWrapper):
|
|||||||
assets.append(ProductInfo.from_cryptocompare(asset_data))
|
assets.append(ProductInfo.from_cryptocompare(asset_data))
|
||||||
return assets
|
return assets
|
||||||
|
|
||||||
|
@retry_on_failure(max_retries=3, delay=1.0)
|
||||||
|
@handle_api_errors
|
||||||
def get_all_products(self) -> list[ProductInfo]:
|
def get_all_products(self) -> list[ProductInfo]:
|
||||||
raise NotImplementedError("CryptoCompare does not support fetching all assets")
|
"""
|
||||||
|
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"
|
||||||
|
]
|
||||||
|
|
||||||
def get_historical_prices(self, asset_id: str, day_back: int = 10) -> list[dict]:
|
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)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
@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"
|
assert day_back <= 30, "day_back should be less than or equal to 30"
|
||||||
response = self.__request("/data/v2/histohour", params = {
|
response = self.__request("/data/v2/histohour", params = {
|
||||||
"fsym": asset_id,
|
"fsym": asset_id,
|
||||||
|
|||||||
236
src/app/markets/error_handler.py
Normal file
236
src/app/markets/error_handler.py
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
"""
|
||||||
|
Modulo per la gestione robusta degli errori nei market providers.
|
||||||
|
|
||||||
|
Fornisce decoratori e utilità per:
|
||||||
|
- Retry automatico con backoff esponenziale
|
||||||
|
- Logging standardizzato degli errori
|
||||||
|
- Gestione di timeout e rate limiting
|
||||||
|
- Fallback tra provider multipli
|
||||||
|
"""
|
||||||
|
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
from functools import wraps
|
||||||
|
from typing import Any, Callable, Optional, Type, Union, List
|
||||||
|
from requests.exceptions import RequestException, Timeout, ConnectionError
|
||||||
|
from binance.exceptions import BinanceAPIException, BinanceRequestException
|
||||||
|
|
||||||
|
# Configurazione logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class MarketAPIError(Exception):
|
||||||
|
"""Eccezione base per errori delle API di mercato."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class RateLimitError(MarketAPIError):
|
||||||
|
"""Eccezione per errori di rate limiting."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class AuthenticationError(MarketAPIError):
|
||||||
|
"""Eccezione per errori di autenticazione."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class DataNotFoundError(MarketAPIError):
|
||||||
|
"""Eccezione quando i dati richiesti non sono disponibili."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def retry_on_failure(
|
||||||
|
max_retries: int = 3,
|
||||||
|
delay: float = 1.0,
|
||||||
|
backoff_factor: float = 2.0,
|
||||||
|
exceptions: tuple = (RequestException, BinanceAPIException, BinanceRequestException)
|
||||||
|
) -> Callable:
|
||||||
|
"""
|
||||||
|
Decoratore per retry automatico con backoff esponenziale.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
max_retries: Numero massimo di tentativi
|
||||||
|
delay: Delay iniziale in secondi
|
||||||
|
backoff_factor: Fattore di moltiplicazione per il delay
|
||||||
|
exceptions: Tuple di eccezioni da catturare per il retry
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Decoratore per la funzione
|
||||||
|
"""
|
||||||
|
def decorator(func: Callable) -> Callable:
|
||||||
|
@wraps(func)
|
||||||
|
def wrapper(*args, **kwargs) -> Any:
|
||||||
|
last_exception = None
|
||||||
|
current_delay = delay
|
||||||
|
|
||||||
|
for attempt in range(max_retries + 1):
|
||||||
|
try:
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
except exceptions as e:
|
||||||
|
last_exception = e
|
||||||
|
|
||||||
|
if attempt == max_retries:
|
||||||
|
logger.error(
|
||||||
|
f"Function {func.__name__} failed after {max_retries + 1} attempts. "
|
||||||
|
f"Last error: {str(e)}"
|
||||||
|
)
|
||||||
|
raise MarketAPIError(f"Max retries exceeded: {str(e)}") from e
|
||||||
|
|
||||||
|
logger.warning(
|
||||||
|
f"Attempt {attempt + 1}/{max_retries + 1} failed for {func.__name__}: {str(e)}. "
|
||||||
|
f"Retrying in {current_delay:.1f}s..."
|
||||||
|
)
|
||||||
|
|
||||||
|
time.sleep(current_delay)
|
||||||
|
current_delay *= backoff_factor
|
||||||
|
except Exception as e:
|
||||||
|
# Per eccezioni non previste, non fare retry
|
||||||
|
logger.error(f"Unexpected error in {func.__name__}: {str(e)}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
# Questo non dovrebbe mai essere raggiunto
|
||||||
|
if last_exception:
|
||||||
|
raise last_exception
|
||||||
|
else:
|
||||||
|
raise MarketAPIError("Unknown error occurred")
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
def handle_api_errors(func: Callable) -> Callable:
|
||||||
|
"""
|
||||||
|
Decoratore per gestione standardizzata degli errori API.
|
||||||
|
|
||||||
|
Converte errori specifici dei provider in eccezioni standardizzate.
|
||||||
|
"""
|
||||||
|
@wraps(func)
|
||||||
|
def wrapper(*args, **kwargs) -> Any:
|
||||||
|
try:
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
except BinanceAPIException as e:
|
||||||
|
if e.code == -1021: # Timestamp error
|
||||||
|
raise MarketAPIError(f"Binance timestamp error: {e.message}")
|
||||||
|
elif e.code == -1003: # Rate limit
|
||||||
|
raise RateLimitError(f"Binance rate limit exceeded: {e.message}")
|
||||||
|
elif e.code in [-2014, -2015]: # API key errors
|
||||||
|
raise AuthenticationError(f"Binance authentication error: {e.message}")
|
||||||
|
else:
|
||||||
|
raise MarketAPIError(f"Binance API error [{e.code}]: {e.message}")
|
||||||
|
except ConnectionError as e:
|
||||||
|
raise MarketAPIError(f"Connection error: {str(e)}")
|
||||||
|
except Timeout as e:
|
||||||
|
raise MarketAPIError(f"Request timeout: {str(e)}")
|
||||||
|
except RequestException as e:
|
||||||
|
raise MarketAPIError(f"Request error: {str(e)}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Unexpected error in {func.__name__}: {str(e)}")
|
||||||
|
raise MarketAPIError(f"Unexpected error: {str(e)}") from e
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
def safe_execute(
|
||||||
|
func: Callable,
|
||||||
|
default_value: Any = None,
|
||||||
|
log_errors: bool = True,
|
||||||
|
error_message: Optional[str] = None
|
||||||
|
) -> Any:
|
||||||
|
"""
|
||||||
|
Esegue una funzione in modo sicuro, restituendo un valore di default in caso di errore.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
func: Funzione da eseguire
|
||||||
|
default_value: Valore da restituire in caso di errore
|
||||||
|
log_errors: Se loggare gli errori
|
||||||
|
error_message: Messaggio di errore personalizzato
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Risultato della funzione o valore di default
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return func()
|
||||||
|
except Exception as e:
|
||||||
|
if log_errors:
|
||||||
|
message = error_message or f"Error executing {func.__name__}"
|
||||||
|
logger.warning(f"{message}: {str(e)}")
|
||||||
|
return default_value
|
||||||
|
|
||||||
|
class ProviderFallback:
|
||||||
|
"""
|
||||||
|
Classe per gestire il fallback tra provider multipli.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, providers: List[Any]):
|
||||||
|
"""
|
||||||
|
Inizializza con una lista di provider ordinati per priorità.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
providers: Lista di provider ordinati per priorità
|
||||||
|
"""
|
||||||
|
self.providers = providers
|
||||||
|
|
||||||
|
def execute_with_fallback(
|
||||||
|
self,
|
||||||
|
method_name: str,
|
||||||
|
*args,
|
||||||
|
**kwargs
|
||||||
|
) -> Any:
|
||||||
|
"""
|
||||||
|
Esegue un metodo su tutti i provider fino a trovarne uno che funziona.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
method_name: Nome del metodo da chiamare
|
||||||
|
*args: Argomenti posizionali
|
||||||
|
**kwargs: Argomenti nominali
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Risultato del primo provider che funziona
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
MarketAPIError: Se tutti i provider falliscono
|
||||||
|
"""
|
||||||
|
last_error = None
|
||||||
|
|
||||||
|
for i, provider in enumerate(self.providers):
|
||||||
|
try:
|
||||||
|
if hasattr(provider, method_name):
|
||||||
|
method = getattr(provider, method_name)
|
||||||
|
result = method(*args, **kwargs)
|
||||||
|
|
||||||
|
if i > 0: # Se non è il primo provider
|
||||||
|
logger.info(f"Fallback successful: used provider {type(provider).__name__}")
|
||||||
|
|
||||||
|
return result
|
||||||
|
else:
|
||||||
|
logger.warning(f"Provider {type(provider).__name__} doesn't have method {method_name}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
last_error = e
|
||||||
|
logger.warning(
|
||||||
|
f"Provider {type(provider).__name__} failed for {method_name}: {str(e)}"
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Se arriviamo qui, tutti i provider hanno fallito
|
||||||
|
raise MarketAPIError(
|
||||||
|
f"All providers failed for method {method_name}. Last error: {str(last_error)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def validate_response_data(data: Any, required_fields: Optional[List[str]] = None) -> bool:
|
||||||
|
"""
|
||||||
|
Valida che i dati di risposta contengano i campi richiesti.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data: Dati da validare
|
||||||
|
required_fields: Lista di campi richiesti
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True se i dati sono validi, False altrimenti
|
||||||
|
"""
|
||||||
|
if data is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if required_fields is None:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if isinstance(data, dict):
|
||||||
|
return all(field in data for field in required_fields)
|
||||||
|
elif hasattr(data, '__dict__'):
|
||||||
|
return all(hasattr(data, field) for field in required_fields)
|
||||||
|
|
||||||
|
return False
|
||||||
@@ -1,146 +1,596 @@
|
|||||||
|
"""
|
||||||
|
Test suite completo per il sistema di mercato.
|
||||||
|
|
||||||
|
Questo modulo testa approfonditamente tutte le implementazioni di BaseWrapper
|
||||||
|
e verifica la conformità all'interfaccia definita in base.py.
|
||||||
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import pytest
|
import pytest
|
||||||
from app.agents.market import MarketToolkit
|
from unittest.mock import Mock, patch, MagicMock
|
||||||
from app.markets.base import BaseWrapper
|
from typing import Type, List
|
||||||
|
|
||||||
|
# Import delle classi da testare
|
||||||
|
from app.markets.base import BaseWrapper, ProductInfo, Price
|
||||||
from app.markets.coinbase import CoinBaseWrapper
|
from app.markets.coinbase import CoinBaseWrapper
|
||||||
from app.markets.cryptocompare import CryptoCompareWrapper
|
from app.markets.cryptocompare import CryptoCompareWrapper
|
||||||
|
from app.markets.binance import BinanceWrapper
|
||||||
|
from app.markets.binance_public import PublicBinanceAgent
|
||||||
from app.markets import MarketAPIs
|
from app.markets import MarketAPIs
|
||||||
|
|
||||||
class TestMarketSystem:
|
|
||||||
"""Test suite per il sistema di mercato (wrappers + toolkit)"""
|
|
||||||
|
|
||||||
@pytest.fixture(scope="class")
|
class TestBaseWrapperInterface:
|
||||||
def market_wrapper(self) -> BaseWrapper:
|
"""Test per verificare che tutte le implementazioni rispettino l'interfaccia BaseWrapper."""
|
||||||
return MarketAPIs("USD")
|
|
||||||
|
|
||||||
def test_wrapper_initialization(self, market_wrapper):
|
def test_all_wrappers_extend_basewrapper(self):
|
||||||
assert market_wrapper is not None
|
"""Verifica che tutte le classi wrapper estendano BaseWrapper."""
|
||||||
assert hasattr(market_wrapper, 'get_product')
|
wrapper_classes = [
|
||||||
assert hasattr(market_wrapper, 'get_products')
|
CoinBaseWrapper,
|
||||||
assert hasattr(market_wrapper, 'get_all_products')
|
CryptoCompareWrapper,
|
||||||
assert hasattr(market_wrapper, 'get_historical_prices')
|
BinanceWrapper,
|
||||||
|
PublicBinanceAgent,
|
||||||
|
MarketAPIs
|
||||||
|
]
|
||||||
|
|
||||||
def test_providers_configuration(self):
|
for wrapper_class in wrapper_classes:
|
||||||
available_providers = []
|
assert issubclass(wrapper_class, BaseWrapper), f"{wrapper_class.__name__} deve estendere BaseWrapper"
|
||||||
if os.getenv('CDP_API_KEY_NAME') and os.getenv('CDP_API_PRIVATE_KEY'):
|
|
||||||
available_providers.append('coinbase')
|
|
||||||
if os.getenv('CRYPTOCOMPARE_API_KEY'):
|
|
||||||
available_providers.append('cryptocompare')
|
|
||||||
assert len(available_providers) > 0
|
|
||||||
|
|
||||||
def test_wrapper_capabilities(self, market_wrapper):
|
def test_all_wrappers_implement_required_methods(self):
|
||||||
capabilities = []
|
"""Verifica che tutte le classi implementino i metodi richiesti dall'interfaccia."""
|
||||||
if hasattr(market_wrapper, 'get_product'):
|
wrapper_classes = [
|
||||||
capabilities.append('single_product')
|
CoinBaseWrapper,
|
||||||
if hasattr(market_wrapper, 'get_products'):
|
CryptoCompareWrapper,
|
||||||
capabilities.append('multiple_products')
|
BinanceWrapper,
|
||||||
if hasattr(market_wrapper, 'get_historical_prices'):
|
PublicBinanceAgent,
|
||||||
capabilities.append('historical_data')
|
MarketAPIs
|
||||||
assert len(capabilities) > 0
|
]
|
||||||
|
|
||||||
def test_market_data_retrieval(self, market_wrapper):
|
required_methods = ['get_product', 'get_products', 'get_all_products', 'get_historical_prices']
|
||||||
btc_product = market_wrapper.get_product("BTC")
|
|
||||||
assert btc_product is not None
|
|
||||||
assert hasattr(btc_product, 'symbol')
|
|
||||||
assert hasattr(btc_product, 'price')
|
|
||||||
assert btc_product.price > 0
|
|
||||||
|
|
||||||
def test_market_toolkit_integration(self, market_wrapper):
|
for wrapper_class in wrapper_classes:
|
||||||
try:
|
for method in required_methods:
|
||||||
toolkit = MarketToolkit()
|
assert hasattr(wrapper_class, method), f"{wrapper_class.__name__} deve implementare {method}"
|
||||||
assert toolkit is not None
|
assert callable(getattr(wrapper_class, method)), f"{method} deve essere callable in {wrapper_class.__name__}"
|
||||||
assert hasattr(toolkit, 'market_agent')
|
|
||||||
assert toolkit.market_api is not None
|
|
||||||
|
|
||||||
tools = toolkit.tools
|
|
||||||
assert len(tools) > 0
|
|
||||||
|
|
||||||
except Exception as e:
|
class TestProductInfoModel:
|
||||||
print(f"MarketToolkit test failed: {e}")
|
"""Test per la classe ProductInfo e i suoi metodi di conversione."""
|
||||||
# Non fail completamente - il toolkit potrebbe avere dipendenze specifiche
|
|
||||||
|
def test_productinfo_initialization(self):
|
||||||
|
"""Test inizializzazione di ProductInfo."""
|
||||||
|
product = ProductInfo()
|
||||||
|
assert product.id == ""
|
||||||
|
assert product.symbol == ""
|
||||||
|
assert product.price == 0.0
|
||||||
|
assert product.volume_24h == 0.0
|
||||||
|
assert product.status == ""
|
||||||
|
assert product.quote_currency == ""
|
||||||
|
|
||||||
|
def test_productinfo_with_data(self):
|
||||||
|
"""Test ProductInfo con dati specifici."""
|
||||||
|
product = ProductInfo(
|
||||||
|
id="BTC-USD",
|
||||||
|
symbol="BTC",
|
||||||
|
price=50000.0,
|
||||||
|
volume_24h=1000000.0,
|
||||||
|
status="TRADING",
|
||||||
|
quote_currency="USD"
|
||||||
|
)
|
||||||
|
assert product.id == "BTC-USD"
|
||||||
|
assert product.symbol == "BTC"
|
||||||
|
assert product.price == 50000.0
|
||||||
|
assert product.volume_24h == 1000000.0
|
||||||
|
assert product.status == "TRADING"
|
||||||
|
assert product.quote_currency == "USD"
|
||||||
|
|
||||||
|
def test_productinfo_from_cryptocompare(self):
|
||||||
|
"""Test conversione da dati CryptoCompare."""
|
||||||
|
mock_data = {
|
||||||
|
'FROMSYMBOL': 'BTC',
|
||||||
|
'TOSYMBOL': 'USD',
|
||||||
|
'PRICE': 50000.0,
|
||||||
|
'VOLUME24HOUR': 1000000.0
|
||||||
|
}
|
||||||
|
|
||||||
|
product = ProductInfo.from_cryptocompare(mock_data)
|
||||||
|
assert product.id == "BTC-USD"
|
||||||
|
assert product.symbol == "BTC"
|
||||||
|
assert product.price == 50000.0
|
||||||
|
assert product.volume_24h == 1000000.0
|
||||||
|
assert product.status == ""
|
||||||
|
|
||||||
|
def test_productinfo_from_binance(self):
|
||||||
|
"""Test conversione da dati Binance."""
|
||||||
|
ticker_data = {'symbol': 'BTCUSDT', 'price': '50000.0'}
|
||||||
|
ticker_24h_data = {'volume': '1000000.0'}
|
||||||
|
|
||||||
|
product = ProductInfo.from_binance(ticker_data, ticker_24h_data)
|
||||||
|
assert product.id == "BTCUSDT"
|
||||||
|
assert product.symbol == "BTC"
|
||||||
|
assert product.price == 50000.0
|
||||||
|
assert product.volume_24h == 1000000.0
|
||||||
|
assert product.status == "TRADING"
|
||||||
|
assert product.quote_currency == "USDT"
|
||||||
|
|
||||||
|
|
||||||
|
class TestPriceModel:
|
||||||
|
"""Test per la classe Price e i suoi metodi di conversione."""
|
||||||
|
|
||||||
|
def test_price_initialization(self):
|
||||||
|
"""Test inizializzazione di Price."""
|
||||||
|
price = Price()
|
||||||
|
assert price.high == 0.0
|
||||||
|
assert price.low == 0.0
|
||||||
|
assert price.open == 0.0
|
||||||
|
assert price.close == 0.0
|
||||||
|
assert price.volume == 0.0
|
||||||
|
assert price.time == ""
|
||||||
|
|
||||||
|
def test_price_with_data(self):
|
||||||
|
"""Test Price con dati specifici."""
|
||||||
|
price = Price(
|
||||||
|
high=51000.0,
|
||||||
|
low=49000.0,
|
||||||
|
open=50000.0,
|
||||||
|
close=50500.0,
|
||||||
|
volume=1000.0,
|
||||||
|
time="2024-01-01T00:00:00Z"
|
||||||
|
)
|
||||||
|
assert price.high == 51000.0
|
||||||
|
assert price.low == 49000.0
|
||||||
|
assert price.open == 50000.0
|
||||||
|
assert price.close == 50500.0
|
||||||
|
assert price.volume == 1000.0
|
||||||
|
assert price.time == "2024-01-01T00:00:00Z"
|
||||||
|
|
||||||
|
def test_price_from_cryptocompare(self):
|
||||||
|
"""Test conversione da dati CryptoCompare."""
|
||||||
|
mock_data = {
|
||||||
|
'high': 51000.0,
|
||||||
|
'low': 49000.0,
|
||||||
|
'open': 50000.0,
|
||||||
|
'close': 50500.0,
|
||||||
|
'volumeto': 1000.0,
|
||||||
|
'time': 1704067200
|
||||||
|
}
|
||||||
|
|
||||||
|
price = Price.from_cryptocompare(mock_data)
|
||||||
|
assert price.high == 51000.0
|
||||||
|
assert price.low == 49000.0
|
||||||
|
assert price.open == 50000.0
|
||||||
|
assert price.close == 50500.0
|
||||||
|
assert price.volume == 1000.0
|
||||||
|
assert price.time == "1704067200"
|
||||||
|
|
||||||
|
|
||||||
|
class TestCoinBaseWrapper:
|
||||||
|
"""Test specifici per CoinBaseWrapper."""
|
||||||
|
|
||||||
|
@pytest.mark.skipif(
|
||||||
|
not (os.getenv('COINBASE_API_KEY') and os.getenv('COINBASE_API_SECRET')),
|
||||||
|
reason="Credenziali Coinbase non configurate"
|
||||||
|
)
|
||||||
|
def test_coinbase_initialization_with_env_vars(self):
|
||||||
|
"""Test inizializzazione con variabili d'ambiente."""
|
||||||
|
wrapper = CoinBaseWrapper(currency="USD")
|
||||||
|
assert wrapper.currency == "USD"
|
||||||
|
assert wrapper.client is not None
|
||||||
|
|
||||||
|
@patch.dict(os.environ, {}, clear=True)
|
||||||
|
def test_coinbase_initialization_with_params(self):
|
||||||
|
"""Test inizializzazione con parametri espliciti quando non ci sono variabili d'ambiente."""
|
||||||
|
with pytest.raises(AssertionError, match="API key is required"):
|
||||||
|
CoinBaseWrapper(api_key=None, api_private_key=None)
|
||||||
|
|
||||||
|
@patch('app.markets.coinbase.RESTClient')
|
||||||
|
def test_coinbase_asset_formatting_behavior(self, mock_client):
|
||||||
|
"""Test comportamento di formattazione asset ID attraverso get_product."""
|
||||||
|
mock_response = Mock()
|
||||||
|
mock_response.product_id = "BTC-USD"
|
||||||
|
mock_response.base_currency_id = "BTC"
|
||||||
|
mock_response.price = "50000.0"
|
||||||
|
mock_response.volume_24h = "1000000.0"
|
||||||
|
mock_response.status = "TRADING"
|
||||||
|
|
||||||
|
mock_client_instance = Mock()
|
||||||
|
mock_client_instance.get_product.return_value = mock_response
|
||||||
|
mock_client.return_value = mock_client_instance
|
||||||
|
|
||||||
|
wrapper = CoinBaseWrapper(api_key="test", api_private_key="test")
|
||||||
|
|
||||||
|
# Test che entrambi i formati funzionino
|
||||||
|
wrapper.get_product("BTC")
|
||||||
|
wrapper.get_product("BTC-USD")
|
||||||
|
|
||||||
|
# Verifica che get_product sia stato chiamato con il formato corretto
|
||||||
|
assert mock_client_instance.get_product.call_count == 2
|
||||||
|
|
||||||
|
@patch('app.markets.coinbase.RESTClient')
|
||||||
|
def test_coinbase_get_product(self, mock_client):
|
||||||
|
"""Test get_product con mock."""
|
||||||
|
mock_response = Mock()
|
||||||
|
mock_response.product_id = "BTC-USD"
|
||||||
|
mock_response.base_currency_id = "BTC"
|
||||||
|
mock_response.price = "50000.0"
|
||||||
|
mock_response.volume_24h = "1000000.0"
|
||||||
|
mock_response.status = "TRADING"
|
||||||
|
|
||||||
|
mock_client_instance = Mock()
|
||||||
|
mock_client_instance.get_product.return_value = mock_response
|
||||||
|
mock_client.return_value = mock_client_instance
|
||||||
|
|
||||||
|
wrapper = CoinBaseWrapper(api_key="test", api_private_key="test")
|
||||||
|
product = wrapper.get_product("BTC")
|
||||||
|
|
||||||
|
assert isinstance(product, ProductInfo)
|
||||||
|
assert product.symbol == "BTC"
|
||||||
|
mock_client_instance.get_product.assert_called_once_with("BTC-USD")
|
||||||
|
|
||||||
|
|
||||||
|
class TestCryptoCompareWrapper:
|
||||||
|
"""Test specifici per CryptoCompareWrapper."""
|
||||||
|
|
||||||
@pytest.mark.skipif(
|
@pytest.mark.skipif(
|
||||||
not os.getenv('CRYPTOCOMPARE_API_KEY'),
|
not os.getenv('CRYPTOCOMPARE_API_KEY'),
|
||||||
reason="CRYPTOCOMPARE_API_KEY not configured"
|
reason="CRYPTOCOMPARE_API_KEY non configurata"
|
||||||
)
|
)
|
||||||
def test_cryptocompare_wrapper(self):
|
def test_cryptocompare_initialization_with_env_var(self):
|
||||||
try:
|
"""Test inizializzazione con variabile d'ambiente."""
|
||||||
api_key = os.getenv('CRYPTOCOMPARE_API_KEY')
|
wrapper = CryptoCompareWrapper(currency="USD")
|
||||||
wrapper = CryptoCompareWrapper(api_key=api_key, currency="USD")
|
assert wrapper.currency == "USD"
|
||||||
|
assert wrapper.api_key is not None
|
||||||
|
|
||||||
btc_product = wrapper.get_product("BTC")
|
def test_cryptocompare_initialization_with_param(self):
|
||||||
assert btc_product is not None
|
"""Test inizializzazione con parametro esplicito."""
|
||||||
assert btc_product.symbol == "BTC"
|
wrapper = CryptoCompareWrapper(api_key="test_key", currency="EUR")
|
||||||
assert btc_product.price > 0
|
assert wrapper.api_key == "test_key"
|
||||||
|
assert wrapper.currency == "EUR"
|
||||||
|
|
||||||
products = wrapper.get_products(["BTC", "ETH"])
|
@patch('app.markets.cryptocompare.requests.get')
|
||||||
assert isinstance(products, list)
|
def test_cryptocompare_get_product(self, mock_get):
|
||||||
assert len(products) > 0
|
"""Test get_product con mock."""
|
||||||
|
mock_response = Mock()
|
||||||
|
mock_response.json.return_value = {
|
||||||
|
'RAW': {
|
||||||
|
'BTC': {
|
||||||
|
'USD': {
|
||||||
|
'FROMSYMBOL': 'BTC',
|
||||||
|
'TOSYMBOL': 'USD',
|
||||||
|
'PRICE': 50000.0,
|
||||||
|
'VOLUME24HOUR': 1000000.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mock_response.raise_for_status.return_value = None
|
||||||
|
mock_get.return_value = mock_response
|
||||||
|
|
||||||
for product in products:
|
wrapper = CryptoCompareWrapper(api_key="test_key")
|
||||||
if product.symbol in ["BTC", "ETH"]:
|
product = wrapper.get_product("BTC")
|
||||||
assert product.price > 0
|
|
||||||
|
|
||||||
except Exception as e:
|
assert isinstance(product, ProductInfo)
|
||||||
print(f"CryptoCompare test failed: {e}")
|
assert product.symbol == "BTC"
|
||||||
# Non fail il test se c'è un errore di rete o API
|
assert product.price == 50000.0
|
||||||
|
|
||||||
@pytest.mark.skipif(
|
def test_cryptocompare_get_all_products_workaround(self):
|
||||||
not (os.getenv('CDP_API_KEY_NAME') and os.getenv('CDP_API_PRIVATE_KEY')),
|
"""Test che get_all_products funzioni con il workaround implementato."""
|
||||||
reason="Coinbase credentials not configured"
|
wrapper = CryptoCompareWrapper(api_key="test_key")
|
||||||
)
|
# Il metodo ora dovrebbe restituire una lista di ProductInfo invece di sollevare NotImplementedError
|
||||||
def test_coinbase_wrapper(self):
|
products = wrapper.get_all_products()
|
||||||
try:
|
assert isinstance(products, list)
|
||||||
api_key = os.getenv('CDP_API_KEY_NAME')
|
# Verifica che la lista non sia vuota (dovrebbe contenere almeno alcuni asset popolari)
|
||||||
api_secret = os.getenv('CDP_API_PRIVATE_KEY')
|
assert len(products) > 0
|
||||||
wrapper = CoinBaseWrapper(
|
# Verifica che ogni elemento sia un ProductInfo
|
||||||
api_key=api_key,
|
for product in products:
|
||||||
api_private_key=api_secret,
|
assert isinstance(product, ProductInfo)
|
||||||
currency="USD"
|
|
||||||
)
|
|
||||||
|
|
||||||
btc_product = wrapper.get_product("BTC")
|
|
||||||
assert btc_product is not None
|
|
||||||
assert btc_product.symbol == "BTC"
|
|
||||||
assert btc_product.price > 0
|
|
||||||
|
|
||||||
products = wrapper.get_products(["BTC", "ETH"])
|
class TestBinanceWrapper:
|
||||||
assert isinstance(products, list)
|
"""Test specifici per BinanceWrapper."""
|
||||||
assert len(products) > 0
|
|
||||||
|
|
||||||
except Exception as e:
|
def test_binance_initialization_without_credentials(self):
|
||||||
print(f"Coinbase test failed: {e}")
|
"""Test che l'inizializzazione fallisca senza credenziali."""
|
||||||
# Non fail il test se c'è un errore di credenziali o rete
|
# Assicuriamoci che le variabili d'ambiente siano vuote per questo test
|
||||||
|
with patch.dict(os.environ, {}, clear=True):
|
||||||
|
with pytest.raises(AssertionError, match="API key is required"):
|
||||||
|
BinanceWrapper(api_key=None, api_secret="test")
|
||||||
|
|
||||||
def test_provider_selection_mechanism(self):
|
with pytest.raises(AssertionError, match="API secret is required"):
|
||||||
potential_providers = 0
|
BinanceWrapper(api_key="test", api_secret=None)
|
||||||
if os.getenv('CDP_API_KEY_NAME') and os.getenv('CDP_API_PRIVATE_KEY'):
|
|
||||||
potential_providers += 1
|
|
||||||
if os.getenv('CRYPTOCOMPARE_API_KEY'):
|
|
||||||
potential_providers += 1
|
|
||||||
|
|
||||||
if potential_providers == 0:
|
@patch('app.markets.binance.Client')
|
||||||
with pytest.raises(AssertionError, match="No valid API keys"):
|
def test_binance_symbol_formatting_behavior(self, mock_client):
|
||||||
MarketAPIs.get_list_available_market_apis()
|
"""Test comportamento di formattazione simbolo attraverso get_product."""
|
||||||
else:
|
mock_client_instance = Mock()
|
||||||
wrapper = MarketAPIs("USD")
|
mock_client_instance.get_symbol_ticker.return_value = {
|
||||||
assert wrapper is not None
|
'symbol': 'BTCUSDT',
|
||||||
assert hasattr(wrapper, 'get_product')
|
'price': '50000.0'
|
||||||
|
}
|
||||||
|
mock_client_instance.get_ticker.return_value = {
|
||||||
|
'volume': '1000000.0'
|
||||||
|
}
|
||||||
|
mock_client.return_value = mock_client_instance
|
||||||
|
|
||||||
def test_error_handling(self, market_wrapper):
|
wrapper = BinanceWrapper(api_key="test", api_secret="test")
|
||||||
try:
|
|
||||||
fake_product = market_wrapper.get_product("NONEXISTENT_CRYPTO_SYMBOL_12345")
|
|
||||||
assert fake_product is None or fake_product.price == 0
|
|
||||||
except Exception as e:
|
|
||||||
pass
|
|
||||||
|
|
||||||
try:
|
# Test che entrambi i formati funzionino
|
||||||
empty_products = market_wrapper.get_products([])
|
wrapper.get_product("BTC")
|
||||||
assert isinstance(empty_products, list)
|
wrapper.get_product("BTCUSDT")
|
||||||
except Exception as e:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_wrapper_currency_support(self, market_wrapper):
|
# Verifica che i metodi siano stati chiamati
|
||||||
assert hasattr(market_wrapper, 'currency')
|
assert mock_client_instance.get_symbol_ticker.call_count == 2
|
||||||
assert isinstance(market_wrapper.currency, str)
|
|
||||||
assert len(market_wrapper.currency) >= 3 # USD, EUR, etc.
|
@patch('app.markets.binance.Client')
|
||||||
|
def test_binance_get_product(self, mock_client):
|
||||||
|
"""Test get_product con mock."""
|
||||||
|
mock_client_instance = Mock()
|
||||||
|
mock_client_instance.get_symbol_ticker.return_value = {
|
||||||
|
'symbol': 'BTCUSDT',
|
||||||
|
'price': '50000.0'
|
||||||
|
}
|
||||||
|
mock_client_instance.get_ticker.return_value = {
|
||||||
|
'volume': '1000000.0'
|
||||||
|
}
|
||||||
|
mock_client.return_value = mock_client_instance
|
||||||
|
|
||||||
|
wrapper = BinanceWrapper(api_key="test", api_secret="test")
|
||||||
|
product = wrapper.get_product("BTC")
|
||||||
|
|
||||||
|
assert isinstance(product, ProductInfo)
|
||||||
|
assert product.symbol == "BTC"
|
||||||
|
assert product.price == 50000.0
|
||||||
|
|
||||||
|
|
||||||
|
class TestPublicBinanceAgent:
|
||||||
|
"""Test specifici per PublicBinanceAgent."""
|
||||||
|
|
||||||
|
@patch('app.markets.binance_public.Client')
|
||||||
|
def test_public_binance_initialization(self, mock_client):
|
||||||
|
"""Test inizializzazione senza credenziali."""
|
||||||
|
agent = PublicBinanceAgent()
|
||||||
|
assert agent.client is not None
|
||||||
|
mock_client.assert_called_once_with()
|
||||||
|
|
||||||
|
@patch('app.markets.binance_public.Client')
|
||||||
|
def test_public_binance_symbol_formatting_behavior(self, mock_client):
|
||||||
|
"""Test comportamento di formattazione simbolo attraverso get_product."""
|
||||||
|
mock_client_instance = Mock()
|
||||||
|
mock_client_instance.get_symbol_ticker.return_value = {
|
||||||
|
'symbol': 'BTCUSDT',
|
||||||
|
'price': '50000.0'
|
||||||
|
}
|
||||||
|
mock_client_instance.get_ticker.return_value = {
|
||||||
|
'volume': '1000000.0'
|
||||||
|
}
|
||||||
|
mock_client.return_value = mock_client_instance
|
||||||
|
|
||||||
|
agent = PublicBinanceAgent()
|
||||||
|
|
||||||
|
# Test che entrambi i formati funzionino
|
||||||
|
agent.get_product("BTC")
|
||||||
|
agent.get_product("BTCUSDT")
|
||||||
|
|
||||||
|
# Verifica che i metodi siano stati chiamati
|
||||||
|
assert mock_client_instance.get_symbol_ticker.call_count == 2
|
||||||
|
|
||||||
|
@patch('app.markets.binance_public.Client')
|
||||||
|
def test_public_binance_get_product(self, mock_client):
|
||||||
|
"""Test get_product con mock."""
|
||||||
|
mock_client_instance = Mock()
|
||||||
|
mock_client_instance.get_symbol_ticker.return_value = {
|
||||||
|
'symbol': 'BTCUSDT',
|
||||||
|
'price': '50000.0'
|
||||||
|
}
|
||||||
|
mock_client_instance.get_ticker.return_value = {
|
||||||
|
'volume': '1000000.0'
|
||||||
|
}
|
||||||
|
mock_client.return_value = mock_client_instance
|
||||||
|
|
||||||
|
agent = PublicBinanceAgent()
|
||||||
|
product = agent.get_product("BTC")
|
||||||
|
|
||||||
|
assert isinstance(product, ProductInfo)
|
||||||
|
assert product.symbol == "BTC"
|
||||||
|
assert product.price == 50000.0
|
||||||
|
|
||||||
|
@patch('app.markets.binance_public.Client')
|
||||||
|
def test_public_binance_get_all_products(self, mock_client):
|
||||||
|
"""Test get_all_products restituisce asset principali."""
|
||||||
|
mock_client_instance = Mock()
|
||||||
|
mock_client_instance.get_symbol_ticker.return_value = {
|
||||||
|
'symbol': 'BTCUSDT',
|
||||||
|
'price': '50000.0'
|
||||||
|
}
|
||||||
|
mock_client_instance.get_ticker.return_value = {
|
||||||
|
'volume': '1000000.0'
|
||||||
|
}
|
||||||
|
mock_client.return_value = mock_client_instance
|
||||||
|
|
||||||
|
agent = PublicBinanceAgent()
|
||||||
|
products = agent.get_all_products()
|
||||||
|
|
||||||
|
assert isinstance(products, list)
|
||||||
|
assert len(products) == 8 # Numero di asset principali definiti
|
||||||
|
for product in products:
|
||||||
|
assert isinstance(product, ProductInfo)
|
||||||
|
|
||||||
|
@patch('app.markets.binance_public.Client')
|
||||||
|
def test_public_binance_get_public_prices(self, mock_client):
|
||||||
|
"""Test metodo specifico get_public_prices."""
|
||||||
|
mock_client_instance = Mock()
|
||||||
|
mock_client_instance.get_symbol_ticker.return_value = {'price': '50000.0'}
|
||||||
|
mock_client_instance.get_server_time.return_value = {'serverTime': 1704067200000}
|
||||||
|
mock_client.return_value = mock_client_instance
|
||||||
|
|
||||||
|
agent = PublicBinanceAgent()
|
||||||
|
prices = agent.get_public_prices(["BTCUSDT"])
|
||||||
|
|
||||||
|
assert isinstance(prices, dict)
|
||||||
|
assert 'BTC_USD' in prices
|
||||||
|
assert prices['BTC_USD'] == 50000.0
|
||||||
|
assert 'source' in prices
|
||||||
|
assert prices['source'] == 'binance_public'
|
||||||
|
|
||||||
|
|
||||||
|
class TestMarketAPIs:
|
||||||
|
"""Test per la classe MarketAPIs che aggrega i wrapper."""
|
||||||
|
|
||||||
|
def test_market_apis_initialization_no_providers(self):
|
||||||
|
"""Test che l'inizializzazione fallisca senza provider disponibili."""
|
||||||
|
with patch.dict(os.environ, {}, clear=True):
|
||||||
|
with pytest.raises(AssertionError, match="No market API keys"):
|
||||||
|
MarketAPIs("USD")
|
||||||
|
|
||||||
|
@patch('app.markets.CoinBaseWrapper')
|
||||||
|
def test_market_apis_with_coinbase_only(self, mock_coinbase):
|
||||||
|
"""Test con solo Coinbase disponibile."""
|
||||||
|
mock_instance = Mock()
|
||||||
|
mock_coinbase.return_value = mock_instance
|
||||||
|
|
||||||
|
with patch('app.markets.CryptoCompareWrapper', side_effect=Exception("No API key")):
|
||||||
|
apis = MarketAPIs("USD")
|
||||||
|
assert len(apis.wrappers) == 1
|
||||||
|
assert apis.wrappers[0] == mock_instance
|
||||||
|
|
||||||
|
@patch('app.markets.CoinBaseWrapper')
|
||||||
|
@patch('app.markets.CryptoCompareWrapper')
|
||||||
|
def test_market_apis_delegation(self, mock_crypto, mock_coinbase):
|
||||||
|
"""Test che i metodi vengano delegati al primo wrapper disponibile."""
|
||||||
|
mock_coinbase_instance = Mock()
|
||||||
|
mock_crypto_instance = Mock()
|
||||||
|
mock_coinbase.return_value = mock_coinbase_instance
|
||||||
|
mock_crypto.return_value = mock_crypto_instance
|
||||||
|
|
||||||
|
apis = MarketAPIs("USD")
|
||||||
|
|
||||||
|
# Test delegazione get_product
|
||||||
|
apis.get_product("BTC")
|
||||||
|
mock_coinbase_instance.get_product.assert_called_once_with("BTC")
|
||||||
|
|
||||||
|
# Test delegazione get_products
|
||||||
|
apis.get_products(["BTC", "ETH"])
|
||||||
|
mock_coinbase_instance.get_products.assert_called_once_with(["BTC", "ETH"])
|
||||||
|
|
||||||
|
# Test delegazione get_all_products
|
||||||
|
apis.get_all_products()
|
||||||
|
mock_coinbase_instance.get_all_products.assert_called_once()
|
||||||
|
|
||||||
|
# Test delegazione get_historical_prices
|
||||||
|
apis.get_historical_prices("BTC")
|
||||||
|
mock_coinbase_instance.get_historical_prices.assert_called_once_with("BTC")
|
||||||
|
|
||||||
|
|
||||||
|
class TestErrorHandling:
|
||||||
|
"""Test per la gestione degli errori in tutti i wrapper."""
|
||||||
|
|
||||||
|
@patch('app.markets.binance_public.Client')
|
||||||
|
def test_public_binance_error_handling(self, mock_client):
|
||||||
|
"""Test gestione errori in PublicBinanceAgent."""
|
||||||
|
mock_client_instance = Mock()
|
||||||
|
mock_client_instance.get_symbol_ticker.side_effect = Exception("API Error")
|
||||||
|
mock_client.return_value = mock_client_instance
|
||||||
|
|
||||||
|
agent = PublicBinanceAgent()
|
||||||
|
product = agent.get_product("INVALID")
|
||||||
|
|
||||||
|
# Dovrebbe restituire un ProductInfo vuoto invece di sollevare eccezione
|
||||||
|
assert isinstance(product, ProductInfo)
|
||||||
|
assert product.id == "INVALID"
|
||||||
|
assert product.symbol == "INVALID"
|
||||||
|
|
||||||
|
@patch('app.markets.cryptocompare.requests.get')
|
||||||
|
def test_cryptocompare_network_error(self, mock_get):
|
||||||
|
"""Test gestione errori di rete in CryptoCompareWrapper."""
|
||||||
|
mock_get.side_effect = Exception("Network Error")
|
||||||
|
|
||||||
|
wrapper = CryptoCompareWrapper(api_key="test")
|
||||||
|
|
||||||
|
with pytest.raises(Exception):
|
||||||
|
wrapper.get_product("BTC")
|
||||||
|
|
||||||
|
@patch('app.markets.binance.Client')
|
||||||
|
def test_binance_api_error_in_get_products(self, mock_client):
|
||||||
|
"""Test gestione errori in BinanceWrapper.get_products."""
|
||||||
|
mock_client_instance = Mock()
|
||||||
|
mock_client_instance.get_symbol_ticker.side_effect = Exception("API Error")
|
||||||
|
mock_client.return_value = mock_client_instance
|
||||||
|
|
||||||
|
wrapper = BinanceWrapper(api_key="test", api_secret="test")
|
||||||
|
products = wrapper.get_products(["BTC", "ETH"])
|
||||||
|
|
||||||
|
# Dovrebbe restituire lista vuota invece di sollevare eccezione
|
||||||
|
assert isinstance(products, list)
|
||||||
|
assert len(products) == 0
|
||||||
|
|
||||||
|
|
||||||
|
class TestIntegrationScenarios:
|
||||||
|
"""Test di integrazione per scenari reali."""
|
||||||
|
|
||||||
|
def test_wrapper_method_signatures(self):
|
||||||
|
"""Verifica che tutti i wrapper abbiano le stesse signature dei metodi."""
|
||||||
|
wrapper_classes = [CoinBaseWrapper, CryptoCompareWrapper, BinanceWrapper, PublicBinanceAgent]
|
||||||
|
|
||||||
|
for wrapper_class in wrapper_classes:
|
||||||
|
# Verifica get_product
|
||||||
|
assert hasattr(wrapper_class, 'get_product')
|
||||||
|
|
||||||
|
# Verifica get_products
|
||||||
|
assert hasattr(wrapper_class, 'get_products')
|
||||||
|
|
||||||
|
# Verifica get_all_products
|
||||||
|
assert hasattr(wrapper_class, 'get_all_products')
|
||||||
|
|
||||||
|
# Verifica get_historical_prices
|
||||||
|
assert hasattr(wrapper_class, 'get_historical_prices')
|
||||||
|
|
||||||
|
def test_productinfo_consistency(self):
|
||||||
|
"""Test che tutti i metodi from_* di ProductInfo restituiscano oggetti consistenti."""
|
||||||
|
# Test from_cryptocompare
|
||||||
|
crypto_data = {
|
||||||
|
'FROMSYMBOL': 'BTC',
|
||||||
|
'TOSYMBOL': 'USD',
|
||||||
|
'PRICE': 50000.0,
|
||||||
|
'VOLUME24HOUR': 1000000.0
|
||||||
|
}
|
||||||
|
crypto_product = ProductInfo.from_cryptocompare(crypto_data)
|
||||||
|
|
||||||
|
# Test from_binance
|
||||||
|
binance_ticker = {'symbol': 'BTCUSDT', 'price': '50000.0'}
|
||||||
|
binance_24h = {'volume': '1000000.0'}
|
||||||
|
binance_product = ProductInfo.from_binance(binance_ticker, binance_24h)
|
||||||
|
|
||||||
|
# Verifica che entrambi abbiano gli stessi campi
|
||||||
|
assert hasattr(crypto_product, 'id')
|
||||||
|
assert hasattr(crypto_product, 'symbol')
|
||||||
|
assert hasattr(crypto_product, 'price')
|
||||||
|
assert hasattr(crypto_product, 'volume_24h')
|
||||||
|
|
||||||
|
assert hasattr(binance_product, 'id')
|
||||||
|
assert hasattr(binance_product, 'symbol')
|
||||||
|
assert hasattr(binance_product, 'price')
|
||||||
|
assert hasattr(binance_product, 'volume_24h')
|
||||||
|
|
||||||
|
def test_price_consistency(self):
|
||||||
|
"""Test che tutti i metodi from_* di Price restituiscano oggetti consistenti."""
|
||||||
|
# Test from_cryptocompare
|
||||||
|
crypto_data = {
|
||||||
|
'high': 51000.0,
|
||||||
|
'low': 49000.0,
|
||||||
|
'open': 50000.0,
|
||||||
|
'close': 50500.0,
|
||||||
|
'volumeto': 1000.0,
|
||||||
|
'time': 1704067200
|
||||||
|
}
|
||||||
|
crypto_price = Price.from_cryptocompare(crypto_data)
|
||||||
|
|
||||||
|
# Verifica che abbia tutti i campi richiesti
|
||||||
|
assert hasattr(crypto_price, 'high')
|
||||||
|
assert hasattr(crypto_price, 'low')
|
||||||
|
assert hasattr(crypto_price, 'open')
|
||||||
|
assert hasattr(crypto_price, 'close')
|
||||||
|
assert hasattr(crypto_price, 'volume')
|
||||||
|
assert hasattr(crypto_price, 'time')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
pytest.main([__file__, "-v"])
|
||||||
|
|||||||
Reference in New Issue
Block a user