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.
@@ -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