Add Telegram bot support #23
11
README.md
11
README.md
@@ -86,7 +86,7 @@ uv pip install -e .
|
|||||||
A questo punto si può già modificare il codice e, quando necessario, far partire il progetto tramite il comando:
|
A questo punto si può già modificare il codice e, quando necessario, far partire il progetto tramite il comando:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
uv run python src/app
|
uv run src/app
|
||||||
```
|
```
|
||||||
|
|
||||||
# **Applicazione**
|
# **Applicazione**
|
||||||
@@ -115,10 +115,11 @@ src
|
|||||||
└── app
|
└── app
|
||||||
├── __main__.py
|
├── __main__.py
|
||||||
├── agents <-- Agenti, modelli, prompts e simili
|
├── agents <-- Agenti, modelli, prompts e simili
|
||||||
├── base <-- Classi base per le API
|
├── api <-- Tutte le API esterne
|
||||||
├── markets <-- Market data provider (Es. Binance)
|
│ ├── base <-- Classi base per le API
|
||||||
├── news <-- News data provider (Es. NewsAPI)
|
│ ├── markets <-- Market data provider (Es. Binance)
|
||||||
├── social <-- Social data provider (Es. Reddit)
|
│ ├── news <-- News data provider (Es. NewsAPI)
|
||||||
|
│ └── social <-- Social data provider (Es. Reddit)
|
||||||
└── utils <-- Codice di utilità generale
|
└── utils <-- Codice di utilità generale
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
from app.base.markets import ProductInfo
|
from app.api.base.markets import ProductInfo
|
||||||
|
|
||||||
|
|
||||||
class PredictorStyle(Enum):
|
class PredictorStyle(Enum):
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
from agno.team import Team
|
from agno.team import Team
|
||||||
from app.agents import AppModels
|
from app.agents import AppModels
|
||||||
from app.markets import MarketAPIsTool
|
from app.api.markets import MarketAPIsTool
|
||||||
from app.news import NewsAPIsTool
|
from app.api.news import NewsAPIsTool
|
||||||
from app.social import SocialAPIsTool
|
from app.api.social import SocialAPIsTool
|
||||||
|
|
||||||
|
|
||||||
def create_team_with(models: AppModels, coordinator: AppModels | None = None) -> Team:
|
def create_team_with(models: AppModels, coordinator: AppModels | None = None) -> Team:
|
||||||
|
|||||||
0
src/app/api/base/__init__.py
Normal file
0
src/app/api/base/__init__.py
Normal file
152
src/app/api/base/markets.py
Normal file
152
src/app/api/base/markets.py
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
import statistics
|
||||||
|
from datetime import datetime
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class ProductInfo(BaseModel):
|
||||||
|
"""
|
||||||
|
Product information as obtained from market APIs.
|
||||||
|
Implements conversion methods from raw API data.
|
||||||
|
"""
|
||||||
|
id: str = ""
|
||||||
|
symbol: str = ""
|
||||||
|
price: float = 0.0
|
||||||
|
volume_24h: float = 0.0
|
||||||
|
currency: str = ""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def aggregate(products: dict[str, list['ProductInfo']]) -> list['ProductInfo']:
|
||||||
|
"""
|
||||||
|
Aggregates a list of ProductInfo by symbol.
|
||||||
|
Args:
|
||||||
|
products (dict[str, list[ProductInfo]]): Map provider -> list of ProductInfo
|
||||||
|
Returns:
|
||||||
|
list[ProductInfo]: List of ProductInfo aggregated by symbol
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Costruzione mappa symbol -> lista di ProductInfo
|
||||||
|
symbols_infos: dict[str, list[ProductInfo]] = {}
|
||||||
|
for _, product_list in products.items():
|
||||||
|
for product in product_list:
|
||||||
|
symbols_infos.setdefault(product.symbol, []).append(product)
|
||||||
|
|
||||||
|
# Aggregazione per ogni symbol
|
||||||
|
aggregated_products: list[ProductInfo] = []
|
||||||
|
for symbol, product_list in symbols_infos.items():
|
||||||
|
product = ProductInfo()
|
||||||
|
|
||||||
|
product.id = f"{symbol}_AGGREGATED"
|
||||||
|
product.symbol = symbol
|
||||||
|
product.currency = next(p.currency for p in product_list if p.currency)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
prices = sum(p.price * p.volume_24h for p in product_list)
|
||||||
|
product.price = (prices / volume_sum) if volume_sum > 0 else 0.0
|
||||||
|
|
||||||
|
aggregated_products.append(product)
|
||||||
|
return aggregated_products
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Price(BaseModel):
|
||||||
|
"""
|
||||||
|
Represents price data for an asset as obtained from market APIs.
|
||||||
|
Implements conversion methods from raw API data.
|
||||||
|
"""
|
||||||
|
high: float = 0.0
|
||||||
|
low: float = 0.0
|
||||||
|
open: float = 0.0
|
||||||
|
close: float = 0.0
|
||||||
|
volume: float = 0.0
|
||||||
|
timestamp: str = ""
|
||||||
|
"""Timestamp in format YYYY-MM-DD HH:MM"""
|
||||||
|
|
||||||
|
def set_timestamp(self, timestamp_ms: int | None = None, timestamp_s: int | None = None) -> None:
|
||||||
|
"""
|
||||||
|
Sets the timestamp from milliseconds or seconds.
|
||||||
|
The timestamp is saved as a formatted string 'YYYY-MM-DD HH:MM'.
|
||||||
|
Args:
|
||||||
|
timestamp_ms: Timestamp in milliseconds.
|
||||||
|
timestamp_s: Timestamp in seconds.
|
||||||
|
Raises:
|
||||||
|
ValueError: If neither timestamp_ms nor timestamp_s is provided.
|
||||||
|
"""
|
||||||
|
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')
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def aggregate(prices: dict[str, list['Price']]) -> list['Price']:
|
||||||
|
"""
|
||||||
|
Aggregates historical prices for the same symbol by calculating the mean.
|
||||||
|
Args:
|
||||||
|
prices (dict[str, list[Price]]): Map provider -> list of Price.
|
||||||
|
The map must contain only Price objects for the same symbol.
|
||||||
|
Returns:
|
||||||
|
list[Price]: List of Price objects aggregated by timestamp.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Costruiamo una mappa timestamp -> lista di Price
|
||||||
|
timestamped_prices: dict[str, list[Price]] = {}
|
||||||
|
for _, price_list in prices.items():
|
||||||
|
for price in price_list:
|
||||||
|
timestamped_prices.setdefault(price.timestamp, []).append(price)
|
||||||
|
|
||||||
|
# Ora aggregiamo i prezzi per ogni timestamp
|
||||||
|
aggregated_prices: list[Price] = []
|
||||||
|
for time, price_list in timestamped_prices.items():
|
||||||
|
price = Price()
|
||||||
|
price.timestamp = time
|
||||||
|
price.high = statistics.mean([p.high 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.close = statistics.mean([p.close for p in price_list])
|
||||||
|
price.volume = statistics.mean([p.volume for p in price_list])
|
||||||
|
aggregated_prices.append(price)
|
||||||
|
return aggregated_prices
|
||||||
|
|
||||||
|
class MarketWrapper:
|
||||||
|
"""
|
||||||
|
Base class for market API wrappers.
|
||||||
|
All market API wrappers should inherit from this class and implement the methods.
|
||||||
|
Provides interface for retrieving product and price information from market APIs.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_product(self, asset_id: str) -> ProductInfo:
|
||||||
|
"""
|
||||||
|
Get product information for a specific asset ID.
|
||||||
|
Args:
|
||||||
|
asset_id (str): The asset ID to retrieve information for.
|
||||||
|
Returns:
|
||||||
|
ProductInfo: An object containing product information.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError("This method should be overridden by subclasses")
|
||||||
|
|
||||||
|
def get_products(self, asset_ids: list[str]) -> list[ProductInfo]:
|
||||||
|
"""
|
||||||
|
Get product information for multiple asset IDs.
|
||||||
|
Args:
|
||||||
|
asset_ids (list[str]): The list of asset IDs to retrieve information for.
|
||||||
|
Returns:
|
||||||
|
list[ProductInfo]: A list of objects containing product information.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError("This method should be overridden by subclasses")
|
||||||
|
|
||||||
|
def get_historical_prices(self, asset_id: str, limit: int = 100) -> list[Price]:
|
||||||
|
"""
|
||||||
|
Get historical price data for a specific asset ID.
|
||||||
|
Args:
|
||||||
|
asset_id (str): The asset ID to retrieve price data for.
|
||||||
|
limit (int): The maximum number of price data points to return.
|
||||||
|
Returns:
|
||||||
|
list[Price]: A list of Price objects.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError("This method should be overridden by subclasses")
|
||||||
@@ -2,6 +2,9 @@ from pydantic import BaseModel
|
|||||||
|
|
||||||
|
|
||||||
class Article(BaseModel):
|
class Article(BaseModel):
|
||||||
|
"""
|
||||||
|
Represents a news article with source, time, title, and description.
|
||||||
|
"""
|
||||||
source: str = ""
|
source: str = ""
|
||||||
time: str = ""
|
time: str = ""
|
||||||
title: str = ""
|
title: str = ""
|
||||||
@@ -11,11 +14,12 @@ class NewsWrapper:
|
|||||||
"""
|
"""
|
||||||
Base class for news API wrappers.
|
Base class for news API wrappers.
|
||||||
All news API wrappers should inherit from this class and implement the methods.
|
All news API wrappers should inherit from this class and implement the methods.
|
||||||
|
Provides interface for retrieving news articles from news APIs.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get_top_headlines(self, limit: int = 100) -> list[Article]:
|
def get_top_headlines(self, limit: int = 100) -> list[Article]:
|
||||||
"""
|
"""
|
||||||
Get top headlines, optionally limited by limit.
|
Retrieve top headlines, optionally limited by the specified number.
|
||||||
Args:
|
Args:
|
||||||
limit (int): The maximum number of articles to return.
|
limit (int): The maximum number of articles to return.
|
||||||
Returns:
|
Returns:
|
||||||
@@ -25,7 +29,7 @@ class NewsWrapper:
|
|||||||
|
|
||||||
def get_latest_news(self, query: str, limit: int = 100) -> list[Article]:
|
def get_latest_news(self, query: str, limit: int = 100) -> list[Article]:
|
||||||
"""
|
"""
|
||||||
Get latest news based on a query.
|
Retrieve the latest news based on a search query.
|
||||||
Args:
|
Args:
|
||||||
query (str): The search query.
|
query (str): The search query.
|
||||||
limit (int): The maximum number of articles to return.
|
limit (int): The maximum number of articles to return.
|
||||||
@@ -2,12 +2,18 @@ from pydantic import BaseModel
|
|||||||
|
|
||||||
|
|
||||||
class SocialPost(BaseModel):
|
class SocialPost(BaseModel):
|
||||||
|
"""
|
||||||
|
Represents a social media post with time, title, description, and comments.
|
||||||
|
"""
|
||||||
time: str = ""
|
time: str = ""
|
||||||
title: str = ""
|
title: str = ""
|
||||||
description: str = ""
|
description: str = ""
|
||||||
comments: list["SocialComment"] = []
|
comments: list["SocialComment"] = []
|
||||||
|
|
||||||
class SocialComment(BaseModel):
|
class SocialComment(BaseModel):
|
||||||
|
"""
|
||||||
|
Represents a comment on a social media post.
|
||||||
|
"""
|
||||||
time: str = ""
|
time: str = ""
|
||||||
description: str = ""
|
description: str = ""
|
||||||
|
|
||||||
@@ -16,11 +22,12 @@ class SocialWrapper:
|
|||||||
"""
|
"""
|
||||||
Base class for social media API wrappers.
|
Base class for social media API wrappers.
|
||||||
All social media API wrappers should inherit from this class and implement the methods.
|
All social media API wrappers should inherit from this class and implement the methods.
|
||||||
|
Provides interface for retrieving social media posts and comments from APIs.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get_top_crypto_posts(self, limit: int = 5) -> list[SocialPost]:
|
def get_top_crypto_posts(self, limit: int = 5) -> list[SocialPost]:
|
||||||
"""
|
"""
|
||||||
Get top cryptocurrency-related posts, optionally limited by total.
|
Retrieve top cryptocurrency-related posts, optionally limited by the specified number.
|
||||||
Args:
|
Args:
|
||||||
limit (int): The maximum number of posts to return.
|
limit (int): The maximum number of posts to return.
|
||||||
Returns:
|
Returns:
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
from agno.tools import Toolkit
|
from agno.tools import Toolkit
|
||||||
from app.base.markets import MarketWrapper, Price, ProductInfo
|
from app.api.wrapper_handler import WrapperHandler
|
||||||
from app.markets.binance import BinanceWrapper
|
from app.api.base.markets import MarketWrapper, Price, ProductInfo
|
||||||
from app.markets.coinbase import CoinBaseWrapper
|
from app.api.markets.binance import BinanceWrapper
|
||||||
from app.markets.cryptocompare import CryptoCompareWrapper
|
from app.api.markets.coinbase import CoinBaseWrapper
|
||||||
from app.markets.yfinance import YFinanceWrapper
|
from app.api.markets.cryptocompare import CryptoCompareWrapper
|
||||||
from app.utils import aggregate_history_prices, aggregate_product_info, WrapperHandler
|
from app.api.markets.yfinance import YFinanceWrapper
|
||||||
|
|
||||||
__all__ = [ "MarketAPIsTool", "BinanceWrapper", "CoinBaseWrapper", "CryptoCompareWrapper", "YFinanceWrapper", "ProductInfo", "Price" ]
|
__all__ = [ "MarketAPIsTool", "BinanceWrapper", "CoinBaseWrapper", "CryptoCompareWrapper", "YFinanceWrapper", "ProductInfo", "Price" ]
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ class MarketAPIsTool(MarketWrapper, Toolkit):
|
|||||||
"""
|
"""
|
||||||
kwargs = {"currency": currency or "USD"}
|
kwargs = {"currency": currency or "USD"}
|
||||||
wrappers: list[type[MarketWrapper]] = [BinanceWrapper, YFinanceWrapper, CoinBaseWrapper, CryptoCompareWrapper]
|
wrappers: list[type[MarketWrapper]] = [BinanceWrapper, YFinanceWrapper, CoinBaseWrapper, CryptoCompareWrapper]
|
||||||
self.wrappers = WrapperHandler.build_wrappers(wrappers, kwargs=kwargs)
|
self.handler = WrapperHandler.build_wrappers(wrappers, kwargs=kwargs)
|
||||||
|
|
||||||
Toolkit.__init__( # type: ignore
|
Toolkit.__init__( # type: ignore
|
||||||
self,
|
self,
|
||||||
@@ -49,11 +49,11 @@ class MarketAPIsTool(MarketWrapper, Toolkit):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def get_product(self, asset_id: str) -> ProductInfo:
|
def get_product(self, asset_id: str) -> ProductInfo:
|
||||||
return self.wrappers.try_call(lambda w: w.get_product(asset_id))
|
return self.handler.try_call(lambda w: w.get_product(asset_id))
|
||||||
def get_products(self, asset_ids: list[str]) -> list[ProductInfo]:
|
def get_products(self, asset_ids: list[str]) -> list[ProductInfo]:
|
||||||
return self.wrappers.try_call(lambda w: w.get_products(asset_ids))
|
return self.handler.try_call(lambda w: w.get_products(asset_ids))
|
||||||
def get_historical_prices(self, asset_id: str, limit: int = 100) -> list[Price]:
|
def get_historical_prices(self, asset_id: str, limit: int = 100) -> list[Price]:
|
||||||
return self.wrappers.try_call(lambda w: w.get_historical_prices(asset_id, limit))
|
return self.handler.try_call(lambda w: w.get_historical_prices(asset_id, limit))
|
||||||
|
|
||||||
|
|
||||||
def get_products_aggregated(self, asset_ids: list[str]) -> list[ProductInfo]:
|
def get_products_aggregated(self, asset_ids: list[str]) -> list[ProductInfo]:
|
||||||
@@ -67,8 +67,8 @@ class MarketAPIsTool(MarketWrapper, Toolkit):
|
|||||||
Raises:
|
Raises:
|
||||||
Exception: If all wrappers fail to provide results.
|
Exception: If all wrappers fail to provide results.
|
||||||
"""
|
"""
|
||||||
all_products = self.wrappers.try_call_all(lambda w: w.get_products(asset_ids))
|
all_products = self.handler.try_call_all(lambda w: w.get_products(asset_ids))
|
||||||
return aggregate_product_info(all_products)
|
return ProductInfo.aggregate(all_products)
|
||||||
|
|
||||||
def get_historical_prices_aggregated(self, asset_id: str = "BTC", limit: int = 100) -> list[Price]:
|
def get_historical_prices_aggregated(self, asset_id: str = "BTC", limit: int = 100) -> list[Price]:
|
||||||
"""
|
"""
|
||||||
@@ -82,5 +82,5 @@ class MarketAPIsTool(MarketWrapper, Toolkit):
|
|||||||
Raises:
|
Raises:
|
||||||
Exception: If all wrappers fail to provide results.
|
Exception: If all wrappers fail to provide results.
|
||||||
"""
|
"""
|
||||||
all_prices = self.wrappers.try_call_all(lambda w: w.get_historical_prices(asset_id, limit))
|
all_prices = self.handler.try_call_all(lambda w: w.get_historical_prices(asset_id, limit))
|
||||||
return aggregate_history_prices(all_prices)
|
return Price.aggregate(all_prices)
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import os
|
import os
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from binance.client import Client # type: ignore
|
from binance.client import Client # type: ignore
|
||||||
from app.base.markets import ProductInfo, MarketWrapper, Price
|
from app.api.base.markets import ProductInfo, MarketWrapper, Price
|
||||||
|
|
||||||
|
|
||||||
def extract_product(currency: str, ticker_data: dict[str, Any]) -> ProductInfo:
|
def extract_product(currency: str, ticker_data: dict[str, Any]) -> ProductInfo:
|
||||||
@@ -25,6 +25,12 @@ def extract_price(kline_data: list[Any]) -> Price:
|
|||||||
price.set_timestamp(timestamp_ms=timestamp)
|
price.set_timestamp(timestamp_ms=timestamp)
|
||||||
return price
|
return price
|
||||||
|
|
||||||
|
|
||||||
|
# Add here eventual other fiat not supported by Binance
|
||||||
|
FIAT_TO_STABLECOIN = {
|
||||||
|
"USD": "USDT",
|
||||||
|
}
|
||||||
|
|
||||||
class BinanceWrapper(MarketWrapper):
|
class BinanceWrapper(MarketWrapper):
|
||||||
"""
|
"""
|
||||||
Wrapper per le API autenticate di Binance.\n
|
Wrapper per le API autenticate di Binance.\n
|
||||||
@@ -36,16 +42,15 @@ class BinanceWrapper(MarketWrapper):
|
|||||||
def __init__(self, currency: str = "USD"):
|
def __init__(self, currency: str = "USD"):
|
||||||
"""
|
"""
|
||||||
Inizializza il wrapper di Binance con le credenziali API e la valuta di riferimento.
|
Inizializza il wrapper di Binance con le credenziali API e la valuta di riferimento.
|
||||||
Se viene fornita una valuta fiat come "USD", questa viene automaticamente convertita in una stablecoin Tether ("USDT") per compatibilità con Binance,
|
Alcune valute fiat non sono supportate direttamente da Binance (es. "USD").
|
||||||
poiché Binance non supporta direttamente le valute fiat per il trading di criptovalute.
|
Infatti, se viene fornita una valuta fiat come "USD", questa viene automaticamente convertita in una stablecoin Tether ("USDT") per compatibilità con Binance.
|
||||||
Tutti i prezzi e volumi restituiti saranno quindi denominati nella stablecoin (ad esempio, "USDT") e non nella valuta fiat originale.
|
|
||||||
Args:
|
Args:
|
||||||
currency (str): Valuta in cui restituire i prezzi. Se "USD" viene fornito, verrà utilizzato "USDT". Default è "USD".
|
currency (str): Valuta in cui restituire i prezzi. Se "USD" viene fornito, verrà utilizzato "USDT". Default è "USD".
|
||||||
"""
|
"""
|
||||||
api_key = os.getenv("BINANCE_API_KEY")
|
api_key = os.getenv("BINANCE_API_KEY")
|
||||||
api_secret = os.getenv("BINANCE_API_SECRET")
|
api_secret = os.getenv("BINANCE_API_SECRET")
|
||||||
|
|
||||||
self.currency = f"{currency}T"
|
self.currency = currency if currency not in FIAT_TO_STABLECOIN else FIAT_TO_STABLECOIN[currency]
|
||||||
self.client = Client(api_key=api_key, api_secret=api_secret)
|
self.client = Client(api_key=api_key, api_secret=api_secret)
|
||||||
|
|
||||||
def __format_symbol(self, asset_id: str) -> str:
|
def __format_symbol(self, asset_id: str) -> str:
|
||||||
@@ -3,7 +3,7 @@ from enum import Enum
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from coinbase.rest import RESTClient # type: ignore
|
from coinbase.rest import RESTClient # type: ignore
|
||||||
from coinbase.rest.types.product_types import Candle, GetProductResponse, Product # type: ignore
|
from coinbase.rest.types.product_types import Candle, GetProductResponse, Product # type: ignore
|
||||||
from app.base.markets import ProductInfo, MarketWrapper, Price
|
from app.api.base.markets import ProductInfo, MarketWrapper, Price
|
||||||
|
|
||||||
|
|
||||||
def extract_product(product_data: GetProductResponse | Product) -> ProductInfo:
|
def extract_product(product_data: GetProductResponse | Product) -> ProductInfo:
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import os
|
import os
|
||||||
from typing import Any
|
from typing import Any
|
||||||
import requests
|
import requests
|
||||||
from app.base.markets import ProductInfo, MarketWrapper, Price
|
from app.api.base.markets import ProductInfo, MarketWrapper, Price
|
||||||
|
|
||||||
|
|
||||||
def extract_product(asset_data: dict[str, Any]) -> ProductInfo:
|
def extract_product(asset_data: dict[str, Any]) -> ProductInfo:
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import json
|
import json
|
||||||
from agno.tools.yfinance import YFinanceTools
|
from agno.tools.yfinance import YFinanceTools
|
||||||
from app.base.markets import MarketWrapper, ProductInfo, Price
|
from app.api.base.markets import MarketWrapper, ProductInfo, Price
|
||||||
|
|
||||||
|
|
||||||
def extract_product(stock_data: dict[str, str]) -> ProductInfo:
|
def extract_product(stock_data: dict[str, str]) -> ProductInfo:
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
from agno.tools import Toolkit
|
from agno.tools import Toolkit
|
||||||
from app.utils import WrapperHandler
|
from app.api.wrapper_handler import WrapperHandler
|
||||||
from app.base.news import NewsWrapper, Article
|
from app.api.base.news import NewsWrapper, Article
|
||||||
from app.news.news_api import NewsApiWrapper
|
from app.api.news.news_api import NewsApiWrapper
|
||||||
from app.news.googlenews import GoogleNewsWrapper
|
from app.api.news.googlenews import GoogleNewsWrapper
|
||||||
from app.news.cryptopanic_api import CryptoPanicWrapper
|
from app.api.news.cryptopanic_api import CryptoPanicWrapper
|
||||||
from app.news.duckduckgo import DuckDuckGoWrapper
|
from app.api.news.duckduckgo import DuckDuckGoWrapper
|
||||||
|
|
||||||
__all__ = ["NewsAPIsTool", "NewsApiWrapper", "GoogleNewsWrapper", "CryptoPanicWrapper", "DuckDuckGoWrapper", "Article"]
|
__all__ = ["NewsAPIsTool", "NewsApiWrapper", "GoogleNewsWrapper", "CryptoPanicWrapper", "DuckDuckGoWrapper", "Article"]
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ class NewsAPIsTool(NewsWrapper, Toolkit):
|
|||||||
- CryptoPanicWrapper.
|
- CryptoPanicWrapper.
|
||||||
"""
|
"""
|
||||||
wrappers: list[type[NewsWrapper]] = [GoogleNewsWrapper, DuckDuckGoWrapper, NewsApiWrapper, CryptoPanicWrapper]
|
wrappers: list[type[NewsWrapper]] = [GoogleNewsWrapper, DuckDuckGoWrapper, NewsApiWrapper, CryptoPanicWrapper]
|
||||||
self.wrapper_handler = WrapperHandler.build_wrappers(wrappers)
|
self.handler = WrapperHandler.build_wrappers(wrappers)
|
||||||
|
|
||||||
Toolkit.__init__( # type: ignore
|
Toolkit.__init__( # type: ignore
|
||||||
self,
|
self,
|
||||||
@@ -48,9 +48,9 @@ class NewsAPIsTool(NewsWrapper, Toolkit):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def get_top_headlines(self, limit: int = 100) -> list[Article]:
|
def get_top_headlines(self, limit: int = 100) -> list[Article]:
|
||||||
return self.wrapper_handler.try_call(lambda w: w.get_top_headlines(limit))
|
return self.handler.try_call(lambda w: w.get_top_headlines(limit))
|
||||||
def get_latest_news(self, query: str, limit: int = 100) -> list[Article]:
|
def get_latest_news(self, query: str, limit: int = 100) -> list[Article]:
|
||||||
return self.wrapper_handler.try_call(lambda w: w.get_latest_news(query, limit))
|
return self.handler.try_call(lambda w: w.get_latest_news(query, limit))
|
||||||
|
|
||||||
def get_top_headlines_aggregated(self, limit: int = 100) -> dict[str, list[Article]]:
|
def get_top_headlines_aggregated(self, limit: int = 100) -> dict[str, list[Article]]:
|
||||||
"""
|
"""
|
||||||
@@ -62,7 +62,7 @@ class NewsAPIsTool(NewsWrapper, Toolkit):
|
|||||||
Raises:
|
Raises:
|
||||||
Exception: If all wrappers fail to provide results.
|
Exception: If all wrappers fail to provide results.
|
||||||
"""
|
"""
|
||||||
return self.wrapper_handler.try_call_all(lambda w: w.get_top_headlines(limit))
|
return self.handler.try_call_all(lambda w: w.get_top_headlines(limit))
|
||||||
|
|
||||||
def get_latest_news_aggregated(self, query: str, limit: int = 100) -> dict[str, list[Article]]:
|
def get_latest_news_aggregated(self, query: str, limit: int = 100) -> dict[str, list[Article]]:
|
||||||
"""
|
"""
|
||||||
@@ -75,4 +75,4 @@ class NewsAPIsTool(NewsWrapper, Toolkit):
|
|||||||
Raises:
|
Raises:
|
||||||
Exception: If all wrappers fail to provide results.
|
Exception: If all wrappers fail to provide results.
|
||||||
"""
|
"""
|
||||||
return self.wrapper_handler.try_call_all(lambda w: w.get_latest_news(query, limit))
|
return self.handler.try_call_all(lambda w: w.get_latest_news(query, limit))
|
||||||
@@ -2,7 +2,7 @@ import os
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
import requests
|
import requests
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from app.base.news import NewsWrapper, Article
|
from app.api.base.news import NewsWrapper, Article
|
||||||
|
|
||||||
|
|
||||||
class CryptoPanicFilter(Enum):
|
class CryptoPanicFilter(Enum):
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import json
|
import json
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from agno.tools.duckduckgo import DuckDuckGoTools
|
from agno.tools.duckduckgo import DuckDuckGoTools
|
||||||
from app.base.news import Article, NewsWrapper
|
from app.api.base.news import Article, NewsWrapper
|
||||||
|
|
||||||
|
|
||||||
def extract_article(result: dict[str, Any]) -> Article:
|
def extract_article(result: dict[str, Any]) -> Article:
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
from gnews import GNews # type: ignore
|
from gnews import GNews # type: ignore
|
||||||
from app.base.news import Article, NewsWrapper
|
from app.api.base.news import Article, NewsWrapper
|
||||||
|
|
||||||
|
|
||||||
def extract_article(result: dict[str, Any]) -> Article:
|
def extract_article(result: dict[str, Any]) -> Article:
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import os
|
import os
|
||||||
from typing import Any
|
from typing import Any
|
||||||
import newsapi # type: ignore
|
import newsapi # type: ignore
|
||||||
from app.base.news import Article, NewsWrapper
|
from app.api.base.news import Article, NewsWrapper
|
||||||
|
|
||||||
|
|
||||||
def extract_article(result: dict[str, Any]) -> Article:
|
def extract_article(result: dict[str, Any]) -> Article:
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
from agno.tools import Toolkit
|
from agno.tools import Toolkit
|
||||||
from app.utils import WrapperHandler
|
from app.api.wrapper_handler import WrapperHandler
|
||||||
from app.base.social import SocialPost, SocialWrapper
|
from app.api.base.social import SocialPost, SocialWrapper
|
||||||
from app.social.reddit import RedditWrapper
|
from app.api.social.reddit import RedditWrapper
|
||||||
|
|
||||||
__all__ = ["SocialAPIsTool", "RedditWrapper", "SocialPost"]
|
__all__ = ["SocialAPIsTool", "RedditWrapper", "SocialPost"]
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ class SocialAPIsTool(SocialWrapper, Toolkit):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
wrappers: list[type[SocialWrapper]] = [RedditWrapper]
|
wrappers: list[type[SocialWrapper]] = [RedditWrapper]
|
||||||
self.wrapper_handler = WrapperHandler.build_wrappers(wrappers)
|
self.handler = WrapperHandler.build_wrappers(wrappers)
|
||||||
|
|
||||||
Toolkit.__init__( # type: ignore
|
Toolkit.__init__( # type: ignore
|
||||||
self,
|
self,
|
||||||
@@ -38,7 +38,7 @@ class SocialAPIsTool(SocialWrapper, Toolkit):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def get_top_crypto_posts(self, limit: int = 5) -> list[SocialPost]:
|
def get_top_crypto_posts(self, limit: int = 5) -> list[SocialPost]:
|
||||||
return self.wrapper_handler.try_call(lambda w: w.get_top_crypto_posts(limit))
|
return self.handler.try_call(lambda w: w.get_top_crypto_posts(limit))
|
||||||
|
|
||||||
def get_top_crypto_posts_aggregated(self, limit_per_wrapper: int = 5) -> dict[str, list[SocialPost]]:
|
def get_top_crypto_posts_aggregated(self, limit_per_wrapper: int = 5) -> dict[str, list[SocialPost]]:
|
||||||
"""
|
"""
|
||||||
@@ -50,4 +50,4 @@ class SocialAPIsTool(SocialWrapper, Toolkit):
|
|||||||
Raises:
|
Raises:
|
||||||
Exception: If all wrappers fail to provide results.
|
Exception: If all wrappers fail to provide results.
|
||||||
"""
|
"""
|
||||||
return self.wrapper_handler.try_call_all(lambda w: w.get_top_crypto_posts(limit_per_wrapper))
|
return self.handler.try_call_all(lambda w: w.get_top_crypto_posts(limit_per_wrapper))
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import os
|
import os
|
||||||
from praw import Reddit # type: ignore
|
from praw import Reddit # type: ignore
|
||||||
from praw.models import Submission # type: ignore
|
from praw.models import Submission # type: ignore
|
||||||
from app.base.social import SocialWrapper, SocialPost, SocialComment
|
from app.api.base.social import SocialWrapper, SocialPost, SocialComment
|
||||||
|
|
||||||
|
|
||||||
MAX_COMMENTS = 5
|
MAX_COMMENTS = 5
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
from datetime import datetime
|
|
||||||
from pydantic import BaseModel
|
|
||||||
|
|
||||||
|
|
||||||
class ProductInfo(BaseModel):
|
|
||||||
"""
|
|
||||||
Informazioni sul prodotto, come ottenute dalle API di mercato.
|
|
||||||
Implementa i metodi di conversione dai dati grezzi delle API.
|
|
||||||
"""
|
|
||||||
id: str = ""
|
|
||||||
symbol: str = ""
|
|
||||||
price: float = 0.0
|
|
||||||
volume_24h: float = 0.0
|
|
||||||
currency: str = ""
|
|
||||||
|
|
||||||
class Price(BaseModel):
|
|
||||||
"""
|
|
||||||
Rappresenta i dati di prezzo per un asset, come ottenuti dalle API di mercato.
|
|
||||||
Implementa i metodi di conversione dai dati grezzi delle API.
|
|
||||||
"""
|
|
||||||
high: float = 0.0
|
|
||||||
low: float = 0.0
|
|
||||||
open: float = 0.0
|
|
||||||
close: float = 0.0
|
|
||||||
volume: float = 0.0
|
|
||||||
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 a partire da millisecondi o secondi.
|
|
||||||
IL timestamp viene salvato come stringa formattata 'YYYY-MM-DD HH:MM'.
|
|
||||||
Args:
|
|
||||||
timestamp_ms: Timestamp in millisecondi.
|
|
||||||
timestamp_s: Timestamp in secondi.
|
|
||||||
Raises:
|
|
||||||
"""
|
|
||||||
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:
|
|
||||||
"""
|
|
||||||
Base class for market API wrappers.
|
|
||||||
All market API wrappers should inherit from this class and implement the methods.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def get_product(self, asset_id: str) -> ProductInfo:
|
|
||||||
"""
|
|
||||||
Get product information for a specific asset ID.
|
|
||||||
Args:
|
|
||||||
asset_id (str): The asset ID to retrieve information for.
|
|
||||||
Returns:
|
|
||||||
ProductInfo: An object containing product information.
|
|
||||||
"""
|
|
||||||
raise NotImplementedError("This method should be overridden by subclasses")
|
|
||||||
|
|
||||||
def get_products(self, asset_ids: list[str]) -> list[ProductInfo]:
|
|
||||||
"""
|
|
||||||
Get product information for multiple asset IDs.
|
|
||||||
Args:
|
|
||||||
asset_ids (list[str]): The list of asset IDs to retrieve information for.
|
|
||||||
Returns:
|
|
||||||
list[ProductInfo]: A list of objects containing product information.
|
|
||||||
"""
|
|
||||||
raise NotImplementedError("This method should be overridden by subclasses")
|
|
||||||
|
|
||||||
def get_historical_prices(self, asset_id: str, limit: int = 100) -> list[Price]:
|
|
||||||
"""
|
|
||||||
Get historical price data for a specific asset ID.
|
|
||||||
Args:
|
|
||||||
asset_id (str): The asset ID to retrieve price data for.
|
|
||||||
limit (int): The maximum number of price data points to return.
|
|
||||||
Returns:
|
|
||||||
list[Price]: A list of Price objects.
|
|
||||||
"""
|
|
||||||
raise NotImplementedError("This method should be overridden by subclasses")
|
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
from app.utils.market_aggregation import aggregate_history_prices, aggregate_product_info
|
|
||||||
from app.utils.wrapper_handler import WrapperHandler
|
|
||||||
from app.utils.chat_manager import ChatManager
|
from app.utils.chat_manager import ChatManager
|
||||||
from app.utils.telegram_app import BotFunctions
|
from app.utils.telegram_app import BotFunctions
|
||||||
|
|
||||||
__all__ = ["aggregate_history_prices", "aggregate_product_info", "WrapperHandler", "ChatManager", "BotFunctions"]
|
__all__ = ["ChatManager", "BotFunctions"]
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
import statistics
|
|
||||||
from app.base.markets import ProductInfo, Price
|
|
||||||
|
|
||||||
|
|
||||||
def aggregate_history_prices(prices: dict[str, list[Price]]) -> list[Price]:
|
|
||||||
"""
|
|
||||||
Aggrega i prezzi storici per symbol calcolando la media.
|
|
||||||
Args:
|
|
||||||
prices (dict[str, list[Price]]): Mappa provider -> lista di Price
|
|
||||||
Returns:
|
|
||||||
list[Price]: Lista di Price aggregati per timestamp
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Costruiamo una mappa timestamp -> lista di Price
|
|
||||||
timestamped_prices: dict[str, list[Price]] = {}
|
|
||||||
for _, price_list in prices.items():
|
|
||||||
for price in price_list:
|
|
||||||
timestamped_prices.setdefault(price.timestamp, []).append(price)
|
|
||||||
|
|
||||||
# Ora aggregiamo i prezzi per ogni timestamp
|
|
||||||
aggregated_prices: list[Price] = []
|
|
||||||
for time, price_list in timestamped_prices.items():
|
|
||||||
price = Price()
|
|
||||||
price.timestamp = time
|
|
||||||
price.high = statistics.mean([p.high 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.close = statistics.mean([p.close for p in price_list])
|
|
||||||
price.volume = statistics.mean([p.volume for p in price_list])
|
|
||||||
aggregated_prices.append(price)
|
|
||||||
return aggregated_prices
|
|
||||||
|
|
||||||
def aggregate_product_info(products: dict[str, list[ProductInfo]]) -> list[ProductInfo]:
|
|
||||||
"""
|
|
||||||
Aggrega una lista di ProductInfo per symbol.
|
|
||||||
Args:
|
|
||||||
products (dict[str, list[ProductInfo]]): Mappa provider -> lista di ProductInfo
|
|
||||||
Returns:
|
|
||||||
list[ProductInfo]: Lista di ProductInfo aggregati per symbol
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Costruzione mappa symbol -> lista di ProductInfo
|
|
||||||
symbols_infos: dict[str, list[ProductInfo]] = {}
|
|
||||||
for _, product_list in products.items():
|
|
||||||
for product in product_list:
|
|
||||||
symbols_infos.setdefault(product.symbol, []).append(product)
|
|
||||||
|
|
||||||
# Aggregazione per ogni symbol
|
|
||||||
aggregated_products: list[ProductInfo] = []
|
|
||||||
for symbol, product_list in symbols_infos.items():
|
|
||||||
product = ProductInfo()
|
|
||||||
|
|
||||||
product.id = f"{symbol}_AGGREGATED"
|
|
||||||
product.symbol = symbol
|
|
||||||
product.currency = next(p.currency for p in product_list if p.currency)
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
prices = sum(p.price * p.volume_24h for p in product_list)
|
|
||||||
product.price = (prices / volume_sum) if volume_sum > 0 else 0.0
|
|
||||||
|
|
||||||
aggregated_products.append(product)
|
|
||||||
return aggregated_products
|
|
||||||
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from app.agents import AppModels
|
from app.agents import AppModels
|
||||||
from app.agents.predictor import PREDICTOR_INSTRUCTIONS, PredictorInput, PredictorOutput, PredictorStyle
|
from app.agents.predictor import PREDICTOR_INSTRUCTIONS, PredictorInput, PredictorOutput, PredictorStyle
|
||||||
from app.base.markets import ProductInfo
|
from app.api.base.markets import ProductInfo
|
||||||
|
|
||||||
def unified_checks(model: AppModels, input: PredictorInput) -> None:
|
def unified_checks(model: AppModels, input: PredictorInput) -> None:
|
||||||
llm = model.get_agent(PREDICTOR_INSTRUCTIONS, output_schema=PredictorOutput) # type: ignore[arg-type]
|
llm = model.get_agent(PREDICTOR_INSTRUCTIONS, output_schema=PredictorOutput) # type: ignore[arg-type]
|
||||||
|
|||||||
@@ -1,5 +1,18 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from app.markets.binance import BinanceWrapper
|
import asyncio
|
||||||
|
from app.api.markets.binance import BinanceWrapper
|
||||||
|
|
||||||
|
# fix warning about no event loop
|
||||||
|
@pytest.fixture(scope="session", autouse=True)
|
||||||
|
def event_loop():
|
||||||
|
"""
|
||||||
|
Ensure there is an event loop for the duration of the tests.
|
||||||
|
"""
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
|
asyncio.set_event_loop(loop)
|
||||||
|
yield loop
|
||||||
|
loop.close()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.market
|
@pytest.mark.market
|
||||||
@pytest.mark.api
|
@pytest.mark.api
|
||||||
@@ -51,3 +64,18 @@ class TestBinance:
|
|||||||
assert entry.close > 0
|
assert entry.close > 0
|
||||||
assert entry.high > 0
|
assert entry.high > 0
|
||||||
assert entry.timestamp != ''
|
assert entry.timestamp != ''
|
||||||
|
|
||||||
|
def test_binance_fiat_conversion(self):
|
||||||
|
market = BinanceWrapper(currency="USD")
|
||||||
|
assert market.currency == "USDT"
|
||||||
|
product = market.get_product("BTC")
|
||||||
|
assert product is not None
|
||||||
|
assert product.symbol == "BTC"
|
||||||
|
assert product.price > 0
|
||||||
|
|
||||||
|
market = BinanceWrapper(currency="EUR")
|
||||||
|
assert market.currency == "EUR"
|
||||||
|
product = market.get_product("BTC")
|
||||||
|
assert product is not None
|
||||||
|
assert product.symbol == "BTC"
|
||||||
|
assert product.price > 0
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import os
|
import os
|
||||||
import pytest
|
import pytest
|
||||||
from app.markets import CoinBaseWrapper
|
from app.api.markets import CoinBaseWrapper
|
||||||
|
|
||||||
@pytest.mark.market
|
@pytest.mark.market
|
||||||
@pytest.mark.api
|
@pytest.mark.api
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import os
|
import os
|
||||||
import pytest
|
import pytest
|
||||||
from app.markets import CryptoCompareWrapper
|
from app.api.markets import CryptoCompareWrapper
|
||||||
|
|
||||||
@pytest.mark.market
|
@pytest.mark.market
|
||||||
@pytest.mark.api
|
@pytest.mark.api
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import os
|
import os
|
||||||
import pytest
|
import pytest
|
||||||
from app.news import CryptoPanicWrapper
|
from app.api.news import CryptoPanicWrapper
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.limited
|
@pytest.mark.limited
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from app.news import DuckDuckGoWrapper
|
from app.api.news import DuckDuckGoWrapper
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.news
|
@pytest.mark.news
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from app.news import GoogleNewsWrapper
|
from app.api.news import GoogleNewsWrapper
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.news
|
@pytest.mark.news
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import os
|
import os
|
||||||
import pytest
|
import pytest
|
||||||
from app.news import NewsApiWrapper
|
from app.api.news import NewsApiWrapper
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.news
|
@pytest.mark.news
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import os
|
import os
|
||||||
import pytest
|
import pytest
|
||||||
from app.social.reddit import MAX_COMMENTS, RedditWrapper
|
from app.api.social.reddit import MAX_COMMENTS, RedditWrapper
|
||||||
|
|
||||||
@pytest.mark.social
|
@pytest.mark.social
|
||||||
@pytest.mark.api
|
@pytest.mark.api
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from app.markets import YFinanceWrapper
|
from app.api.markets import YFinanceWrapper
|
||||||
|
|
||||||
@pytest.mark.market
|
@pytest.mark.market
|
||||||
@pytest.mark.api
|
@pytest.mark.api
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from app.markets import MarketAPIsTool
|
from app.api.markets import MarketAPIsTool
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.tools
|
@pytest.mark.tools
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from app.news import NewsAPIsTool
|
from app.api.news import NewsAPIsTool
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.tools
|
@pytest.mark.tools
|
||||||
@@ -12,7 +12,7 @@ class TestNewsAPITool:
|
|||||||
|
|
||||||
def test_news_api_tool_get_top(self):
|
def test_news_api_tool_get_top(self):
|
||||||
tool = NewsAPIsTool()
|
tool = NewsAPIsTool()
|
||||||
result = tool.wrapper_handler.try_call(lambda w: w.get_top_headlines(limit=2))
|
result = tool.handler.try_call(lambda w: w.get_top_headlines(limit=2))
|
||||||
assert isinstance(result, list)
|
assert isinstance(result, list)
|
||||||
assert len(result) > 0
|
assert len(result) > 0
|
||||||
for article in result:
|
for article in result:
|
||||||
@@ -21,7 +21,7 @@ class TestNewsAPITool:
|
|||||||
|
|
||||||
def test_news_api_tool_get_latest(self):
|
def test_news_api_tool_get_latest(self):
|
||||||
tool = NewsAPIsTool()
|
tool = NewsAPIsTool()
|
||||||
result = tool.wrapper_handler.try_call(lambda w: w.get_latest_news(query="crypto", limit=2))
|
result = tool.handler.try_call(lambda w: w.get_latest_news(query="crypto", limit=2))
|
||||||
assert isinstance(result, list)
|
assert isinstance(result, list)
|
||||||
assert len(result) > 0
|
assert len(result) > 0
|
||||||
for article in result:
|
for article in result:
|
||||||
@@ -30,7 +30,7 @@ class TestNewsAPITool:
|
|||||||
|
|
||||||
def test_news_api_tool_get_top__all_results(self):
|
def test_news_api_tool_get_top__all_results(self):
|
||||||
tool = NewsAPIsTool()
|
tool = NewsAPIsTool()
|
||||||
result = tool.wrapper_handler.try_call_all(lambda w: w.get_top_headlines(limit=2))
|
result = tool.handler.try_call_all(lambda w: w.get_top_headlines(limit=2))
|
||||||
assert isinstance(result, dict)
|
assert isinstance(result, dict)
|
||||||
assert len(result.keys()) > 0
|
assert len(result.keys()) > 0
|
||||||
for _provider, articles in result.items():
|
for _provider, articles in result.items():
|
||||||
@@ -40,7 +40,7 @@ class TestNewsAPITool:
|
|||||||
|
|
||||||
def test_news_api_tool_get_latest__all_results(self):
|
def test_news_api_tool_get_latest__all_results(self):
|
||||||
tool = NewsAPIsTool()
|
tool = NewsAPIsTool()
|
||||||
result = tool.wrapper_handler.try_call_all(lambda w: w.get_latest_news(query="crypto", limit=2))
|
result = tool.handler.try_call_all(lambda w: w.get_latest_news(query="crypto", limit=2))
|
||||||
assert isinstance(result, dict)
|
assert isinstance(result, dict)
|
||||||
assert len(result.keys()) > 0
|
assert len(result.keys()) > 0
|
||||||
for _provider, articles in result.items():
|
for _provider, articles in result.items():
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from app.social import SocialAPIsTool
|
from app.api.social import SocialAPIsTool
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.tools
|
@pytest.mark.tools
|
||||||
@@ -12,7 +12,7 @@ class TestSocialAPIsTool:
|
|||||||
|
|
||||||
def test_social_api_tool_get_top(self):
|
def test_social_api_tool_get_top(self):
|
||||||
tool = SocialAPIsTool()
|
tool = SocialAPIsTool()
|
||||||
result = tool.wrapper_handler.try_call(lambda w: w.get_top_crypto_posts(limit=2))
|
result = tool.handler.try_call(lambda w: w.get_top_crypto_posts(limit=2))
|
||||||
assert isinstance(result, list)
|
assert isinstance(result, list)
|
||||||
assert len(result) > 0
|
assert len(result) > 0
|
||||||
for post in result:
|
for post in result:
|
||||||
@@ -21,10 +21,10 @@ class TestSocialAPIsTool:
|
|||||||
|
|
||||||
def test_social_api_tool_get_top__all_results(self):
|
def test_social_api_tool_get_top__all_results(self):
|
||||||
tool = SocialAPIsTool()
|
tool = SocialAPIsTool()
|
||||||
result = tool.wrapper_handler.try_call_all(lambda w: w.get_top_crypto_posts(limit=2))
|
result = tool.handler.try_call_all(lambda w: w.get_top_crypto_posts(limit=2))
|
||||||
assert isinstance(result, dict)
|
assert isinstance(result, dict)
|
||||||
assert len(result.keys()) > 0
|
assert len(result.keys()) > 0
|
||||||
for provider, posts in result.items():
|
for _provider, posts in result.items():
|
||||||
for post in posts:
|
for post in posts:
|
||||||
assert post.title is not None
|
assert post.title is not None
|
||||||
assert post.time is not None
|
assert post.time is not None
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from app.base.markets import ProductInfo, Price
|
from app.api.base.markets import ProductInfo, Price
|
||||||
from app.utils.market_aggregation import aggregate_history_prices, aggregate_product_info
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.aggregator
|
@pytest.mark.aggregator
|
||||||
@@ -34,7 +33,7 @@ class TestMarketDataAggregator:
|
|||||||
"Provider3": [self.__product("BTC", 49900.0, 900.0, "USD")],
|
"Provider3": [self.__product("BTC", 49900.0, 900.0, "USD")],
|
||||||
}
|
}
|
||||||
|
|
||||||
aggregated = aggregate_product_info(products)
|
aggregated = ProductInfo.aggregate(products)
|
||||||
assert len(aggregated) == 1
|
assert len(aggregated) == 1
|
||||||
|
|
||||||
info = aggregated[0]
|
info = aggregated[0]
|
||||||
@@ -58,7 +57,7 @@ class TestMarketDataAggregator:
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
aggregated = aggregate_product_info(products)
|
aggregated = ProductInfo.aggregate(products)
|
||||||
assert len(aggregated) == 2
|
assert len(aggregated) == 2
|
||||||
|
|
||||||
btc_info = next((p for p in aggregated if p.symbol == "BTC"), None)
|
btc_info = next((p for p in aggregated if p.symbol == "BTC"), None)
|
||||||
@@ -81,7 +80,7 @@ class TestMarketDataAggregator:
|
|||||||
"Provider1": [],
|
"Provider1": [],
|
||||||
"Provider2": [],
|
"Provider2": [],
|
||||||
}
|
}
|
||||||
aggregated = aggregate_product_info(products)
|
aggregated = ProductInfo.aggregate(products)
|
||||||
assert len(aggregated) == 0
|
assert len(aggregated) == 0
|
||||||
|
|
||||||
def test_aggregate_product_info_with_partial_data(self):
|
def test_aggregate_product_info_with_partial_data(self):
|
||||||
@@ -89,7 +88,7 @@ class TestMarketDataAggregator:
|
|||||||
"Provider1": [self.__product("BTC", 50000.0, 1000.0, "USD")],
|
"Provider1": [self.__product("BTC", 50000.0, 1000.0, "USD")],
|
||||||
"Provider2": [],
|
"Provider2": [],
|
||||||
}
|
}
|
||||||
aggregated = aggregate_product_info(products)
|
aggregated = ProductInfo.aggregate(products)
|
||||||
assert len(aggregated) == 1
|
assert len(aggregated) == 1
|
||||||
info = aggregated[0]
|
info = aggregated[0]
|
||||||
assert info.symbol == "BTC"
|
assert info.symbol == "BTC"
|
||||||
@@ -120,7 +119,7 @@ class TestMarketDataAggregator:
|
|||||||
price.set_timestamp(timestamp_s=timestamp_2h_ago)
|
price.set_timestamp(timestamp_s=timestamp_2h_ago)
|
||||||
timestamp_2h_ago = price.timestamp
|
timestamp_2h_ago = price.timestamp
|
||||||
|
|
||||||
aggregated = aggregate_history_prices(prices)
|
aggregated = Price.aggregate(prices)
|
||||||
assert len(aggregated) == 2
|
assert len(aggregated) == 2
|
||||||
assert aggregated[0].timestamp == timestamp_1h_ago
|
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
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from app.utils.wrapper_handler import WrapperHandler
|
from app.api.wrapper_handler import WrapperHandler
|
||||||
|
|
||||||
class MockWrapper:
|
class MockWrapper:
|
||||||
def do_something(self) -> str:
|
def do_something(self) -> str:
|
||||||
|
|||||||
Reference in New Issue
Block a user