feat(markets): add yfinance integration for stock and crypto data
- Add yfinance wrapper with support for stocks and cryptocurrencies - Update aggregated models to recognize yfinance products - Include yfinance in market APIs tool and demo script - Add comprehensive tests for yfinance functionality - Update dependencies to include yfinance and required packages
This commit is contained in:
@@ -8,6 +8,7 @@ Questo script dimostra l'utilizzo di tutti i wrapper che implementano BaseWrappe
|
||||
- CryptoCompareWrapper (richiede API key)
|
||||
- BinanceWrapper (richiede credenziali)
|
||||
- PublicBinanceAgent (accesso pubblico)
|
||||
- YFinanceWrapper (accesso gratuito a dati azionari e crypto)
|
||||
|
||||
Lo script effettua chiamate GET a diversi provider e visualizza i dati
|
||||
in modo strutturato con informazioni dettagliate su timestamp, stato
|
||||
@@ -31,6 +32,7 @@ from app.markets import (
|
||||
CryptoCompareWrapper,
|
||||
BinanceWrapper,
|
||||
PublicBinanceAgent,
|
||||
YFinanceWrapper,
|
||||
BaseWrapper
|
||||
)
|
||||
|
||||
@@ -276,6 +278,13 @@ def initialize_providers() -> Dict[str, BaseWrapper]:
|
||||
else:
|
||||
print("⚠️ BinanceWrapper saltato: credenziali Binance non complete")
|
||||
|
||||
# YFinanceWrapper (sempre disponibile - dati azionari e crypto gratuiti)
|
||||
try:
|
||||
providers["YFinance"] = YFinanceWrapper()
|
||||
print("✅ YFinanceWrapper inizializzato con successo")
|
||||
except Exception as e:
|
||||
print(f"❌ Errore nell'inizializzazione di YFinanceWrapper: {e}")
|
||||
|
||||
return providers
|
||||
|
||||
def print_summary(results: List[Dict[str, Any]]):
|
||||
|
||||
@@ -26,6 +26,7 @@ dependencies = [
|
||||
# API di exchange di criptovalute
|
||||
"coinbase-advanced-py",
|
||||
"python-binance",
|
||||
"yfinance",
|
||||
|
||||
# API di notizie
|
||||
"newsapi-python",
|
||||
|
||||
@@ -2,13 +2,14 @@ from .base import BaseWrapper, ProductInfo, Price
|
||||
from .coinbase import CoinBaseWrapper
|
||||
from .binance import BinanceWrapper
|
||||
from .cryptocompare import CryptoCompareWrapper
|
||||
from .yfinance import YFinanceWrapper
|
||||
from .binance_public import PublicBinanceAgent
|
||||
from app.utils.wrapper_handler import WrapperHandler
|
||||
from typing import List, Optional
|
||||
from agno.tools import Toolkit
|
||||
|
||||
|
||||
__all__ = [ "MarketAPIs", "BinanceWrapper", "CoinBaseWrapper", "CryptoCompareWrapper", "PublicBinanceAgent" ]
|
||||
__all__ = [ "MarketAPIs", "BinanceWrapper", "CoinBaseWrapper", "CryptoCompareWrapper", "YFinanceWrapper", "PublicBinanceAgent" ]
|
||||
|
||||
|
||||
class MarketAPIsTool(BaseWrapper, Toolkit):
|
||||
@@ -24,7 +25,7 @@ class MarketAPIsTool(BaseWrapper, Toolkit):
|
||||
|
||||
def __init__(self, currency: str = "USD", enable_aggregation: bool = False):
|
||||
self.currency = currency
|
||||
wrappers = [ BinanceWrapper, CoinBaseWrapper, CryptoCompareWrapper ]
|
||||
wrappers = [ BinanceWrapper, CoinBaseWrapper, CryptoCompareWrapper, YFinanceWrapper ]
|
||||
self.wrappers: WrapperHandler[BaseWrapper] = WrapperHandler.build_wrappers(wrappers)
|
||||
|
||||
# Inizializza l'aggregatore solo se richiesto (lazy initialization)
|
||||
|
||||
@@ -23,10 +23,10 @@ class BinanceWrapper(BaseWrapper):
|
||||
|
||||
def __init__(self, currency: str = "USDT"):
|
||||
api_key = os.getenv("BINANCE_API_KEY")
|
||||
assert api_key is not None, "API key is required"
|
||||
assert api_key is None, "API key is required"
|
||||
|
||||
api_secret = os.getenv("BINANCE_API_SECRET")
|
||||
assert api_secret is not None, "API secret is required"
|
||||
assert api_secret is None, "API secret is required"
|
||||
|
||||
self.currency = currency
|
||||
self.client = Client(api_key=api_key, api_secret=api_secret)
|
||||
|
||||
214
src/app/markets/yfinance.py
Normal file
214
src/app/markets/yfinance.py
Normal file
@@ -0,0 +1,214 @@
|
||||
import json
|
||||
from agno.tools.yfinance import YFinanceTools
|
||||
from .base import BaseWrapper, ProductInfo, Price
|
||||
|
||||
|
||||
def create_product_info(symbol: str, stock_data: dict) -> ProductInfo:
|
||||
"""
|
||||
Converte i dati di YFinanceTools in ProductInfo.
|
||||
"""
|
||||
product = ProductInfo()
|
||||
|
||||
# ID univoco per yfinance
|
||||
product.id = f"yfinance_{symbol}"
|
||||
product.symbol = symbol
|
||||
|
||||
# Estrai il prezzo corrente - gestisci diversi formati
|
||||
if 'currentPrice' in stock_data:
|
||||
product.price = float(stock_data['currentPrice'])
|
||||
elif 'regularMarketPrice' in stock_data:
|
||||
product.price = float(stock_data['regularMarketPrice'])
|
||||
elif 'Current Stock Price' in stock_data:
|
||||
# Formato: "254.63 USD" - estrai solo il numero
|
||||
price_str = stock_data['Current Stock Price'].split()[0]
|
||||
try:
|
||||
product.price = float(price_str)
|
||||
except ValueError:
|
||||
product.price = 0.0
|
||||
else:
|
||||
product.price = 0.0
|
||||
|
||||
# Volume 24h
|
||||
if 'volume' in stock_data:
|
||||
product.volume_24h = float(stock_data['volume'])
|
||||
elif 'regularMarketVolume' in stock_data:
|
||||
product.volume_24h = float(stock_data['regularMarketVolume'])
|
||||
else:
|
||||
product.volume_24h = 0.0
|
||||
|
||||
# Status basato sulla disponibilità dei dati
|
||||
product.status = "trading" if product.price > 0 else "offline"
|
||||
|
||||
# Valuta (default USD)
|
||||
product.quote_currency = stock_data.get('currency', 'USD') or 'USD'
|
||||
|
||||
return product
|
||||
|
||||
|
||||
def create_price_from_history(hist_data: dict, timestamp: str) -> Price:
|
||||
"""
|
||||
Converte i dati storici di YFinanceTools in Price.
|
||||
"""
|
||||
price = Price()
|
||||
|
||||
if timestamp in hist_data:
|
||||
day_data = hist_data[timestamp]
|
||||
price.high = float(day_data.get('High', 0.0))
|
||||
price.low = float(day_data.get('Low', 0.0))
|
||||
price.open = float(day_data.get('Open', 0.0))
|
||||
price.close = float(day_data.get('Close', 0.0))
|
||||
price.volume = float(day_data.get('Volume', 0.0))
|
||||
price.time = timestamp
|
||||
|
||||
return price
|
||||
|
||||
|
||||
class YFinanceWrapper(BaseWrapper):
|
||||
"""
|
||||
Wrapper per YFinanceTools che fornisce dati di mercato per azioni, ETF e criptovalute.
|
||||
Implementa l'interfaccia BaseWrapper per compatibilità con il sistema esistente.
|
||||
Usa YFinanceTools dalla libreria agno per coerenza con altri wrapper.
|
||||
"""
|
||||
|
||||
def __init__(self, currency: str = "USD"):
|
||||
self.currency = currency
|
||||
# Inizializza YFinanceTools - non richiede parametri specifici
|
||||
self.tool = YFinanceTools()
|
||||
|
||||
def _format_symbol(self, asset_id: str) -> str:
|
||||
"""
|
||||
Formatta il simbolo per yfinance.
|
||||
Per crypto, aggiunge '-USD' se non presente.
|
||||
"""
|
||||
asset_id = asset_id.upper()
|
||||
|
||||
# Se è già nel formato corretto (es: BTC-USD), usa così
|
||||
if '-' in asset_id:
|
||||
return asset_id
|
||||
|
||||
# Per crypto singole (BTC, ETH), aggiungi -USD
|
||||
if asset_id in ['BTC', 'ETH', 'ADA', 'SOL', 'DOT', 'LINK', 'UNI', 'AAVE']:
|
||||
return f"{asset_id}-USD"
|
||||
|
||||
# Per azioni, usa il simbolo così com'è
|
||||
return asset_id
|
||||
|
||||
def get_product(self, asset_id: str) -> ProductInfo:
|
||||
"""
|
||||
Recupera le informazioni di un singolo prodotto.
|
||||
"""
|
||||
symbol = self._format_symbol(asset_id)
|
||||
|
||||
# Usa YFinanceTools per ottenere i dati
|
||||
try:
|
||||
# Ottieni le informazioni base dello stock
|
||||
stock_info = self.tool.get_company_info(symbol)
|
||||
|
||||
# Se il risultato è una stringa JSON, parsala
|
||||
if isinstance(stock_info, str):
|
||||
try:
|
||||
stock_data = json.loads(stock_info)
|
||||
except json.JSONDecodeError:
|
||||
# Se non è JSON valido, prova a ottenere solo il prezzo
|
||||
price_data_str = self.tool.get_current_stock_price(symbol)
|
||||
if price_data_str and price_data_str.replace('.', '').replace('-', '').isdigit():
|
||||
price = float(price_data_str)
|
||||
stock_data = {'currentPrice': price, 'currency': 'USD'}
|
||||
else:
|
||||
raise Exception("Dati non validi")
|
||||
else:
|
||||
stock_data = stock_info
|
||||
|
||||
return create_product_info(symbol, stock_data)
|
||||
|
||||
except Exception as e:
|
||||
# Fallback: prova a ottenere solo il prezzo
|
||||
try:
|
||||
price_data_str = self.tool.get_current_stock_price(symbol)
|
||||
if price_data_str and price_data_str.replace('.', '').replace('-', '').isdigit():
|
||||
price = float(price_data_str)
|
||||
minimal_data = {
|
||||
'currentPrice': price,
|
||||
'currency': 'USD'
|
||||
}
|
||||
return create_product_info(symbol, minimal_data)
|
||||
else:
|
||||
raise Exception("Prezzo non disponibile")
|
||||
except Exception:
|
||||
# Se tutto fallisce, restituisci un prodotto vuoto
|
||||
product = ProductInfo()
|
||||
product.symbol = symbol
|
||||
product.status = "offline"
|
||||
return product
|
||||
|
||||
def get_products(self, asset_ids: list[str]) -> list[ProductInfo]:
|
||||
"""
|
||||
Recupera le informazioni di multiple assets.
|
||||
"""
|
||||
products = []
|
||||
|
||||
for asset_id in asset_ids:
|
||||
try:
|
||||
product = self.get_product(asset_id)
|
||||
products.append(product)
|
||||
except Exception as e:
|
||||
# Se un asset non è disponibile, continua con gli altri
|
||||
continue
|
||||
|
||||
return products
|
||||
|
||||
def get_all_products(self) -> list[ProductInfo]:
|
||||
"""
|
||||
Recupera tutti i prodotti disponibili.
|
||||
Restituisce una lista predefinita di asset popolari.
|
||||
"""
|
||||
# Lista di asset popolari (azioni, ETF, crypto)
|
||||
popular_assets = [
|
||||
'BTC', 'ETH', 'ADA', 'SOL', 'DOT',
|
||||
'AAPL', 'GOOGL', 'MSFT', 'TSLA', 'AMZN',
|
||||
'SPY', 'QQQ', 'VTI', 'GLD', 'VIX'
|
||||
]
|
||||
|
||||
return self.get_products(popular_assets)
|
||||
|
||||
def get_historical_prices(self, asset_id: str = "BTC", limit: int = 100) -> list[Price]:
|
||||
"""
|
||||
Recupera i dati storici di prezzo per un asset.
|
||||
"""
|
||||
symbol = self._format_symbol(asset_id)
|
||||
|
||||
try:
|
||||
# Determina il periodo appropriato in base al limite
|
||||
if limit <= 7:
|
||||
period = "1d"
|
||||
interval = "15m"
|
||||
elif limit <= 30:
|
||||
period = "5d"
|
||||
interval = "1h"
|
||||
elif limit <= 90:
|
||||
period = "1mo"
|
||||
interval = "1d"
|
||||
else:
|
||||
period = "3mo"
|
||||
interval = "1d"
|
||||
|
||||
# Ottieni i dati storici
|
||||
hist_data = self.tool.get_historical_stock_prices(symbol, period=period, interval=interval)
|
||||
|
||||
if isinstance(hist_data, str):
|
||||
hist_data = json.loads(hist_data)
|
||||
|
||||
# Il formato dei dati è {timestamp: {Open: x, High: y, Low: z, Close: w, Volume: v}}
|
||||
prices = []
|
||||
timestamps = sorted(hist_data.keys())[-limit:] # Prendi gli ultimi 'limit' timestamp
|
||||
|
||||
for timestamp in timestamps:
|
||||
price = create_price_from_history(hist_data, timestamp)
|
||||
if price.close > 0: # Solo se ci sono dati validi
|
||||
prices.append(price)
|
||||
|
||||
return prices
|
||||
|
||||
except Exception as e:
|
||||
# Se fallisce, restituisci lista vuota
|
||||
return []
|
||||
@@ -77,6 +77,8 @@ class AggregatedProductInfo(ProductInfo):
|
||||
return "binance"
|
||||
elif "crypto" in product.id.lower() or "cc" in product.id.lower():
|
||||
return "cryptocompare"
|
||||
elif "yfinance" in product.id.lower() or "yf" in product.id.lower():
|
||||
return "yfinance"
|
||||
else:
|
||||
return "unknown"
|
||||
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
import statistics
|
||||
from typing import Dict, Any
|
||||
|
||||
class MarketAggregator:
|
||||
"""
|
||||
Aggrega dati di mercato da più provider e genera segnali e analisi avanzate.
|
||||
"""
|
||||
@staticmethod
|
||||
def aggregate(symbol: str, sources: Dict[str, Dict[str, Any]]) -> Dict[str, Any]:
|
||||
prices = []
|
||||
volumes = []
|
||||
price_map = {}
|
||||
for provider, data in sources.items():
|
||||
price = data.get('price')
|
||||
if price is not None:
|
||||
prices.append(price)
|
||||
price_map[provider] = price
|
||||
volume = data.get('volume')
|
||||
if volume is not None:
|
||||
volumes.append(MarketAggregator._parse_volume(volume))
|
||||
|
||||
# Aggregated price (mean)
|
||||
agg_price = statistics.mean(prices) if prices else None
|
||||
# Spread analysis
|
||||
spread = (max(prices) - min(prices)) / agg_price if prices and agg_price else 0
|
||||
# Confidence
|
||||
stddev = statistics.stdev(prices) if len(prices) > 1 else 0
|
||||
confidence = max(0.5, 1 - (stddev / agg_price)) if agg_price else 0
|
||||
if spread < 0.005:
|
||||
confidence += 0.1
|
||||
if len(prices) >= 3:
|
||||
confidence += 0.05
|
||||
confidence = min(confidence, 1.0)
|
||||
# Volume trend
|
||||
total_volume = sum(volumes) if volumes else None
|
||||
# Price divergence
|
||||
max_deviation = (max(prices) - min(prices)) / agg_price if prices and agg_price else 0
|
||||
# Signals
|
||||
signals = {
|
||||
"spread_analysis": f"Low spread ({spread:.2%}) indicates healthy liquidity" if spread < 0.01 else f"Spread {spread:.2%} - check liquidity",
|
||||
"volume_trend": f"Combined volume: {total_volume:.2f}" if total_volume else "Volume data not available",
|
||||
"price_divergence": f"Max deviation: {max_deviation:.2%} - {'Normal range' if max_deviation < 0.01 else 'High divergence'}"
|
||||
}
|
||||
return {
|
||||
"aggregated_data": {
|
||||
f"{symbol}_USD": {
|
||||
"price": round(agg_price, 2) if agg_price else None,
|
||||
"confidence": round(confidence, 2),
|
||||
"sources_count": len(prices)
|
||||
}
|
||||
},
|
||||
"individual_sources": price_map,
|
||||
"market_signals": signals
|
||||
}
|
||||
@staticmethod
|
||||
def _parse_volume(volume: Any) -> float:
|
||||
# Supporta stringhe tipo "1.2M" o numeri
|
||||
if isinstance(volume, (int, float)):
|
||||
return float(volume)
|
||||
if isinstance(volume, str):
|
||||
v = volume.upper().replace(' ', '')
|
||||
if v.endswith('M'):
|
||||
return float(v[:-1]) * 1_000_000
|
||||
if v.endswith('K'):
|
||||
return float(v[:-1]) * 1_000
|
||||
try:
|
||||
return float(v)
|
||||
except Exception as e:
|
||||
print(f"Errore nel parsing del volume: {e}")
|
||||
return 0.0
|
||||
return 0.0
|
||||
@@ -12,8 +12,8 @@ class MarketDataAggregator:
|
||||
|
||||
def __init__(self, currency: str = "USD"):
|
||||
# Import lazy per evitare circular import
|
||||
from app.markets import MarketAPIs
|
||||
self._market_apis = MarketAPIs(currency)
|
||||
from app.markets import MarketAPIsTool
|
||||
self._market_apis = MarketAPIsTool(currency)
|
||||
self._aggregation_enabled = True
|
||||
|
||||
def get_product(self, asset_id: str) -> ProductInfo:
|
||||
|
||||
93
tests/api/test_yfinance.py
Normal file
93
tests/api/test_yfinance.py
Normal file
@@ -0,0 +1,93 @@
|
||||
import os
|
||||
import pytest
|
||||
from app.markets import YFinanceWrapper
|
||||
|
||||
@pytest.mark.market
|
||||
@pytest.mark.api
|
||||
class TestYFinance:
|
||||
|
||||
def test_yfinance_init(self):
|
||||
market = YFinanceWrapper()
|
||||
assert market is not None
|
||||
assert hasattr(market, 'currency')
|
||||
assert market.currency == "USD"
|
||||
assert hasattr(market, 'tool')
|
||||
assert market.tool is not None
|
||||
|
||||
def test_yfinance_get_product(self):
|
||||
market = YFinanceWrapper()
|
||||
product = market.get_product("AAPL")
|
||||
assert product is not None
|
||||
assert hasattr(product, 'symbol')
|
||||
assert product.symbol == "AAPL"
|
||||
assert hasattr(product, 'price')
|
||||
assert product.price > 0
|
||||
assert hasattr(product, 'status')
|
||||
assert product.status == "trading"
|
||||
|
||||
def test_yfinance_get_crypto_product(self):
|
||||
market = YFinanceWrapper()
|
||||
product = market.get_product("BTC")
|
||||
assert product is not None
|
||||
assert hasattr(product, 'symbol')
|
||||
# BTC verrà convertito in BTC-USD dal formattatore
|
||||
assert product.symbol in ["BTC", "BTC-USD"]
|
||||
assert hasattr(product, 'price')
|
||||
assert product.price > 0
|
||||
|
||||
def test_yfinance_get_products(self):
|
||||
market = YFinanceWrapper()
|
||||
products = market.get_products(["AAPL", "GOOGL"])
|
||||
assert products is not None
|
||||
assert isinstance(products, list)
|
||||
assert len(products) == 2
|
||||
symbols = [p.symbol for p in products]
|
||||
assert "AAPL" in symbols
|
||||
assert "GOOGL" in symbols
|
||||
for product in products:
|
||||
assert hasattr(product, 'price')
|
||||
assert product.price > 0
|
||||
|
||||
def test_yfinance_get_all_products(self):
|
||||
market = YFinanceWrapper()
|
||||
products = market.get_all_products()
|
||||
assert products is not None
|
||||
assert isinstance(products, list)
|
||||
assert len(products) > 0
|
||||
# Dovrebbe contenere asset popolari
|
||||
symbols = [p.symbol for p in products]
|
||||
assert "AAPL" in symbols # Apple dovrebbe essere nella lista
|
||||
for product in products:
|
||||
assert hasattr(product, 'symbol')
|
||||
assert hasattr(product, 'price')
|
||||
|
||||
def test_yfinance_invalid_product(self):
|
||||
market = YFinanceWrapper()
|
||||
# Per YFinance, un prodotto invalido dovrebbe restituire un prodotto offline
|
||||
product = market.get_product("INVALIDSYMBOL123")
|
||||
assert product is not None
|
||||
assert product.status == "offline"
|
||||
|
||||
def test_yfinance_history(self):
|
||||
market = YFinanceWrapper()
|
||||
history = market.get_historical_prices("AAPL", limit=5)
|
||||
assert history is not None
|
||||
assert isinstance(history, list)
|
||||
assert len(history) == 5
|
||||
for entry in history:
|
||||
assert hasattr(entry, 'time')
|
||||
assert hasattr(entry, 'close')
|
||||
assert hasattr(entry, 'high')
|
||||
assert entry.close > 0
|
||||
assert entry.high > 0
|
||||
|
||||
def test_yfinance_crypto_history(self):
|
||||
market = YFinanceWrapper()
|
||||
history = market.get_historical_prices("BTC", limit=3)
|
||||
assert history is not None
|
||||
assert isinstance(history, list)
|
||||
assert len(history) == 3
|
||||
for entry in history:
|
||||
assert hasattr(entry, 'time')
|
||||
assert hasattr(entry, 'close')
|
||||
assert entry.close > 0
|
||||
@@ -4,7 +4,7 @@ from app.utils.market_data_aggregator import MarketDataAggregator
|
||||
from app.utils.aggregated_models import AggregatedProductInfo
|
||||
from app.markets.base import ProductInfo, Price
|
||||
|
||||
|
||||
@pytest.mark.aggregator
|
||||
class TestMarketDataAggregator:
|
||||
|
||||
def test_initialization(self):
|
||||
91
uv.lock
generated
91
uv.lock
generated
@@ -325,6 +325,27 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/e8/77d17d00981cdd27cc493e81e1749a0b8bbfb843780dbd841e30d7f50743/cryptography-46.0.1-cp38-abi3-win_arm64.whl", hash = "sha256:efc9e51c3e595267ff84adf56e9b357db89ab2279d7e375ffcaf8f678606f3d9", size = 2923149, upload-time = "2025-09-17T00:10:13.236Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "curl-cffi"
|
||||
version = "0.13.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "cffi" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4e/3d/f39ca1f8fdf14408888e7c25e15eed63eac5f47926e206fb93300d28378c/curl_cffi-0.13.0.tar.gz", hash = "sha256:62ecd90a382bd5023750e3606e0aa7cb1a3a8ba41c14270b8e5e149ebf72c5ca", size = 151303, upload-time = "2025-08-06T13:05:42.988Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/19/d1/acabfd460f1de26cad882e5ef344d9adde1507034528cb6f5698a2e6a2f1/curl_cffi-0.13.0-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:434cadbe8df2f08b2fc2c16dff2779fb40b984af99c06aa700af898e185bb9db", size = 5686337, upload-time = "2025-08-06T13:05:28.985Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/1c/cdb4fb2d16a0e9de068e0e5bc02094e105ce58a687ff30b4c6f88e25a057/curl_cffi-0.13.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:59afa877a9ae09efa04646a7d068eeea48915a95d9add0a29854e7781679fcd7", size = 2994613, upload-time = "2025-08-06T13:05:31.027Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/3e/fdf617c1ec18c3038b77065d484d7517bb30f8fb8847224eb1f601a4e8bc/curl_cffi-0.13.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d06ed389e45a7ca97b17c275dbedd3d6524560270e675c720e93a2018a766076", size = 7931353, upload-time = "2025-08-06T13:05:32.273Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/10/6f30c05d251cf03ddc2b9fd19880f3cab8c193255e733444a2df03b18944/curl_cffi-0.13.0-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b4e0de45ab3b7a835c72bd53640c2347415111b43421b5c7a1a0b18deae2e541", size = 7486378, upload-time = "2025-08-06T13:05:33.672Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/81/5bdb7dd0d669a817397b2e92193559bf66c3807f5848a48ad10cf02bf6c7/curl_cffi-0.13.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8eb4083371bbb94e9470d782de235fb5268bf43520de020c9e5e6be8f395443f", size = 8328585, upload-time = "2025-08-06T13:05:35.28Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/c1/df5c6b4cfad41c08442e0f727e449f4fb5a05f8aa564d1acac29062e9e8e/curl_cffi-0.13.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:28911b526e8cd4aa0e5e38401bfe6887e8093907272f1f67ca22e6beb2933a51", size = 8739831, upload-time = "2025-08-06T13:05:37.078Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/91/6dd1910a212f2e8eafe57877bcf97748eb24849e1511a266687546066b8a/curl_cffi-0.13.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6d433ffcb455ab01dd0d7bde47109083aa38b59863aa183d29c668ae4c96bf8e", size = 8711908, upload-time = "2025-08-06T13:05:38.741Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/e4/15a253f9b4bf8d008c31e176c162d2704a7e0c5e24d35942f759df107b68/curl_cffi-0.13.0-cp39-abi3-win_amd64.whl", hash = "sha256:66a6b75ce971de9af64f1b6812e275f60b88880577bac47ef1fa19694fa21cd3", size = 1614510, upload-time = "2025-08-06T13:05:40.451Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/0f/9c5275f17ad6ff5be70edb8e0120fdc184a658c9577ca426d4230f654beb/curl_cffi-0.13.0-cp39-abi3-win_arm64.whl", hash = "sha256:d438a3b45244e874794bc4081dc1e356d2bb926dcc7021e5a8fef2e2105ef1d8", size = 1365753, upload-time = "2025-08-06T13:05:41.879Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dateparser"
|
||||
version = "1.2.2"
|
||||
@@ -428,6 +449,16 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/42/14/42b2651a2f46b022ccd948bca9f2d5af0fd8929c4eec235b8d6d844fbe67/filelock-3.19.1-py3-none-any.whl", hash = "sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d", size = 15988, upload-time = "2025-08-14T16:56:01.633Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "frozendict"
|
||||
version = "2.4.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bb/59/19eb300ba28e7547538bdf603f1c6c34793240a90e1a7b61b65d8517e35e/frozendict-2.4.6.tar.gz", hash = "sha256:df7cd16470fbd26fc4969a208efadc46319334eb97def1ddf48919b351192b8e", size = 316416, upload-time = "2024-10-13T12:15:32.449Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/04/13/d9839089b900fa7b479cce495d62110cddc4bd5630a04d8469916c0e79c5/frozendict-2.4.6-py311-none-any.whl", hash = "sha256:d065db6a44db2e2375c23eac816f1a022feb2fa98cbb50df44a9e83700accbea", size = 16148, upload-time = "2024-10-13T12:15:26.839Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/d0/d482c39cee2ab2978a892558cf130681d4574ea208e162da8958b31e9250/frozendict-2.4.6-py312-none-any.whl", hash = "sha256:49344abe90fb75f0f9fdefe6d4ef6d4894e640fadab71f11009d52ad97f370b9", size = 16146, upload-time = "2024-10-13T12:15:28.16Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "frozenlist"
|
||||
version = "1.7.0"
|
||||
@@ -836,6 +867,12 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/69/b547032297c7e63ba2af494edba695d781af8a0c6e89e4d06cf848b21d80/multidict-6.6.4-py3-none-any.whl", hash = "sha256:27d8f8e125c07cb954e54d75d04905a9bba8a439c1d84aca94949d4d03d8601c", size = 12313, upload-time = "2025-08-11T12:08:46.891Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "multitasking"
|
||||
version = "0.0.12"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/17/0d/74f0293dfd7dcc3837746d0138cbedd60b31701ecc75caec7d3f281feba0/multitasking-0.0.12.tar.gz", hash = "sha256:2fba2fa8ed8c4b85e227c5dd7dc41c7d658de3b6f247927316175a57349b84d1", size = 19984, upload-time = "2025-07-20T21:27:51.636Z" }
|
||||
|
||||
[[package]]
|
||||
name = "newsapi-python"
|
||||
version = "0.2.7"
|
||||
@@ -933,6 +970,12 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/28/30/8114832daff7489f179971dbc1d854109b7f4365a546e3ea75b6516cea95/pandas-2.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:8c13b81a9347eb8c7548f53fd9a4f08d4dfe996836543f805c987bafa03317ae", size = 10983326, upload-time = "2025-08-21T10:27:31.901Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "peewee"
|
||||
version = "3.18.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/04/89/76f6f1b744c8608e0d416b588b9d63c2a500ff800065ae610f7c80f532d6/peewee-3.18.2.tar.gz", hash = "sha256:77a54263eb61aff2ea72f63d2eeb91b140c25c1884148e28e4c0f7c4f64996a0", size = 949220, upload-time = "2025-07-08T12:52:03.941Z" }
|
||||
|
||||
[[package]]
|
||||
name = "pillow"
|
||||
version = "11.3.0"
|
||||
@@ -952,6 +995,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/16/8f/b13447d1bf0b1f7467ce7d86f6e6edf66c0ad7cf44cf5c87a37f9bed9936/pillow-11.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542", size = 2423067, upload-time = "2025-07-01T09:14:33.709Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "platformdirs"
|
||||
version = "4.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/23/e8/21db9c9987b0e728855bd57bff6984f67952bea55d6f75e055c46b5383e8/platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf", size = 21634, upload-time = "2025-08-26T14:32:04.268Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", size = 18654, upload-time = "2025-08-26T14:32:02.735Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
version = "1.6.0"
|
||||
@@ -1028,6 +1080,20 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/35/cc0aaecf278bb4575b8555f2b137de5ab821595ddae9da9d3cd1da4072c7/propcache-0.3.2-py3-none-any.whl", hash = "sha256:98f1ec44fb675f5052cccc8e609c46ed23a35a1cfd18545ad4e29002d858a43f", size = 12663, upload-time = "2025-06-09T22:56:04.484Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "protobuf"
|
||||
version = "6.32.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fa/a4/cc17347aa2897568beece2e674674359f911d6fe21b0b8d6268cd42727ac/protobuf-6.32.1.tar.gz", hash = "sha256:ee2469e4a021474ab9baafea6cd070e5bf27c7d29433504ddea1a4ee5850f68d", size = 440635, upload-time = "2025-09-11T21:38:42.935Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/98/645183ea03ab3995d29086b8bf4f7562ebd3d10c9a4b14ee3f20d47cfe50/protobuf-6.32.1-cp310-abi3-win32.whl", hash = "sha256:a8a32a84bc9f2aad712041b8b366190f71dde248926da517bde9e832e4412085", size = 424411, upload-time = "2025-09-11T21:38:27.427Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/f3/6f58f841f6ebafe076cebeae33fc336e900619d34b1c93e4b5c97a81fdfa/protobuf-6.32.1-cp310-abi3-win_amd64.whl", hash = "sha256:b00a7d8c25fa471f16bc8153d0e53d6c9e827f0953f3c09aaa4331c718cae5e1", size = 435738, upload-time = "2025-09-11T21:38:30.959Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/56/a8a3f4e7190837139e68c7002ec749190a163af3e330f65d90309145a210/protobuf-6.32.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d8c7e6eb619ffdf105ee4ab76af5a68b60a9d0f66da3ea12d1640e6d8dab7281", size = 426454, upload-time = "2025-09-11T21:38:34.076Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/be/8dd0a927c559b37d7a6c8ab79034fd167dcc1f851595f2e641ad62be8643/protobuf-6.32.1-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:2f5b80a49e1eb7b86d85fcd23fe92df154b9730a725c3b38c4e43b9d77018bf4", size = 322874, upload-time = "2025-09-11T21:38:35.509Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/f6/88d77011b605ef979aace37b7703e4eefad066f7e84d935e5a696515c2dd/protobuf-6.32.1-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:b1864818300c297265c83a4982fd3169f97122c299f56a56e2445c3698d34710", size = 322013, upload-time = "2025-09-11T21:38:37.017Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/b7/15cc7d93443d6c6a84626ae3258a91f4c6ac8c0edd5df35ea7658f71b79c/protobuf-6.32.1-py3-none-any.whl", hash = "sha256:2601b779fc7d32a866c6b4404f9d42a3f67c5b9f3f15b4db3cccabe06b95c346", size = 169289, upload-time = "2025-09-11T21:38:41.234Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyasn1"
|
||||
version = "0.6.1"
|
||||
@@ -1545,6 +1611,7 @@ dependencies = [
|
||||
{ name = "praw" },
|
||||
{ name = "pytest" },
|
||||
{ name = "python-binance" },
|
||||
{ name = "yfinance" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
@@ -1561,6 +1628,7 @@ requires-dist = [
|
||||
{ name = "praw" },
|
||||
{ name = "pytest" },
|
||||
{ name = "python-binance" },
|
||||
{ name = "yfinance" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1644,3 +1712,26 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/83/5d9092950565481b413b31a23e75dd3418ff0a277d6e0abf3729d4d1ce25/yarl-1.20.1-cp312-cp312-win_amd64.whl", hash = "sha256:48ea7d7f9be0487339828a4de0360d7ce0efc06524a48e1810f945c45b813698", size = 86710, upload-time = "2025-06-10T00:44:16.716Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/2d/2345fce04cfd4bee161bf1e7d9cdc702e3e16109021035dbb24db654a622/yarl-1.20.1-py3-none-any.whl", hash = "sha256:83b8eb083fe4683c6115795d9fc1cfaf2cbbefb19b3a1cb68f6527460f483a77", size = 46542, upload-time = "2025-06-10T00:46:07.521Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yfinance"
|
||||
version = "0.2.66"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "beautifulsoup4" },
|
||||
{ name = "curl-cffi" },
|
||||
{ name = "frozendict" },
|
||||
{ name = "multitasking" },
|
||||
{ name = "numpy" },
|
||||
{ name = "pandas" },
|
||||
{ name = "peewee" },
|
||||
{ name = "platformdirs" },
|
||||
{ name = "protobuf" },
|
||||
{ name = "pytz" },
|
||||
{ name = "requests" },
|
||||
{ name = "websockets" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/59/73/50450b9906c5137d2d02fde6f7360865366c72baea1f8d0550cc990829ce/yfinance-0.2.66.tar.gz", hash = "sha256:fae354cc1649109444b2c84194724afcc52c2a7799551ce44c739424ded6af9c", size = 132820, upload-time = "2025-09-17T11:22:35.422Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/16/bf/7c0c89ff8ba53592b9cb5157f70e90d8bbb04d60094fc4f10035e158b981/yfinance-0.2.66-py2.py3-none-any.whl", hash = "sha256:511a1a40a687f277aae3a02543009a8aeaa292fce5509671f58915078aebb5c7", size = 123427, upload-time = "2025-09-17T11:22:33.972Z" },
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user