refactor: remove status attribute from ProductInfo and update related methods to use timestamp_ms

This commit is contained in:
2025-10-01 23:38:50 +02:00
parent 1c884b67dd
commit 59a38c6e32
13 changed files with 84 additions and 94 deletions

View File

@@ -154,7 +154,7 @@ class ProviderTester:
if product: if product:
print(f"📦 Product: {product.symbol} (ID: {product.id})") print(f"📦 Product: {product.symbol} (ID: {product.id})")
print(f" Price: ${product.price:.2f}, Quote: {product.quote_currency}") 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: else:
print(f"📦 Product: Nessun prodotto trovato per {symbol}") print(f"📦 Product: Nessun prodotto trovato per {symbol}")

View File

@@ -46,7 +46,6 @@ class ProductInfo(BaseModel):
symbol: str = "" symbol: str = ""
price: float = 0.0 price: float = 0.0
volume_24h: float = 0.0 volume_24h: float = 0.0
status: str = ""
quote_currency: str = "" quote_currency: str = ""
class Price(BaseModel): class Price(BaseModel):
@@ -59,4 +58,4 @@ class Price(BaseModel):
open: float = 0.0 open: float = 0.0
close: float = 0.0 close: float = 0.0
volume: float = 0.0 volume: float = 0.0
time: str = "" timestamp_ms: int = 0 # Timestamp in milliseconds

View File

@@ -3,16 +3,25 @@ from datetime import datetime
from binance.client import Client from binance.client import Client
from .base import ProductInfo, BaseWrapper, Price 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 = ProductInfo()
product.id = ticker_data.get('symbol') product.id = ticker_data.get('symbol')
product.symbol = ticker_data.get('symbol', '').replace(currency, '') product.symbol = ticker_data.get('symbol', '').replace(currency, '')
product.price = float(ticker_data.get('price', 0)) product.price = float(ticker_data.get('price', 0))
product.volume_24h = float(ticker_data.get('volume', 0)) product.volume_24h = float(ticker_data.get('volume', 0))
product.status = "TRADING" # Binance non fornisce status esplicito
product.quote_currency = currency product.quote_currency = currency
return product 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): class BinanceWrapper(BaseWrapper):
""" """
Wrapper per le API autenticate di Binance.\n Wrapper per le API autenticate di Binance.\n
@@ -63,15 +72,5 @@ class BinanceWrapper(BaseWrapper):
interval=Client.KLINE_INTERVAL_1HOUR, interval=Client.KLINE_INTERVAL_1HOUR,
limit=limit, 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

View File

@@ -6,24 +6,22 @@ from coinbase.rest.types.product_types import Candle, GetProductResponse, Produc
from .base import ProductInfo, BaseWrapper, Price 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 = ProductInfo()
product.id = product_data.product_id or "" product.id = product_data.product_id or ""
product.symbol = product_data.base_currency_id or "" product.symbol = product_data.base_currency_id or ""
product.price = float(product_data.price) if product_data.price else 0.0 product.price = float(product_data.price) if product_data.price else 0.0
product.volume_24h = float(product_data.volume_24h) if product_data.volume_24h else 0.0 product.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 return product
def get_price(candle_data: Candle) -> 'Price': def get_price(candle_data: Candle) -> Price:
price = Price() price = Price()
price.high = float(candle_data.high) if candle_data.high else 0.0 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.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.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.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.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 return price

View File

