Lista di cryptovalute #36
@@ -32,7 +32,6 @@ models:
|
||||
api:
|
||||
retry_attempts: 3
|
||||
retry_delay_seconds: 2
|
||||
currency: USD
|
||||
# TODO Magari implementare un sistema per settare i providers
|
||||
market_providers: [BinanceWrapper, YFinanceWrapper]
|
||||
news_providers: [GoogleNewsWrapper, DuckDuckGoWrapper]
|
||||
|
||||
@@ -14,6 +14,7 @@ dependencies = [
|
||||
"dotenv", # Gestire variabili d'ambiente (generalmente API keys od opzioni)
|
||||
"gradio", # UI web semplice con user_input e output
|
||||
"colorlog", # Log colorati in console
|
||||
"html5lib", # Parsing HTML & Scraping
|
||||
|
||||
# Per costruire agenti (ovvero modelli che possono fare più cose tramite tool) https://github.com/agno-agi/agno
|
||||
# altamente consigliata dato che ha anche tools integrati per fare scraping, calcoli e molto altro
|
||||
|
||||
9570
resources/cryptos.csv
Normal file
@@ -96,7 +96,7 @@ class PipelineInputs:
|
||||
return Team(
|
||||
model=self.team_leader_model.get_model(TEAM_LEADER_INSTRUCTIONS),
|
||||
name="CryptoAnalysisTeam",
|
||||
tools=[ReasoningTools(), PlanMemoryTool()],
|
||||
tools=[ReasoningTools(), PlanMemoryTool(), CryptoSymbolsTools()],
|
||||
members=[market_agent, news_agent, social_agent],
|
||||
)
|
||||
|
||||
@@ -112,7 +112,7 @@ class PipelineInputs:
|
||||
"""
|
||||
api = self.configs.api
|
||||
|
||||
market_tool = MarketAPIsTool(currency=api.currency)
|
||||
market_tool = MarketAPIsTool()
|
||||
market_tool.handler.set_retries(api.retry_attempts, api.retry_delay_seconds)
|
||||
news_tool = NewsAPIsTool()
|
||||
news_tool.handler.set_retries(api.retry_attempts, api.retry_delay_seconds)
|
||||
|
||||
@@ -57,7 +57,9 @@ class BinanceWrapper(MarketWrapper):
|
||||
"""
|
||||
|
|
||||
Formatta l'asset_id nel formato richiesto da Binance.
|
||||
"""
|
||||
return asset_id.replace('-', '') if '-' in asset_id else f"{asset_id}{self.currency}"
|
||||
i = asset_id.find('-')
|
||||
if i != -1: asset_id = asset_id[:i]
|
||||
return f"{asset_id}{self.currency}" if self.currency not in asset_id else asset_id
|
||||
|
||||
def get_product(self, asset_id: str) -> ProductInfo:
|
||||
symbol = self.__format_symbol(asset_id)
|
||||
|
||||
@@ -61,7 +61,9 @@ class CoinBaseWrapper(MarketWrapper):
|
||||
)
|
||||
|
The The `str.index()` method raises a `ValueError` when the substring is not found, rather than returning -1. This will cause a runtime error when processing symbols without a hyphen. Use `asset_id.find('-')` instead, which returns -1 when not found, or handle the `ValueError` exception.
```suggestion
i = asset_id.find('-')
```
|
||||
|
||||
def __format(self, asset_id: str) -> str:
|
||||
return asset_id if '-' in asset_id else f"{asset_id}-{self.currency}"
|
||||
i = asset_id.find('-')
|
||||
if i != -1: asset_id = asset_id[:i]
|
||||
return f"{asset_id}-{self.currency}"
|
||||
|
||||
def get_product(self, asset_id: str) -> ProductInfo:
|
||||
asset_id = self.__format(asset_id)
|
||||
|
||||
@@ -47,8 +47,9 @@ class YFinanceWrapper(MarketWrapper):
|
||||
Formatta il simbolo per yfinance.
|
||||
|
The The `str.index()` method raises a `ValueError` when the substring is not found, rather than returning -1. This will cause a runtime error when processing symbols without a hyphen. Use `asset_id.find('-')` instead, which returns -1 when not found, or handle the `ValueError` exception.
```suggestion
i = asset_id.find('-')
```
|
||||
Per crypto, aggiunge '-' e la valuta (es. BTC -> BTC-USD).
|
||||
"""
|
||||
asset_id = asset_id.upper()
|
||||
return f"{asset_id}-{self.currency}" if '-' not in asset_id else asset_id
|
||||
i = asset_id.find('-')
|
||||
if i != -1: asset_id = asset_id[:i]
|
||||
return f"{asset_id}-{self.currency}"
|
||||
|
||||
def get_product(self, asset_id: str) -> ProductInfo:
|
||||
symbol = self._format_symbol(asset_id)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from app.api.tools.market_tool import MarketAPIsTool
|
||||
from app.api.tools.social_tool import SocialAPIsTool
|
||||
from app.api.tools.news_tool import NewsAPIsTool
|
||||
from app.api.tools.symbols_tool import CryptoSymbolsTools
|
||||
|
||||
__all__ = ["MarketAPIsTool", "NewsAPIsTool", "SocialAPIsTool"]
|
||||
__all__ = ["MarketAPIsTool", "NewsAPIsTool", "SocialAPIsTool", "CryptoSymbolsTools"]
|
||||
@@ -15,7 +15,7 @@ class MarketAPIsTool(MarketWrapper, Toolkit):
|
||||
- CryptoCompareWrapper
|
||||
"""
|
||||
|
||||
def __init__(self, currency: str = "USD"):
|
||||
def __init__(self):
|
||||
"""
|
||||
Initialize the MarketAPIsTool with multiple market API wrappers.
|
||||
The following wrappers are included in this order:
|
||||
@@ -23,12 +23,9 @@ class MarketAPIsTool(MarketWrapper, Toolkit):
|
||||
- YFinanceWrapper
|
||||
- CoinBaseWrapper
|
||||
- CryptoCompareWrapper
|
||||
Args:
|
||||
currency (str): Valuta in cui restituire i prezzi. Default è "USD".
|
||||
"""
|
||||
kwargs = {"currency": currency or "USD"}
|
||||
wrappers: list[type[MarketWrapper]] = [BinanceWrapper, YFinanceWrapper, CoinBaseWrapper, CryptoCompareWrapper]
|
||||
self.handler = WrapperHandler.build_wrappers(wrappers, kwargs=kwargs)
|
||||
self.handler = WrapperHandler.build_wrappers(wrappers)
|
||||
|
||||
Toolkit.__init__( # type: ignore
|
||||
self,
|
||||
|
||||
103
src/app/api/tools/symbols_tool.py
Normal file
@@ -0,0 +1,103 @@
|
||||
import os
|
||||
|
The return type documentation states 'list[str]' but the actual return type is 'list[tuple[str, str]]'. Update the docstring to reflect the correct return type. The return type documentation states 'list[str]' but the actual return type is 'list[tuple[str, str]]'. Update the docstring to reflect the correct return type.
```suggestion
list[tuple[str, str]]: Lista di tuple (simbolo, nome) che contengono la query.
```
Corrected grammar in comment from 'It looks like is the max' to 'It looks like this is the max'. Corrected grammar in comment from 'It looks like is the max' to 'It looks like this is the max'.
```suggestion
num_currencies = 250 # It looks like this is the max per page; otherwise, Yahoo returns 26
```
|
||||
import httpx
|
||||
import asyncio
|
||||
import logging
|
||||
import pandas as pd
|
||||
from io import StringIO
|
||||
from agno.tools.toolkit import Toolkit
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logging = logging.getLogger("crypto_symbols")
|
||||
|
||||
|
||||
|
||||
BASE_URL = "https://finance.yahoo.com/markets/crypto/all/"
|
||||
|
||||
class CryptoSymbolsTools(Toolkit):
|
||||
"""
|
||||
Classe per ottenere i simboli delle criptovalute tramite Yahoo Finance.
|
||||
"""
|
||||
|
||||
def __init__(self, cache_file: str = 'resources/cryptos.csv'):
|
||||
self.cache_file = cache_file
|
||||
self.final_table = pd.read_csv(self.cache_file) if os.path.exists(self.cache_file) else pd.DataFrame() # type: ignore
|
||||
|
[nitpick] This line is overly complex with multiple operations. Consider splitting it into separate lines for better readability: check file existence, then read CSV or create empty DataFrame. [nitpick] This line is overly complex with multiple operations. Consider splitting it into separate lines for better readability: check file existence, then read CSV or create empty DataFrame.
```suggestion
if os.path.exists(self.cache_file):
self.final_table = pd.read_csv(self.cache_file) # type: ignore
else:
self.final_table = pd.DataFrame() # type: ignore
```
|
||||
Toolkit.__init__(self, # type: ignore
|
||||
name="Crypto Symbols Tool",
|
||||
instructions="Tool to get cryptocurrency symbols and search them by name.",
|
||||
tools=[
|
||||
self.get_all_symbols,
|
||||
self.get_symbols_by_name,
|
||||
],
|
||||
)
|
||||
|
||||
def get_all_symbols(self) -> list[str]:
|
||||
"""
|
||||
Restituisce tutti i simboli delle criptovalute.
|
||||
Returns:
|
||||
list[str]: Lista di tutti i simboli delle criptovalute.
|
||||
"""
|
||||
return self.final_table['Symbol'].tolist() if not self.final_table.empty else []
|
||||
|
||||
def get_symbols_by_name(self, query: str) -> list[tuple[str, str]]:
|
||||
"""
|
||||
Cerca i simboli che contengono la query.
|
||||
Args:
|
||||
query (str): Query di ricerca.
|
||||
Returns:
|
||||
list[tuple[str, str]]: Lista di tuple (simbolo, nome) che contengono la query.
|
||||
"""
|
||||
query_lower = query.lower()
|
||||
positions = self.final_table['Name'].str.lower().str.contains(query_lower)
|
||||
return self.final_table[positions][['Symbol', 'Name']].apply(tuple, axis=1).tolist()
|
||||
|
||||
async def fetch_crypto_symbols(self, force_refresh: bool = False) -> None:
|
||||
"""
|
||||
Recupera tutti i simboli delle criptovalute da Yahoo Finance e li memorizza in cache.
|
||||
Args:
|
||||
force_refresh (bool): Se True, forza il recupero anche se i dati sono già in cache.
|
||||
"""
|
||||
if not force_refresh and not self.final_table.empty:
|
||||
return
|
||||
|
||||
num_currencies = 250 # It looks like this is the max per page otherwise yahoo returns 26
|
||||
offset = 0
|
||||
stop = not self.final_table.empty
|
||||
table = self.final_table.copy()
|
||||
|
||||
while not stop:
|
||||
text = await self.___request(offset, num_currencies)
|
||||
tables = pd.read_html(text) # type: ignore
|
||||
df = tables[0]
|
||||
df.columns = table.columns if not table.empty else df.columns
|
||||
table = pd.concat([table, df], ignore_index=True)
|
||||
|
||||
total_rows = df.shape[0]
|
||||
offset += total_rows
|
||||
if total_rows < num_currencies:
|
||||
stop = True
|
||||
|
||||
table.dropna(axis=0, how='all', inplace=True) # type: ignore
|
||||
table.dropna(axis=1, how='all', inplace=True) # type: ignore
|
||||
table.to_csv(self.cache_file, index=False)
|
||||
self.final_table = table
|
||||
|
||||
async def ___request(self, offset: int, num_currencies: int) -> StringIO:
|
||||
while True:
|
||||
async with httpx.AsyncClient() as client:
|
||||
resp = await client.get(f"{BASE_URL}?start={offset}&count={num_currencies}", headers={"User-Agent": "Mozilla/5.0"})
|
||||
if resp.status_code == 429: # Too many requests
|
||||
secs = int(resp.headers.get("Retry-After", 2))
|
||||
logging.warning(f"Rate limit exceeded, waiting {secs}s before retrying...")
|
||||
await asyncio.sleep(secs)
|
||||
continue
|
||||
if resp.status_code != 200:
|
||||
logging.error(f"Error fetching crypto symbols: [{resp.status_code}] {resp.text}")
|
||||
break
|
||||
return StringIO(resp.text)
|
||||
return StringIO("")
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
crypto_symbols = CryptoSymbolsTools()
|
||||
asyncio.run(crypto_symbols.fetch_crypto_symbols(force_refresh=True))
|
||||
@@ -57,7 +57,6 @@ class AppModel(BaseModel):
|
||||
class APIConfig(BaseModel):
|
||||
retry_attempts: int = 3
|
||||
retry_delay_seconds: int = 2
|
||||
currency: str = "USD"
|
||||
|
||||
class Strategy(BaseModel):
|
||||
name: str = "Conservative"
|
||||
|
||||
27
tests/tools/test_crypto_symbols.py
Normal file
@@ -0,0 +1,27 @@
|
||||
import pytest
|
||||
from app.api.tools import CryptoSymbolsTools
|
||||
|
||||
@pytest.mark.tools
|
||||
class TestCryptoSymbolsTools:
|
||||
|
||||
def test_get_symbols(self):
|
||||
tool = CryptoSymbolsTools()
|
||||
symbols = tool.get_all_symbols()
|
||||
assert isinstance(symbols, list)
|
||||
assert "BTC-USD" in symbols
|
||||
|
||||
def test_get_symbol_by_name(self):
|
||||
tool = CryptoSymbolsTools()
|
||||
results = tool.get_symbols_by_name("Bitcoin")
|
||||
assert isinstance(results, list)
|
||||
assert ("BTC-USD", "Bitcoin USD") in results
|
||||
|
||||
results = tool.get_symbols_by_name("Banana")
|
||||
assert isinstance(results, list)
|
||||
assert ("BANANA28886-USD", "BananaCoin USD") in results
|
||||
|
||||
def test_get_symbol_by_invalid_name(self):
|
||||
tool = CryptoSymbolsTools()
|
||||
results = tool.get_symbols_by_name("InvalidName")
|
||||
assert isinstance(results, list)
|
||||
assert not results
|
||||
24
uv.lock
generated
@@ -690,6 +690,19 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/07/c6/80c95b1b2b94682a72cbdbfb85b81ae2daffa4291fbfa1b1464502ede10d/hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496", size = 34357, upload-time = "2025-01-22T21:44:56.92Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "html5lib"
|
||||
version = "1.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "six" },
|
||||
{ name = "webencodings" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ac/b6/b55c3f49042f1df3dcd422b7f224f939892ee94f22abcf503a9b7339eaf2/html5lib-1.1.tar.gz", hash = "sha256:b2e5b40261e20f354d198eae92afc10d750afb487ed5e50f9c4eaf07c184146f", size = 272215, upload-time = "2020-06-22T23:32:38.834Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/dd/a834df6482147d48e225a49515aabc28974ad5a4ca3215c18a882565b028/html5lib-1.1-py2.py3-none-any.whl", hash = "sha256:0d78f8fde1c230e99fe37986a60526d7049ed4bf8a9fadbad5f00e22e58e041d", size = 112173, upload-time = "2020-06-22T23:32:36.781Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpcore"
|
||||
version = "1.0.9"
|
||||
@@ -1662,6 +1675,7 @@ dependencies = [
|
||||
{ name = "gnews" },
|
||||
{ name = "google-genai" },
|
||||
{ name = "gradio" },
|
||||
{ name = "html5lib" },
|
||||
{ name = "markdown-pdf" },
|
||||
{ name = "newsapi-python" },
|
||||
{ name = "ollama" },
|
||||
@@ -1682,6 +1696,7 @@ requires-dist = [
|
||||
{ name = "gnews" },
|
||||
{ name = "google-genai" },
|
||||
{ name = "gradio" },
|
||||
{ name = "html5lib" },
|
||||
{ name = "markdown-pdf" },
|
||||
{ name = "newsapi-python" },
|
||||
{ name = "ollama" },
|
||||
@@ -1714,6 +1729,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/85/cd/584a2ceb5532af99dd09e50919e3615ba99aa127e9850eafe5f31ddfdb9a/uvicorn-0.37.0-py3-none-any.whl", hash = "sha256:913b2b88672343739927ce381ff9e2ad62541f9f8289664fa1d1d3803fa2ce6c", size = 67976, upload-time = "2025-09-23T13:33:45.842Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webencodings"
|
||||
version = "0.5.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721, upload-time = "2017-04-05T20:21:34.189Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774, upload-time = "2017-04-05T20:21:32.581Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "websocket-client"
|
||||
version = "1.8.0"
|
||||
|
||||
The
str.index()method raises aValueErrorwhen the substring is not found, rather than returning -1. This will cause a runtime error when processing symbols without a hyphen. Useasset_id.find('-')instead, which returns -1 when not found, or handle theValueErrorexception.