alphadataforge 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- alphadataforge/__init__.py +2 -0
- alphadataforge/config/__init__.py +0 -0
- alphadataforge/config/endpoints.py +52 -0
- alphadataforge/config/settings.py +18 -0
- alphadataforge/core/__init__.py +0 -0
- alphadataforge/core/base_fetcher.py +357 -0
- alphadataforge/core/exceptions.py +19 -0
- alphadataforge/data/fundamental.py +107 -0
- alphadataforge/data/price.py +207 -0
- alphadataforge/features/__init__.py +0 -0
- alphadataforge/providers/__init__.py +0 -0
- alphadataforge/providers/alphavantage_fetcher.py +328 -0
- alphadataforge/providers/fmp_fetcher.py +183 -0
- alphadataforge/providers/hybrid_fetcher.py +87 -0
- alphadataforge/providers/stockfit_fetcher.py +265 -0
- alphadataforge/providers/tiingo_fetcher.py +312 -0
- alphadataforge/providers/yfinance_fetcher.py +282 -0
- alphadataforge/utils/__init__.py +0 -0
- alphadataforge/utils/finance_math.py +125 -0
- alphadataforge/utils/logger.py +30 -0
- alphadataforge-0.1.0.dist-info/METADATA +770 -0
- alphadataforge-0.1.0.dist-info/RECORD +24 -0
- alphadataforge-0.1.0.dist-info/WHEEL +5 -0
- alphadataforge-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
import os
|
|
3
|
+
from typing import Optional, List, Dict, Any
|
|
4
|
+
from tiingo import TiingoClient
|
|
5
|
+
from ..core.base_fetcher import BaseDataFetcher
|
|
6
|
+
from ..core.exceptions import ProviderConfigurationError
|
|
7
|
+
from ..config.settings import config
|
|
8
|
+
from ..utils.logger import setup_logger
|
|
9
|
+
|
|
10
|
+
logger = setup_logger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TiingoFetcher(BaseDataFetcher):
|
|
14
|
+
"""
|
|
15
|
+
Data fetcher for Tiingo API using the official tiingo-python client.
|
|
16
|
+
Supports: stock price, news, fundamentals, and crypto.
|
|
17
|
+
|
|
18
|
+
Requires TIINGO_API_KEY environment variable to be set.
|
|
19
|
+
Free tier: 500 requests/hr, EOD prices + news headlines.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, api_key: Optional[str] = None):
|
|
23
|
+
super().__init__()
|
|
24
|
+
self.api_key = api_key or config.TIINGO_API_KEY
|
|
25
|
+
self._client = None
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def client(self) -> TiingoClient:
|
|
29
|
+
if not self._client:
|
|
30
|
+
if not self.api_key:
|
|
31
|
+
raise ProviderConfigurationError(
|
|
32
|
+
"TIINGO_API_KEY is not set. Please set it in your .env file or environment variables."
|
|
33
|
+
)
|
|
34
|
+
# Temporarily inject key to OS env for TiingoClient
|
|
35
|
+
os.environ['TIINGO_API_KEY'] = self.api_key
|
|
36
|
+
self._client = TiingoClient({'session': True})
|
|
37
|
+
return self._client
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# ------------------------------------------------------------------
|
|
41
|
+
# Required by BaseDataFetcher — fetches PRICE for 1 symbol
|
|
42
|
+
# ------------------------------------------------------------------
|
|
43
|
+
def fetch_single(
|
|
44
|
+
self,
|
|
45
|
+
symbol: str,
|
|
46
|
+
start_date: Optional[str] = None,
|
|
47
|
+
end_date: Optional[str] = None,
|
|
48
|
+
frequency: str = "daily",
|
|
49
|
+
**kwargs
|
|
50
|
+
) -> pd.DataFrame:
|
|
51
|
+
"""
|
|
52
|
+
Fetch EOD price data for a single stock/ETF symbol.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
symbol: Ticker symbol, e.g. "AAPL"
|
|
56
|
+
start_date: "YYYY-MM-DD" (inclusive)
|
|
57
|
+
end_date: "YYYY-MM-DD" (inclusive)
|
|
58
|
+
frequency: "daily" | "weekly" | "monthly" | "annually" | "1min" | "5min" etc.
|
|
59
|
+
**kwargs: Any extra params forwarded to TiingoClient.get_dataframe()
|
|
60
|
+
|
|
61
|
+
Raises:
|
|
62
|
+
ValueError: If symbol or date inputs are invalid.
|
|
63
|
+
Exception: If Tiingo API call fails (network, auth, etc.)
|
|
64
|
+
"""
|
|
65
|
+
logger.info("Fetching price for %s (%s)...", symbol, frequency)
|
|
66
|
+
self._validate_inputs(symbol, start_date, end_date)
|
|
67
|
+
|
|
68
|
+
# Pop adjusted if present, since Tiingo returns adj natively
|
|
69
|
+
kwargs.pop("adjusted", None)
|
|
70
|
+
|
|
71
|
+
df = self.client.get_dataframe(
|
|
72
|
+
tickers=symbol,
|
|
73
|
+
startDate=start_date,
|
|
74
|
+
endDate=end_date,
|
|
75
|
+
frequency=frequency,
|
|
76
|
+
**kwargs
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
if df.empty:
|
|
80
|
+
logger.warning("Tiingo returned empty DataFrame for %s. Check if the ticker is valid.", symbol)
|
|
81
|
+
|
|
82
|
+
return self._normalize_ohlcv(df)
|
|
83
|
+
|
|
84
|
+
# ------------------------------------------------------------------
|
|
85
|
+
# Fetch price for multiple symbols at once
|
|
86
|
+
# ------------------------------------------------------------------
|
|
87
|
+
def fetch_multiple(
|
|
88
|
+
self,
|
|
89
|
+
symbols: List[str],
|
|
90
|
+
start_date: Optional[str] = None,
|
|
91
|
+
end_date: Optional[str] = None,
|
|
92
|
+
frequency: str = "daily",
|
|
93
|
+
metric_name: str = "adjClose",
|
|
94
|
+
**kwargs
|
|
95
|
+
) -> Dict[str, pd.DataFrame]:
|
|
96
|
+
"""
|
|
97
|
+
Fetch price data for multiple symbols.
|
|
98
|
+
Uses tiingo's built-in batch method when metric_name is specified,
|
|
99
|
+
which is more efficient than looping.
|
|
100
|
+
|
|
101
|
+
Symbols that fail or return empty data are logged and excluded.
|
|
102
|
+
|
|
103
|
+
Returns: {symbol: DataFrame} (only successfully fetched symbols)
|
|
104
|
+
"""
|
|
105
|
+
logger.info("Batch fetching %d symbols...", len(symbols))
|
|
106
|
+
if not symbols:
|
|
107
|
+
return {}
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
combined: pd.DataFrame = self.client.get_dataframe(
|
|
111
|
+
symbols,
|
|
112
|
+
startDate=start_date,
|
|
113
|
+
endDate=end_date,
|
|
114
|
+
frequency=frequency,
|
|
115
|
+
metric_name=metric_name,
|
|
116
|
+
**kwargs
|
|
117
|
+
)
|
|
118
|
+
except Exception as e:
|
|
119
|
+
logger.error(
|
|
120
|
+
"Tiingo batch fetch failed entirely for %s symbols: %s. "
|
|
121
|
+
"Falling back to per-symbol fetch.", len(symbols), e
|
|
122
|
+
)
|
|
123
|
+
# Fallback: use base class loop so partial results are still returned
|
|
124
|
+
return super().fetch_multiple(
|
|
125
|
+
symbols, start_date=start_date, end_date=end_date,
|
|
126
|
+
frequency=frequency, **kwargs
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# When passing a list, tiingo returns a DataFrame with symbol columns
|
|
130
|
+
results = {}
|
|
131
|
+
for sym in symbols:
|
|
132
|
+
if sym not in combined.columns:
|
|
133
|
+
logger.warning("Symbol %s not found in Tiingo batch result — excluding.", sym)
|
|
134
|
+
continue
|
|
135
|
+
normalized = self._normalize_ohlcv(combined[[sym]].rename(columns={sym: metric_name}))
|
|
136
|
+
if normalized.empty:
|
|
137
|
+
logger.warning("Symbol %s returned empty data — excluding.", sym)
|
|
138
|
+
continue
|
|
139
|
+
results[sym] = normalized
|
|
140
|
+
|
|
141
|
+
succeeded = len(results)
|
|
142
|
+
failed = len(symbols) - succeeded
|
|
143
|
+
if failed > 0:
|
|
144
|
+
logger.warning(
|
|
145
|
+
"Tiingo batch fetch finished: %d/%d succeeded, %d excluded.",
|
|
146
|
+
succeeded, len(symbols), failed,
|
|
147
|
+
)
|
|
148
|
+
else:
|
|
149
|
+
logger.info("Tiingo batch fetch finished: all %d symbols succeeded.", succeeded)
|
|
150
|
+
|
|
151
|
+
return results
|
|
152
|
+
|
|
153
|
+
# ------------------------------------------------------------------
|
|
154
|
+
# News — curated financial news with ticker + tag filtering
|
|
155
|
+
# ------------------------------------------------------------------
|
|
156
|
+
def fetch_news(
|
|
157
|
+
self,
|
|
158
|
+
tickers: Optional[List[str]] = None,
|
|
159
|
+
tags: Optional[List[str]] = None,
|
|
160
|
+
sources: Optional[List[str]] = None,
|
|
161
|
+
start_date: Optional[str] = None,
|
|
162
|
+
end_date: Optional[str] = None,
|
|
163
|
+
limit: int = 100,
|
|
164
|
+
) -> pd.DataFrame:
|
|
165
|
+
"""
|
|
166
|
+
Fetch financial news articles from Tiingo.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
tickers: Filter by ticker symbols, e.g. ["AAPL", "GOOGL"]
|
|
170
|
+
tags: Filter by topic tags, e.g. ["Earnings"]
|
|
171
|
+
sources: Filter by news source domain, e.g. ["reuters.com"]
|
|
172
|
+
start_date: "YYYY-MM-DD"
|
|
173
|
+
end_date: "YYYY-MM-DD"
|
|
174
|
+
limit: Max number of articles to return
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
DataFrame with columns: publishedDate, title, description, url, tickers, tags
|
|
178
|
+
|
|
179
|
+
Raises:
|
|
180
|
+
Exception: If Tiingo news API call fails.
|
|
181
|
+
"""
|
|
182
|
+
logger.info("Fetching news (tickers=%s, tags=%s)...", tickers, tags)
|
|
183
|
+
articles = self.client.get_news(
|
|
184
|
+
tickers=tickers,
|
|
185
|
+
tags=tags,
|
|
186
|
+
sources=sources,
|
|
187
|
+
startDate=start_date,
|
|
188
|
+
endDate=end_date,
|
|
189
|
+
limit=limit,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
if not articles:
|
|
193
|
+
logger.warning("No news articles found for tickers=%s.", tickers)
|
|
194
|
+
|
|
195
|
+
return pd.DataFrame(articles)
|
|
196
|
+
|
|
197
|
+
# # ------------------------------------------------------------------
|
|
198
|
+
# # Fundamentals — daily (marketCap etc.) and quarterly statements
|
|
199
|
+
# # ------------------------------------------------------------------
|
|
200
|
+
# def fetch_fundamentals_daily(
|
|
201
|
+
# self,
|
|
202
|
+
# symbol: str,
|
|
203
|
+
# start_date: Optional[str] = None,
|
|
204
|
+
# end_date: Optional[str] = None,
|
|
205
|
+
# ) -> pd.DataFrame:
|
|
206
|
+
# """
|
|
207
|
+
# Fetch daily-updated fundamental metrics (e.g. marketCap, EV).
|
|
208
|
+
# NOTE: Requires Tiingo paid plan.
|
|
209
|
+
# """
|
|
210
|
+
# logger.info("Fetching daily fundamentals for %s...", symbol)
|
|
211
|
+
# data = self.client.get_fundamentals_daily(
|
|
212
|
+
# symbol, startDate=start_date, endDate=end_date
|
|
213
|
+
# )
|
|
214
|
+
# return pd.DataFrame(data)
|
|
215
|
+
|
|
216
|
+
# def fetch_fundamentals_statements(
|
|
217
|
+
# self,
|
|
218
|
+
# symbol: str,
|
|
219
|
+
# start_date: Optional[str] = None,
|
|
220
|
+
# end_date: Optional[str] = None,
|
|
221
|
+
# as_reported: bool = False,
|
|
222
|
+
# ) -> pd.DataFrame:
|
|
223
|
+
# """
|
|
224
|
+
# Fetch quarterly financial statements (income, balance sheet, cash flow).
|
|
225
|
+
# NOTE: Requires Tiingo paid plan.
|
|
226
|
+
|
|
227
|
+
# Args:
|
|
228
|
+
# as_reported: If True, returns raw SEC-reported data (no corrections).
|
|
229
|
+
# """
|
|
230
|
+
# logger.info("Fetching quarterly statements for %s...", symbol)
|
|
231
|
+
# data = self.client.get_fundamentals_statements(
|
|
232
|
+
# symbol,
|
|
233
|
+
# startDate=start_date,
|
|
234
|
+
# endDate=end_date,
|
|
235
|
+
# asReported=as_reported,
|
|
236
|
+
# )
|
|
237
|
+
# return pd.DataFrame(data)
|
|
238
|
+
|
|
239
|
+
# ------------------------------------------------------------------
|
|
240
|
+
# Crypto — historical OHLCV for crypto pairs
|
|
241
|
+
# ------------------------------------------------------------------
|
|
242
|
+
def fetch_crypto(
|
|
243
|
+
self,
|
|
244
|
+
tickers: List[str],
|
|
245
|
+
start_date: Optional[str] = None,
|
|
246
|
+
end_date: Optional[str] = None,
|
|
247
|
+
resample_freq: str = "1Day",
|
|
248
|
+
) -> Dict[str, pd.DataFrame]:
|
|
249
|
+
"""
|
|
250
|
+
Fetch historical cryptocurrency price data.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
tickers: Crypto pairs as a list, e.g. ["BTCUSD", "ETHUSD"]
|
|
254
|
+
start_date: "YYYY-MM-DD"
|
|
255
|
+
end_date: "YYYY-MM-DD"
|
|
256
|
+
resample_freq: e.g. "1Day", "1Hour", "30Min"
|
|
257
|
+
|
|
258
|
+
Returns: {ticker: DataFrame} (only successfully fetched tickers)
|
|
259
|
+
|
|
260
|
+
Raises:
|
|
261
|
+
Exception: If Tiingo crypto API call fails.
|
|
262
|
+
"""
|
|
263
|
+
logger.info("Fetching crypto: %s...", tickers)
|
|
264
|
+
if not tickers:
|
|
265
|
+
return {}
|
|
266
|
+
|
|
267
|
+
data = self.client.get_crypto_price_history(
|
|
268
|
+
tickers=tickers,
|
|
269
|
+
startDate=start_date,
|
|
270
|
+
endDate=end_date,
|
|
271
|
+
resampleFreq=resample_freq,
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
# Tiingo returns a list of dicts, each with 'ticker' and 'priceData'
|
|
275
|
+
result = {}
|
|
276
|
+
for item in data:
|
|
277
|
+
ticker = item.get('ticker', '<unknown>')
|
|
278
|
+
price_data = item.get('priceData', [])
|
|
279
|
+
if not price_data:
|
|
280
|
+
logger.warning("Crypto ticker '%s' has no priceData — skipping.", ticker)
|
|
281
|
+
continue
|
|
282
|
+
try:
|
|
283
|
+
df = pd.DataFrame(price_data)
|
|
284
|
+
normalized = self._normalize_ohlcv(df)
|
|
285
|
+
if normalized.empty:
|
|
286
|
+
logger.warning("Crypto ticker '%s' normalized to empty — skipping.", ticker)
|
|
287
|
+
continue
|
|
288
|
+
result[ticker] = normalized
|
|
289
|
+
except Exception as e:
|
|
290
|
+
logger.error("Failed to process crypto ticker '%s': %s — skipping.", ticker, e)
|
|
291
|
+
|
|
292
|
+
return result
|
|
293
|
+
|
|
294
|
+
def fetch_info(self, symbol: str) -> dict:
|
|
295
|
+
"""
|
|
296
|
+
Fetch company profile via Tiingo.
|
|
297
|
+
"""
|
|
298
|
+
logger.info("Fetching info for %s via Tiingo...", symbol)
|
|
299
|
+
self._validate_inputs(symbol)
|
|
300
|
+
try:
|
|
301
|
+
return self.client.get_ticker_metadata(symbol)
|
|
302
|
+
except Exception as e:
|
|
303
|
+
logger.warning("Failed to fetch info for %s from Tiingo: %s", symbol, e)
|
|
304
|
+
return {}
|
|
305
|
+
|
|
306
|
+
def fetch_financials(self, symbol: str, statement: str = "income", period: str = "annual") -> pd.DataFrame:
|
|
307
|
+
"""
|
|
308
|
+
Tiingo does not provide full standardized financial statements in the free tier
|
|
309
|
+
(only basic daily fundamentals). Returning empty DataFrame.
|
|
310
|
+
"""
|
|
311
|
+
logger.warning("Tiingo free tier does not support full %s statements. Returning empty.", statement)
|
|
312
|
+
return pd.DataFrame()
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import yfinance as yf
|
|
2
|
+
import pandas as pd
|
|
3
|
+
from typing import Optional, List, Dict
|
|
4
|
+
from ..core.base_fetcher import BaseDataFetcher
|
|
5
|
+
from ..utils.logger import setup_logger
|
|
6
|
+
|
|
7
|
+
logger = setup_logger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class YFinanceFetcher(BaseDataFetcher):
|
|
11
|
+
"""
|
|
12
|
+
Data fetcher for Yahoo Finance API using the yfinance library.
|
|
13
|
+
Inherits from BaseDataFetcher.
|
|
14
|
+
|
|
15
|
+
Supports: stock/ETF/crypto/forex price, news, and fundamentals.
|
|
16
|
+
All data is free with no API key required.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
# ------------------------------------------------------------------
|
|
20
|
+
# Required by BaseDataFetcher — fetches PRICE for 1 symbol
|
|
21
|
+
# ------------------------------------------------------------------
|
|
22
|
+
def fetch_single(
|
|
23
|
+
self,
|
|
24
|
+
symbol: str,
|
|
25
|
+
start_date: Optional[str] = None,
|
|
26
|
+
end_date: Optional[str] = None,
|
|
27
|
+
interval: str = "1d",
|
|
28
|
+
**kwargs
|
|
29
|
+
) -> pd.DataFrame:
|
|
30
|
+
"""
|
|
31
|
+
Fetch historical OHLCV price data for a single symbol.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
symbol: Ticker, e.g. "AAPL", "BTC-USD", "EURUSD=X", "^VIX"
|
|
35
|
+
start_date: "YYYY-MM-DD" (inclusive)
|
|
36
|
+
end_date: "YYYY-MM-DD" (exclusive)
|
|
37
|
+
interval: "1m","2m","5m","15m","30m","60m","90m","1h",
|
|
38
|
+
"1d","5d","1wk","1mo","3mo"
|
|
39
|
+
Note: intraday intervals only available for last 60 days
|
|
40
|
+
**kwargs: Any extra yf.download() params (e.g. auto_adjust=False)
|
|
41
|
+
|
|
42
|
+
Raises:
|
|
43
|
+
ValueError: If symbol or date inputs are invalid.
|
|
44
|
+
Exception: If yfinance download fails (network, invalid ticker, etc.)
|
|
45
|
+
"""
|
|
46
|
+
logger.info("Fetching price for %s (%s)...", symbol, interval)
|
|
47
|
+
self._validate_inputs(symbol, start_date, end_date)
|
|
48
|
+
|
|
49
|
+
# Pop adjusted if present, since yf.download doesn't accept it
|
|
50
|
+
kwargs.pop("adjusted", None)
|
|
51
|
+
|
|
52
|
+
# Force auto_adjust=False to get raw Close and Adj Close separately
|
|
53
|
+
# This allows our backfill_adjusted_columns to generate full Adj OHLC
|
|
54
|
+
download_kwargs = {
|
|
55
|
+
"auto_adjust": False,
|
|
56
|
+
"multi_level_index": False
|
|
57
|
+
}
|
|
58
|
+
download_kwargs.update(kwargs)
|
|
59
|
+
|
|
60
|
+
df = yf.download(
|
|
61
|
+
tickers=symbol,
|
|
62
|
+
start=start_date,
|
|
63
|
+
end=end_date,
|
|
64
|
+
interval=interval,
|
|
65
|
+
progress=False,
|
|
66
|
+
**download_kwargs
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
if df.empty:
|
|
70
|
+
logger.warning("yfinance returned empty DataFrame for %s. Check if the ticker is valid.", symbol)
|
|
71
|
+
|
|
72
|
+
return self._normalize_ohlcv(df)
|
|
73
|
+
|
|
74
|
+
# ------------------------------------------------------------------
|
|
75
|
+
# Fetch price for multiple symbols at once (batch download)
|
|
76
|
+
# ------------------------------------------------------------------
|
|
77
|
+
def fetch_multiple(
|
|
78
|
+
self,
|
|
79
|
+
symbols: List[str],
|
|
80
|
+
start_date: Optional[str] = None,
|
|
81
|
+
end_date: Optional[str] = None,
|
|
82
|
+
interval: str = "1d",
|
|
83
|
+
**kwargs
|
|
84
|
+
) -> Dict[str, pd.DataFrame]:
|
|
85
|
+
"""
|
|
86
|
+
Batch fetch OHLCV price data for multiple symbols in a single API call.
|
|
87
|
+
More efficient than calling fetch_single() in a loop.
|
|
88
|
+
|
|
89
|
+
Symbols that return empty or all-NaN data are logged and excluded.
|
|
90
|
+
|
|
91
|
+
Returns: {symbol: DataFrame} (only successfully fetched symbols)
|
|
92
|
+
"""
|
|
93
|
+
logger.info("Batch downloading %d symbols...", len(symbols))
|
|
94
|
+
# Add basic validation for empty list
|
|
95
|
+
if not symbols:
|
|
96
|
+
return {}
|
|
97
|
+
|
|
98
|
+
kwargs['group_by'] = 'ticker'
|
|
99
|
+
|
|
100
|
+
df = yf.download(
|
|
101
|
+
symbols,
|
|
102
|
+
start=start_date,
|
|
103
|
+
end=end_date,
|
|
104
|
+
interval=interval,
|
|
105
|
+
progress=False,
|
|
106
|
+
**kwargs
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# Single symbol: yfinance doesn't create a MultiIndex, return directly
|
|
110
|
+
if len(symbols) == 1:
|
|
111
|
+
normalized = self._normalize_ohlcv(df)
|
|
112
|
+
if normalized.empty:
|
|
113
|
+
logger.warning("Batch download returned no data for %s — excluding.", symbols[0])
|
|
114
|
+
return {}
|
|
115
|
+
return {symbols[0]: normalized}
|
|
116
|
+
|
|
117
|
+
results = {}
|
|
118
|
+
for symbol in symbols:
|
|
119
|
+
if symbol not in df.columns:
|
|
120
|
+
logger.warning("Symbol %s not found in batch result — excluding.", symbol)
|
|
121
|
+
continue
|
|
122
|
+
normalized = self._normalize_ohlcv(df[symbol])
|
|
123
|
+
if normalized.empty:
|
|
124
|
+
logger.warning("Symbol %s returned empty data — excluding.", symbol)
|
|
125
|
+
continue
|
|
126
|
+
results[symbol] = normalized
|
|
127
|
+
|
|
128
|
+
succeeded = len(results)
|
|
129
|
+
failed = len(symbols) - succeeded
|
|
130
|
+
if failed > 0:
|
|
131
|
+
logger.warning(
|
|
132
|
+
"Batch fetch finished: %d/%d succeeded, %d excluded.",
|
|
133
|
+
succeeded, len(symbols), failed,
|
|
134
|
+
)
|
|
135
|
+
else:
|
|
136
|
+
logger.info("Batch fetch finished: all %d symbols succeeded.", succeeded)
|
|
137
|
+
|
|
138
|
+
return results
|
|
139
|
+
|
|
140
|
+
# ------------------------------------------------------------------
|
|
141
|
+
# News — recent news articles for a given ticker
|
|
142
|
+
# ------------------------------------------------------------------
|
|
143
|
+
def fetch_news(
|
|
144
|
+
self,
|
|
145
|
+
symbol: str,
|
|
146
|
+
count: int = 10,
|
|
147
|
+
) -> pd.DataFrame:
|
|
148
|
+
"""
|
|
149
|
+
Fetch recent news articles for a given ticker from Yahoo Finance.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
symbol: Ticker symbol, e.g. "AAPL"
|
|
153
|
+
count: Max number of articles to return
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
DataFrame with columns: title, publisher, link, providerPublishTime, type
|
|
157
|
+
|
|
158
|
+
Raises:
|
|
159
|
+
Exception: If yfinance ticker lookup fails.
|
|
160
|
+
"""
|
|
161
|
+
logger.info("Fetching news for %s...", symbol)
|
|
162
|
+
self._validate_inputs(symbol)
|
|
163
|
+
if not isinstance(count, int) or count <= 0:
|
|
164
|
+
raise ValueError(f"count must be a positive integer, got: {count!r}")
|
|
165
|
+
ticker = yf.Ticker(symbol)
|
|
166
|
+
articles = ticker.news
|
|
167
|
+
if not articles:
|
|
168
|
+
logger.warning("No news articles found for %s.", symbol)
|
|
169
|
+
return pd.DataFrame()
|
|
170
|
+
# Extract the 'content' dictionary which contains title, pubDate, etc.
|
|
171
|
+
valid_articles = [a['content'] for a in articles[:count] if 'content' in a]
|
|
172
|
+
df = pd.DataFrame(valid_articles)
|
|
173
|
+
|
|
174
|
+
# Convert pubDate to datetime
|
|
175
|
+
if 'pubDate' in df.columns:
|
|
176
|
+
df['pubDate'] = pd.to_datetime(df['pubDate'])
|
|
177
|
+
|
|
178
|
+
return df
|
|
179
|
+
|
|
180
|
+
# ------------------------------------------------------------------
|
|
181
|
+
# Fundamentals — summary info, financials, balance sheet, cash flow
|
|
182
|
+
# ------------------------------------------------------------------
|
|
183
|
+
def fetch_info(self, symbol: str) -> dict:
|
|
184
|
+
"""
|
|
185
|
+
Fetch ticker metadata and key fundamental metrics (free-tier).
|
|
186
|
+
Includes: sector, industry, marketCap, trailingPE, forwardPE,
|
|
187
|
+
dividendYield, 52-week high/low, beta, etc.
|
|
188
|
+
|
|
189
|
+
Returns: dict (use pd.Series(result) to convert if needed)
|
|
190
|
+
"""
|
|
191
|
+
logger.info("Fetching info for %s...", symbol)
|
|
192
|
+
self._validate_inputs(symbol)
|
|
193
|
+
return yf.Ticker(symbol).info
|
|
194
|
+
|
|
195
|
+
def fetch_financials(
|
|
196
|
+
self,
|
|
197
|
+
symbol: str,
|
|
198
|
+
statement: str = "income",
|
|
199
|
+
period: str = "annual",
|
|
200
|
+
) -> pd.DataFrame:
|
|
201
|
+
"""
|
|
202
|
+
Fetch financial statements for a ticker.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
symbol: Ticker symbol
|
|
206
|
+
statement: "income" | "balance" | "cashflow"
|
|
207
|
+
period: "annual" | "quarterly".
|
|
208
|
+
|
|
209
|
+
Returns: Normalized DataFrame with Dates as rows and metrics as columns
|
|
210
|
+
|
|
211
|
+
Raises:
|
|
212
|
+
ValueError: If statement type is not recognized.
|
|
213
|
+
"""
|
|
214
|
+
logger.info(
|
|
215
|
+
"Fetching %s statement for %s (%s)...",
|
|
216
|
+
statement, symbol, period,
|
|
217
|
+
)
|
|
218
|
+
self._validate_inputs(symbol)
|
|
219
|
+
ticker = yf.Ticker(symbol)
|
|
220
|
+
|
|
221
|
+
quarterly = (period.lower() == "quarterly")
|
|
222
|
+
|
|
223
|
+
if statement == "income":
|
|
224
|
+
df = ticker.quarterly_income_stmt if quarterly else ticker.income_stmt
|
|
225
|
+
elif statement == "balance":
|
|
226
|
+
df = ticker.quarterly_balance_sheet if quarterly else ticker.balance_sheet
|
|
227
|
+
elif statement == "cashflow":
|
|
228
|
+
df = ticker.quarterly_cashflow if quarterly else ticker.cashflow
|
|
229
|
+
else:
|
|
230
|
+
raise ValueError(
|
|
231
|
+
f"Unknown statement type: '{statement}'. "
|
|
232
|
+
"Choose 'income', 'balance', or 'cashflow'."
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
# yfinance returns dates as columns and metrics as rows. Transpose it.
|
|
236
|
+
df = df.T
|
|
237
|
+
|
|
238
|
+
# Normalize columns (Net_Income, Total_Revenue, etc.)
|
|
239
|
+
return self._normalize_financials(df)
|
|
240
|
+
|
|
241
|
+
# ------------------------------------------------------------------
|
|
242
|
+
# Crypto & Forex — yfinance supports these natively, no extra setup
|
|
243
|
+
# ------------------------------------------------------------------
|
|
244
|
+
def fetch_crypto(
|
|
245
|
+
self,
|
|
246
|
+
symbols: List[str],
|
|
247
|
+
start_date: Optional[str] = None,
|
|
248
|
+
end_date: Optional[str] = None,
|
|
249
|
+
interval: str = "1d",
|
|
250
|
+
**kwargs
|
|
251
|
+
) -> Dict[str, pd.DataFrame]:
|
|
252
|
+
"""
|
|
253
|
+
Fetch historical OHLCV data for crypto pairs.
|
|
254
|
+
Uses the same yfinance engine — just pass crypto symbols.
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
symbols: List of crypto pairs, e.g. ["BTC-USD", "ETH-USD", "SOL-USD"]
|
|
258
|
+
|
|
259
|
+
Returns: {symbol: DataFrame}
|
|
260
|
+
"""
|
|
261
|
+
logger.info("Fetching crypto: %s...", symbols)
|
|
262
|
+
return self.fetch_multiple(symbols, start_date, end_date, interval, **kwargs)
|
|
263
|
+
|
|
264
|
+
def fetch_forex(
|
|
265
|
+
self,
|
|
266
|
+
pairs: List[str],
|
|
267
|
+
start_date: Optional[str] = None,
|
|
268
|
+
end_date: Optional[str] = None,
|
|
269
|
+
interval: str = "1d",
|
|
270
|
+
**kwargs
|
|
271
|
+
) -> Dict[str, pd.DataFrame]:
|
|
272
|
+
"""
|
|
273
|
+
Fetch historical FX rate data.
|
|
274
|
+
Yahoo Finance forex symbol format: "EURUSD=X", "GBPUSD=X", "JPYUSD=X"
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
pairs: List of forex pair symbols, e.g. ["EURUSD=X", "GBPUSD=X"]
|
|
278
|
+
|
|
279
|
+
Returns: {symbol: DataFrame}
|
|
280
|
+
"""
|
|
281
|
+
logger.info("Fetching forex: %s...", pairs)
|
|
282
|
+
return self.fetch_multiple(pairs, start_date, end_date, interval, **kwargs)
|
|
File without changes
|