diff --git a/demos/market_providers_api_demo.py b/demos/market_providers_api_demo.py index 393a478..fc05c26 100644 --- a/demos/market_providers_api_demo.py +++ b/demos/market_providers_api_demo.py @@ -154,7 +154,7 @@ class ProviderTester: if product: print(f"📦 Product: {product.symbol} (ID: {product.id})") print(f" Price: ${product.price:.2f}, Quote: {product.quote_currency}") - print(f" Status: {product.status}, Volume 24h: {product.volume_24h:,.2f}") + print(f" Volume 24h: {product.volume_24h:,.2f}") else: print(f"📦 Product: Nessun prodotto trovato per {symbol}") diff --git a/src/app/markets/base.py b/src/app/markets/base.py index 12892bb..4224014 100644 --- a/src/app/markets/base.py +++ b/src/app/markets/base.py @@ -46,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): @@ -59,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 diff --git a/src/app/markets/binance.py b/src/app/markets/binance.py index 2eb85f0..8e941c8 100644 --- a/src/app/markets/binance.py +++ b/src/app/markets/binance.py @@ -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 @@ -63,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 diff --git a/src/app/markets/coinbase.py b/src/app/markets/coinbase.py index ea944e4..54409c1 100644 --- a/src/app/markets/coinbase.py +++ b/src/app/markets/coinbase.py @@ -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 diff --git a/src/app/markets/cryptocompare.py b/src/app/markets/cryptocompare.py index 2986b33..f4b96e9 100644 --- a/src/app/markets/cryptocompare.py +++ b/src/app/markets/cryptocompare.py @@ -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 @@ -39,7 +39,7 @@ class CryptoCompareWrapper(BaseWrapper): 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,7 +67,7 @@ class CryptoCompareWrapper(BaseWrapper): assets.append(get_product(asset_data)) return assets - 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, diff --git a/src/app/markets/yfinance.py b/src/app/markets/yfinance.py index 02af2f6..1b54e0a 100644 --- a/src/app/markets/yfinance.py +++ b/src/app/markets/yfinance.py @@ -12,7 +12,6 @@ def create_product_info(stock_data: dict[str, str]) -> ProductInfo: 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.status = "trading" if product.price > 0 else "offline" product.quote_currency = product.id.split('-')[0] # La valuta è la parte dopo il '-' return product @@ -26,7 +25,7 @@ def create_price_from_history(hist_data: dict[str, str]) -> Price: 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.time = hist_data.get('Timestamp', '') + price.timestamp_ms = int(hist_data.get('Timestamp', '0')) return price diff --git a/src/app/utils/market_aggregation.py b/src/app/utils/market_aggregation.py index 3d7b6b8..fb4f00a 100644 --- a/src/app/utils/market_aggregation.py +++ b/src/app/utils/market_aggregation.py @@ -2,7 +2,7 @@ import statistics from app.markets.base import ProductInfo, Price -def aggregate_history_prices(prices: dict[str, list[Price]]) -> list[float]: +def aggregate_history_prices(prices: dict[str, list[Price]]) -> list[Price]: """Aggrega i prezzi storici per symbol calcolando la media""" raise NotImplementedError("Funzione non ancora implementata per problemi di timestamp he deve essere uniformato prima di usare questa funzione.") # TODO implementare l'aggregazione dopo aver modificato la classe Price in modo che abbia un timestamp integer @@ -40,11 +40,6 @@ def aggregate_product_info(products: dict[str, list[ProductInfo]]) -> list[Produ product.symbol = symbol product.quote_currency = next(p.quote_currency for p in product_list if p.quote_currency) - statuses = {} - for p in product_list: - statuses[p.status] = statuses.get(p.status, 0) + 1 - product.status = max(statuses, key=statuses.get) if statuses else "" - prices = [p.price for p in product_list] product.price = statistics.mean(prices) diff --git a/tests/api/test_binance.py b/tests/api/test_binance.py index e4e0c20..dc4bfcb 100644 --- a/tests/api/test_binance.py +++ b/tests/api/test_binance.py @@ -45,8 +45,9 @@ class TestBinance: assert isinstance(history, list) assert len(history) == 5 for entry in history: - assert hasattr(entry, 'time') + assert hasattr(entry, 'timestamp_ms') assert hasattr(entry, 'close') assert hasattr(entry, 'high') assert entry.close > 0 assert entry.high > 0 + assert entry.timestamp_ms > 0 diff --git a/tests/api/test_coinbase.py b/tests/api/test_coinbase.py index b5f92e8..3ab8d43 100644 --- a/tests/api/test_coinbase.py +++ b/tests/api/test_coinbase.py @@ -47,8 +47,9 @@ class TestCoinBase: assert isinstance(history, list) assert len(history) == 5 for entry in history: - assert hasattr(entry, 'time') + assert hasattr(entry, 'timestamp_ms') assert hasattr(entry, 'close') assert hasattr(entry, 'high') assert entry.close > 0 assert entry.high > 0 + assert entry.timestamp_ms > 0 diff --git a/tests/api/test_cryptocompare.py b/tests/api/test_cryptocompare.py index 52aef9a..3c9133a 100644 --- a/tests/api/test_cryptocompare.py +++ b/tests/api/test_cryptocompare.py @@ -49,8 +49,9 @@ class TestCryptoCompare: assert isinstance(history, list) assert len(history) == 5 for entry in history: - assert hasattr(entry, 'time') + assert hasattr(entry, 'timestamp_ms') assert hasattr(entry, 'close') assert hasattr(entry, 'high') assert entry.close > 0 assert entry.high > 0 + assert entry.timestamp_ms > 0 diff --git a/tests/api/test_reddit.py b/tests/api/test_reddit.py index c82f56e..59cd61f 100644 --- a/tests/api/test_reddit.py +++ b/tests/api/test_reddit.py @@ -5,7 +5,7 @@ from app.social.reddit import MAX_COMMENTS, RedditWrapper @pytest.mark.social @pytest.mark.api -@pytest.mark.skipif(not(os.getenv("REDDIT_CLIENT_ID")) or not(os.getenv("REDDIT_API_CLIENT_ID")) or not os.getenv("REDDIT_API_CLIENT_SECRET"), reason="REDDIT_CLIENT_ID and REDDIT_API_CLIENT_SECRET not set in environment variables") +@pytest.mark.skipif(not(os.getenv("REDDIT_API_CLIENT_ID")) or not os.getenv("REDDIT_API_CLIENT_SECRET"), reason="REDDIT_CLIENT_ID and REDDIT_API_CLIENT_SECRET not set in environment variables") class TestRedditWrapper: def test_initialization(self): wrapper = RedditWrapper() diff --git a/tests/api/test_yfinance.py b/tests/api/test_yfinance.py index 8c70dfd..4971ccd 100644 --- a/tests/api/test_yfinance.py +++ b/tests/api/test_yfinance.py @@ -43,13 +43,14 @@ class TestYFinance: def test_yfinance_crypto_history(self): market = YFinanceWrapper() - history = market.get_historical_prices("BTC", limit=3) + history = market.get_historical_prices("BTC", limit=5) assert history is not None assert isinstance(history, list) - assert len(history) == 3 + assert len(history) == 5 for entry in history: - assert hasattr(entry, 'time') + assert hasattr(entry, 'timestamp_ms') assert hasattr(entry, 'close') + assert hasattr(entry, 'high') assert entry.close > 0 - assert hasattr(entry, 'open') - assert entry.open > 0 \ No newline at end of file + assert entry.high > 0 + assert entry.timestamp_ms > 0 diff --git a/tests/utils/test_market_aggregator.py b/tests/utils/test_market_aggregator.py index 57ef4a1..8f075e3 100644 --- a/tests/utils/test_market_aggregator.py +++ b/tests/utils/test_market_aggregator.py @@ -7,21 +7,30 @@ from app.utils.market_aggregation import aggregate_history_prices, aggregate_pro @pytest.mark.market class TestMarketDataAggregator: - def __product(self, symbol: str, price: float, volume: float, status: str, currency: str) -> ProductInfo: + def __product(self, symbol: str, price: float, volume: float, currency: str) -> ProductInfo: prod = ProductInfo() prod.id=f"{symbol}-{currency}" prod.symbol=symbol prod.price=price prod.volume_24h=volume - prod.status=status prod.quote_currency=currency return prod + def __price(self, timestamp_ms: int, high: float, low: float, open: float, close: float, volume: float) -> Price: + price = Price() + price.timestamp_ms = timestamp_ms + price.high = high + price.low = low + price.open = open + price.close = close + price.volume = volume + return price + def test_aggregate_product_info(self): products: dict[str, list[ProductInfo]] = { - "Provider1": [self.__product("BTC", 50000.0, 1000.0, "active", "USD")], - "Provider2": [self.__product("BTC", 50100.0, 1100.0, "active", "USD")], - "Provider3": [self.__product("BTC", 49900.0, 900.0, "inactive", "USD")], + "Provider1": [self.__product("BTC", 50000.0, 1000.0, "USD")], + "Provider2": [self.__product("BTC", 50100.0, 1100.0, "USD")], + "Provider3": [self.__product("BTC", 49900.0, 900.0, "USD")], } aggregated = aggregate_product_info(products) @@ -35,18 +44,17 @@ class TestMarketDataAggregator: avg_weighted_volume = (50000.0 * 1000.0 + 50100.0 * 1100.0 + 49900.0 * 900.0) / (1000.0 + 1100.0 + 900.0) assert info.volume_24h == pytest.approx(avg_weighted_volume, rel=1e-3) - assert info.status == "active" assert info.quote_currency == "USD" def test_aggregate_product_info_multiple_symbols(self): products = { "Provider1": [ - self.__product("BTC", 50000.0, 1000.0, "active", "USD"), - self.__product("ETH", 4000.0, 2000.0, "active", "USD"), + self.__product("BTC", 50000.0, 1000.0, "USD"), + self.__product("ETH", 4000.0, 2000.0, "USD"), ], "Provider2": [ - self.__product("BTC", 50100.0, 1100.0, "active", "USD"), - self.__product("ETH", 4050.0, 2100.0, "active", "USD"), + self.__product("BTC", 50100.0, 1100.0, "USD"), + self.__product("ETH", 4050.0, 2100.0, "USD"), ], } @@ -60,45 +68,33 @@ class TestMarketDataAggregator: assert btc_info.price == pytest.approx(50050.0, rel=1e-3) avg_weighted_volume_btc = (50000.0 * 1000.0 + 50100.0 * 1100.0) / (1000.0 + 1100.0) assert btc_info.volume_24h == pytest.approx(avg_weighted_volume_btc, rel=1e-3) - assert btc_info.status == "active" assert btc_info.quote_currency == "USD" assert eth_info is not None assert eth_info.price == pytest.approx(4025.0, rel=1e-3) avg_weighted_volume_eth = (4000.0 * 2000.0 + 4050.0 * 2100.0) / (2000.0 + 2100.0) assert eth_info.volume_24h == pytest.approx(avg_weighted_volume_eth, rel=1e-3) - assert eth_info.status == "active" assert eth_info.quote_currency == "USD" def test_aggregate_history_prices(self): """Test aggregazione di prezzi storici usando aggregate_history_prices""" - price1 = Price( - timestamp="2024-06-01T00:00:00Z", - price=50000.0, - source="exchange1" - ) - price2 = Price( - timestamp="2024-06-01T00:00:00Z", - price=50100.0, - source="exchange2" - ) - price3 = Price( - timestamp="2024-06-01T01:00:00Z", - price=50200.0, - source="exchange1" - ) - price4 = Price( - timestamp="2024-06-01T01:00:00Z", - price=50300.0, - source="exchange2" - ) + prices = { + "Provider1": [ + self.__price(1685577600000, 50000.0, 49500.0, 49600.0, 49900.0, 150.0), + self.__price(1685581200000, 50200.0, 49800.0, 50000.0, 50100.0, 200.0), + ], + "Provider2": [ + self.__price(1685577600000, 50100.0, 49600.0, 49700.0, 50000.0, 180.0), + self.__price(1685581200000, 50300.0, 49900.0, 50100.0, 50200.0, 220.0), + ], + } - prices = [price1, price2, price3, price4] - aggregated_prices = aggregate_history_prices(prices) - - assert len(aggregated_prices) == 2 - assert aggregated_prices[0].timestamp == "2024-06-01T00:00:00Z" - assert aggregated_prices[0].price == pytest.approx(50050.0, rel=1e-3) - assert aggregated_prices[1].timestamp == "2024-06-01T01:00:00Z" - assert aggregated_prices[1].price == pytest.approx(50250.0, rel=1e-3) + aggregated = aggregate_history_prices(prices) + assert len(aggregated) == 2 + assert aggregated[0].timestamp_ms == 1685577600000 + assert aggregated[0].high == pytest.approx(50050.0, rel=1e-3) + assert aggregated[0].low == pytest.approx(49500.0, rel=1e-3) + assert aggregated[1].timestamp_ms == 1685581200000 + assert aggregated[1].high == pytest.approx(50250.0, rel=1e-3) + assert aggregated[1].low == pytest.approx(49800.0, rel=1e-3)