diff --git a/.env.example b/.env.example index 4cfc34a..06a53cb 100644 --- a/.env.example +++ b/.env.example @@ -6,6 +6,9 @@ # Vedi https://docs.agno.com/examples/models per vedere tutti i modelli supportati 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 ############################################################################### diff --git a/demos/market_providers_api_demo.py b/demos/market_providers_api_demo.py index fea2245..8c368e8 100644 --- a/demos/market_providers_api_demo.py +++ b/demos/market_providers_api_demo.py @@ -8,6 +8,7 @@ Questo script dimostra l'utilizzo di tutti i wrapper che implementano BaseWrappe - CryptoCompareWrapper (richiede API key) - BinanceWrapper (richiede credenziali) - PublicBinanceAgent (accesso pubblico) +- YFinanceWrapper (accesso gratuito a dati azionari e crypto) Lo script effettua chiamate GET a diversi provider e visualizza i dati in modo strutturato con informazioni dettagliate su timestamp, stato @@ -29,7 +30,8 @@ from dotenv import load_dotenv from app.markets import ( CoinBaseWrapper, CryptoCompareWrapper, - BinanceWrapper, + BinanceWrapper, + YFinanceWrapper, BaseWrapper ) @@ -259,13 +261,18 @@ def initialize_providers() -> Dict[str, BaseWrapper]: print("⚠️ CoinBaseWrapper saltato: credenziali Coinbase non complete") # BinanceWrapper - try: providers["Binance"] = BinanceWrapper() print("✅ BinanceWrapper inizializzato con successo") except Exception as e: print(f"❌ Errore nell'inizializzazione di BinanceWrapper: {e}") + # YFinanceWrapper (sempre disponibile - dati azionari e crypto gratuiti) + try: + providers["YFinance"] = YFinanceWrapper() + print("✅ YFinanceWrapper inizializzato con successo") + except Exception as e: + print(f"❌ Errore nell'inizializzazione di YFinanceWrapper: {e}") return providers def print_summary(results: List[Dict[str, Any]]): diff --git a/pyproject.toml b/pyproject.toml index e091aba..2e90e39 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,7 @@ dependencies = [ # API di exchange di criptovalute "coinbase-advanced-py", "python-binance", + "yfinance", # API di notizie "newsapi-python", diff --git a/src/app/markets/__init__.py b/src/app/markets/__init__.py index e5853d5..eefc442 100644 --- a/src/app/markets/__init__.py +++ b/src/app/markets/__init__.py @@ -2,13 +2,14 @@ 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", "PublicBinanceAgent" ] +__all__ = [ "MarketAPIs", "BinanceWrapper", "CoinBaseWrapper", "CryptoCompareWrapper", "YFinanceWrapper", "PublicBinanceAgent" ] class MarketAPIsTool(BaseWrapper, Toolkit): @@ -24,7 +25,7 @@ class MarketAPIsTool(BaseWrapper, Toolkit): def __init__(self, currency: str = "USD", enable_aggregation: bool = False): self.currency = currency - wrappers = [ BinanceWrapper, CoinBaseWrapper, CryptoCompareWrapper ] + wrappers = [ BinanceWrapper, CoinBaseWrapper, CryptoCompareWrapper, YFinanceWrapper ] self.wrappers: WrapperHandler[BaseWrapper] = WrapperHandler.build_wrappers(wrappers) # Inizializza l'aggregatore solo se richiesto (lazy initialization) diff --git a/src/app/markets/binance_public.py b/src/app/markets/binance_public.py new file mode 100644 index 0000000..c1d9896 --- /dev/null +++ b/src/app/markets/binance_public.py @@ -0,0 +1,218 @@ +""" +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) \ No newline at end of file diff --git a/src/app/markets/yfinance.py b/src/app/markets/yfinance.py new file mode 100644 index 0000000..f0e5d6d --- /dev/null +++ b/src/app/markets/yfinance.py @@ -0,0 +1,214 @@ +import json +from agno.tools.yfinance import YFinanceTools +from .base import BaseWrapper, ProductInfo, Price + + +def create_product_info(symbol: str, stock_data: dict) -> 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' + + return product + + +def create_price_from_history(hist_data: dict, timestamp: 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 + + return price + + +class YFinanceWrapper(BaseWrapper): + """ + Wrapper per YFinanceTools che fornisce dati di mercato per azioni, ETF e criptovalute. + 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. + """ + 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 + + 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 + + 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 + + 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 [] \ No newline at end of file diff --git a/src/app/utils/aggregated_models.py b/src/app/utils/aggregated_models.py index 8eba8a5..ee9f3ef 100644 --- a/src/app/utils/aggregated_models.py +++ b/src/app/utils/aggregated_models.py @@ -77,6 +77,8 @@ class AggregatedProductInfo(ProductInfo): 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" diff --git a/src/app/utils/market_aggregator.py b/src/app/utils/market_aggregator.py deleted file mode 100644 index 639bb9b..0000000 --- a/src/app/utils/market_aggregator.py +++ /dev/null @@ -1,71 +0,0 @@ -import statistics -from typing import Dict, Any - -class MarketAggregator: - """ - Aggrega dati di mercato da più provider e genera segnali e analisi avanzate. - """ - @staticmethod - def aggregate(symbol: str, sources: Dict[str, Dict[str, Any]]) -> Dict[str, Any]: - prices = [] - volumes = [] - price_map = {} - for provider, data in sources.items(): - price = data.get('price') - if price is not None: - prices.append(price) - price_map[provider] = price - volume = data.get('volume') - if volume is not None: - volumes.append(MarketAggregator._parse_volume(volume)) - - # Aggregated price (mean) - agg_price = statistics.mean(prices) if prices else None - # Spread analysis - spread = (max(prices) - min(prices)) / agg_price if prices and agg_price else 0 - # Confidence - stddev = statistics.stdev(prices) if len(prices) > 1 else 0 - confidence = max(0.5, 1 - (stddev / agg_price)) if agg_price else 0 - if spread < 0.005: - confidence += 0.1 - if len(prices) >= 3: - confidence += 0.05 - confidence = min(confidence, 1.0) - # Volume trend - total_volume = sum(volumes) if volumes else None - # Price divergence - max_deviation = (max(prices) - min(prices)) / agg_price if prices and agg_price else 0 - # Signals - signals = { - "spread_analysis": f"Low spread ({spread:.2%}) indicates healthy liquidity" if spread < 0.01 else f"Spread {spread:.2%} - check liquidity", - "volume_trend": f"Combined volume: {total_volume:.2f}" if total_volume else "Volume data not available", - "price_divergence": f"Max deviation: {max_deviation:.2%} - {'Normal range' if max_deviation < 0.01 else 'High divergence'}" - } - return { - "aggregated_data": { - f"{symbol}_USD": { - "price": round(agg_price, 2) if agg_price else None, - "confidence": round(confidence, 2), - "sources_count": len(prices) - } - }, - "individual_sources": price_map, - "market_signals": signals - } - @staticmethod - def _parse_volume(volume: Any) -> float: - # Supporta stringhe tipo "1.2M" o numeri - if isinstance(volume, (int, float)): - return float(volume) - if isinstance(volume, str): - v = volume.upper().replace(' ', '') - if v.endswith('M'): - return float(v[:-1]) * 1_000_000 - if v.endswith('K'): - return float(v[:-1]) * 1_000 - try: - return float(v) - except Exception as e: - print(f"Errore nel parsing del volume: {e}") - return 0.0 - return 0.0 diff --git a/src/app/utils/market_data_aggregator.py b/src/app/utils/market_data_aggregator.py index f72e91c..ea2d7c0 100644 --- a/src/app/utils/market_data_aggregator.py +++ b/src/app/utils/market_data_aggregator.py @@ -12,8 +12,8 @@ class MarketDataAggregator: def __init__(self, currency: str = "USD"): # Import lazy per evitare circular import - from app.markets import MarketAPIs - self._market_apis = MarketAPIs(currency) + from app.markets import MarketAPIsTool + self._market_apis = MarketAPIsTool(currency) self._aggregation_enabled = True def get_product(self, asset_id: str) -> ProductInfo: diff --git a/tests/api/test_yfinance.py b/tests/api/test_yfinance.py new file mode 100644 index 0000000..c0e9ba2 --- /dev/null +++ b/tests/api/test_yfinance.py @@ -0,0 +1,93 @@ +import os +import pytest +from app.markets import YFinanceWrapper + +@pytest.mark.market +@pytest.mark.api +class TestYFinance: + + def test_yfinance_init(self): + market = YFinanceWrapper() + assert market is not None + assert hasattr(market, 'currency') + assert market.currency == "USD" + assert hasattr(market, 'tool') + assert market.tool is not None + + def test_yfinance_get_product(self): + market = YFinanceWrapper() + product = market.get_product("AAPL") + assert product is not None + assert hasattr(product, 'symbol') + assert product.symbol == "AAPL" + assert hasattr(product, 'price') + assert product.price > 0 + assert hasattr(product, 'status') + assert product.status == "trading" + + def test_yfinance_get_crypto_product(self): + market = YFinanceWrapper() + product = market.get_product("BTC") + assert product is not None + assert hasattr(product, 'symbol') + # BTC verrà convertito in BTC-USD dal formattatore + assert product.symbol in ["BTC", "BTC-USD"] + assert hasattr(product, 'price') + assert product.price > 0 + + def test_yfinance_get_products(self): + market = YFinanceWrapper() + products = market.get_products(["AAPL", "GOOGL"]) + assert products is not None + assert isinstance(products, list) + assert len(products) == 2 + symbols = [p.symbol for p in products] + assert "AAPL" in symbols + assert "GOOGL" in symbols + for product in products: + assert hasattr(product, 'price') + assert product.price > 0 + + def test_yfinance_get_all_products(self): + market = YFinanceWrapper() + products = market.get_all_products() + assert products is not None + assert isinstance(products, list) + assert len(products) > 0 + # Dovrebbe contenere asset popolari + symbols = [p.symbol for p in products] + assert "AAPL" in symbols # Apple dovrebbe essere nella lista + for product in products: + assert hasattr(product, 'symbol') + assert hasattr(product, 'price') + + def test_yfinance_invalid_product(self): + market = YFinanceWrapper() + # Per YFinance, un prodotto invalido dovrebbe restituire un prodotto offline + product = market.get_product("INVALIDSYMBOL123") + assert product is not None + assert product.status == "offline" + + def test_yfinance_history(self): + market = YFinanceWrapper() + history = market.get_historical_prices("AAPL", limit=5) + assert history is not None + assert isinstance(history, list) + assert len(history) == 5 + for entry in history: + assert hasattr(entry, 'time') + assert hasattr(entry, 'close') + assert hasattr(entry, 'high') + assert entry.close > 0 + assert entry.high > 0 + + def test_yfinance_crypto_history(self): + market = YFinanceWrapper() + history = market.get_historical_prices("BTC", limit=3) + assert history is not None + assert isinstance(history, list) + assert len(history) == 3 + for entry in history: + assert hasattr(entry, 'time') + assert hasattr(entry, 'close') + assert entry.close > 0 \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index c792e04..2b7cf90 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -24,6 +24,7 @@ def pytest_configure(config:pytest.Config): ("limited", "marks tests that have limited execution due to API constraints"), ("wrapper", "marks tests for wrapper handler"), ("tools", "marks tests for tools"), + ("aggregator", "marks tests for market data aggregator"), ] for marker in markers: line = f"{marker[0]}: {marker[1]}" diff --git a/tests/utils/test_market_data_aggregator.py b/tests/utils/test_market_data_aggregator.py index 236d2a4..e8d1a6f 100644 --- a/tests/utils/test_market_data_aggregator.py +++ b/tests/utils/test_market_data_aggregator.py @@ -3,7 +3,7 @@ from app.utils.market_data_aggregator import MarketDataAggregator from app.utils.aggregated_models import AggregatedProductInfo from app.markets.base import ProductInfo, Price - +@pytest.mark.aggregator @pytest.mark.limited @pytest.mark.market @pytest.mark.api diff --git a/uv.lock b/uv.lock index 2d7d6a1..9c977c3 100644 --- a/uv.lock +++ b/uv.lock @@ -325,6 +325,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ff/e8/77d17d00981cdd27cc493e81e1749a0b8bbfb843780dbd841e30d7f50743/cryptography-46.0.1-cp38-abi3-win_arm64.whl", hash = "sha256:efc9e51c3e595267ff84adf56e9b357db89ab2279d7e375ffcaf8f678606f3d9", size = 2923149, upload-time = "2025-09-17T00:10:13.236Z" }, ] +[[package]] +name = "curl-cffi" +version = "0.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4e/3d/f39ca1f8fdf14408888e7c25e15eed63eac5f47926e206fb93300d28378c/curl_cffi-0.13.0.tar.gz", hash = "sha256:62ecd90a382bd5023750e3606e0aa7cb1a3a8ba41c14270b8e5e149ebf72c5ca", size = 151303, upload-time = "2025-08-06T13:05:42.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/d1/acabfd460f1de26cad882e5ef344d9adde1507034528cb6f5698a2e6a2f1/curl_cffi-0.13.0-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:434cadbe8df2f08b2fc2c16dff2779fb40b984af99c06aa700af898e185bb9db", size = 5686337, upload-time = "2025-08-06T13:05:28.985Z" }, + { url = "https://files.pythonhosted.org/packages/2c/1c/cdb4fb2d16a0e9de068e0e5bc02094e105ce58a687ff30b4c6f88e25a057/curl_cffi-0.13.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:59afa877a9ae09efa04646a7d068eeea48915a95d9add0a29854e7781679fcd7", size = 2994613, upload-time = "2025-08-06T13:05:31.027Z" }, + { url = "https://files.pythonhosted.org/packages/04/3e/fdf617c1ec18c3038b77065d484d7517bb30f8fb8847224eb1f601a4e8bc/curl_cffi-0.13.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d06ed389e45a7ca97b17c275dbedd3d6524560270e675c720e93a2018a766076", size = 7931353, upload-time = "2025-08-06T13:05:32.273Z" }, + { url = "https://files.pythonhosted.org/packages/3d/10/6f30c05d251cf03ddc2b9fd19880f3cab8c193255e733444a2df03b18944/curl_cffi-0.13.0-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b4e0de45ab3b7a835c72bd53640c2347415111b43421b5c7a1a0b18deae2e541", size = 7486378, upload-time = "2025-08-06T13:05:33.672Z" }, + { url = "https://files.pythonhosted.org/packages/77/81/5bdb7dd0d669a817397b2e92193559bf66c3807f5848a48ad10cf02bf6c7/curl_cffi-0.13.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8eb4083371bbb94e9470d782de235fb5268bf43520de020c9e5e6be8f395443f", size = 8328585, upload-time = "2025-08-06T13:05:35.28Z" }, + { url = "https://files.pythonhosted.org/packages/ce/c1/df5c6b4cfad41c08442e0f727e449f4fb5a05f8aa564d1acac29062e9e8e/curl_cffi-0.13.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:28911b526e8cd4aa0e5e38401bfe6887e8093907272f1f67ca22e6beb2933a51", size = 8739831, upload-time = "2025-08-06T13:05:37.078Z" }, + { url = "https://files.pythonhosted.org/packages/1a/91/6dd1910a212f2e8eafe57877bcf97748eb24849e1511a266687546066b8a/curl_cffi-0.13.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6d433ffcb455ab01dd0d7bde47109083aa38b59863aa183d29c668ae4c96bf8e", size = 8711908, upload-time = "2025-08-06T13:05:38.741Z" }, + { url = "https://files.pythonhosted.org/packages/6d/e4/15a253f9b4bf8d008c31e176c162d2704a7e0c5e24d35942f759df107b68/curl_cffi-0.13.0-cp39-abi3-win_amd64.whl", hash = "sha256:66a6b75ce971de9af64f1b6812e275f60b88880577bac47ef1fa19694fa21cd3", size = 1614510, upload-time = "2025-08-06T13:05:40.451Z" }, + { url = "https://files.pythonhosted.org/packages/f9/0f/9c5275f17ad6ff5be70edb8e0120fdc184a658c9577ca426d4230f654beb/curl_cffi-0.13.0-cp39-abi3-win_arm64.whl", hash = "sha256:d438a3b45244e874794bc4081dc1e356d2bb926dcc7021e5a8fef2e2105ef1d8", size = 1365753, upload-time = "2025-08-06T13:05:41.879Z" }, +] + [[package]] name = "dateparser" version = "1.2.2" @@ -428,6 +449,16 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/42/14/42b2651a2f46b022ccd948bca9f2d5af0fd8929c4eec235b8d6d844fbe67/filelock-3.19.1-py3-none-any.whl", hash = "sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d", size = 15988, upload-time = "2025-08-14T16:56:01.633Z" }, ] +[[package]] +name = "frozendict" +version = "2.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/59/19eb300ba28e7547538bdf603f1c6c34793240a90e1a7b61b65d8517e35e/frozendict-2.4.6.tar.gz", hash = "sha256:df7cd16470fbd26fc4969a208efadc46319334eb97def1ddf48919b351192b8e", size = 316416, upload-time = "2024-10-13T12:15:32.449Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/13/d9839089b900fa7b479cce495d62110cddc4bd5630a04d8469916c0e79c5/frozendict-2.4.6-py311-none-any.whl", hash = "sha256:d065db6a44db2e2375c23eac816f1a022feb2fa98cbb50df44a9e83700accbea", size = 16148, upload-time = "2024-10-13T12:15:26.839Z" }, + { url = "https://files.pythonhosted.org/packages/ba/d0/d482c39cee2ab2978a892558cf130681d4574ea208e162da8958b31e9250/frozendict-2.4.6-py312-none-any.whl", hash = "sha256:49344abe90fb75f0f9fdefe6d4ef6d4894e640fadab71f11009d52ad97f370b9", size = 16146, upload-time = "2024-10-13T12:15:28.16Z" }, +] + [[package]] name = "frozenlist" version = "1.7.0" @@ -933,6 +964,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/28/30/8114832daff7489f179971dbc1d854109b7f4365a546e3ea75b6516cea95/pandas-2.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:8c13b81a9347eb8c7548f53fd9a4f08d4dfe996836543f805c987bafa03317ae", size = 10983326, upload-time = "2025-08-21T10:27:31.901Z" }, ] +[[package]] +name = "peewee" +version = "3.18.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/89/76f6f1b744c8608e0d416b588b9d63c2a500ff800065ae610f7c80f532d6/peewee-3.18.2.tar.gz", hash = "sha256:77a54263eb61aff2ea72f63d2eeb91b140c25c1884148e28e4c0f7c4f64996a0", size = 949220, upload-time = "2025-07-08T12:52:03.941Z" } + [[package]] name = "pillow" version = "11.3.0" @@ -952,6 +989,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/16/8f/b13447d1bf0b1f7467ce7d86f6e6edf66c0ad7cf44cf5c87a37f9bed9936/pillow-11.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542", size = 2423067, upload-time = "2025-07-01T09:14:33.709Z" }, ] +[[package]] +name = "platformdirs" +version = "4.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/23/e8/21db9c9987b0e728855bd57bff6984f67952bea55d6f75e055c46b5383e8/platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf", size = 21634, upload-time = "2025-08-26T14:32:04.268Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", size = 18654, upload-time = "2025-08-26T14:32:02.735Z" }, +] + [[package]] name = "pluggy" version = "1.6.0" @@ -1028,6 +1074,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cc/35/cc0aaecf278bb4575b8555f2b137de5ab821595ddae9da9d3cd1da4072c7/propcache-0.3.2-py3-none-any.whl", hash = "sha256:98f1ec44fb675f5052cccc8e609c46ed23a35a1cfd18545ad4e29002d858a43f", size = 12663, upload-time = "2025-06-09T22:56:04.484Z" }, ] +[[package]] +name = "protobuf" +version = "6.32.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/a4/cc17347aa2897568beece2e674674359f911d6fe21b0b8d6268cd42727ac/protobuf-6.32.1.tar.gz", hash = "sha256:ee2469e4a021474ab9baafea6cd070e5bf27c7d29433504ddea1a4ee5850f68d", size = 440635, upload-time = "2025-09-11T21:38:42.935Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/98/645183ea03ab3995d29086b8bf4f7562ebd3d10c9a4b14ee3f20d47cfe50/protobuf-6.32.1-cp310-abi3-win32.whl", hash = "sha256:a8a32a84bc9f2aad712041b8b366190f71dde248926da517bde9e832e4412085", size = 424411, upload-time = "2025-09-11T21:38:27.427Z" }, + { url = "https://files.pythonhosted.org/packages/8c/f3/6f58f841f6ebafe076cebeae33fc336e900619d34b1c93e4b5c97a81fdfa/protobuf-6.32.1-cp310-abi3-win_amd64.whl", hash = "sha256:b00a7d8c25fa471f16bc8153d0e53d6c9e827f0953f3c09aaa4331c718cae5e1", size = 435738, upload-time = "2025-09-11T21:38:30.959Z" }, + { url = "https://files.pythonhosted.org/packages/10/56/a8a3f4e7190837139e68c7002ec749190a163af3e330f65d90309145a210/protobuf-6.32.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d8c7e6eb619ffdf105ee4ab76af5a68b60a9d0f66da3ea12d1640e6d8dab7281", size = 426454, upload-time = "2025-09-11T21:38:34.076Z" }, + { url = "https://files.pythonhosted.org/packages/3f/be/8dd0a927c559b37d7a6c8ab79034fd167dcc1f851595f2e641ad62be8643/protobuf-6.32.1-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:2f5b80a49e1eb7b86d85fcd23fe92df154b9730a725c3b38c4e43b9d77018bf4", size = 322874, upload-time = "2025-09-11T21:38:35.509Z" }, + { url = "https://files.pythonhosted.org/packages/5c/f6/88d77011b605ef979aace37b7703e4eefad066f7e84d935e5a696515c2dd/protobuf-6.32.1-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:b1864818300c297265c83a4982fd3169f97122c299f56a56e2445c3698d34710", size = 322013, upload-time = "2025-09-11T21:38:37.017Z" }, + { url = "https://files.pythonhosted.org/packages/97/b7/15cc7d93443d6c6a84626ae3258a91f4c6ac8c0edd5df35ea7658f71b79c/protobuf-6.32.1-py3-none-any.whl", hash = "sha256:2601b779fc7d32a866c6b4404f9d42a3f67c5b9f3f15b4db3cccabe06b95c346", size = 169289, upload-time = "2025-09-11T21:38:41.234Z" }, +] + [[package]] name = "pyasn1" version = "0.6.1" @@ -1545,6 +1605,7 @@ dependencies = [ { name = "praw" }, { name = "pytest" }, { name = "python-binance" }, + { name = "yfinance" }, ] [package.metadata] @@ -1561,6 +1622,7 @@ requires-dist = [ { name = "praw" }, { name = "pytest" }, { name = "python-binance" }, + { name = "yfinance" }, ] [[package]] @@ -1644,3 +1706,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/eb/83/5d9092950565481b413b31a23e75dd3418ff0a277d6e0abf3729d4d1ce25/yarl-1.20.1-cp312-cp312-win_amd64.whl", hash = "sha256:48ea7d7f9be0487339828a4de0360d7ce0efc06524a48e1810f945c45b813698", size = 86710, upload-time = "2025-06-10T00:44:16.716Z" }, { url = "https://files.pythonhosted.org/packages/b4/2d/2345fce04cfd4bee161bf1e7d9cdc702e3e16109021035dbb24db654a622/yarl-1.20.1-py3-none-any.whl", hash = "sha256:83b8eb083fe4683c6115795d9fc1cfaf2cbbefb19b3a1cb68f6527460f483a77", size = 46542, upload-time = "2025-06-10T00:46:07.521Z" }, ] + +[[package]] +name = "yfinance" +version = "0.2.66" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "curl-cffi" }, + { name = "frozendict" }, + { name = "multitasking" }, + { name = "numpy" }, + { name = "pandas" }, + { name = "peewee" }, + { name = "platformdirs" }, + { name = "protobuf" }, + { name = "pytz" }, + { name = "requests" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/59/73/50450b9906c5137d2d02fde6f7360865366c72baea1f8d0550cc990829ce/yfinance-0.2.66.tar.gz", hash = "sha256:fae354cc1649109444b2c84194724afcc52c2a7799551ce44c739424ded6af9c", size = 132820, upload-time = "2025-09-17T11:22:35.422Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/bf/7c0c89ff8ba53592b9cb5157f70e90d8bbb04d60094fc4f10035e158b981/yfinance-0.2.66-py2.py3-none-any.whl", hash = "sha256:511a1a40a687f277aae3a02543009a8aeaa292fce5509671f58915078aebb5c7", size = 123427, upload-time = "2025-09-17T11:22:33.972Z" }, +]