Rinominato 'quote_currency' in 'currency' e aggiornato il trattamento del timestamp in Price

This commit is contained in:
2025-10-05 19:06:39 +02:00
parent ac356c3753
commit f5816bb74f
12 changed files with 73 additions and 43 deletions

View File

@@ -1,3 +1,4 @@
from datetime import datetime
from pydantic import BaseModel from pydantic import BaseModel
@@ -10,7 +11,7 @@ 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
quote_currency: str = "" currency: str = ""
class Price(BaseModel): class Price(BaseModel):
""" """
@@ -22,7 +23,24 @@ 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
timestamp_ms: int = 0 # Timestamp in milliseconds timestamp: str = ""
"""Timestamp con formato YYYY-MM-DD HH:MM"""
def set_timestamp(self, timestamp_ms: int | None = None, timestamp_s: int | None = None) -> None:
"""
Imposta il timestamp in millisecondi.
Args:
timestamp (int | datetime): Il timestamp in millisecondi o come oggetto datetime.
"""
if timestamp_ms is not None:
timestamp = timestamp_ms // 1000
elif timestamp_s is not None:
timestamp = timestamp_s
else:
raise ValueError("Either timestamp_ms or timestamp_s must be provided")
assert timestamp > 0, "Invalid timestamp data received"
self.timestamp = datetime.fromtimestamp(timestamp).strftime('%Y-%m-%d %H:%M')
class MarketWrapper: class MarketWrapper:
""" """

View File

@@ -10,17 +10,19 @@ def extract_product(currency: str, ticker_data: dict[str, Any]) -> ProductInfo:
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.quote_currency = currency product.currency = currency
return product return product
def extract_price(kline_data: list[Any]) -> Price: def extract_price(kline_data: list[Any]) -> Price:
timestamp = kline_data[0]
price = Price() price = Price()
price.open = float(kline_data[1]) price.open = float(kline_data[1])
price.high = float(kline_data[2]) price.high = float(kline_data[2])
price.low = float(kline_data[3]) price.low = float(kline_data[3])
price.close = float(kline_data[4]) price.close = float(kline_data[4])
price.volume = float(kline_data[5]) price.volume = float(kline_data[5])
price.timestamp_ms = kline_data[0] price.set_timestamp(timestamp_ms=timestamp)
return price return price
class BinanceWrapper(MarketWrapper): class BinanceWrapper(MarketWrapper):

View File

@@ -15,13 +15,15 @@ def extract_product(product_data: GetProductResponse | Product) -> ProductInfo:
return product return product
def extract_price(candle_data: Candle) -> Price: def extract_price(candle_data: Candle) -> Price:
timestamp = int(candle_data.start) if candle_data.start else 0
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.timestamp_ms = int(candle_data.start) * 1000 if candle_data.start else 0 price.set_timestamp(timestamp_s=timestamp)
return price return price

View File

@@ -14,14 +14,15 @@ def extract_product(asset_data: dict[str, Any]) -> ProductInfo:
return product return product
def extract_price(price_data: dict[str, Any]) -> Price: def extract_price(price_data: dict[str, Any]) -> Price:
timestamp = price_data.get('time', 0)
price = Price() price = Price()
price.high = float(price_data.get('high', 0)) price.high = float(price_data.get('high', 0))
price.low = float(price_data.get('low', 0)) price.low = float(price_data.get('low', 0))
price.open = float(price_data.get('open', 0)) price.open = float(price_data.get('open', 0))
price.close = float(price_data.get('close', 0)) price.close = float(price_data.get('close', 0))
price.volume = float(price_data.get('volumeto', 0)) price.volume = float(price_data.get('volumeto', 0))
price.timestamp_ms = price_data.get('time', 0) * 1000 price.set_timestamp(timestamp_s=timestamp)
assert price.timestamp_ms > 0, "Invalid timestamp data received from CryptoCompare"
return price return price

View File

