Lista di cryptovalute (#36)
* CryptoSymbolsTool per recuperare TUTTI i simboli delle criptovalute da Yahoo Finance * CryptoSymbolsTools integrazione nel sistema di strumenti. * Removed currency from config * fix format symbol in all markets
This commit was merged in pull request #36.
This commit is contained in:
committed by
GitHub
parent
12339ccbff
commit
2e092d3f25
@@ -32,7 +32,6 @@ models:
|
|||||||
api:
|
api:
|
||||||
retry_attempts: 3
|
retry_attempts: 3
|
||||||
retry_delay_seconds: 2
|
retry_delay_seconds: 2
|
||||||
currency: USD
|
|
||||||
# TODO Magari implementare un sistema per settare i providers
|
# TODO Magari implementare un sistema per settare i providers
|
||||||
market_providers: [BinanceWrapper, YFinanceWrapper]
|
market_providers: [BinanceWrapper, YFinanceWrapper]
|
||||||
news_providers: [GoogleNewsWrapper, DuckDuckGoWrapper]
|
news_providers: [GoogleNewsWrapper, DuckDuckGoWrapper]
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ dependencies = [
|
|||||||
"dotenv", # Gestire variabili d'ambiente (generalmente API keys od opzioni)
|
"dotenv", # Gestire variabili d'ambiente (generalmente API keys od opzioni)
|
||||||
"gradio", # UI web semplice con user_input e output
|
"gradio", # UI web semplice con user_input e output
|
||||||
"colorlog", # Log colorati in console
|
"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
|
# 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
|
# altamente consigliata dato che ha anche tools integrati per fare scraping, calcoli e molto altro
|
||||||
|
|||||||
9570
resources/cryptos.csv
Normal file
9570
resources/cryptos.csv
Normal file
File diff suppressed because it is too large
Load Diff
@@ -96,7 +96,7 @@ class PipelineInputs:
|
|||||||
return Team(
|
return Team(
|
||||||
model=self.team_leader_model.get_model(TEAM_LEADER_INSTRUCTIONS),
|
model=self.team_leader_model.get_model(TEAM_LEADER_INSTRUCTIONS),
|
||||||
name="CryptoAnalysisTeam",
|
name="CryptoAnalysisTeam",
|
||||||
tools=[ReasoningTools(), PlanMemoryTool()],
|
tools=[ReasoningTools(), PlanMemoryTool(), CryptoSymbolsTools()],
|
||||||
members=[market_agent, news_agent, social_agent],
|
members=[market_agent, news_agent, social_agent],
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -112,7 +112,7 @@ class PipelineInputs:
|
|||||||
"""
|
"""
|
||||||
api = self.configs.api
|
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)
|
market_tool.handler.set_retries(api.retry_attempts, api.retry_delay_seconds)
|
||||||
news_tool = NewsAPIsTool()
|
news_tool = NewsAPIsTool()
|
||||||
news_tool.handler.set_retries(api.retry_attempts, api.retry_delay_seconds)
|
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.
|
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:
|
def get_product(self, asset_id: str) -> ProductInfo:
|
||||||
symbol = self.__format_symbol(asset_id)
|
symbol = self.__format_symbol(asset_id)
|
||||||
|
|||||||
@@ -61,7 +61,9 @@ class CoinBaseWrapper(MarketWrapper):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def __format(self, asset_id: str) -> str:
|
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:
|
def get_product(self, asset_id: str) -> ProductInfo:
|
||||||
asset_id = self.__format(asset_id)
|
asset_id = self.__format(asset_id)
|
||||||
|
|||||||
@@ -47,8 +47,9 @@ class YFinanceWrapper(MarketWrapper):
|
|||||||
Formatta il simbolo per yfinance.
|
Formatta il simbolo per yfinance.
|
||||||
Per crypto, aggiunge '-' e la valuta (es. BTC -> BTC-USD).
|
Per crypto, aggiunge '-' e la valuta (es. BTC -> BTC-USD).
|
||||||
"""
|
"""
|
||||||
asset_id = asset_id.upper()
|
i = asset_id.find('-')
|
||||||
return f"{asset_id}-{self.currency}" if '-' not in asset_id else asset_id
|
if i != -1: asset_id = asset_id[:i]
|
||||||
|
return f"{asset_id}-{self.currency}"
|
||||||
|
|
||||||
def get_product(self, asset_id: str) -> ProductInfo:
|
def get_product(self, asset_id: str) -> ProductInfo:
|
||||||
symbol = self._format_symbol(asset_id)
|
symbol = self._format_symbol(asset_id)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from app.api.tools.market_tool import MarketAPIsTool
|
from app.api.tools.market_tool import MarketAPIsTool
|
||||||
from app.api.tools.social_tool import SocialAPIsTool
|
from app.api.tools.social_tool import SocialAPIsTool
|
||||||
from app.api.tools.news_tool import NewsAPIsTool
|
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
|
- CryptoCompareWrapper
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, currency: str = "USD"):
|
def __init__(self):
|
||||||
"""
|
"""
|
||||||
Initialize the MarketAPIsTool with multiple market API wrappers.
|
Initialize the MarketAPIsTool with multiple market API wrappers.
|
||||||
The following wrappers are included in this order:
|
The following wrappers are included in this order:
|
||||||
@@ -23,12 +23,9 @@ class MarketAPIsTool(MarketWrapper, Toolkit):
|
|||||||
- YFinanceWrapper
|
- YFinanceWrapper
|
||||||
- CoinBaseWrapper
|
- CoinBaseWrapper
|
||||||
- CryptoCompareWrapper
|
- 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]
|
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
|
Toolkit.__init__( # type: ignore
|
||||||
self,
|
self,
|
||||||
|
|||||||
103
src/app/api/tools/symbols_tool.py
Normal file
103
src/app/api/tools/symbols_tool.py
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import os
|
||||||
|
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
|
||||||
|
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):
|
class APIConfig(BaseModel):
|
||||||
retry_attempts: int = 3
|
retry_attempts: int = 3
|
||||||
retry_delay_seconds: int = 2
|
retry_delay_seconds: int = 2
|
||||||
currency: str = "USD"
|
|
||||||
|
|
||||||
class Strategy(BaseModel):
|
class Strategy(BaseModel):
|
||||||
name: str = "Conservative"
|
name: str = "Conservative"
|
||||||
|
|||||||
27
tests/tools/test_crypto_symbols.py
Normal file
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
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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "httpcore"
|
name = "httpcore"
|
||||||
version = "1.0.9"
|
version = "1.0.9"
|
||||||
@@ -1662,6 +1675,7 @@ dependencies = [
|
|||||||
{ name = "gnews" },
|
{ name = "gnews" },
|
||||||
{ name = "google-genai" },
|
{ name = "google-genai" },
|
||||||
{ name = "gradio" },
|
{ name = "gradio" },
|
||||||
|
{ name = "html5lib" },
|
||||||
{ name = "markdown-pdf" },
|
{ name = "markdown-pdf" },
|
||||||
{ name = "newsapi-python" },
|
{ name = "newsapi-python" },
|
||||||
{ name = "ollama" },
|
{ name = "ollama" },
|
||||||
@@ -1682,6 +1696,7 @@ requires-dist = [
|
|||||||
{ name = "gnews" },
|
{ name = "gnews" },
|
||||||
{ name = "google-genai" },
|
{ name = "google-genai" },
|
||||||
{ name = "gradio" },
|
{ name = "gradio" },
|
||||||
|
{ name = "html5lib" },
|
||||||
{ name = "markdown-pdf" },
|
{ name = "markdown-pdf" },
|
||||||
{ name = "newsapi-python" },
|
{ name = "newsapi-python" },
|
||||||
{ name = "ollama" },
|
{ 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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "websocket-client"
|
name = "websocket-client"
|
||||||
version = "1.8.0"
|
version = "1.8.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user