@@ -1,26 +1,26 @@
import os import os
import requests import requests
from typing import Optional, Dict, Any
from .base import ProductInfo, BaseWrapper, Price from .base import ProductInfo, BaseWrapper, Price
def get_product(asset_data: dict) -> 'ProductInfo': def get_product(asset_data: dict) -> ProductInfo:
product = ProductInfo() product = ProductInfo()
product.id = asset_data['FROMSYMBOL'] + '-' + asset_data['TOSYMBOL'] product.id = asset_data.get('FROMSYMBOL', '') + '-' + asset_data.get('TOSYMBOL', '')
product.symbol = asset_data['FROMSYMBOL'] product.symbol = asset_data.get('FROMSYMBOL', '')
product.price = float(asset_data['PRICE']) product.price = float(asset_data.get('PRICE', 0))
product.volume_24h = float(asset_data['VOLUME24HOUR']) product.volume_24h = float(asset_data.get('VOLUME24HOUR', 0))
product.status = "" # Cryptocompare does not provide status assert product.price > 0, "Invalid price data received from CryptoCompare"
return product return product
def get_price(price_data: dict) -> 'Price': def get_price(price_data: dict) -> Price:
price = Price() price = Price()
price.high = float(price_data['high']) price.high = float(price_data.get('high', 0))
price.low = float(price_data['low']) price.low = float(price_data.get('low', 0))
price.open = float(price_data['open']) price.open = float(price_data.get('open', 0))
price.close = float(price_data['close']) price.close = float(price_data.get('close', 0))
price.volume = float(price_data['volumeto']) price.volume = float(price_data.get('volumeto', 0))
price.time = str(price_data['time']) price.timestamp_ms = price_data.get('time', 0) * 1000
assert price.timestamp_ms > 0, "Invalid timestamp data received from CryptoCompare"
return price return price
@@ -39,7 +39,7 @@ class CryptoCompareWrapper(BaseWrapper):
self.api_key = api_key self.api_key = api_key
self.currency = currency self.currency = currency
def __request(self, endpoint: str, params: 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: if params is None:
params = {} params = {}
params['api_key'] = self.api_key params['api_key'] = self.api_key
@@ -67,7 +67,7 @@ class CryptoCompareWrapper(BaseWrapper):
assets.append(get_product(asset_data)) assets.append(get_product(asset_data))
return assets 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 = { response = self.__request("/data/v2/histohour", params = {
"fsym": asset_id, "fsym": asset_id,
"tsym": self.currency, "tsym": self.currency,

View File

@@ -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.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.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.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 '-' product.quote_currency = product.id.split('-')[0] # La valuta è la parte dopo il '-'
return product 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.open = float(hist_data.get('Open', 0.0))
price.close = float(hist_data.get('Close', 0.0)) price.close = float(hist_data.get('Close', 0.0))
price.volume = float(hist_data.get('Volume', 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 return price

View File

@@ -2,7 +2,7 @@ import statistics
from app.markets.base import ProductInfo, Price 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""" """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.") 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 # 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.symbol = symbol
product.quote_currency = next(p.quote_currency for p in product_list if p.quote_currency) 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] prices = [p.price for p in product_list]
product.price = statistics.mean(prices) product.price = statistics.mean(prices)

View File

@@ -45,8 +45,9 @@ class TestBinance:
assert isinstance(history, list) assert isinstance(history, list)
assert len(history) == 5 assert len(history) == 5
for entry in history: for entry in history:
assert hasattr(entry, 'time') assert hasattr(entry, 'timestamp_ms')
assert hasattr(entry, 'close') assert hasattr(entry, 'close')
assert hasattr(entry, 'high') assert hasattr(entry, 'high')
assert entry.close > 0 assert entry.close > 0
assert entry.high > 0 assert entry.high > 0
assert entry.timestamp_ms > 0

View File

@@ -47,8 +47,9 @@ class TestCoinBase:
assert isinstance(history, list) assert isinstance(history, list)
assert len(history) == 5 assert len(history) == 5
for entry in history: for entry in history:
assert hasattr(entry, 'time') assert hasattr(entry, 'timestamp_ms')
assert hasattr(entry, 'close') assert hasattr(entry, 'close')
assert hasattr(entry, 'high') assert hasattr(entry, 'high')
assert entry.close > 0 assert entry.close > 0
assert entry.high > 0 assert entry.high > 0
assert entry.timestamp_ms > 0

View File

@@ -49,8 +49,9 @@ class TestCryptoCompare:
assert isinstance(history, list) assert isinstance(history, list)
assert len(history) == 5 assert len(history) == 5
for entry in history: for entry in history:
assert hasattr(entry, 'time') assert hasattr(entry, 'timestamp_ms')
assert hasattr(entry, 'close') assert hasattr(entry, 'close')
assert hasattr(entry, 'high') assert hasattr(entry, 'high')
assert entry.close > 0 assert entry.close > 0
assert entry.high > 0 assert entry.high > 0
assert entry.timestamp_ms > 0

View File

@@ -5,7 +5,7 @@ from app.social.reddit import MAX_COMMENTS, RedditWrapper
@pytest.mark.social @pytest.mark.social
@pytest.mark.api @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: class TestRedditWrapper:
def test_initialization(self): def test_initialization(self):
wrapper = RedditWrapper() wrapper = RedditWrapper()

View File

@@ -43,13 +43,14 @@ class TestYFinance:
def test_yfinance_crypto_history(self): def test_yfinance_crypto_history(self):
market = YFinanceWrapper() market = YFinanceWrapper()
history = market.get_historical_prices("BTC", limit=3) history = market.get_historical_prices("BTC", limit=5)
assert history is not None assert history is not None
assert isinstance(history, list) assert isinstance(history, list)
assert len(history) == 3 assert len(history) == 5
for entry in history: for entry in history:
assert hasattr(entry, 'time') assert hasattr(entry, 'timestamp_ms')
assert hasattr(entry, 'close') assert hasattr(entry, 'close')
assert hasattr(entry, 'high')
assert entry.close > 0 assert entry.close > 0
assert hasattr(entry, 'open') assert entry.high > 0
assert entry.open > 0 assert entry.timestamp_ms > 0

View File

@@ -7,21 +7,30 @@ from app.utils.market_aggregation import aggregate_history_prices, aggregate_pro
@pytest.mark.market @pytest.mark.market
class TestMarketDataAggregator: 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 = ProductInfo()
prod.id=f"{symbol}-{currency}" prod.id=f"{symbol}-{currency}"
prod.symbol=symbol prod.symbol=symbol
prod.price=price prod.price=price
prod.volume_24h=volume prod.volume_24h=volume
prod.status=status
prod.quote_currency=currency prod.quote_currency=currency
return prod 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): def test_aggregate_product_info(self):
products: dict[str, list[ProductInfo]] = { products: dict[str, list[ProductInfo]] = {
"Provider1": [self.__product("BTC", 50000.0, 1000.0, "active", "USD")], "Provider1": [self.__product("BTC", 50000.0, 1000.0, "USD")],
"Provider2": [self.__product("BTC", 50100.0, 1100.0, "active", "USD")], "Provider2": [self.__product("BTC", 50100.0, 1100.0, "USD")],
"Provider3": [self.__product("BTC", 49900.0, 900.0, "inactive", "USD")], "Provider3": [self.__product("BTC", 49900.0, 900.0, "USD")],
} }
aggregated = aggregate_product_info(products) 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) 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.volume_24h == pytest.approx(avg_weighted_volume, rel=1e-3)
assert info.status == "active"
assert info.quote_currency == "USD" assert info.quote_currency == "USD"
def test_aggregate_product_info_multiple_symbols(self): def test_aggregate_product_info_multiple_symbols(self):
products = { products = {
"Provider1": [ "Provider1": [
self.__product("BTC", 50000.0, 1000.0, "active", "USD"), self.__product("BTC", 50000.0, 1000.0, "USD"),
self.__product("ETH", 4000.0, 2000.0, "active", "USD"), self.__product("ETH", 4000.0, 2000.0, "USD"),
], ],
"Provider2": [ "Provider2": [
self.__product("BTC", 50100.0, 1100.0, "active", "USD"), self.__product("BTC", 50100.0, 1100.0, "USD"),
self.__product("ETH", 4050.0, 2100.0, "active", "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) 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) 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.volume_24h == pytest.approx(avg_weighted_volume_btc, rel=1e-3)
assert btc_info.status == "active"
assert btc_info.quote_currency == "USD" assert btc_info.quote_currency == "USD"
assert eth_info is not None assert eth_info is not None
assert eth_info.price == pytest.approx(4025.0, rel=1e-3) 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) 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.volume_24h == pytest.approx(avg_weighted_volume_eth, rel=1e-3)
assert eth_info.status == "active"
assert eth_info.quote_currency == "USD" assert eth_info.quote_currency == "USD"
def test_aggregate_history_prices(self): def test_aggregate_history_prices(self):
"""Test aggregazione di prezzi storici usando aggregate_history_prices""" """Test aggregazione di prezzi storici usando aggregate_history_prices"""
price1 = Price( prices = {
timestamp="2024-06-01T00:00:00Z", "Provider1": [
price=50000.0, self.__price(1685577600000, 50000.0, 49500.0, 49600.0, 49900.0, 150.0),
source="exchange1" self.__price(1685581200000, 50200.0, 49800.0, 50000.0, 50100.0, 200.0),
) ],
price2 = Price( "Provider2": [
timestamp="2024-06-01T00:00:00Z", self.__price(1685577600000, 50100.0, 49600.0, 49700.0, 50000.0, 180.0),
price=50100.0, self.__price(1685581200000, 50300.0, 49900.0, 50100.0, 50200.0, 220.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 = [price1, price2, price3, price4] aggregated = aggregate_history_prices(prices)
aggregated_prices = aggregate_history_prices(prices) assert len(aggregated) == 2
assert aggregated[0].timestamp_ms == 1685577600000
assert len(aggregated_prices) == 2 assert aggregated[0].high == pytest.approx(50050.0, rel=1e-3)
assert aggregated_prices[0].timestamp == "2024-06-01T00:00:00Z" assert aggregated[0].low == pytest.approx(49500.0, rel=1e-3)
assert aggregated_prices[0].price == pytest.approx(50050.0, rel=1e-3) assert aggregated[1].timestamp_ms == 1685581200000
assert aggregated_prices[1].timestamp == "2024-06-01T01:00:00Z" assert aggregated[1].high == pytest.approx(50250.0, rel=1e-3)
assert aggregated_prices[1].price == pytest.approx(50250.0, rel=1e-3) assert aggregated[1].low == pytest.approx(49800.0, rel=1e-3)