@@ -12,20 +12,22 @@ def extract_product(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.quote_currency = product.id.split('-')[1] # La valuta è la parte dopo il '-' product.currency = product.id.split('-')[1] # La valuta è la parte dopo il '-'
return product return product
def extract_price(hist_data: dict[str, str]) -> Price: def extract_price(hist_data: dict[str, str]) -> Price:
""" """
Converte i dati storici di YFinanceTools in Price. Converte i dati storici di YFinanceTools in Price.
""" """
timestamp = int(hist_data.get('Timestamp', '0'))
price = Price() price = Price()
price.high = float(hist_data.get('High', 0.0)) price.high = float(hist_data.get('High', 0.0))
price.low = float(hist_data.get('Low', 0.0)) price.low = float(hist_data.get('Low', 0.0))
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.timestamp_ms = int(hist_data.get('Timestamp', '0')) price.set_timestamp(timestamp_ms=timestamp)
return price return price

View File

@@ -4,25 +4,24 @@ from app.base.markets import ProductInfo, Price
def aggregate_history_prices(prices: dict[str, list[Price]]) -> list[Price]: def aggregate_history_prices(prices: dict[str, list[Price]]) -> list[Price]:
""" """
Aggrega i prezzi storici per symbol calcolando la media oraria. Aggrega i prezzi storici per symbol calcolando la media.
Args: Args:
prices (dict[str, list[Price]]): Mappa provider -> lista di Price prices (dict[str, list[Price]]): Mappa provider -> lista di Price
Returns: Returns:
list[Price]: Lista di Price aggregati per ora list[Price]: Lista di Price aggregati per timestamp
""" """
# Costruiamo una mappa timestamp_h -> lista di Price # Costruiamo una mappa timestamp -> lista di Price
timestamped_prices: dict[int, list[Price]] = {} timestamped_prices: dict[str, list[Price]] = {}
for _, price_list in prices.items(): for _, price_list in prices.items():
for price in price_list: for price in price_list:
time = price.timestamp_ms - (price.timestamp_ms % 3600000) # arrotonda all'ora (non dovrebbe essere necessario) timestamped_prices.setdefault(price.timestamp, []).append(price)
timestamped_prices.setdefault(time, []).append(price)
# Ora aggregiamo i prezzi per ogni ora # Ora aggregiamo i prezzi per ogni timestamp
aggregated_prices: list[Price] = [] aggregated_prices: list[Price] = []
for time, price_list in timestamped_prices.items(): for time, price_list in timestamped_prices.items():
price = Price() price = Price()
price.timestamp_ms = time price.timestamp = time
price.high = statistics.mean([p.high for p in price_list]) price.high = statistics.mean([p.high for p in price_list])
price.low = statistics.mean([p.low 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.open = statistics.mean([p.open for p in price_list])
@@ -53,7 +52,7 @@ def aggregate_product_info(products: dict[str, list[ProductInfo]]) -> list[Produ
product.id = f"{symbol}_AGGREGATED" product.id = f"{symbol}_AGGREGATED"
product.symbol = symbol product.symbol = symbol
product.quote_currency = next(p.quote_currency for p in product_list if p.quote_currency) product.currency = next(p.currency for p in product_list if p.currency)
volume_sum = sum(p.volume_24h for p in product_list) 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 product.volume_24h = volume_sum / len(product_list) if product_list else 0.0

View File

@@ -41,10 +41,6 @@ class TestPredictor:
inputs = self.inputs() inputs = self.inputs()
unified_checks(AppModels.GEMINI, inputs) unified_checks(AppModels.GEMINI, inputs)
def test_ollama_qwen_1b_model_output(self):
inputs = self.inputs()
unified_checks(AppModels.OLLAMA_QWEN_1B, inputs)
def test_ollama_qwen_4b_model_output(self): def test_ollama_qwen_4b_model_output(self):
inputs = self.inputs() inputs = self.inputs()
unified_checks(AppModels.OLLAMA_QWEN_4B, inputs) unified_checks(AppModels.OLLAMA_QWEN_4B, inputs)

View File

@@ -45,9 +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, 'timestamp_ms') assert hasattr(entry, 'timestamp')
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 assert entry.timestamp != ''

View File

@@ -47,9 +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, 'timestamp_ms') assert hasattr(entry, 'timestamp')
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 assert entry.timestamp != ''

View File

@@ -49,9 +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, 'timestamp_ms') assert hasattr(entry, 'timestamp')
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 assert entry.timestamp != ''

View File

@@ -48,9 +48,9 @@ class TestYFinance:
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, 'timestamp_ms') assert hasattr(entry, 'timestamp')
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 assert entry.timestamp != ''

