Refactor Predictor and market data handling

- Added Predictor class with input preparation and instructions for financial strategy generation.
- Removed PredictorAgent class and integrated its functionality into the new Predictor module.
- Created a base market API wrapper and specific implementations for Coinbase and CryptoCompare.
- Introduced PublicBinanceAgent for fetching public prices from Binance.
- Refactored ToolAgent to utilize the new Predictor and market API wrappers for improved data handling and predictions.
- Updated models to streamline the selection of available LLM providers.
- Removed deprecated signer classes for Coinbase and CryptoCompare.
This commit is contained in:
2025-09-26 03:43:31 +02:00
parent 48502fc6c7
commit 148bff7cfd
18 changed files with 362 additions and 2324 deletions

View File

@@ -0,0 +1,29 @@
import os
from app.markets.base import BaseWrapper
from app.markets.coinbase import CoinBaseWrapper
from app.markets.cryptocompare import CryptoCompareWrapper
# TODO Dare la priorità in base alla qualità del servizio
# TODO Aggiungere altri wrapper se necessario
def get_first_available_market_api(currency:str = "USD") -> BaseWrapper:
"""
Restituisce il primo wrapper disponibile in base alle configurazioni del file .env e alle chiavi API presenti.
La priorità è data a Coinbase, poi a CryptoCompare.
Se non sono presenti chiavi API, restituisce una eccezione.
:param currency: Valuta di riferimento (default "USD")
:return: Lista di istanze di wrapper
"""
wrappers = []
api_key = os.getenv("COINBASE_API_KEY")
api_secret = os.getenv("COINBASE_API_SECRET")
if api_key and api_secret:
wrappers.append(CoinBaseWrapper(api_key=api_key, api_private_key=api_secret, currency=currency))
api_key = os.getenv("CRYPTOCOMPARE_API_KEY")
if api_key:
wrappers.append(CryptoCompareWrapper(api_key=api_key, currency=currency))
assert wrappers, "No valid API keys set in environment variables."
return wrappers[0]

67
src/app/markets/base.py Normal file
View File

@@ -0,0 +1,67 @@
from coinbase.rest.types.product_types import Candle, GetProductResponse
class BaseWrapper:
def get_product(self, asset_id: str) -> 'ProductInfo':
raise NotImplementedError
def get_products(self, asset_ids: list[str]) -> list['ProductInfo']:
raise NotImplementedError
def get_all_products(self) -> list['ProductInfo']:
raise NotImplementedError
def get_historical_prices(self, asset_id: str = "BTC") -> list['Price']:
raise NotImplementedError
class ProductInfo:
id: str
symbol: str
price: float
volume_24h: float
status: str
quote_currency: str
def from_coinbase(product_data: GetProductResponse) -> 'ProductInfo':
product = ProductInfo()
product.id = product_data.product_id
product.symbol = product_data.base_currency_id
product.price = float(product_data.price)
product.volume_24h = float(product_data.volume_24h) if product_data.volume_24h else 0
# TODO Check what status means in Coinbase
product.status = product_data.status
return product
def from_cryptocompare(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
return product
class Price:
high: float
low: float
open: float
close: float
volume: float
time: str
def from_coinbase(candle_data: Candle) -> 'Price':
price = Price()
price.high = float(candle_data.high)
price.low = float(candle_data.low)
price.open = float(candle_data.open)
price.close = float(candle_data.close)
price.volume = float(candle_data.volume)
price.time = str(candle_data.start)
return price
def from_cryptocompare(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'])
return price

View File

@@ -0,0 +1,27 @@
# Versione pubblica senza autenticazione
from binance.client import Client
class PublicBinanceAgent:
def __init__(self):
# Client pubblico (senza credenziali)
self.client = Client()
def get_public_prices(self):
"""Ottiene prezzi pubblici"""
try:
btc_price = self.client.get_symbol_ticker(symbol="BTCUSDT")
eth_price = self.client.get_symbol_ticker(symbol="ETHUSDT")
return {
'BTC_USD': float(btc_price['price']),
'ETH_USD': float(eth_price['price']),
'source': 'binance_public'
}
except Exception as e:
print(f"Errore: {e}")
return None
# Uso senza credenziali
public_agent = PublicBinanceAgent()
public_prices = public_agent.get_public_prices()
print(public_prices)

View File

@@ -0,0 +1,35 @@
from coinbase.rest import RESTClient
from app.markets.base import ProductInfo, BaseWrapper, Price
class CoinBaseWrapper(BaseWrapper):
def __init__(self, api_key:str, api_private_key:str, currency: str = "USD"):
assert api_key is not None, "API key is required"
assert api_private_key is not None, "API private key is required"
self.currency = currency
self.client: RESTClient = RESTClient(
api_key=api_key,
api_secret=api_private_key
)
def __format(self, asset_id: str) -> str:
return asset_id if '-' in asset_id else f"{asset_id}-{self.currency}"
def get_product(self, asset_id: str) -> ProductInfo:
asset_id = self.__format(asset_id)
asset = self.client.get_product(asset_id)
return ProductInfo.from_coinbase(asset)
def get_products(self, asset_ids: list[str]) -> list[ProductInfo]:
all_asset_ids = [self.__format(asset_id) for asset_id in asset_ids]
assets = self.client.get_products(all_asset_ids)
return [ProductInfo.from_coinbase(asset) for asset in assets.products]
def get_all_products(self) -> list[ProductInfo]:
assets = self.client.get_products()
return [ProductInfo.from_coinbase(asset) for asset in assets.products]
def get_historical_prices(self, asset_id: str = "BTC") -> list[Price]:
asset_id = self.__format(asset_id)
data = self.client.get_candles(product_id=asset_id)
return [Price.from_coinbase(candle) for candle in data.candles]

View File

@@ -0,0 +1,54 @@
import requests
from app.markets.base import ProductInfo, BaseWrapper, Price
BASE_URL = "https://min-api.cryptocompare.com"
class CryptoCompareWrapper(BaseWrapper):
def __init__(self, api_key:str, currency:str='USD'):
assert api_key is not None, "API key is required"
self.api_key = api_key
self.currency = currency
def __request(self, endpoint: str, params: dict = None) -> dict:
if params is None:
params = {}
params['api_key'] = self.api_key
response = requests.get(f"{BASE_URL}{endpoint}", params=params)
return response.json()
def get_product(self, asset_id: str) -> ProductInfo:
response = self.__request("/data/pricemultifull", params = {
"fsyms": asset_id,
"tsyms": self.currency
})
data = response.get('RAW', {}).get(asset_id, {}).get(self.currency, {})
return ProductInfo.from_cryptocompare(data)
def get_products(self, asset_ids: list[str]) -> list[ProductInfo]:
response = self.__request("/data/pricemultifull", params = {
"fsyms": ",".join(asset_ids),
"tsyms": self.currency
})
assets = []
data = response.get('RAW', {})
for asset_id in asset_ids:
asset_data = data.get(asset_id, {}).get(self.currency, {})
assets.append(ProductInfo.from_cryptocompare(asset_data))
return assets
def get_all_products(self) -> list[ProductInfo]:
raise NotImplementedError("CryptoCompare does not support fetching all assets")
def get_historical_prices(self, asset_id: str, day_back: int = 10) -> list[dict]:
assert day_back <= 30, "day_back should be less than or equal to 30"
response = self.__request("/data/v2/histohour", params = {
"fsym": asset_id,
"tsym": self.currency,
"limit": day_back * 24
})
data = response.get('Data', {}).get('Data', [])
prices = [Price.from_cryptocompare(price_data) for price_data in data]
return prices