12 fix docs (#13)
* fix dependencies uv.lock * refactor test markers for clarity * refactor: clean up imports and remove unused files * refactor: remove unused agent files and clean up market API instructions * refactor: enhance wrapper initialization with keyword arguments and clean up tests * refactor: remove PublicBinanceAgent * refactor: aggregator - simplified MarketDataAggregator and related models to functions * refactor: update README and .env.example to reflect the latest changes to the project * refactor: simplify product info and price creation in YFinanceWrapper * refactor: remove get_all_products method from market API wrappers and update documentation * fix: environment variable assertions * refactor: remove status attribute from ProductInfo and update related methods to use timestamp_ms * feat: implement aggregate_history_prices function to calculate hourly price averages * refactor: update docker-compose and app.py for improved environment variable handling and compatibility * feat: add detailed market instructions and improve error handling in price aggregation methods * feat: add aggregated news retrieval methods for top headlines and latest news * refactor: improve error messages in WrapperHandler for better clarity * fix: correct quote currency extraction in create_product_info and remove debug prints from tests
This commit was merged in pull request #13.
This commit is contained in:
committed by
GitHub
parent
a8755913d8
commit
d2fbc0ceea
@@ -42,6 +42,7 @@ if __name__ == "__main__":
|
||||
analyze_btn = gr.Button("🔎 Analizza")
|
||||
analyze_btn.click(fn=pipeline.interact, inputs=[user_input], outputs=output)
|
||||
|
||||
server, port = ("0.0.0.0", 8000)
|
||||
log_info(f"Starting UPO AppAI on http://{server}:{port}")
|
||||
server, port = ("0.0.0.0", 8000) # 0.0.0.0 per docker compatibility
|
||||
server_log = "localhost" if server == "0.0.0.0" else server
|
||||
log_info(f"Starting UPO AppAI on http://{server_log}:{port}")
|
||||
demo.launch(server_name=server, server_port=port, quiet=True)
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
from typing import Union, List, Dict, Optional, Any, Iterator, Sequence
|
||||
from agno.agent import Agent
|
||||
from agno.models.message import Message
|
||||
from agno.run.agent import RunOutput, RunOutputEvent
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.toolkits.market_toolkit import MarketToolkit
|
||||
from app.markets.base import ProductInfo # modello dati già definito nel tuo progetto
|
||||
|
||||
|
||||
class MarketAgent(Agent):
|
||||
"""
|
||||
Wrapper che trasforma MarketToolkit in un Agent compatibile con Team.
|
||||
Produce sia output leggibile (content) che dati strutturati (metadata).
|
||||
"""
|
||||
|
||||
def __init__(self, currency: str = "USD"):
|
||||
super().__init__()
|
||||
self.toolkit = MarketToolkit()
|
||||
self.currency = currency
|
||||
self.name = "MarketAgent"
|
||||
|
||||
def run(
|
||||
self,
|
||||
input: Union[str, List, Dict, Message, BaseModel, List[Message]],
|
||||
*,
|
||||
stream: Optional[bool] = None,
|
||||
stream_intermediate_steps: Optional[bool] = None,
|
||||
user_id: Optional[str] = None,
|
||||
session_id: Optional[str] = None,
|
||||
session_state: Optional[Dict[str, Any]] = None,
|
||||
audio: Optional[Sequence[Any]] = None,
|
||||
images: Optional[Sequence[Any]] = None,
|
||||
videos: Optional[Sequence[Any]] = None,
|
||||
files: Optional[Sequence[Any]] = None,
|
||||
retries: Optional[int] = None,
|
||||
knowledge_filters: Optional[Dict[str, Any]] = None,
|
||||
add_history_to_context: Optional[bool] = None,
|
||||
add_dependencies_to_context: Optional[bool] = None,
|
||||
add_session_state_to_context: Optional[bool] = None,
|
||||
dependencies: Optional[Dict[str, Any]] = None,
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
yield_run_response: bool = False,
|
||||
debug_mode: Optional[bool] = None,
|
||||
**kwargs: Any,
|
||||
) -> Union[RunOutput, Iterator[Union[RunOutputEvent, RunOutput]]]:
|
||||
# 1. Estraggo la query dal parametro "input"
|
||||
if isinstance(input, str):
|
||||
query = input
|
||||
elif isinstance(input, dict) and "query" in input:
|
||||
query = input["query"]
|
||||
elif isinstance(input, Message):
|
||||
query = input.content
|
||||
elif isinstance(input, BaseModel):
|
||||
query = str(input)
|
||||
elif isinstance(input, list) and input and isinstance(input[0], Message):
|
||||
query = input[0].content
|
||||
else:
|
||||
query = str(input)
|
||||
|
||||
# 2. Individuo i simboli da analizzare
|
||||
symbols = []
|
||||
for token in query.upper().split():
|
||||
if token in ("BTC", "ETH", "XRP", "LTC", "BCH"): # TODO: estendere dinamicamente
|
||||
symbols.append(token)
|
||||
|
||||
if not symbols:
|
||||
symbols = ["BTC", "ETH"] # default
|
||||
|
||||
# 3. Recupero i dati dal toolkit
|
||||
results = []
|
||||
products: List[ProductInfo] = []
|
||||
|
||||
try:
|
||||
products.extend(self.toolkit.get_current_prices(symbols)) # supponiamo ritorni un ProductInfo o simile
|
||||
# Usa list comprehension per iterare symbols e products insieme
|
||||
results.extend([
|
||||
f"{symbol}: ${product.price:.2f}" if hasattr(product, 'price') and product.price else f"{symbol}: N/A"
|
||||
for symbol, product in zip(symbols, products)
|
||||
])
|
||||
except Exception as e:
|
||||
results.extend(f"Errore: Impossibile recuperare i dati di mercato\n{str(e)}")
|
||||
|
||||
# 4. Preparo output leggibile + metadati strutturati
|
||||
output_text = "📊 Dati di mercato:\n" + "\n".join(results)
|
||||
|
||||
return RunOutput(
|
||||
content=output_text,
|
||||
metadata={"products": products}
|
||||
)
|
||||
@@ -1,35 +0,0 @@
|
||||
from agno.agent import Agent
|
||||
|
||||
class NewsAgent(Agent):
|
||||
"""
|
||||
Gli agenti devono esporre un metodo run con questa firma.
|
||||
|
||||
def run(
|
||||
self,
|
||||
input: Union[str, List, Dict, Message, BaseModel, List[Message]],
|
||||
*,
|
||||
stream: Optional[bool] = None,
|
||||
stream_intermediate_steps: Optional[bool] = None,
|
||||
user_id: Optional[str] = None,
|
||||
session_id: Optional[str] = None,
|
||||
session_state: Optional[Dict[str, Any]] = None,
|
||||
audio: Optional[Sequence[Any]] = None,
|
||||
images: Optional[Sequence[Any]] = None,
|
||||
videos: Optional[Sequence[Any]] = None,
|
||||
files: Optional[Sequence[Any]] = None,
|
||||
retries: Optional[int] = None,
|
||||
knowledge_filters: Optional[Dict[str, Any]] = None,
|
||||
add_history_to_context: Optional[bool] = None,
|
||||
add_dependencies_to_context: Optional[bool] = None,
|
||||
add_session_state_to_context: Optional[bool] = None,
|
||||
dependencies: Optional[Dict[str, Any]] = None,
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
yield_run_response: bool = False,
|
||||
debug_mode: Optional[bool] = None,
|
||||
**kwargs: Any,
|
||||
) -> Union[RunOutput, Iterator[Union[RunOutputEvent, RunOutput]]]:
|
||||
"""
|
||||
@staticmethod
|
||||
def analyze(query: str) -> str:
|
||||
# Mock analisi news
|
||||
return "📰 Sentiment news: ottimismo sul mercato crypto grazie all'adozione istituzionale."
|
||||
@@ -1,36 +0,0 @@
|
||||
from agno.agent import Agent
|
||||
|
||||
|
||||
class SocialAgent(Agent):
|
||||
"""
|
||||
Gli agenti devono esporre un metodo run con questa firma.
|
||||
|
||||
def run(
|
||||
self,
|
||||
input: Union[str, List, Dict, Message, BaseModel, List[Message]],
|
||||
*,
|
||||
stream: Optional[bool] = None,
|
||||
stream_intermediate_steps: Optional[bool] = None,
|
||||
user_id: Optional[str] = None,
|
||||
session_id: Optional[str] = None,
|
||||
session_state: Optional[Dict[str, Any]] = None,
|
||||
audio: Optional[Sequence[Any]] = None,
|
||||
images: Optional[Sequence[Any]] = None,
|
||||
videos: Optional[Sequence[Any]] = None,
|
||||
files: Optional[Sequence[Any]] = None,
|
||||
retries: Optional[int] = None,
|
||||
knowledge_filters: Optional[Dict[str, Any]] = None,
|
||||
add_history_to_context: Optional[bool] = None,
|
||||
add_dependencies_to_context: Optional[bool] = None,
|
||||
add_session_state_to_context: Optional[bool] = None,
|
||||
dependencies: Optional[Dict[str, Any]] = None,
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
yield_run_response: bool = False,
|
||||
debug_mode: Optional[bool] = None,
|
||||
**kwargs: Any,
|
||||
) -> Union[RunOutput, Iterator[Union[RunOutputEvent, RunOutput]]]:
|
||||
"""
|
||||
@staticmethod
|
||||
def analyze(query: str) -> str:
|
||||
# Mock analisi social
|
||||
return "💬 Sentiment social: forte interesse retail su nuove altcoin emergenti."
|
||||
@@ -1,97 +1,106 @@
|
||||
from agno.tools import Toolkit
|
||||
from app.utils.wrapper_handler import WrapperHandler
|
||||
from app.utils.market_aggregation import aggregate_product_info, aggregate_history_prices
|
||||
from .base import BaseWrapper, ProductInfo, Price
|
||||
from .coinbase import CoinBaseWrapper
|
||||
from .binance import BinanceWrapper
|
||||
from .cryptocompare import CryptoCompareWrapper
|
||||
from .yfinance import YFinanceWrapper
|
||||
from .binance_public import PublicBinanceAgent
|
||||
from app.utils.wrapper_handler import WrapperHandler
|
||||
from typing import List, Optional
|
||||
from agno.tools import Toolkit
|
||||
|
||||
|
||||
__all__ = [ "MarketAPIs", "BinanceWrapper", "CoinBaseWrapper", "CryptoCompareWrapper", "YFinanceWrapper", "PublicBinanceAgent" ]
|
||||
__all__ = [ "MarketAPIs", "BinanceWrapper", "CoinBaseWrapper", "CryptoCompareWrapper", "YFinanceWrapper" ]
|
||||
|
||||
|
||||
class MarketAPIsTool(BaseWrapper, Toolkit):
|
||||
"""
|
||||
Classe per gestire le API di mercato disponibili.
|
||||
|
||||
Supporta due modalità:
|
||||
1. **Modalità standard** (default): usa il primo wrapper disponibile
|
||||
2. **Modalità aggregazione**: aggrega dati da tutte le fonti disponibili
|
||||
|
||||
L'aggregazione può essere abilitata/disabilitata dinamicamente.
|
||||
Class that aggregates multiple market API wrappers and manages them using WrapperHandler.
|
||||
This class supports retrieving product information and historical prices.
|
||||
This class can also aggregate data from multiple sources to provide a more comprehensive view of the market.
|
||||
The following wrappers are included in this order:
|
||||
- BinanceWrapper
|
||||
- YFinanceWrapper
|
||||
- CoinBaseWrapper
|
||||
- CryptoCompareWrapper
|
||||
"""
|
||||
|
||||
def __init__(self, currency: str = "USD", enable_aggregation: bool = False):
|
||||
self.currency = currency
|
||||
wrappers = [ BinanceWrapper, CoinBaseWrapper, CryptoCompareWrapper, YFinanceWrapper ]
|
||||
self.wrappers: WrapperHandler[BaseWrapper] = WrapperHandler.build_wrappers(wrappers)
|
||||
|
||||
# Inizializza l'aggregatore solo se richiesto (lazy initialization)
|
||||
self._aggregator = None
|
||||
self._aggregation_enabled = enable_aggregation
|
||||
|
||||
def __init__(self, currency: str = "USD"):
|
||||
"""
|
||||
Initialize the MarketAPIsTool with multiple market API wrappers.
|
||||
The following wrappers are included in this order:
|
||||
- BinanceWrapper
|
||||
- YFinanceWrapper
|
||||
- CoinBaseWrapper
|
||||
- CryptoCompareWrapper
|
||||
Args:
|
||||
currency (str): Valuta in cui restituire i prezzi. Default è "USD".
|
||||
"""
|
||||
kwargs = {"currency": currency or "USD"}
|
||||
wrappers = [ BinanceWrapper, YFinanceWrapper, CoinBaseWrapper, CryptoCompareWrapper ]
|
||||
self.wrappers: WrapperHandler[BaseWrapper] = WrapperHandler.build_wrappers(wrappers, kwargs=kwargs)
|
||||
|
||||
Toolkit.__init__(
|
||||
self,
|
||||
name="Market APIs Toolkit",
|
||||
tools=[
|
||||
self.get_product,
|
||||
self.get_products,
|
||||
self.get_all_products,
|
||||
self.get_historical_prices,
|
||||
self.get_products_aggregated,
|
||||
self.get_historical_prices_aggregated,
|
||||
],
|
||||
)
|
||||
|
||||
def _get_aggregator(self):
|
||||
"""Lazy initialization dell'aggregatore"""
|
||||
if self._aggregator is None:
|
||||
from app.utils.market_data_aggregator import MarketDataAggregator
|
||||
self._aggregator = MarketDataAggregator(self.currency)
|
||||
self._aggregator.enable_aggregation(self._aggregation_enabled)
|
||||
return self._aggregator
|
||||
|
||||
def get_product(self, asset_id: str) -> Optional[ProductInfo]:
|
||||
"""Ottieni informazioni su un prodotto specifico"""
|
||||
if self._aggregation_enabled:
|
||||
return self._get_aggregator().get_product(asset_id)
|
||||
def get_product(self, asset_id: str) -> ProductInfo:
|
||||
return self.wrappers.try_call(lambda w: w.get_product(asset_id))
|
||||
|
||||
def get_products(self, asset_ids: List[str]) -> List[ProductInfo]:
|
||||
"""Ottieni informazioni su multiple prodotti"""
|
||||
if self._aggregation_enabled:
|
||||
return self._get_aggregator().get_products(asset_ids)
|
||||
def get_products(self, asset_ids: list[str]) -> list[ProductInfo]:
|
||||
return self.wrappers.try_call(lambda w: w.get_products(asset_ids))
|
||||
|
||||
def get_all_products(self) -> List[ProductInfo]:
|
||||
"""Ottieni tutti i prodotti disponibili"""
|
||||
if self._aggregation_enabled:
|
||||
return self._get_aggregator().get_all_products()
|
||||
return self.wrappers.try_call(lambda w: w.get_all_products())
|
||||
|
||||
def get_historical_prices(self, asset_id: str = "BTC", limit: int = 100) -> List[Price]:
|
||||
"""Ottieni dati storici dei prezzi"""
|
||||
if self._aggregation_enabled:
|
||||
return self._get_aggregator().get_historical_prices(asset_id, limit)
|
||||
def get_historical_prices(self, asset_id: str = "BTC", limit: int = 100) -> list[Price]:
|
||||
return self.wrappers.try_call(lambda w: w.get_historical_prices(asset_id, limit))
|
||||
|
||||
# Metodi per controllare l'aggregazione
|
||||
def enable_aggregation(self, enabled: bool = True):
|
||||
"""Abilita/disabilita la modalità aggregazione"""
|
||||
self._aggregation_enabled = enabled
|
||||
if self._aggregator:
|
||||
self._aggregator.enable_aggregation(enabled)
|
||||
|
||||
def is_aggregation_enabled(self) -> bool:
|
||||
"""Verifica se l'aggregazione è abilitata"""
|
||||
return self._aggregation_enabled
|
||||
|
||||
# Metodo speciale per debugging (opzionale)
|
||||
def get_aggregated_product_with_debug(self, asset_id: str) -> dict:
|
||||
|
||||
|
||||
def get_products_aggregated(self, asset_ids: list[str]) -> list[ProductInfo]:
|
||||
"""
|
||||
Metodo speciale per ottenere dati aggregati con informazioni di debug.
|
||||
Disponibile solo quando l'aggregazione è abilitata.
|
||||
Restituisce i dati aggregati per una lista di asset_id.\n
|
||||
Attenzione che si usano tutte le fonti, quindi potrebbe usare molte chiamate API (che potrebbero essere a pagamento).
|
||||
Args:
|
||||
asset_ids (list[str]): Lista di asset_id da cercare.
|
||||
Returns:
|
||||
list[ProductInfo]: Lista di ProductInfo aggregati.
|
||||
"""
|
||||
if not self._aggregation_enabled:
|
||||
raise RuntimeError("L'aggregazione deve essere abilitata per usare questo metodo")
|
||||
return self._get_aggregator().get_aggregated_product_with_debug(asset_id)
|
||||
all_products = self.wrappers.try_call_all(lambda w: w.get_products(asset_ids))
|
||||
return aggregate_product_info(all_products)
|
||||
|
||||
def get_historical_prices_aggregated(self, asset_id: str = "BTC", limit: int = 100) -> list[Price]:
|
||||
"""
|
||||
Restituisce i dati storici aggregati per un asset_id. Usa i dati di tutte le fonti disponibili e li aggrega.\n
|
||||
Attenzione che si usano tutte le fonti, quindi potrebbe usare molte chiamate API (che potrebbero essere a pagamento).
|
||||
Args:
|
||||
asset_id (str): Asset ID da cercare.
|
||||
limit (int): Numero massimo di dati storici da restituire.
|
||||
Returns:
|
||||
list[Price]: Lista di Price aggregati.
|
||||
"""
|
||||
all_prices = self.wrappers.try_call_all(lambda w: w.get_historical_prices(asset_id, limit))
|
||||
return aggregate_history_prices(all_prices)
|
||||
|
||||
MARKET_INSTRUCTIONS = """
|
||||
**TASK:** You are a specialized **Crypto Price Data Retrieval Agent**. Your primary goal is to fetch the most recent and/or historical price data for requested cryptocurrency assets (e.g., 'BTC', 'ETH', 'SOL'). You must provide the data in a clear and structured format.
|
||||
|
||||
**AVAILABLE TOOLS:**
|
||||
1. `get_products(asset_ids: list[str])`: Get **current** product/price info for a list of assets. **(PREFERITA: usa questa per i prezzi live)**
|
||||
2. `get_historical_prices(asset_id: str, limit: int)`: Get historical price data for one asset. Default limit is 100. **(PREFERITA: usa questa per i dati storici)**
|
||||
3. `get_products_aggregated(asset_ids: list[str])`: Get **aggregated current** product/price info for a list of assets. **(USA SOLO SE richiesto 'aggregato' o se `get_products` fallisce)**
|
||||
4. `get_historical_prices_aggregated(asset_id: str, limit: int)`: Get **aggregated historical** price data for one asset. **(USA SOLO SE richiesto 'aggregato' o se `get_historical_prices` fallisce)**
|
||||
|
||||
**USAGE GUIDELINE:**
|
||||
* **Asset ID:** Always convert common names (e.g., 'Bitcoin', 'Ethereum') into their official ticker/ID (e.g., 'BTC', 'ETH').
|
||||
* **Cost Management (Cruciale per LLM locale):**
|
||||
* **Priorità Bassa per Aggregazione:** **Non** usare i metodi `*aggregated` a meno che l'utente non lo richieda esplicitamente o se i metodi non-aggregati falliscono.
|
||||
* **Limitazione Storica:** Il limite predefinito per i dati storici deve essere **20** punti dati, a meno che l'utente non specifichi un limite diverso.
|
||||
* **Fallimento Tool:** Se lo strumento non restituisce dati per un asset specifico, rispondi per quell'asset con: "Dati di prezzo non trovati per [Asset ID]."
|
||||
|
||||
**REPORTING REQUIREMENT:**
|
||||
1. **Format:** Output the results in a clear, easy-to-read list or table.
|
||||
2. **Live Price Request:** If an asset's *current price* is requested, report the **Asset ID**, **Latest Price**, and **Time/Date of the price**.
|
||||
3. **Historical Price Request:** If *historical data* is requested, report the **Asset ID**, the **Limit** of points returned, and the **First** and **Last** entries from the list of historical prices (Date, Price). Non stampare l'intera lista di dati storici.
|
||||
4. **Output:** For all requests, fornire un **unico e conciso riepilogo** dei dati reperiti.
|
||||
"""
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
class BaseWrapper:
|
||||
@@ -15,7 +14,7 @@ class BaseWrapper:
|
||||
Returns:
|
||||
ProductInfo: An object containing product information.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
raise NotImplementedError("This method should be overridden by subclasses")
|
||||
|
||||
def get_products(self, asset_ids: list[str]) -> list['ProductInfo']:
|
||||
"""
|
||||
@@ -25,15 +24,7 @@ class BaseWrapper:
|
||||
Returns:
|
||||
list[ProductInfo]: A list of objects containing product information.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def get_all_products(self) -> list['ProductInfo']:
|
||||
"""
|
||||
Get product information for all available assets.
|
||||
Returns:
|
||||
list[ProductInfo]: A list of objects containing product information.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
raise NotImplementedError("This method should be overridden by subclasses")
|
||||
|
||||
def get_historical_prices(self, asset_id: str = "BTC", limit: int = 100) -> list['Price']:
|
||||
"""
|
||||
@@ -44,7 +35,7 @@ class BaseWrapper:
|
||||
Returns:
|
||||
list[Price]: A list of Price objects.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
raise NotImplementedError("This method should be overridden by subclasses")
|
||||
|
||||
class ProductInfo(BaseModel):
|
||||
"""
|
||||
@@ -55,7 +46,6 @@ class ProductInfo(BaseModel):
|
||||
symbol: str = ""
|
||||
price: float = 0.0
|
||||
volume_24h: float = 0.0
|
||||
status: str = ""
|
||||
quote_currency: str = ""
|
||||
|
||||
class Price(BaseModel):
|
||||
@@ -68,4 +58,4 @@ class Price(BaseModel):
|
||||
open: float = 0.0
|
||||
close: float = 0.0
|
||||
volume: float = 0.0
|
||||
time: str = ""
|
||||
timestamp_ms: int = 0 # Timestamp in milliseconds
|
||||
|
||||
@@ -3,16 +3,25 @@ from datetime import datetime
|
||||
from binance.client import Client
|
||||
from .base import ProductInfo, BaseWrapper, Price
|
||||
|
||||
def get_product(currency: str, ticker_data: dict[str, str]) -> 'ProductInfo':
|
||||
def get_product(currency: str, ticker_data: dict[str, str]) -> ProductInfo:
|
||||
product = ProductInfo()
|
||||
product.id = ticker_data.get('symbol')
|
||||
product.symbol = ticker_data.get('symbol', '').replace(currency, '')
|
||||
product.price = float(ticker_data.get('price', 0))
|
||||
product.volume_24h = float(ticker_data.get('volume', 0))
|
||||
product.status = "TRADING" # Binance non fornisce status esplicito
|
||||
product.quote_currency = currency
|
||||
return product
|
||||
|
||||
def get_price(kline_data: list) -> Price:
|
||||
price = Price()
|
||||
price.open = float(kline_data[1])
|
||||
price.high = float(kline_data[2])
|
||||
price.low = float(kline_data[3])
|
||||
price.close = float(kline_data[4])
|
||||
price.volume = float(kline_data[5])
|
||||
price.timestamp_ms = kline_data[0]
|
||||
return price
|
||||
|
||||
class BinanceWrapper(BaseWrapper):
|
||||
"""
|
||||
Wrapper per le API autenticate di Binance.\n
|
||||
@@ -54,17 +63,6 @@ class BinanceWrapper(BaseWrapper):
|
||||
|
||||
return [get_product(self.currency, ticker) for ticker in tickers]
|
||||
|
||||
def get_all_products(self) -> list[ProductInfo]:
|
||||
all_tickers = self.client.get_ticker()
|
||||
products = []
|
||||
|
||||
for ticker in all_tickers:
|
||||
# Filtra solo i simboli che terminano con la valuta di default
|
||||
if ticker['symbol'].endswith(self.currency):
|
||||
product = get_product(self.currency, ticker)
|
||||
products.append(product)
|
||||
return products
|
||||
|
||||
def get_historical_prices(self, asset_id: str = "BTC", limit: int = 100) -> list[Price]:
|
||||
symbol = self.__format_symbol(asset_id)
|
||||
|
||||
@@ -74,15 +72,5 @@ class BinanceWrapper(BaseWrapper):
|
||||
interval=Client.KLINE_INTERVAL_1HOUR,
|
||||
limit=limit,
|
||||
)
|
||||
return [get_price(kline) for kline in klines]
|
||||
|
||||
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
|
||||
|
||||
@@ -1,218 +0,0 @@
|
||||
"""
|
||||
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
|
||||
|
||||
|
||||
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"
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
@@ -6,24 +6,22 @@ from coinbase.rest.types.product_types import Candle, GetProductResponse, Produc
|
||||
from .base import ProductInfo, BaseWrapper, Price
|
||||
|
||||
|
||||
def get_product(product_data: GetProductResponse | Product) -> 'ProductInfo':
|
||||
def get_product(product_data: GetProductResponse | 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
|
||||
# TODO Check what status means in Coinbase
|
||||
product.status = product_data.status or ""
|
||||
return product
|
||||
|
||||
def get_price(candle_data: Candle) -> 'Price':
|
||||
def get_price(candle_data: Candle) -> Price:
|
||||
price = Price()
|
||||
price.high = float(candle_data.high) if candle_data.high else 0.0
|
||||
price.low = float(candle_data.low) if candle_data.low else 0.0
|
||||
price.open = float(candle_data.open) if candle_data.open else 0.0
|
||||
price.close = float(candle_data.close) if candle_data.close else 0.0
|
||||
price.volume = float(candle_data.volume) if candle_data.volume else 0.0
|
||||
price.time = str(candle_data.start) if candle_data.start else ""
|
||||
price.timestamp_ms = int(candle_data.start) * 1000 if candle_data.start else 0
|
||||
return price
|
||||
|
||||
|
||||
@@ -49,10 +47,10 @@ class CoinBaseWrapper(BaseWrapper):
|
||||
|
||||
def __init__(self, currency: str = "USD"):
|
||||
api_key = os.getenv("COINBASE_API_KEY")
|
||||
assert api_key is not None, "API key is required"
|
||||
assert api_key, "COINBASE_API_KEY environment variable not set"
|
||||
|
||||
api_private_key = os.getenv("COINBASE_API_SECRET")
|
||||
assert api_private_key is not None, "API private key is required"
|
||||
assert api_private_key, "COINBASE_API_SECRET environment variable not set"
|
||||
|
||||
self.currency = currency
|
||||
self.client: RESTClient = RESTClient(
|
||||
@@ -73,10 +71,6 @@ class CoinBaseWrapper(BaseWrapper):
|
||||
assets = self.client.get_products(product_ids=all_asset_ids)
|
||||
return [get_product(asset) for asset in assets.products]
|
||||
|
||||
def get_all_products(self) -> list[ProductInfo]:
|
||||
assets = self.client.get_products()
|
||||
return [get_product(asset) for asset in assets.products]
|
||||
|
||||
def get_historical_prices(self, asset_id: str = "BTC", limit: int = 100) -> list[Price]:
|
||||
asset_id = self.__format(asset_id)
|
||||
end_time = datetime.now()
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
import os
|
||||
import requests
|
||||
from typing import Optional, Dict, Any
|
||||
from .base import ProductInfo, BaseWrapper, Price
|
||||
|
||||
|
||||
def get_product(asset_data: dict) -> 'ProductInfo':
|
||||
def get_product(asset_data: dict) -> ProductInfo:
|
||||
product = ProductInfo()
|
||||
product.id = asset_data['FROMSYMBOL'] + '-' + asset_data['TOSYMBOL']
|
||||
product.symbol = asset_data['FROMSYMBOL']
|
||||
product.price = float(asset_data['PRICE'])
|
||||
product.volume_24h = float(asset_data['VOLUME24HOUR'])
|
||||
product.status = "" # Cryptocompare does not provide status
|
||||
product.id = asset_data.get('FROMSYMBOL', '') + '-' + asset_data.get('TOSYMBOL', '')
|
||||
product.symbol = asset_data.get('FROMSYMBOL', '')
|
||||
product.price = float(asset_data.get('PRICE', 0))
|
||||
product.volume_24h = float(asset_data.get('VOLUME24HOUR', 0))
|
||||
assert product.price > 0, "Invalid price data received from CryptoCompare"
|
||||
return product
|
||||
|
||||
def get_price(price_data: dict) -> 'Price':
|
||||
def get_price(price_data: dict) -> Price:
|
||||
price = Price()
|
||||
price.high = float(price_data['high'])
|
||||
price.low = float(price_data['low'])
|
||||
price.open = float(price_data['open'])
|
||||
price.close = float(price_data['close'])
|
||||
price.volume = float(price_data['volumeto'])
|
||||
price.time = str(price_data['time'])
|
||||
price.high = float(price_data.get('high', 0))
|
||||
price.low = float(price_data.get('low', 0))
|
||||
price.open = float(price_data.get('open', 0))
|
||||
price.close = float(price_data.get('close', 0))
|
||||
price.volume = float(price_data.get('volumeto', 0))
|
||||
price.timestamp_ms = price_data.get('time', 0) * 1000
|
||||
assert price.timestamp_ms > 0, "Invalid timestamp data received from CryptoCompare"
|
||||
return price
|
||||
|
||||
|
||||
@@ -34,12 +34,12 @@ class CryptoCompareWrapper(BaseWrapper):
|
||||
"""
|
||||
def __init__(self, currency:str='USD'):
|
||||
api_key = os.getenv("CRYPTOCOMPARE_API_KEY")
|
||||
assert api_key is not None, "API key is required"
|
||||
assert api_key, "CRYPTOCOMPARE_API_KEY environment variable not set"
|
||||
|
||||
self.api_key = api_key
|
||||
self.currency = currency
|
||||
|
||||
def __request(self, endpoint: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||
def __request(self, endpoint: str, params: dict[str, str] | None = None) -> dict[str, str]:
|
||||
if params is None:
|
||||
params = {}
|
||||
params['api_key'] = self.api_key
|
||||
@@ -67,11 +67,7 @@ class CryptoCompareWrapper(BaseWrapper):
|
||||
assets.append(get_product(asset_data))
|
||||
return assets
|
||||
|
||||
def get_all_products(self) -> list[ProductInfo]:
|
||||
# TODO serve davvero il workaroud qui? Possiamo prendere i dati da un altro endpoint intanto
|
||||
raise NotImplementedError("get_all_products is not supported by CryptoCompare API")
|
||||
|
||||
def get_historical_prices(self, asset_id: str, limit: int = 100) -> list[dict]:
|
||||
def get_historical_prices(self, asset_id: str, limit: int = 100) -> list[Price]:
|
||||
response = self.__request("/data/v2/histohour", params = {
|
||||
"fsym": asset_id,
|
||||
"tsym": self.currency,
|
||||
|
||||
@@ -3,63 +3,29 @@ from agno.tools.yfinance import YFinanceTools
|
||||
from .base import BaseWrapper, ProductInfo, Price
|
||||
|
||||
|
||||
def create_product_info(symbol: str, stock_data: dict) -> ProductInfo:
|
||||
def create_product_info(stock_data: dict[str, str]) -> ProductInfo:
|
||||
"""
|
||||
Converte i dati di YFinanceTools in ProductInfo.
|
||||
"""
|
||||
product = ProductInfo()
|
||||
|
||||
# ID univoco per yfinance
|
||||
product.id = f"yfinance_{symbol}"
|
||||
product.symbol = symbol
|
||||
|
||||
# Estrai il prezzo corrente - gestisci diversi formati
|
||||
if 'currentPrice' in stock_data:
|
||||
product.price = float(stock_data['currentPrice'])
|
||||
elif 'regularMarketPrice' in stock_data:
|
||||
product.price = float(stock_data['regularMarketPrice'])
|
||||
elif 'Current Stock Price' in stock_data:
|
||||
# Formato: "254.63 USD" - estrai solo il numero
|
||||
price_str = stock_data['Current Stock Price'].split()[0]
|
||||
try:
|
||||
product.price = float(price_str)
|
||||
except ValueError:
|
||||
product.price = 0.0
|
||||
else:
|
||||
product.price = 0.0
|
||||
|
||||
# Volume 24h
|
||||
if 'volume' in stock_data:
|
||||
product.volume_24h = float(stock_data['volume'])
|
||||
elif 'regularMarketVolume' in stock_data:
|
||||
product.volume_24h = float(stock_data['regularMarketVolume'])
|
||||
else:
|
||||
product.volume_24h = 0.0
|
||||
|
||||
# Status basato sulla disponibilità dei dati
|
||||
product.status = "trading" if product.price > 0 else "offline"
|
||||
|
||||
# Valuta (default USD)
|
||||
product.quote_currency = stock_data.get('currency', 'USD') or 'USD'
|
||||
|
||||
product.id = stock_data.get('Symbol', '')
|
||||
product.symbol = product.id.split('-')[0] # Rimuovi il suffisso della valuta per le crypto
|
||||
product.price = float(stock_data.get('Current Stock Price', f"0.0 USD").split(" ")[0]) # prende solo il numero
|
||||
product.volume_24h = 0.0 # YFinance non fornisce il volume 24h direttamente
|
||||
product.quote_currency = product.id.split('-')[1] # La valuta è la parte dopo il '-'
|
||||
return product
|
||||
|
||||
|
||||
def create_price_from_history(hist_data: dict, timestamp: str) -> Price:
|
||||
def create_price_from_history(hist_data: dict[str, str]) -> Price:
|
||||
"""
|
||||
Converte i dati storici di YFinanceTools in Price.
|
||||
"""
|
||||
price = Price()
|
||||
|
||||
if timestamp in hist_data:
|
||||
day_data = hist_data[timestamp]
|
||||
price.high = float(day_data.get('High', 0.0))
|
||||
price.low = float(day_data.get('Low', 0.0))
|
||||
price.open = float(day_data.get('Open', 0.0))
|
||||
price.close = float(day_data.get('Close', 0.0))
|
||||
price.volume = float(day_data.get('Volume', 0.0))
|
||||
price.time = timestamp
|
||||
|
||||
price.high = float(hist_data.get('High', 0.0))
|
||||
price.low = float(hist_data.get('Low', 0.0))
|
||||
price.open = float(hist_data.get('Open', 0.0))
|
||||
price.close = float(hist_data.get('Close', 0.0))
|
||||
price.volume = float(hist_data.get('Volume', 0.0))
|
||||
price.timestamp_ms = int(hist_data.get('Timestamp', '0'))
|
||||
return price
|
||||
|
||||
|
||||
@@ -69,146 +35,46 @@ class YFinanceWrapper(BaseWrapper):
|
||||
Implementa l'interfaccia BaseWrapper per compatibilità con il sistema esistente.
|
||||
Usa YFinanceTools dalla libreria agno per coerenza con altri wrapper.
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self, currency: str = "USD"):
|
||||
self.currency = currency
|
||||
# Inizializza YFinanceTools - non richiede parametri specifici
|
||||
self.tool = YFinanceTools()
|
||||
|
||||
|
||||
def _format_symbol(self, asset_id: str) -> str:
|
||||
"""
|
||||
Formatta il simbolo per yfinance.
|
||||
Per crypto, aggiunge '-USD' se non presente.
|
||||
Per crypto, aggiunge '-' e la valuta (es. BTC -> BTC-USD).
|
||||
"""
|
||||
asset_id = asset_id.upper()
|
||||
|
||||
# Se è già nel formato corretto (es: BTC-USD), usa così
|
||||
if '-' in asset_id:
|
||||
return asset_id
|
||||
|
||||
# Per crypto singole (BTC, ETH), aggiungi -USD
|
||||
if asset_id in ['BTC', 'ETH', 'ADA', 'SOL', 'DOT', 'LINK', 'UNI', 'AAVE']:
|
||||
return f"{asset_id}-USD"
|
||||
|
||||
# Per azioni, usa il simbolo così com'è
|
||||
return asset_id
|
||||
|
||||
return f"{asset_id}-{self.currency}" if '-' not in asset_id else asset_id
|
||||
|
||||
def get_product(self, asset_id: str) -> ProductInfo:
|
||||
"""
|
||||
Recupera le informazioni di un singolo prodotto.
|
||||
"""
|
||||
symbol = self._format_symbol(asset_id)
|
||||
|
||||
# Usa YFinanceTools per ottenere i dati
|
||||
try:
|
||||
# Ottieni le informazioni base dello stock
|
||||
stock_info = self.tool.get_company_info(symbol)
|
||||
|
||||
# Se il risultato è una stringa JSON, parsala
|
||||
if isinstance(stock_info, str):
|
||||
try:
|
||||
stock_data = json.loads(stock_info)
|
||||
except json.JSONDecodeError:
|
||||
# Se non è JSON valido, prova a ottenere solo il prezzo
|
||||
price_data_str = self.tool.get_current_stock_price(symbol)
|
||||
if price_data_str and price_data_str.replace('.', '').replace('-', '').isdigit():
|
||||
price = float(price_data_str)
|
||||
stock_data = {'currentPrice': price, 'currency': 'USD'}
|
||||
else:
|
||||
raise Exception("Dati non validi")
|
||||
else:
|
||||
stock_data = stock_info
|
||||
|
||||
return create_product_info(symbol, stock_data)
|
||||
|
||||
except Exception as e:
|
||||
# Fallback: prova a ottenere solo il prezzo
|
||||
try:
|
||||
price_data_str = self.tool.get_current_stock_price(symbol)
|
||||
if price_data_str and price_data_str.replace('.', '').replace('-', '').isdigit():
|
||||
price = float(price_data_str)
|
||||
minimal_data = {
|
||||
'currentPrice': price,
|
||||
'currency': 'USD'
|
||||
}
|
||||
return create_product_info(symbol, minimal_data)
|
||||
else:
|
||||
raise Exception("Prezzo non disponibile")
|
||||
except Exception:
|
||||
# Se tutto fallisce, restituisci un prodotto vuoto
|
||||
product = ProductInfo()
|
||||
product.symbol = symbol
|
||||
product.status = "offline"
|
||||
return product
|
||||
|
||||
stock_info = self.tool.get_company_info(symbol)
|
||||
stock_info = json.loads(stock_info)
|
||||
return create_product_info(stock_info)
|
||||
|
||||
def get_products(self, asset_ids: list[str]) -> list[ProductInfo]:
|
||||
"""
|
||||
Recupera le informazioni di multiple assets.
|
||||
"""
|
||||
products = []
|
||||
|
||||
for asset_id in asset_ids:
|
||||
try:
|
||||
product = self.get_product(asset_id)
|
||||
products.append(product)
|
||||
except Exception as e:
|
||||
# Se un asset non è disponibile, continua con gli altri
|
||||
continue
|
||||
|
||||
product = self.get_product(asset_id)
|
||||
products.append(product)
|
||||
return products
|
||||
|
||||
def get_all_products(self) -> list[ProductInfo]:
|
||||
"""
|
||||
Recupera tutti i prodotti disponibili.
|
||||
Restituisce una lista predefinita di asset popolari.
|
||||
"""
|
||||
# Lista di asset popolari (azioni, ETF, crypto)
|
||||
popular_assets = [
|
||||
'BTC', 'ETH', 'ADA', 'SOL', 'DOT',
|
||||
'AAPL', 'GOOGL', 'MSFT', 'TSLA', 'AMZN',
|
||||
'SPY', 'QQQ', 'VTI', 'GLD', 'VIX'
|
||||
]
|
||||
|
||||
return self.get_products(popular_assets)
|
||||
|
||||
|
||||
def get_historical_prices(self, asset_id: str = "BTC", limit: int = 100) -> list[Price]:
|
||||
"""
|
||||
Recupera i dati storici di prezzo per un asset.
|
||||
"""
|
||||
symbol = self._format_symbol(asset_id)
|
||||
|
||||
try:
|
||||
# Determina il periodo appropriato in base al limite
|
||||
if limit <= 7:
|
||||
period = "1d"
|
||||
interval = "15m"
|
||||
elif limit <= 30:
|
||||
period = "5d"
|
||||
interval = "1h"
|
||||
elif limit <= 90:
|
||||
period = "1mo"
|
||||
interval = "1d"
|
||||
else:
|
||||
period = "3mo"
|
||||
interval = "1d"
|
||||
|
||||
# Ottieni i dati storici
|
||||
hist_data = self.tool.get_historical_stock_prices(symbol, period=period, interval=interval)
|
||||
|
||||
if isinstance(hist_data, str):
|
||||
hist_data = json.loads(hist_data)
|
||||
|
||||
# Il formato dei dati è {timestamp: {Open: x, High: y, Low: z, Close: w, Volume: v}}
|
||||
prices = []
|
||||
timestamps = sorted(hist_data.keys())[-limit:] # Prendi gli ultimi 'limit' timestamp
|
||||
|
||||
for timestamp in timestamps:
|
||||
price = create_price_from_history(hist_data, timestamp)
|
||||
if price.close > 0: # Solo se ci sono dati validi
|
||||
prices.append(price)
|
||||
|
||||
return prices
|
||||
|
||||
except Exception as e:
|
||||
# Se fallisce, restituisci lista vuota
|
||||
return []
|
||||
|
||||
days = limit // 24 + 1 # Arrotonda per eccesso
|
||||
hist_data = self.tool.get_historical_stock_prices(symbol, period=f"{days}d", interval="1h")
|
||||
hist_data = json.loads(hist_data)
|
||||
|
||||
# Il formato dei dati è {timestamp: {Open: x, High: y, Low: z, Close: w, Volume: v}}
|
||||
timestamps = sorted(hist_data.keys())[-limit:]
|
||||
|
||||
prices = []
|
||||
for timestamp in timestamps:
|
||||
temp = hist_data[timestamp]
|
||||
temp['Timestamp'] = timestamp
|
||||
price = create_price_from_history(temp)
|
||||
prices.append(price)
|
||||
return prices
|
||||
|
||||
@@ -45,13 +45,32 @@ class NewsAPIsTool(NewsWrapper, Toolkit):
|
||||
],
|
||||
)
|
||||
|
||||
# TODO Pensare se ha senso restituire gli articoli da TUTTI i wrapper o solo dal primo che funziona
|
||||
# la modifica è banale, basta usare try_call_all invece di try_call
|
||||
def get_top_headlines(self, limit: int = 100) -> list[Article]:
|
||||
return self.wrapper_handler.try_call(lambda w: w.get_top_headlines(limit))
|
||||
def get_latest_news(self, query: str, limit: int = 100) -> list[Article]:
|
||||
return self.wrapper_handler.try_call(lambda w: w.get_latest_news(query, limit))
|
||||
|
||||
def get_top_headlines_aggregated(self, limit: int = 100) -> dict[str, list[Article]]:
|
||||
"""
|
||||
Calls get_top_headlines on all wrappers/providers and returns a dictionary mapping their names to their articles.
|
||||
Args:
|
||||
limit (int): Maximum number of articles to retrieve from each provider.
|
||||
Returns:
|
||||
dict[str, list[Article]]: A dictionary mapping providers names to their list of Articles
|
||||
"""
|
||||
return self.wrapper_handler.try_call_all(lambda w: w.get_top_headlines(limit))
|
||||
|
||||
def get_latest_news_aggregated(self, query: str, limit: int = 100) -> dict[str, list[Article]]:
|
||||
"""
|
||||
Calls get_latest_news on all wrappers/providers and returns a dictionary mapping their names to their articles.
|
||||
Args:
|
||||
query (str): The search query to find relevant news articles.
|
||||
limit (int): Maximum number of articles to retrieve from each provider.
|
||||
Returns:
|
||||
dict[str, list[Article]]: A dictionary mapping providers names to their list of Articles
|
||||
"""
|
||||
return self.wrapper_handler.try_call_all(lambda w: w.get_latest_news(query, limit))
|
||||
|
||||
|
||||
NEWS_INSTRUCTIONS = """
|
||||
**TASK:** You are a specialized **Crypto News Analyst**. Your goal is to fetch the latest news or top headlines related to cryptocurrencies, and then **analyze the sentiment** of the content to provide a concise report to the team leader. Prioritize 'crypto' or specific cryptocurrency names (e.g., 'Bitcoin', 'Ethereum') in your searches.
|
||||
@@ -59,6 +78,8 @@ NEWS_INSTRUCTIONS = """
|
||||
**AVAILABLE TOOLS:**
|
||||
1. `get_latest_news(query: str, limit: int)`: Get the 'limit' most recent news articles for a specific 'query'.
|
||||
2. `get_top_headlines(limit: int)`: Get the 'limit' top global news headlines.
|
||||
3. `get_latest_news_aggregated(query: str, limit: int)`: Get aggregated latest news articles for a specific 'query'.
|
||||
4. `get_top_headlines_aggregated(limit: int)`: Get aggregated top global news headlines.
|
||||
|
||||
**USAGE GUIDELINE:**
|
||||
* Always use `get_latest_news` with a relevant crypto-related query first.
|
||||
|
||||
@@ -19,7 +19,7 @@ class NewsApiWrapper(NewsWrapper):
|
||||
|
||||
def __init__(self):
|
||||
api_key = os.getenv("NEWS_API_KEY")
|
||||
assert api_key is not None, "NEWS_API_KEY environment variable not set"
|
||||
assert api_key, "NEWS_API_KEY environment variable not set"
|
||||
|
||||
self.client = newsapi.NewsApiClient(api_key=api_key)
|
||||
self.category = "business" # Cryptocurrency is under business
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
from typing import List
|
||||
|
||||
from agno.team import Team
|
||||
from agno.utils.log import log_info
|
||||
|
||||
from app.agents.market_agent import MarketAgent
|
||||
from app.agents.news_agent import NewsAgent
|
||||
from app.agents.social_agent import SocialAgent
|
||||
from app.news import NewsAPIsTool, NEWS_INSTRUCTIONS
|
||||
from app.social import SocialAPIsTool, SOCIAL_INSTRUCTIONS
|
||||
from app.markets import MarketAPIsTool, MARKET_INSTRUCTIONS
|
||||
from app.models import AppModels
|
||||
from app.predictor import PredictorStyle, PredictorInput, PredictorOutput, PREDICTOR_INSTRUCTIONS
|
||||
|
||||
@@ -17,12 +15,38 @@ class Pipeline:
|
||||
|
||||
def __init__(self):
|
||||
# Inizializza gli agenti
|
||||
self.market_agent = MarketAgent()
|
||||
self.news_agent = NewsAgent()
|
||||
self.social_agent = SocialAgent()
|
||||
market_agent = AppModels.OLLAMA_QWEN_1B.get_agent(
|
||||
instructions=MARKET_INSTRUCTIONS,
|
||||
name="MarketAgent",
|
||||
tools=[MarketAPIsTool()]
|
||||
)
|
||||
news_agent = AppModels.OLLAMA_QWEN_1B.get_agent(
|
||||
instructions=NEWS_INSTRUCTIONS,
|
||||
name="NewsAgent",
|
||||
tools=[NewsAPIsTool()]
|
||||
)
|
||||
social_agent = AppModels.OLLAMA_QWEN_1B.get_agent(
|
||||
instructions=SOCIAL_INSTRUCTIONS,
|
||||
name="SocialAgent",
|
||||
tools=[SocialAPIsTool()]
|
||||
)
|
||||
|
||||
# Crea il Team
|
||||
self.team = Team(name="CryptoAnalysisTeam", members=[self.market_agent, self.news_agent, self.social_agent])
|
||||
prompt = """
|
||||
You are the coordinator of a team of analysts specialized in cryptocurrency market analysis.
|
||||
Your role is to gather insights from various sources, including market data, news articles, and social media trends.
|
||||
Based on the information provided by your team members, you will synthesize a comprehensive sentiment analysis for each cryptocurrency discussed.
|
||||
Your analysis should consider the following aspects:
|
||||
1. Market Trends: Evaluate the current market trends and price movements.
|
||||
2. News Impact: Assess the impact of recent news articles on market sentiment.
|
||||
3. Social Media Buzz: Analyze social media discussions and trends related to the cryptocurrencies.
|
||||
Your final output should be a well-rounded sentiment analysis that can guide investment decisions.
|
||||
""" # TODO migliorare il prompt
|
||||
self.team = Team(
|
||||
model = AppModels.OLLAMA_QWEN_1B.get_model(prompt),
|
||||
name="CryptoAnalysisTeam",
|
||||
members=[market_agent, news_agent, social_agent]
|
||||
)
|
||||
|
||||
# Modelli disponibili e Predictor
|
||||
self.available_models = AppModels.availables()
|
||||
@@ -76,8 +100,8 @@ class Pipeline:
|
||||
|
||||
return output
|
||||
|
||||
def list_providers(self) -> List[str]:
|
||||
def list_providers(self) -> list[str]:
|
||||
return [m.name for m in self.available_models]
|
||||
|
||||
def list_styles(self) -> List[str]:
|
||||
def list_styles(self) -> list[str]:
|
||||
return [s.value for s in self.styles]
|
||||
|
||||
@@ -4,8 +4,8 @@ from praw.models import Submission, MoreComments
|
||||
from .base import SocialWrapper, SocialPost, SocialComment
|
||||
|
||||
MAX_COMMENTS = 5
|
||||
# TODO mettere piu' subreddit?
|
||||
# scelti da https://lkiconsulting.io/marketing/best-crypto-subreddits/
|
||||
# metterne altri se necessario.
|
||||
# fonti: https://lkiconsulting.io/marketing/best-crypto-subreddits/
|
||||
SUBREDDITS = [
|
||||
"CryptoCurrency",
|
||||
"Bitcoin",
|
||||
@@ -51,10 +51,10 @@ class RedditWrapper(SocialWrapper):
|
||||
|
||||
def __init__(self):
|
||||
client_id = os.getenv("REDDIT_API_CLIENT_ID")
|
||||
assert client_id is not None, "REDDIT_API_CLIENT_ID environment variable is not set"
|
||||
assert client_id, "REDDIT_API_CLIENT_ID environment variable is not set"
|
||||
|
||||
client_secret = os.getenv("REDDIT_API_CLIENT_SECRET")
|
||||
assert client_secret is not None, "REDDIT_API_CLIENT_SECRET environment variable is not set"
|
||||
assert client_secret, "REDDIT_API_CLIENT_SECRET environment variable is not set"
|
||||
|
||||
self.tool = Reddit(
|
||||
client_id=client_id,
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
from agno.tools import Toolkit
|
||||
from app.markets import MarketAPIsTool
|
||||
|
||||
|
||||
# TODO (?) in futuro fare in modo che la LLM faccia da sé per il mercato
|
||||
# Non so se può essere utile, per ora lo lascio qui
|
||||
# per ora mettiamo tutto statico e poi, se abbiamo API-Key senza limiti
|
||||
# possiamo fare in modo di far scegliere alla LLM quale crypto proporre
|
||||
# in base alle sue proprie chiamate API
|
||||
class MarketToolkit(Toolkit):
|
||||
def __init__(self):
|
||||
self.market_api = MarketAPIs()
|
||||
|
||||
super().__init__(
|
||||
name="Market Toolkit",
|
||||
tools=[
|
||||
self.market_api.get_historical_prices,
|
||||
self.market_api.get_product,
|
||||
],
|
||||
)
|
||||
|
||||
def instructions():
|
||||
return """
|
||||
Utilizza questo strumento per ottenere dati di mercato storici e attuali per criptovalute specifiche.
|
||||
Puoi richiedere i prezzi storici o il prezzo attuale di una criptovaluta specifica.
|
||||
Esempio di utilizzo:
|
||||
- get_historical_prices("BTC", limit=10) # ottieni gli ultimi 10 prezzi storici di Bitcoin
|
||||
- get_product("ETH")
|
||||
"""
|
||||
@@ -1,186 +0,0 @@
|
||||
import statistics
|
||||
from typing import Dict, List, Optional, Set
|
||||
from pydantic import BaseModel, Field, PrivateAttr
|
||||
from app.markets.base import ProductInfo
|
||||
|
||||
class AggregationMetadata(BaseModel):
|
||||
"""Metadati nascosti per debugging e audit trail"""
|
||||
sources_used: Set[str] = Field(default_factory=set, description="Exchange usati nell'aggregazione")
|
||||
sources_ignored: Set[str] = Field(default_factory=set, description="Exchange ignorati (errori)")
|
||||
aggregation_timestamp: str = Field(default="", description="Timestamp dell'aggregazione")
|
||||
confidence_score: float = Field(default=0.0, description="Score 0-1 sulla qualità dei dati")
|
||||
|
||||
class Config:
|
||||
# Nasconde questi campi dalla serializzazione di default
|
||||
extra = "forbid"
|
||||
|
||||
class AggregatedProductInfo(ProductInfo):
|
||||
"""
|
||||
Versione aggregata di ProductInfo che mantiene la trasparenza per l'utente finale
|
||||
mentre fornisce metadati di debugging opzionali.
|
||||
"""
|
||||
|
||||
# Override dei campi con logica di aggregazione
|
||||
id: str = Field(description="ID aggregato basato sul simbolo standardizzato")
|
||||
status: str = Field(description="Status aggregato (majority vote o conservative)")
|
||||
|
||||
# Campi privati per debugging (non visibili di default)
|
||||
_metadata: Optional[AggregationMetadata] = PrivateAttr(default=None)
|
||||
_source_data: Optional[Dict[str, ProductInfo]] = PrivateAttr(default=None)
|
||||
|
||||
@classmethod
|
||||
def from_multiple_sources(cls, products: List[ProductInfo]) -> 'AggregatedProductInfo':
|
||||
"""
|
||||
Crea un AggregatedProductInfo da una lista di ProductInfo.
|
||||
Usa strategie intelligenti per gestire ID e status.
|
||||
"""
|
||||
if not products:
|
||||
raise ValueError("Nessun prodotto da aggregare")
|
||||
|
||||
# Raggruppa per symbol (la chiave vera per l'aggregazione)
|
||||
symbol_groups = {}
|
||||
for product in products:
|
||||
if product.symbol not in symbol_groups:
|
||||
symbol_groups[product.symbol] = []
|
||||
symbol_groups[product.symbol].append(product)
|
||||
|
||||
# Per ora gestiamo un symbol alla volta
|
||||
if len(symbol_groups) > 1:
|
||||
raise ValueError(f"Simboli multipli non supportati: {list(symbol_groups.keys())}")
|
||||
|
||||
symbol_products = list(symbol_groups.values())[0]
|
||||
|
||||
# Estrai tutte le fonti
|
||||
sources = []
|
||||
for product in symbol_products:
|
||||
# Determina la fonte dall'ID o da altri metadati se disponibili
|
||||
source = cls._detect_source(product)
|
||||
sources.append(source)
|
||||
|
||||
# Aggrega i dati
|
||||
aggregated_data = cls._aggregate_products(symbol_products, sources)
|
||||
|
||||
# Crea l'istanza e assegna gli attributi privati
|
||||
instance = cls(**aggregated_data)
|
||||
instance._metadata = aggregated_data.get("_metadata")
|
||||
instance._source_data = aggregated_data.get("_source_data")
|
||||
|
||||
return instance
|
||||
|
||||
@staticmethod
|
||||
def _detect_source(product: ProductInfo) -> str:
|
||||
"""Rileva la fonte da un ProductInfo"""
|
||||
# Strategia semplice: usa pattern negli ID
|
||||
if "coinbase" in product.id.lower() or "cb" in product.id.lower():
|
||||
return "coinbase"
|
||||
elif "binance" in product.id.lower() or "bn" in product.id.lower():
|
||||
return "binance"
|
||||
elif "crypto" in product.id.lower() or "cc" in product.id.lower():
|
||||
return "cryptocompare"
|
||||
elif "yfinance" in product.id.lower() or "yf" in product.id.lower():
|
||||
return "yfinance"
|
||||
else:
|
||||
return "unknown"
|
||||
|
||||
@classmethod
|
||||
def _aggregate_products(cls, products: List[ProductInfo], sources: List[str]) -> dict:
|
||||
"""
|
||||
Logica di aggregazione principale.
|
||||
Gestisce ID, status e altri campi numerici.
|
||||
"""
|
||||
import statistics
|
||||
from datetime import datetime
|
||||
|
||||
# ID: usa il symbol come chiave standardizzata
|
||||
symbol = products[0].symbol
|
||||
aggregated_id = f"{symbol}_AGG"
|
||||
|
||||
# Status: strategia "conservativa" - il più restrittivo vince
|
||||
# Ordine: trading_only < limit_only < auction < maintenance < offline
|
||||
status_priority = {
|
||||
"trading": 1,
|
||||
"limit_only": 2,
|
||||
"auction": 3,
|
||||
"maintenance": 4,
|
||||
"offline": 5,
|
||||
"": 0 # Default se non specificato
|
||||
}
|
||||
|
||||
statuses = [p.status for p in products if p.status]
|
||||
if statuses:
|
||||
# Prendi lo status con priorità più alta (più restrittivo)
|
||||
aggregated_status = max(statuses, key=lambda s: status_priority.get(s, 0))
|
||||
else:
|
||||
aggregated_status = "trading" # Default ottimistico
|
||||
|
||||
# Prezzo: media semplice (uso diretto del campo price come float)
|
||||
prices = [p.price for p in products if p.price > 0]
|
||||
aggregated_price = statistics.mean(prices) if prices else 0.0
|
||||
|
||||
# Volume: somma (assumendo che i volumi siano esclusivi per exchange)
|
||||
volumes = [p.volume_24h for p in products if p.volume_24h > 0]
|
||||
total_volume = sum(volumes)
|
||||
aggregated_volume = sum(price_i * volume_i for price_i, volume_i in zip((p.price for p in products), (volume for volume in volumes))) / total_volume
|
||||
aggregated_volume = round(aggregated_volume, 5)
|
||||
# aggregated_volume = sum(volumes) if volumes else 0.0 # NOTE old implementation
|
||||
|
||||
# Valuta: prendi la prima (dovrebbero essere tutte uguali)
|
||||
quote_currency = next((p.quote_currency for p in products if p.quote_currency), "USD")
|
||||
|
||||
# Calcola confidence score
|
||||
confidence = cls._calculate_confidence(products, sources)
|
||||
|
||||
# Crea metadati per debugging
|
||||
metadata = AggregationMetadata(
|
||||
sources_used=set(sources),
|
||||
aggregation_timestamp=datetime.now().isoformat(),
|
||||
confidence_score=confidence
|
||||
)
|
||||
|
||||
# Salva dati sorgente per debugging
|
||||
source_data = dict(zip(sources, products))
|
||||
|
||||
return {
|
||||
"symbol": symbol,
|
||||
"price": aggregated_price,
|
||||
"volume_24h": aggregated_volume,
|
||||
"quote_currency": quote_currency,
|
||||
"id": aggregated_id,
|
||||
"status": aggregated_status,
|
||||
"_metadata": metadata,
|
||||
"_source_data": source_data
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _calculate_confidence(products: List[ProductInfo], sources: List[str]) -> float:
|
||||
"""Calcola un punteggio di confidenza 0-1"""
|
||||
if not products:
|
||||
return 0.0
|
||||
|
||||
score = 1.0
|
||||
|
||||
# Riduci score se pochi dati
|
||||
if len(products) < 2:
|
||||
score *= 0.7
|
||||
|
||||
# Riduci score se prezzi troppo diversi
|
||||
prices = [p.price for p in products if p.price > 0]
|
||||
if len(prices) > 1:
|
||||
price_std = (max(prices) - min(prices)) / statistics.mean(prices)
|
||||
if price_std > 0.05: # >5% variazione
|
||||
score *= 0.8
|
||||
|
||||
# Riduci score se fonti sconosciute
|
||||
unknown_sources = sum(1 for s in sources if s == "unknown")
|
||||
if unknown_sources > 0:
|
||||
score *= (1 - unknown_sources / len(sources))
|
||||
|
||||
return max(0.0, min(1.0, score))
|
||||
|
||||
def get_debug_info(self) -> dict:
|
||||
"""Metodo opzionale per ottenere informazioni di debug"""
|
||||
return {
|
||||
"aggregated_product": self.dict(),
|
||||
"metadata": self._metadata.dict() if self._metadata else None,
|
||||
"sources": list(self._source_data.keys()) if self._source_data else []
|
||||
}
|
||||
91
src/app/utils/market_aggregation.py
Normal file
91
src/app/utils/market_aggregation.py
Normal file
@@ -0,0 +1,91 @@
|
||||
import statistics
|
||||
from app.markets.base import ProductInfo, Price
|
||||
|
||||
|
||||
def aggregate_history_prices(prices: dict[str, list[Price]]) -> list[Price]:
|
||||
"""
|
||||
Aggrega i prezzi storici per symbol calcolando la media oraria.
|
||||
Args:
|
||||
prices (dict[str, list[Price]]): Mappa provider -> lista di Price
|
||||
Returns:
|
||||
list[Price]: Lista di Price aggregati per ora
|
||||
"""
|
||||
|
||||
# Costruiamo una mappa timestamp_h -> lista di Price
|
||||
timestamped_prices: dict[int, list[Price]] = {}
|
||||
for _, price_list in prices.items():
|
||||
for price in price_list:
|
||||
time = price.timestamp_ms - (price.timestamp_ms % 3600000) # arrotonda all'ora (non dovrebbe essere necessario)
|
||||
timestamped_prices.setdefault(time, []).append(price)
|
||||
|
||||
# Ora aggregiamo i prezzi per ogni ora
|
||||
aggregated_prices = []
|
||||
for time, price_list in timestamped_prices.items():
|
||||
price = Price()
|
||||
price.timestamp_ms = time
|
||||
price.high = statistics.mean([p.high for p in price_list])
|
||||
price.low = statistics.mean([p.low for p in price_list])
|
||||
price.open = statistics.mean([p.open for p in price_list])
|
||||
price.close = statistics.mean([p.close for p in price_list])
|
||||
price.volume = statistics.mean([p.volume for p in price_list])
|
||||
aggregated_prices.append(price)
|
||||
return aggregated_prices
|
||||
|
||||
def aggregate_product_info(products: dict[str, list[ProductInfo]]) -> list[ProductInfo]:
|
||||
"""
|
||||
Aggrega una lista di ProductInfo per symbol.
|
||||
Args:
|
||||
products (dict[str, list[ProductInfo]]): Mappa provider -> lista di ProductInfo
|
||||
Returns:
|
||||
list[ProductInfo]: Lista di ProductInfo aggregati per symbol
|
||||
"""
|
||||
|
||||
# Costruzione mappa symbol -> lista di ProductInfo
|
||||
symbols_infos: dict[str, list[ProductInfo]] = {}
|
||||
for _, product_list in products.items():
|
||||
for product in product_list:
|
||||
symbols_infos.setdefault(product.symbol, []).append(product)
|
||||
|
||||
# Aggregazione per ogni symbol
|
||||
sources = list(products.keys())
|
||||
aggregated_products = []
|
||||
for symbol, product_list in symbols_infos.items():
|
||||
product = ProductInfo()
|
||||
|
||||
product.id = f"{symbol}_AGGREGATED"
|
||||
product.symbol = symbol
|
||||
product.quote_currency = next(p.quote_currency for p in product_list if p.quote_currency)
|
||||
|
||||
volume_sum = sum(p.volume_24h for p in product_list)
|
||||
product.volume_24h = volume_sum / len(product_list) if product_list else 0.0
|
||||
|
||||
prices = sum(p.price * p.volume_24h for p in product_list)
|
||||
product.price = (prices / volume_sum) if volume_sum > 0 else 0.0
|
||||
|
||||
aggregated_products.append(product)
|
||||
return aggregated_products
|
||||
|
||||
def _calculate_confidence(products: list[ProductInfo], sources: list[str]) -> float:
|
||||
"""Calcola un punteggio di confidenza 0-1"""
|
||||
if not products:
|
||||
return 0.0
|
||||
|
||||
score = 1.0
|
||||
|
||||
# Riduci score se pochi dati
|
||||
if len(products) < 2:
|
||||
score *= 0.7
|
||||
|
||||
# Riduci score se prezzi troppo diversi
|
||||
prices = [p.price for p in products if p.price > 0]
|
||||
if len(prices) > 1:
|
||||
price_std = (max(prices) - min(prices)) / statistics.mean(prices)
|
||||
if price_std > 0.05: # >5% variazione
|
||||
score *= 0.8
|
||||
|
||||
# Riduci score se fonti sconosciute
|
||||
unknown_sources = sum(1 for s in sources if s == "unknown")
|
||||
if unknown_sources > 0:
|
||||
score *= (1 - unknown_sources / len(sources))
|
||||
|
||||
return max(0.0, min(1.0, score))
|
||||
@@ -1,184 +0,0 @@
|
||||
from typing import List, Optional, Dict, Any
|
||||
from app.markets.base import ProductInfo, Price
|
||||
from app.utils.aggregated_models import AggregatedProductInfo
|
||||
|
||||
class MarketDataAggregator:
|
||||
"""
|
||||
Aggregatore di dati di mercato che mantiene la trasparenza per l'utente.
|
||||
|
||||
Compone MarketAPIs per fornire gli stessi metodi, ma restituisce dati aggregati
|
||||
da tutte le fonti disponibili. L'utente finale non vede la complessità.
|
||||
"""
|
||||
|
||||
def __init__(self, currency: str = "USD"):
|
||||
# Import lazy per evitare circular import
|
||||
from app.markets import MarketAPIsTool
|
||||
self._market_apis = MarketAPIsTool(currency)
|
||||
self._aggregation_enabled = True
|
||||
|
||||
def get_product(self, asset_id: str) -> ProductInfo:
|
||||
"""
|
||||
Override che aggrega dati da tutte le fonti disponibili.
|
||||
Per l'utente sembra un normale ProductInfo.
|
||||
"""
|
||||
if not self._aggregation_enabled:
|
||||
return self._market_apis.get_product(asset_id)
|
||||
|
||||
# Raccogli dati da tutte le fonti
|
||||
try:
|
||||
raw_results = self.wrappers.try_call_all(
|
||||
lambda wrapper: wrapper.get_product(asset_id)
|
||||
)
|
||||
|
||||
# Converti in ProductInfo se necessario
|
||||
products = []
|
||||
for wrapper_class, result in raw_results.items():
|
||||
if isinstance(result, ProductInfo):
|
||||
products.append(result)
|
||||
elif isinstance(result, dict):
|
||||
# Converti dizionario in ProductInfo
|
||||
products.append(ProductInfo(**result))
|
||||
|
||||
if not products:
|
||||
raise Exception("Nessun dato disponibile")
|
||||
|
||||
# Aggrega i risultati
|
||||
aggregated = AggregatedProductInfo.from_multiple_sources(products)
|
||||
|
||||
# Restituisci come ProductInfo normale (nascondi la complessità)
|
||||
return ProductInfo(**aggregated.dict(exclude={"_metadata", "_source_data"}))
|
||||
|
||||
except Exception as e:
|
||||
# Fallback: usa il comportamento normale se l'aggregazione fallisce
|
||||
return self._market_apis.get_product(asset_id)
|
||||
|
||||
def get_products(self, asset_ids: List[str]) -> List[ProductInfo]:
|
||||
"""
|
||||
Aggrega dati per multiple asset.
|
||||
"""
|
||||
if not self._aggregation_enabled:
|
||||
return self._market_apis.get_products(asset_ids)
|
||||
|
||||
aggregated_products = []
|
||||
|
||||
for asset_id in asset_ids:
|
||||
try:
|
||||
product = self.get_product(asset_id)
|
||||
aggregated_products.append(product)
|
||||
except Exception as e:
|
||||
# Salta asset che non riescono ad aggregare
|
||||
continue
|
||||
|
||||
return aggregated_products
|
||||
|
||||
def get_all_products(self) -> List[ProductInfo]:
|
||||
"""
|
||||
Aggrega tutti i prodotti disponibili.
|
||||
"""
|
||||
if not self._aggregation_enabled:
|
||||
return self._market_apis.get_all_products()
|
||||
|
||||
# Raccogli tutti i prodotti da tutte le fonti
|
||||
try:
|
||||
all_products_by_source = self.wrappers.try_call_all(
|
||||
lambda wrapper: wrapper.get_all_products()
|
||||
)
|
||||
|
||||
# Raggruppa per symbol per aggregare
|
||||
symbol_groups = {}
|
||||
for wrapper_class, products in all_products_by_source.items():
|
||||
if not isinstance(products, list):
|
||||
continue
|
||||
|
||||
for product in products:
|
||||
if isinstance(product, dict):
|
||||
product = ProductInfo(**product)
|
||||
|
||||
if product.symbol not in symbol_groups:
|
||||
symbol_groups[product.symbol] = []
|
||||
symbol_groups[product.symbol].append(product)
|
||||
|
||||
# Aggrega ogni gruppo
|
||||
aggregated_products = []
|
||||
for symbol, products in symbol_groups.items():
|
||||
try:
|
||||
aggregated = AggregatedProductInfo.from_multiple_sources(products)
|
||||
# Restituisci come ProductInfo normale
|
||||
aggregated_products.append(
|
||||
ProductInfo(**aggregated.dict(exclude={"_metadata", "_source_data"}))
|
||||
)
|
||||
except Exception:
|
||||
# Se l'aggregazione fallisce, usa il primo disponibile
|
||||
if products:
|
||||
aggregated_products.append(products[0])
|
||||
|
||||
return aggregated_products
|
||||
|
||||
except Exception as e:
|
||||
# Fallback: usa il comportamento normale
|
||||
return self._market_apis.get_all_products()
|
||||
|
||||
def get_historical_prices(self, asset_id: str = "BTC", limit: int = 100) -> List[Price]:
|
||||
"""
|
||||
Per i dati storici, usa una strategia diversa:
|
||||
prendi i dati dalla fonte più affidabile o aggrega se possibile.
|
||||
"""
|
||||
if not self._aggregation_enabled:
|
||||
return self._market_apis.get_historical_prices(asset_id, limit)
|
||||
|
||||
# Per dati storici, usa il primo wrapper che funziona
|
||||
# (l'aggregazione di dati storici è più complessa)
|
||||
try:
|
||||
return self.wrappers.try_call(
|
||||
lambda wrapper: wrapper.get_historical_prices(asset_id, limit)
|
||||
)
|
||||
except Exception as e:
|
||||
# Fallback: usa il comportamento normale
|
||||
return self._market_apis.get_historical_prices(asset_id, limit)
|
||||
|
||||
def enable_aggregation(self, enabled: bool = True):
|
||||
"""Abilita o disabilita l'aggregazione"""
|
||||
self._aggregation_enabled = enabled
|
||||
|
||||
def is_aggregation_enabled(self) -> bool:
|
||||
"""Controlla se l'aggregazione è abilitata"""
|
||||
return self._aggregation_enabled
|
||||
|
||||
# Metodi proxy per completare l'interfaccia BaseWrapper
|
||||
@property
|
||||
def wrappers(self):
|
||||
"""Accesso al wrapper handler per compatibilità"""
|
||||
return self._market_apis.wrappers
|
||||
|
||||
def get_aggregated_product_with_debug(self, asset_id: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Metodo speciale per debugging: restituisce dati aggregati con metadati.
|
||||
Usato solo per testing e monitoraggio.
|
||||
"""
|
||||
try:
|
||||
raw_results = self.wrappers.try_call_all(
|
||||
lambda wrapper: wrapper.get_product(asset_id)
|
||||
)
|
||||
|
||||
products = []
|
||||
for wrapper_class, result in raw_results.items():
|
||||
if isinstance(result, ProductInfo):
|
||||
products.append(result)
|
||||
elif isinstance(result, dict):
|
||||
products.append(ProductInfo(**result))
|
||||
|
||||
if not products:
|
||||
raise Exception("Nessun dato disponibile")
|
||||
|
||||
aggregated = AggregatedProductInfo.from_multiple_sources(products)
|
||||
|
||||
return {
|
||||
"product": aggregated.dict(exclude={"_metadata", "_source_data"}),
|
||||
"debug": aggregated.get_debug_info()
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
"error": str(e),
|
||||
"debug": {"error": str(e)}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import inspect
|
||||
import time
|
||||
import traceback
|
||||
from typing import TypeVar, Callable, Generic, Iterable, Type
|
||||
@@ -45,17 +46,24 @@ class WrapperHandler(Generic[W]):
|
||||
Raises:
|
||||
Exception: If all wrappers fail after retries.
|
||||
"""
|
||||
log_info(f"{inspect.getsource(func).strip()} {inspect.getclosurevars(func).nonlocals}")
|
||||
|
||||
iterations = 0
|
||||
while iterations < len(self.wrappers):
|
||||
wrapper = self.wrappers[self.index]
|
||||
wrapper_name = wrapper.__class__.__name__
|
||||
|
||||
try:
|
||||
wrapper = self.wrappers[self.index]
|
||||
log_info(f"Trying wrapper: {wrapper} - function {func}")
|
||||
log_info(f"try_call {wrapper_name}")
|
||||
result = func(wrapper)
|
||||
log_info(f"{wrapper_name} succeeded")
|
||||
self.retry_count = 0
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
self.retry_count += 1
|
||||
log_warning(f"{wrapper} failed {self.retry_count}/{self.retry_per_wrapper}: {WrapperHandler.__concise_error(e)}")
|
||||
error = WrapperHandler.__concise_error(e)
|
||||
log_warning(f"{wrapper_name} failed {self.retry_count}/{self.retry_per_wrapper}: {error}")
|
||||
|
||||
if self.retry_count >= self.retry_per_wrapper:
|
||||
self.index = (self.index + 1) % len(self.wrappers)
|
||||
@@ -64,7 +72,7 @@ class WrapperHandler(Generic[W]):
|
||||
else:
|
||||
time.sleep(self.retry_delay)
|
||||
|
||||
raise Exception(f"All wrappers failed after retries")
|
||||
raise Exception(f"All wrappers failed, latest error: {error}")
|
||||
|
||||
def try_call_all(self, func: Callable[[W], T]) -> dict[str, T]:
|
||||
"""
|
||||
@@ -78,28 +86,33 @@ class WrapperHandler(Generic[W]):
|
||||
Raises:
|
||||
Exception: If all wrappers fail.
|
||||
"""
|
||||
log_info(f"{inspect.getsource(func).strip()} {inspect.getclosurevars(func).nonlocals}")
|
||||
|
||||
results = {}
|
||||
log_info(f"All wrappers: {[wrapper.__class__ for wrapper in self.wrappers]} - function {func}")
|
||||
for wrapper in self.wrappers:
|
||||
wrapper_name = wrapper.__class__.__name__
|
||||
try:
|
||||
result = func(wrapper)
|
||||
log_info(f"{wrapper_name} succeeded")
|
||||
results[wrapper.__class__] = result
|
||||
except Exception as e:
|
||||
log_warning(f"{wrapper} failed: {WrapperHandler.__concise_error(e)}")
|
||||
error = WrapperHandler.__concise_error(e)
|
||||
log_warning(f"{wrapper_name} failed: {error}")
|
||||
if not results:
|
||||
raise Exception("All wrappers failed")
|
||||
raise Exception(f"All wrappers failed, latest error: {error}")
|
||||
return results
|
||||
|
||||
@staticmethod
|
||||
def __check(wrappers: list[W]) -> bool:
|
||||
return all(w.__class__ is type for w in wrappers)
|
||||
|
||||
@staticmethod
|
||||
def __concise_error(e: Exception) -> str:
|
||||
last_frame = traceback.extract_tb(e.__traceback__)[-1]
|
||||
return f"{e} [\"{last_frame.filename}\", line {last_frame.lineno}]"
|
||||
|
||||
@staticmethod
|
||||
def build_wrappers(constructors: Iterable[Type[W]], try_per_wrapper: int = 3, retry_delay: int = 2) -> 'WrapperHandler[W]':
|
||||
def build_wrappers(constructors: Iterable[Type[W]], try_per_wrapper: int = 3, retry_delay: int = 2, kwargs: dict | None = None) -> 'WrapperHandler[W]':
|
||||
"""
|
||||
Builds a WrapperHandler instance with the given wrapper constructors.
|
||||
It attempts to initialize each wrapper and logs a warning if any cannot be initialized.
|
||||
@@ -108,6 +121,7 @@ class WrapperHandler(Generic[W]):
|
||||
constructors (Iterable[Type[W]]): An iterable of wrapper classes to instantiate. e.g. [WrapperA, WrapperB]
|
||||
try_per_wrapper (int): Number of retries per wrapper before switching to the next.
|
||||
retry_delay (int): Delay in seconds between retries.
|
||||
kwargs (dict | None): Optional dictionary with keyword arguments common to all wrappers.
|
||||
Returns:
|
||||
WrapperHandler[W]: An instance of WrapperHandler with the initialized wrappers.
|
||||
Raises:
|
||||
@@ -118,7 +132,7 @@ class WrapperHandler(Generic[W]):
|
||||
result = []
|
||||
for wrapper_class in constructors:
|
||||
try:
|
||||
wrapper = wrapper_class()
|
||||
wrapper = wrapper_class(**(kwargs or {}))
|
||||
result.append(wrapper)
|
||||
except Exception as e:
|
||||
log_warning(f"{wrapper_class} cannot be initialized: {e}")
|
||||
|
||||
Reference in New Issue
Block a user