View File

@@ -1,4 +1,5 @@
import pytest import pytest
from datetime import datetime
from app.base.markets import ProductInfo, Price from app.base.markets import ProductInfo, Price
from app.utils.market_aggregation import aggregate_history_prices, aggregate_product_info from app.utils.market_aggregation import aggregate_history_prices, aggregate_product_info
@@ -13,12 +14,12 @@ class TestMarketDataAggregator:
prod.symbol=symbol prod.symbol=symbol
prod.price=price prod.price=price
prod.volume_24h=volume prod.volume_24h=volume
prod.quote_currency=currency prod.currency=currency
return prod return prod
def __price(self, timestamp_ms: int, high: float, low: float, open: float, close: float, volume: float) -> Price: def __price(self, timestamp_s: int, high: float, low: float, open: float, close: float, volume: float) -> Price:
price = Price() price = Price()
price.timestamp_ms = timestamp_ms price.set_timestamp(timestamp_s=timestamp_s)
price.high = high price.high = high
price.low = low price.low = low
price.open = open price.open = open
@@ -43,7 +44,7 @@ class TestMarketDataAggregator:
avg_weighted_price = (50000.0 * 1000.0 + 50100.0 * 1100.0 + 49900.0 * 900.0) / (1000.0 + 1100.0 + 900.0) avg_weighted_price = (50000.0 * 1000.0 + 50100.0 * 1100.0 + 49900.0 * 900.0) / (1000.0 + 1100.0 + 900.0)
assert info.price == pytest.approx(avg_weighted_price, rel=1e-3) # type: ignore assert info.price == pytest.approx(avg_weighted_price, rel=1e-3) # type: ignore
assert info.volume_24h == pytest.approx(1000.0, rel=1e-3) # type: ignore assert info.volume_24h == pytest.approx(1000.0, rel=1e-3) # type: ignore
assert info.quote_currency == "USD" assert info.currency == "USD"
def test_aggregate_product_info_multiple_symbols(self): def test_aggregate_product_info_multiple_symbols(self):
products = { products = {
@@ -67,13 +68,13 @@ class TestMarketDataAggregator:
avg_weighted_price_btc = (50000.0 * 1000.0 + 50100.0 * 1100.0) / (1000.0 + 1100.0) avg_weighted_price_btc = (50000.0 * 1000.0 + 50100.0 * 1100.0) / (1000.0 + 1100.0)
assert btc_info.price == pytest.approx(avg_weighted_price_btc, rel=1e-3) # type: ignore assert btc_info.price == pytest.approx(avg_weighted_price_btc, rel=1e-3) # type: ignore
assert btc_info.volume_24h == pytest.approx(1050.0, rel=1e-3) # type: ignore assert btc_info.volume_24h == pytest.approx(1050.0, rel=1e-3) # type: ignore
assert btc_info.quote_currency == "USD" assert btc_info.currency == "USD"
assert eth_info is not None assert eth_info is not None
avg_weighted_price_eth = (4000.0 * 2000.0 + 4050.0 * 2100.0) / (2000.0 + 2100.0) avg_weighted_price_eth = (4000.0 * 2000.0 + 4050.0 * 2100.0) / (2000.0 + 2100.0)
assert eth_info.price == pytest.approx(avg_weighted_price_eth, rel=1e-3) # type: ignore assert eth_info.price == pytest.approx(avg_weighted_price_eth, rel=1e-3) # type: ignore
assert eth_info.volume_24h == pytest.approx(2050.0, rel=1e-3) # type: ignore assert eth_info.volume_24h == pytest.approx(2050.0, rel=1e-3) # type: ignore
assert eth_info.quote_currency == "USD" assert eth_info.currency == "USD"
def test_aggregate_product_info_with_no_data(self): def test_aggregate_product_info_with_no_data(self):
products: dict[str, list[ProductInfo]] = { products: dict[str, list[ProductInfo]] = {
@@ -94,27 +95,36 @@ class TestMarketDataAggregator:
assert info.symbol == "BTC" assert info.symbol == "BTC"
assert info.price == pytest.approx(50000.0, rel=1e-3) # type: ignore assert info.price == pytest.approx(50000.0, rel=1e-3) # type: ignore
assert info.volume_24h == pytest.approx(1000.0, rel=1e-3) # type: ignore assert info.volume_24h == pytest.approx(1000.0, rel=1e-3) # type: ignore
assert info.quote_currency == "USD" assert info.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"""
timestamp_now = datetime.now()
timestamp_1h_ago = int(timestamp_now.replace(hour=timestamp_now.hour - 1).timestamp())
timestamp_2h_ago = int(timestamp_now.replace(hour=timestamp_now.hour - 2).timestamp())
prices = { prices = {
"Provider1": [ "Provider1": [
self.__price(1685577600000, 50000.0, 49500.0, 49600.0, 49900.0, 150.0), self.__price(timestamp_1h_ago, 50000.0, 49500.0, 49600.0, 49900.0, 150.0),
self.__price(1685581200000, 50200.0, 49800.0, 50000.0, 50100.0, 200.0), self.__price(timestamp_2h_ago, 50200.0, 49800.0, 50000.0, 50100.0, 200.0),
], ],
"Provider2": [ "Provider2": [
self.__price(1685577600000, 50100.0, 49600.0, 49700.0, 50000.0, 180.0), self.__price(timestamp_1h_ago, 50100.0, 49600.0, 49700.0, 50000.0, 180.0),
self.__price(1685581200000, 50300.0, 49900.0, 50100.0, 50200.0, 220.0), self.__price(timestamp_2h_ago, 50300.0, 49900.0, 50100.0, 50200.0, 220.0),
], ],
} }
price = Price()
price.set_timestamp(timestamp_s=timestamp_1h_ago)
timestamp_1h_ago = price.timestamp
price.set_timestamp(timestamp_s=timestamp_2h_ago)
timestamp_2h_ago = price.timestamp
aggregated = aggregate_history_prices(prices) aggregated = aggregate_history_prices(prices)
assert len(aggregated) == 2 assert len(aggregated) == 2
assert aggregated[0].timestamp_ms == 1685577600000 assert aggregated[0].timestamp == timestamp_1h_ago
assert aggregated[0].high == pytest.approx(50050.0, rel=1e-3) # type: ignore assert aggregated[0].high == pytest.approx(50050.0, rel=1e-3) # type: ignore
assert aggregated[0].low == pytest.approx(49550.0, rel=1e-3) # type: ignore assert aggregated[0].low == pytest.approx(49550.0, rel=1e-3) # type: ignore
assert aggregated[1].timestamp_ms == 1685581200000 assert aggregated[1].timestamp == timestamp_2h_ago
assert aggregated[1].high == pytest.approx(50250.0, rel=1e-3) # type: ignore assert aggregated[1].high == pytest.approx(50250.0, rel=1e-3) # type: ignore
assert aggregated[1].low == pytest.approx(49850.0, rel=1e-3) # type: ignore assert aggregated[1].low == pytest.approx(49850.0, rel=1e-3) # type: ignore