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,770 @@
1
+ Metadata-Version: 2.4
2
+ Name: alphadataforge
3
+ Version: 0.1.0
4
+ Summary: A modular data fetching and feature engineering pipeline for quantitative trading.
5
+ Project-URL: Homepage, https://github.com/RETCx/alphadataforge
6
+ Project-URL: Issues, https://github.com/RETCx/alphadataforge/issues
7
+ Requires-Python: >=3.8
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: pandas
10
+ Requires-Dist: numpy
11
+ Requires-Dist: yfinance
12
+ Requires-Dist: tiingo
13
+ Requires-Dist: google-cloud-bigquery
14
+ Requires-Dist: requests
15
+ Requires-Dist: requests-cache
16
+ Requires-Dist: tenacity
17
+ Requires-Dist: scikit-learn
18
+ Requires-Dist: python-dotenv
19
+ Provides-Extra: dev
20
+ Requires-Dist: pytest; extra == "dev"
21
+ Requires-Dist: pytest-cov; extra == "dev"
22
+ Requires-Dist: pytest-vcr; extra == "dev"
23
+ Provides-Extra: publish
24
+ Requires-Dist: build; extra == "publish"
25
+ Requires-Dist: twine; extra == "publish"
26
+
27
+ # AlphaDataForge
28
+
29
+ AlphaDataForge is a modular data fetching pipeline for quantitative trading and financial analysis. It provides a unified interface to fetch **Price** and **Fundamental** data across multiple providers with automatic caching, rate limit protection, and standardized output formats.
30
+
31
+ ---
32
+
33
+ ## Quick Start
34
+
35
+ After the first PyPI release is published, install with:
36
+
37
+ ```bash
38
+ python -m pip install alphadataforge
39
+ ```
40
+
41
+ For development or before the first release:
42
+
43
+ ```bash
44
+ git clone https://github.com/RETCx/alphadataforge.git
45
+ cd alphadataforge
46
+ pip install -e .
47
+ ```
48
+
49
+ Maintainers: see [PUBLISHING.md](PUBLISHING.md) for release instructions.
50
+
51
+ Copy the environment template and add your API keys:
52
+ ```bash
53
+ cp .env.example .env
54
+ ```
55
+
56
+ ```
57
+ # .env
58
+ TIINGO_API_KEY="your_tiingo_key"
59
+ ALPHAVANTAGE_API_KEY="your_alphavantage_key"
60
+ FMP_API_KEY="your_fmp_key"
61
+ STOCKFIT_API_KEY="your_stockfit_key"
62
+ ```
63
+
64
+ *YFinance does not require an API key.*
65
+
66
+ ---
67
+
68
+ ## Architecture
69
+
70
+ ```
71
+ alphadataforge/
72
+ ├── data/
73
+ │ ├── price.py ← Unified Price Facade (Price.get(), Price.compare(), Price.consensus())
74
+ │ └── fundamental.py ← Unified Fundamental Facade (Fundamentals.get_info(), etc.)
75
+ │
76
+ ├── providers/
77
+ │ ├── yfinance_fetcher.py
78
+ │ ├── alphavantage_fetcher.py
79
+ │ ├── tiingo_fetcher.py
80
+ │ ├── fmp_fetcher.py
81
+ │ ├── stockfit_fetcher.py ← StockFit API (20yr SEC EDGAR XBRL lineage, health scores)
82
+ │ └── hybrid_fetcher.py ← AlphaVantage (raw) + Tiingo (div/splits) + local math engine
83
+ │
84
+ ├── scripts/
85
+ │ └── snapshot_fundamentals.py ← Offline snapshot utility (Parquet & SQLite)
86
+ │
87
+ ├── utils/
88
+ │ └── finance_math.py ← Local back-adjustment engine (calculate_adjusted_prices)
89
+ │
90
+ └── core/
91
+ ├── base_fetcher.py ← BaseDataFetcher (caching, retry, normalization)
92
+ └── exceptions.py ← AlphaDataForgeError, RateLimitExceededError, etc.
93
+ ```
94
+
95
+ **Built-in features (all providers)**:
96
+ - **SQLite Caching** (24h TTL) — auto-stored in OS temp folder, no setup needed.
97
+ - **Auto-Retry** with exponential backoff for network errors (via `tenacity`).
98
+ - **Normalized Output** — all prices return `[Open, High, Low, Close, Volume, Adj Close]` with a `DatetimeIndex`. All financials return standardized columns (`Net_Income`, `Total_Revenue`, etc.).
99
+
100
+ ---
101
+
102
+ ## Module 1: Price
103
+
104
+ **Entry point:** `from alphadataforge.data.price import Price`
105
+
106
+ ```python
107
+ from alphadataforge.data.price import Price
108
+
109
+ # Single symbol
110
+ df = Price.get("AAPL", start_date="2023-01-01", end_date="2023-12-31")
111
+
112
+ # Multiple symbols (returns dict)
113
+ data = Price.get(["AAPL", "GOOGL", "MSFT"], provider="yfinance")
114
+
115
+ # Specific provider
116
+ df = Price.get("TSLA", provider="tiingo", start_date="2022-01-01")
117
+ ```
118
+
119
+ ### `Price.get()` — All Parameters
120
+
121
+ | Parameter | Type | Default | Description |
122
+ |---|---|---|---|
123
+ | `symbols` | `str` or `List[str]` | *required* | Ticker(s) e.g. `"AAPL"` or `["AAPL", "MSFT"]` |
124
+ | `provider` | `str` | `"yfinance"` | `"yfinance"`, `"alphavantage"`, `"tiingo"`, `"fmp"`, `"hybrid_av_tiingo"` |
125
+ | `start_date` | `str` | `None` | `"YYYY-MM-DD"` |
126
+ | `end_date` | `str` | `None` | `"YYYY-MM-DD"` |
127
+ | `provider_params` | `dict` | `{}` | Extra params passed to the underlying provider (see per-provider sections below) |
128
+
129
+ **Returns:** `pd.DataFrame` (single symbol) or `Dict[str, pd.DataFrame]` (multiple symbols)
130
+
131
+ **Output Columns:**
132
+
133
+ | Column | Type | Description |
134
+ |---|---|---|
135
+ | `Open` | float | Opening price |
136
+ | `High` | float | Daily high |
137
+ | `Low` | float | Daily low |
138
+ | `Close` | float | Closing price |
139
+ | `Volume` | float | Trading volume |
140
+ | `Adj Open` | float | Adjusted opening price (if `adjusted=True`) |
141
+ | `Adj High` | float | Adjusted daily high (if `adjusted=True`) |
142
+ | `Adj Low` | float | Adjusted daily low (if `adjusted=True`) |
143
+ | `Adj Close` | float | Dividend & split adjusted close (if available or `adjusted=True`) |
144
+ | `Adj Volume` | float | Split-adjusted volume (if `adjusted=True`) |
145
+
146
+ > **Note on Adjusted Prices:** If `adjusted=True` is passed (or if the provider returns it natively), AlphaDataForge guarantees the presence of full Adjusted OHLC columns (`Adj Open`, `Adj High`, `Adj Low`, `Adj Close`). For providers like YFinance that only natively return `Adj Close`, our pipeline automatically backfills the remaining adjusted columns using the ratio of `Adj Close` / `Close`.
147
+
148
+ ### Hybrid Provider (`hybrid_av_tiingo`)
149
+ AlphaDataForge offers a special `hybrid_av_tiingo` provider for optimal data quality and API usage:
150
+ - **Raw Prices:** Fetched from Alpha Vantage (deepest historical data, 20+ years).
151
+ - **Dividends & Splits:** Fetched from Tiingo (highly accurate, saves Alpha Vantage premium limits).
152
+ - **Processing:** Fully adjusted prices are calculated locally via our `finance_math` engine.
153
+
154
+ ### Cross-Provider Adjusted Price Comparison
155
+ To ensure data consistency across all platforms, we tested the fully adjusted prices for `AAPL` on `2026-05-05` across all available providers and the hybrid engine. The results perfectly align:
156
+
157
+ **Adjusted Close:**
158
+ - `yfinance`: 283.9184
159
+ - `tiingo`: 283.9181
160
+ - `fmp`: 283.9200
161
+ - `alphavantage`: 283.9184
162
+ - `hybrid_av_tiingo`: 283.9184
163
+
164
+ **Adjusted Open:**
165
+ - `yfinance`: 276.6751
166
+ - `tiingo`: 276.6698
167
+ - `fmp`: 276.6800
168
+ - `alphavantage`: 276.6701
169
+ - `hybrid_av_tiingo`: 276.6701
170
+
171
+ ### `Price.compare()` — Side-by-Side Multi-Provider
172
+
173
+ Fetches from multiple providers **concurrently** (ThreadPoolExecutor) and returns a single **MultiIndex DataFrame** with providers as level-0 columns.
174
+
175
+ ```python
176
+ # Compare all 5 providers with adjusted prices
177
+ multi_df = Price.compare("AAPL", start_date="2023-01-01", provider_params={"adjusted": True})
178
+
179
+ # Access a specific provider's data
180
+ yfinance_df = multi_df["yfinance"]
181
+
182
+ # Extract one column across all providers
183
+ adj_close_all = multi_df.xs("Adj Close", axis=1, level=1)
184
+ ```
185
+
186
+ | Parameter | Type | Default | Description |
187
+ |---|---|---|---|
188
+ | `symbol` | `str` | *required* | Single ticker |
189
+ | `start_date` | `str` | `None` | `"YYYY-MM-DD"` |
190
+ | `end_date` | `str` | `None` | `"YYYY-MM-DD"` |
191
+ | `providers` | `List[str]` | all 5 providers | Limit which providers to query |
192
+ | `provider_params` | `dict` | `{}` | Passed to all providers (e.g. `{"adjusted": True}`) |
193
+ | `custom_data` | `Dict[str, DataFrame]` | `None` | Inject your own DataFrames to compare alongside API data |
194
+
195
+ **Returns:** `pd.DataFrame` with MultiIndex columns `(provider, column_name)`
196
+
197
+ ---
198
+
199
+ ### `Price.consensus()` — Aggregated Median/Mean Price
200
+
201
+ Builds on `compare()` and aggregates across all providers to return a **single reliable DataFrame**, neutralizing outliers from any one source.
202
+
203
+ ```python
204
+ # Median across all providers (recommended — more robust to outliers)
205
+ consensus_df = Price.consensus("AAPL", start_date="2023-01-01", method="median")
206
+
207
+ # Limit to specific providers
208
+ consensus_df = Price.consensus(
209
+ "AAPL",
210
+ providers=["yfinance", "tiingo", "alphavantage"],
211
+ provider_params={"adjusted": True},
212
+ method="median"
213
+ )
214
+ ```
215
+
216
+ | Parameter | Type | Default | Description |
217
+ |---|---|---|---|
218
+ | `symbol` | `str` | *required* | Single ticker |
219
+ | `start_date` | `str` | `None` | `"YYYY-MM-DD"` |
220
+ | `end_date` | `str` | `None` | `"YYYY-MM-DD"` |
221
+ | `providers` | `List[str]` | all 5 providers | Limit which providers to query |
222
+ | `provider_params` | `dict` | `{}` | Passed to all providers |
223
+ | `custom_data` | `Dict[str, DataFrame]` | `None` | Inject your own DataFrames into the aggregation |
224
+ | `method` | `str` | `"median"` | `"median"` (robust to outliers) or `"mean"` |
225
+
226
+ **Returns:** `pd.DataFrame` with standard OHLCV columns sorted in standard order
227
+
228
+ #### Injecting Custom External Data (`custom_data`)
229
+
230
+ Both `compare()` and `consensus()` accept a `custom_data` parameter — a dictionary of your own DataFrames to include in the comparison or aggregation. Your DataFrame must have a `DatetimeIndex` and column names that match the AlphaDataForge standard (`Open`, `High`, `Low`, `Close`, `Volume`, `Adj Close`, etc.).
231
+
232
+ ```python
233
+ import pandas as pd
234
+ from alphadataforge.data.price import Price
235
+ from alphadataforge.providers.tiingo_fetcher import TiingoFetcher
236
+ from alphadataforge.utils.finance_math import calculate_adjusted_prices
237
+
238
+ # Step 1: Load & Normalize column names to AlphaDataForge standard
239
+ df_custom = pd.read_csv('my_data.csv')
240
+ df_custom = df_custom.rename(columns={'Price': 'Close', 'Vol.': 'Volume'})
241
+ df_custom['Date'] = pd.to_datetime(df_custom['Date'])
242
+ df_custom.set_index('Date', inplace=True)
243
+
244
+ # Step 2 (Optional): Back-adjust with Tiingo dividends/splits if your data is raw
245
+ tf = TiingoFetcher()
246
+ df_tiingo = tf.fetch_single('AAPL', start_date='2023-01-01', end_date='2023-12-31')
247
+ dividends_df = df_tiingo[['divCash']].rename(columns={'divCash': 'Dividend'})
248
+ dividends_df = dividends_df[dividends_df['Dividend'] > 0]
249
+ splits_df = df_tiingo[['splitFactor']].rename(columns={'splitFactor': 'SplitFactor'})
250
+ splits_df = splits_df[splits_df['SplitFactor'] != 1.0]
251
+ df_custom_adjusted = calculate_adjusted_prices(df_custom, dividends_df, splits_df)
252
+
253
+ # Step 3: Feed into consensus engine
254
+ consensus_df = Price.consensus(
255
+ "AAPL",
256
+ start_date="2023-01-01",
257
+ providers=["yfinance", "alphavantage"],
258
+ provider_params={"adjusted": True},
259
+ custom_data={"my_local_db": df_custom_adjusted},
260
+ method="median"
261
+ )
262
+ ```
263
+
264
+ ---
265
+
266
+ ## Module 2: Fundamentals
267
+
268
+ **Entry point:** `from alphadataforge.data.fundamental import Fundamentals`
269
+
270
+ ### `Fundamentals.get_info()` — Company Profile
271
+
272
+ ```python
273
+ info = Fundamentals.get_info("AAPL", provider="yfinance")
274
+ print(info["sector"], info["marketCap"])
275
+ ```
276
+
277
+ | Parameter | Type | Default | Description |
278
+ |---|---|---|---|
279
+ | `symbol` | `str` | *required* | Ticker e.g. `"AAPL"` |
280
+ | `provider` | `str` | `"yfinance"` | `"yfinance"`, `"stockfit"`, `"alphavantage"`, `"fmp"` |
281
+
282
+ **Returns:** `dict`
283
+
284
+ ---
285
+
286
+ ### `Fundamentals.get_financials()` — Financial Statements
287
+
288
+ ```python
289
+ df = Fundamentals.get_financials("AAPL", statement="income", period="annual", provider="yfinance")
290
+ ```
291
+
292
+ | Parameter | Type | Default | Description |
293
+ |---|---|---|---|
294
+ | `symbol` | `str` | *required* | Ticker e.g. `"AAPL"` |
295
+ | `statement` | `str` | `"income"` | `"income"`, `"balance"`, `"cashflow"`, `"growth"`, `"scores"`, `"shares_outstanding"` |
296
+ | `period` | `str` | `"annual"` | `"annual"` or `"quarterly"` |
297
+ | `provider` | `str` | `"yfinance"` | `"yfinance"`, `"stockfit"`, `"alphavantage"`, `"fmp"` |
298
+
299
+ **Returns:** `pd.DataFrame` with `DatetimeIndex` and normalized columns
300
+
301
+ **Normalized Columns (Standardized Across All Providers):**
302
+
303
+ | Column | Description |
304
+ |---|---|
305
+ | `Net_Income` | Net profit/loss after tax |
306
+ | `Total_Revenue` | Total revenue |
307
+ | `Operating_Income` | EBIT / Operating profit |
308
+ | `Free_Cash_Flow` | Free cash flow |
309
+ | `Total_Assets` | Total assets |
310
+ | `Total_Liabilities` | Total liabilities |
311
+ | `Total_Equity` | Shareholders' equity |
312
+ | `Shares_Outstanding` | Basic shares outstanding |
313
+
314
+ ---
315
+
316
+
317
+ ### `Fundamentals.get_scores()` — Financial Health Scores (StockFit)
318
+
319
+ ```python
320
+ scores = Fundamentals.get_scores("AAPL", provider="stockfit")
321
+ print("Piotroski F-Score:", scores["piotroskiFScore"])
322
+ # Returns Piotroski details: positiveNetIncome, positiveOperatingCashFlow, risingRoa, etc.
323
+ ```
324
+
325
+ ### SEC XBRL 20-Year Statement History (StockFit)
326
+
327
+ ```python
328
+ # 20 years of annual statements with SEC filing audit lineage
329
+ df_income = Fundamentals.get_financials("AAPL", statement="income", period="annual", provider="stockfit", limit=100)
330
+
331
+ # 70+ quarters of balance sheet history
332
+ df_balance = Fundamentals.get_financials("AAPL", statement="balance", period="quarterly", provider="stockfit", limit=100)
333
+ ```
334
+
335
+ ### Snapshot & Offline Storage (`scripts/snapshot_fundamentals.py`)
336
+
337
+ Capture 20-year financial statement history for target symbols into local **Parquet** and **SQLite** storage:
338
+
339
+ ```bash
340
+ # Snapshot Magnificent 7 (Annual + Quarterly, Profiles, Scores)
341
+ python scripts/snapshot_fundamentals.py --preset mag7
342
+
343
+ # Snapshot top 25 US blue-chips
344
+ python scripts/snapshot_fundamentals.py --preset top25
345
+
346
+ # Snapshot specific tickers
347
+ python scripts/snapshot_fundamentals.py --symbols AAPL,MSFT,NVDA,GOOGL
348
+ ```
349
+ Data is saved to `data_store/stockfit_snapshots/parquet/` and `data_store/stockfit_snapshots/fundamentals.db`.
350
+
351
+ ### `Fundamentals.get_earnings_calendar()` — Earnings Calendar
352
+
353
+ ```python
354
+ df = Fundamentals.get_earnings_calendar(horizon="3month", symbol="AAPL", provider="alphavantage")
355
+ ```
356
+
357
+ | Parameter | Type | Default | Description |
358
+ |---|---|---|---|
359
+ | `horizon` | `str` | `"3month"` | `"3month"`, `"6month"`, or `"12month"` |
360
+ | `symbol` | `str` | `None` | Filter for specific ticker (optional) |
361
+ | `provider` | `str` | `"alphavantage"` | Only `"alphavantage"` supported |
362
+
363
+ **Returns:** `pd.DataFrame` with columns: `symbol`, `name`, `reportDate`, `fiscalDateEnding`, `estimate`, `currency`
364
+
365
+ ---
366
+
367
+ ## Provider Reference
368
+
369
+ ---
370
+
371
+ ### Provider 1: YFinance (`provider="yfinance"`)
372
+
373
+ | | |
374
+ |---|---|
375
+ | **API Key** | No Not required |
376
+ | **Rate Limit** | Practically unlimited |
377
+ | **Best For** | Development, batch fetching, comprehensive fundamentals |
378
+
379
+ #### Price
380
+
381
+ ```python
382
+ from alphadataforge.data.price import Price
383
+
384
+ # Basic fetch
385
+ df = Price.get("AAPL", start_date="2023-01-01")
386
+
387
+ # With intraday interval (available last 60 days only)
388
+ df = Price.get("AAPL", provider_params={"interval": "1h"})
389
+
390
+ # Crypto & Forex (use yfinance ticker format)
391
+ df = Price.get("BTC-USD") # Bitcoin
392
+ df = Price.get("ETH-USD") # Ethereum
393
+ df = Price.get("EURUSD=X") # EUR/USD Forex
394
+ df = Price.get("^VIX") # VIX Index
395
+ ```
396
+
397
+ **Extra `provider_params` for YFinance Price:**
398
+
399
+ | Param | Type | Default | Description |
400
+ |---|---|---|---|
401
+ | `interval` | `str` | `"1d"` | `"1m"`, `"5m"`, `"15m"`, `"30m"`, `"1h"`, `"1d"`, `"1wk"`, `"1mo"`, `"3mo"`. Intraday (< 1d) only for last 60 days. |
402
+ | `auto_adjust` | `bool` | `False` | Pass `True` if you want yfinance to overwrite the Open/High/Low columns with adjusted prices. |
403
+
404
+ **Under the hood (YFinance):**
405
+ - **API Calls:** 1 call per fetch.
406
+ - **Adjusted Prices:** Natively returns `Adj Close`. If you want `Adj Open/High/Low`, you must pass `provider_params={"auto_adjust": True}`.
407
+
408
+ #### Fundamentals (YFinance)
409
+
410
+ | Method | `statement=` value | Notes |
411
+ |---|---|---|
412
+ | `get_info()` | — | Returns 80+ fields: `sector`, `industry`, `marketCap`, `trailingPE`, `forwardPE`, `dividendYield`, `beta`, `52WeekHigh`, `52WeekLow`, etc. |
413
+ | `get_financials()` | `"income"` | Up to 4 years annual / 8 quarters quarterly |
414
+ | `get_financials()` | `"balance"` | Up to 4 years annual / 8 quarters quarterly |
415
+ | `get_financials()` | `"cashflow"` | Up to 4 years annual / 8 quarters quarterly |
416
+
417
+ ```python
418
+ from alphadataforge.data.fundamental import Fundamentals
419
+
420
+ # Company overview
421
+ info = Fundamentals.get_info("AAPL", provider="yfinance")
422
+ print(info["sector"]) # "Technology"
423
+ print(info["marketCap"]) # 3_000_000_000_000
424
+ print(info["trailingPE"]) # 33.2
425
+
426
+ # Financial Statements
427
+ df_income = Fundamentals.get_financials("AAPL", statement="income", period="annual", provider="yfinance")
428
+ df_balance = Fundamentals.get_financials("AAPL", statement="balance", period="quarterly", provider="yfinance")
429
+ df_cashflow = Fundamentals.get_financials("AAPL", statement="cashflow", period="annual", provider="yfinance")
430
+
431
+ # Merge all three by date index
432
+ df_all = df_income.join(df_balance, how="outer", rsuffix="_bal").join(df_cashflow, how="outer", rsuffix="_cf")
433
+ ```
434
+
435
+ ---
436
+
437
+ ### Provider 2: Alpha Vantage (`provider="alphavantage"`)
438
+
439
+ | | |
440
+ |---|---|
441
+ | **API Key** | Yes Required (`ALPHAVANTAGE_API_KEY`) |
442
+ | **Rate Limit (Free)** | 25 requests / day, 1 request / second |
443
+ | **Auto-Sleep** | Yes Built-in 1.2s delay between each request (no manual sleep needed) |
444
+ | **Data Depth** | Up to 20 years of price history, full SEC-normalized financials |
445
+ | **Best For** | Deep historical financial statements, Earnings Calendar |
446
+
447
+ > Warning **25 requests/day** is the hard ceiling on the free plan. Each call to `get_financials()` or `get_info()` costs 1 request. Plan accordingly.
448
+
449
+ #### Price
450
+
451
+ ```python
452
+ from alphadataforge.data.price import Price
453
+
454
+ # Basic EOD price (compact = last 100 days)
455
+ df = Price.get("AAPL", provider="alphavantage")
456
+
457
+ # Full history (~20 years)
458
+ df = Price.get("AAPL", provider="alphavantage", provider_params={"outputsize": "full"})
459
+
460
+ # Adjusted prices (costs 3 API calls: Price + Dividends + Splits)
461
+ df = Price.get("AAPL", provider="alphavantage", provider_params={"adjusted": True, "outputsize": "full"})
462
+ ```
463
+
464
+ **Extra `provider_params` for AlphaVantage Price:**
465
+
466
+ | Param | Type | Default | Description |
467
+ |---|---|---|---|
468
+ | `outputsize` | `str` | `"compact"` | `"compact"` (last 100 days) or `"full"` (full ~20yr history). |
469
+ | `adjusted` | `bool` | `False` | Fetches dividends/splits and back-adjusts all OHLCV. |
470
+
471
+ **Under the hood (Alpha Vantage):**
472
+ - **API Calls (`adjusted=False`):** 1 call (`TIME_SERIES_DAILY`).
473
+ - **API Calls (`adjusted=True`):** **3 calls** (`TIME_SERIES_DAILY`, `DIVIDENDS`, `SPLITS`).
474
+ - **Adjusted Prices:** Since Alpha Vantage made their adjusted endpoint premium, we built a custom math engine. When `adjusted=True`, we fetch raw prices, dividends, and splits on the free endpoints and calculate `Adj Open`, `Adj High`, `Adj Low`, `Adj Close`, `Adj Volume` entirely locally. You get premium-grade adjusted data for free!
475
+
476
+ #### Fundamentals (AlphaVantage)
477
+
478
+ | Method | `statement=` value | Notes |
479
+ |---|---|---|
480
+ | `get_info()` | — | Maps to `OVERVIEW` endpoint. Returns company ratios, P/E, EPS, dividends, 52-week data etc. |
481
+ | `get_financials()` | `"income"` | SEC-normalized annual or quarterly income statement |
482
+ | `get_financials()` | `"balance"` | SEC-normalized annual or quarterly balance sheet |
483
+ | `get_financials()` | `"cashflow"` | SEC-normalized annual or quarterly cash flow |
484
+ | `get_financials()` | `"shares_outstanding"` | Historical quarterly shares (basic + diluted). Note: `period` param ignored — always quarterly. |
485
+ | `get_financials()` | `"earnings"` | Annual/quarterly EPS history with analyst estimates and surprise % |
486
+ | `get_earnings_calendar()` | — | Upcoming earnings dates for next 3/6/12 months |
487
+
488
+ ```python
489
+ from alphadataforge.data.fundamental import Fundamentals
490
+
491
+ # Company overview (maps to OVERVIEW endpoint)
492
+ info = Fundamentals.get_info("IBM", provider="alphavantage")
493
+
494
+ # Income Statement (annual, goes back 5+ years)
495
+ df_income = Fundamentals.get_financials("IBM", statement="income", period="annual", provider="alphavantage")
496
+
497
+ # Balance Sheet (quarterly)
498
+ df_balance = Fundamentals.get_financials("IBM", statement="balance", period="quarterly", provider="alphavantage")
499
+
500
+ # Cash Flow
501
+ df_cf = Fundamentals.get_financials("IBM", statement="cashflow", period="annual", provider="alphavantage")
502
+
503
+ # Historical Shares Outstanding (quarterly, to track buybacks/dilution)
504
+ df_shares = Fundamentals.get_financials("IBM", statement="shares_outstanding", provider="alphavantage")
505
+ # Returns: Shares_Outstanding (basic), shares_outstanding_diluted
506
+
507
+ # Earnings History with analyst surprise
508
+ df_earnings = Fundamentals.get_financials("IBM", statement="earnings", period="quarterly", provider="alphavantage")
509
+ # Returns: reportedEPS, estimatedEPS, surprise, surprisePercentage, reportTime
510
+
511
+ # Earnings Calendar (upcoming earnings dates)
512
+ df_cal = Fundamentals.get_earnings_calendar(horizon="3month", provider="alphavantage") # all upcoming
513
+ df_cal = Fundamentals.get_earnings_calendar(horizon="12month", symbol="AAPL", provider="alphavantage") # specific ticker
514
+ ```
515
+
516
+ ---
517
+
518
+ ### Provider 3: Tiingo (`provider="tiingo"`)
519
+
520
+ | | |
521
+ |---|---|
522
+ | **API Key** | Yes Required (`TIINGO_API_KEY`) |
523
+ | **Rate Limit (Free)** | 500 requests / hour (very generous) |
524
+ | **Best For** | Clean institutional-grade EOD prices, News API with tag filtering |
525
+
526
+ #### Price
527
+
528
+ ```python
529
+ from alphadataforge.data.price import Price
530
+
531
+ # Basic EOD price
532
+ df = Price.get("AAPL", provider="tiingo", start_date="2020-01-01")
533
+
534
+ # With weekly frequency
535
+ df = Price.get("AAPL", provider="tiingo", provider_params={"frequency": "weekly"})
536
+
537
+ # Adjusted prices (natively returned — no extra API calls)
538
+ df = Price.get("AAPL", provider="tiingo", provider_params={"adjusted": True})
539
+ ```
540
+
541
+ **Extra `provider_params` for Tiingo Price:**
542
+
543
+ | Param | Type | Default | Description |
544
+ |---|---|---|---|
545
+ | `frequency` | `str` | `"daily"` | `"daily"`, `"weekly"`, `"monthly"`, `"annually"`. Intraday requires paid plan. |
546
+ | `adjusted` | `bool` | `False` | Returns `Adj Open`, `Adj High`, `Adj Low`, `Adj Close`, `Adj Volume` (natively — no extra API calls). |
547
+
548
+ **Under the hood (Tiingo):**
549
+ - **API Calls:** 1 call per fetch.
550
+ - **Adjusted Prices:** The Tiingo API natively sends a full suite of adjusted prices. The resulting DataFrame will always contain `Adj Open`, `Adj High`, `Adj Low`, `Adj Close`, and `Adj Volume`. No manual calculation needed.
551
+
552
+ #### Crypto (Tiingo)
553
+
554
+ ```python
555
+ # Tiingo crypto uses different ticker format (no dash): BTCUSD, ETHUSD
556
+ from alphadataforge.providers.tiingo_fetcher import TiingoFetcher
557
+
558
+ tiingo = TiingoFetcher()
559
+ crypto_data = tiingo.fetch_crypto(
560
+ tickers=["BTCUSD", "ETHUSD"],
561
+ start_date="2023-01-01",
562
+ resample_freq="1Day" # "1Day", "1Hour", "30Min"
563
+ )
564
+ ```
565
+
566
+ #### News (Tiingo)
567
+
568
+ Tiingo has the most powerful news filtering of all providers.
569
+
570
+ ```python
571
+ from alphadataforge.providers.tiingo_fetcher import TiingoFetcher
572
+
573
+ tiingo = TiingoFetcher()
574
+ news_df = tiingo.fetch_news(
575
+ tickers=["AAPL", "MSFT"], # Filter by ticker (optional)
576
+ tags=["Earnings"], # Filter by tag (optional)
577
+ sources=["reuters.com"], # Filter by source domain (optional)
578
+ start_date="2024-01-01",
579
+ end_date="2024-06-01",
580
+ limit=50
581
+ )
582
+ # Returns columns: publishedDate, title, description, url, tickers, tags
583
+ ```
584
+
585
+ #### Fundamentals (Tiingo)
586
+
587
+ | Method | Support | Notes |
588
+ |---|---|---|
589
+ | `get_info()` | Yes Supported | Returns basic ticker metadata (name, description, exchange, startDate) |
590
+ | `get_financials()` | No Not Supported | Returns empty DataFrame. Full financial statements require Tiingo's paid `fundamentals` plan. |
591
+
592
+ ---
593
+
594
+ ### Provider 4: FMP — Financial Modeling Prep (`provider="fmp"`)
595
+
596
+ | | |
597
+ |---|---|
598
+ | **API Key** | Yes Required (`FMP_API_KEY`) |
599
+ | **Rate Limit (Free)** | 250 requests / day |
600
+ | **Best For** | EOD Price data (free), Deep fundamental history (paid) |
601
+
602
+ > Warning **Important:** Financial statements (`get_financials`) are **locked behind FMP's paid plan** and will return a `403 Forbidden` error with a free API key. `get_info()` and `Price.get()` work on the free tier.
603
+
604
+ #### Price
605
+
606
+ ```python
607
+ from alphadataforge.data.price import Price
608
+
609
+ # Basic EOD price
610
+ df = Price.get("AAPL", provider="fmp", start_date="2023-01-01")
611
+
612
+ # Dividend & split adjusted
613
+ df = Price.get("AAPL", provider="fmp", provider_params={"adjusted": True})
614
+ ```
615
+
616
+ **Extra `provider_params` for FMP Price:**
617
+
618
+ | Param | Type | Default | Description |
619
+ |---|---|---|---|
620
+ | `adjusted` | `bool` | `False` | `False` = raw non-split-adjusted prices. `True` = fully dividend & split adjusted. |
621
+
622
+ **Under the hood (FMP):**
623
+ - **API Calls:** 1 call per fetch (regardless of `adjusted` flag).
624
+ - **Adjusted Prices:** If `adjusted=True`, we hit FMP's `dividend-adjusted` endpoint, which natively returns adjusted values for Open, High, Low, and Close. The resulting DataFrame will contain `Adj Open`, `Adj High`, `Adj Low`, and `Adj Close`.
625
+
626
+ #### Fundamentals (FMP)
627
+
628
+ | Method | `statement=` value | Free Tier | Notes |
629
+ |---|---|---|---|
630
+ | `get_info()` | — | ✅ Free | Returns company profile: `sector`, `industry`, `mktCap`, `beta`, `description`, `ceo`, etc. |
631
+ | `get_financials()` | `"income"` | ✅ Free | 5 periods returned on free tier |
632
+ | `get_financials()` | `"balance"` | ✅ Free | 5 periods returned on free tier |
633
+ | `get_financials()` | `"cashflow"` | ✅ Free | 5 periods returned on free tier |
634
+ | `get_financials()` | `"shares_float"` | ✅ Free | Returns `freeFloat`, `floatShares`, and `outstandingShares` snapshot |
635
+
636
+ ```python
637
+ from alphadataforge.data.fundamental import Fundamentals
638
+
639
+ # Company profile (works on free tier)
640
+ info = Fundamentals.get_info("AAPL", provider="fmp")
641
+ print(info["sector"]) # "Technology"
642
+ print(info["ceo"]) # "Timothy D. Cook"
643
+
644
+ # Financial statements (PAID ONLY — will raise error on free key)
645
+ # df = Fundamentals.get_financials("AAPL", statement="income", provider="fmp")
646
+ ```
647
+
648
+ ---
649
+
650
+ ---
651
+
652
+ ### Provider 5: StockFit (`provider="stockfit"`)
653
+
654
+ | | |
655
+ |---|---|
656
+ | **API Key** | Yes Required (`STOCKFIT_API_KEY`) |
657
+ | **Rate Limit** | 500 requests / minute, 500,000 / day (Pro Plan) |
658
+ | **Data Source** | Direct SEC EDGAR XBRL filings with point-in-time audit lineage |
659
+ | **Best For** | Deep historical US financial statements (20 years), Piotroski Health Scores, Offline Snapshots |
660
+
661
+ #### Financial Statements (20-Year SEC XBRL)
662
+
663
+ StockFit parses official 10-K and 10-Q filings directly, providing up to 20 years of annual statements and 70+ quarters:
664
+
665
+ ```python
666
+ from alphadataforge.data.fundamental import Fundamentals
667
+
668
+ # 1. Income Statement (20 years)
669
+ df_income = Fundamentals.get_financials("AAPL", statement="income", period="annual", provider="stockfit", limit=100)
670
+
671
+ # 2. Balance Sheet (70+ quarters)
672
+ df_balance = Fundamentals.get_financials("AAPL", statement="balance", period="quarterly", provider="stockfit", limit=100)
673
+
674
+ # 3. Cash Flow
675
+ df_cf = Fundamentals.get_financials("AAPL", statement="cashflow", period="annual", provider="stockfit")
676
+
677
+ # 4. Financial Health Scores (Piotroski F-Score)
678
+ scores = Fundamentals.get_scores("AAPL", provider="stockfit")
679
+ print("Piotroski F-Score:", scores["piotroskiFScore"])
680
+ ```
681
+
682
+ #### Cross-Provider Accuracy Benchmark (AAPL)
683
+
684
+ We tested and compared StockFit's normalized financials against YFinance and AlphaVantage:
685
+
686
+ | Metric (2025) | StockFit | YFinance | AlphaVantage | Alignment |
687
+ |---|---|---|---|---|
688
+ | `Total_Revenue` | $416,161,000,000 | $416,161,000,000 | $416,161,000,000 | **100% Identical** |
689
+ | `Net_Income` | $112,010,000,000 | $112,010,000,000 | $112,010,000,000 | **100% Identical** |
690
+ | `Operating_Income` | $133,050,000,000 | $133,050,000,000 | $133,050,000,000 | **100% Identical** |
691
+ | `Total_Assets` | $359,241,000,000 | $359,241,000,000 | $359,241,000,000 | **100% Identical** |
692
+ | `Free_Cash_Flow` | $98,767,000,000 | $98,767,000,000 | N/A | **100% Identical** |
693
+ | **History Depth** | **19–20 Years** | 4 Years | ~5 Years | **StockFit 5x Deeper** |
694
+
695
+ ## Provider Comparison Table
696
+
697
+ ### Price
698
+
699
+ | Feature | YFinance | AlphaVantage | Tiingo | FMP | Hybrid (AV+Tiingo) |
700
+ |---|---|---|---|---|---|
701
+ | API Key Required | No | Yes | Yes | Yes | Yes (both AV + Tiingo) |
702
+ | Rate Limit | Unlimited | 25/day | 500/hr | 250/day | 3 AV calls + 2 Tiingo calls |
703
+ | Adjusted Prices | Yes | Yes (3 calls) | Yes (native) | Yes (native) | Yes (local math engine) |
704
+ | Intraday | Yes (last 60d) | No | Paid only | No | No |
705
+ | Crypto | Yes (`BTC-USD`) | No | Yes (`BTCUSD`) | No | No |
706
+ | Forex | Yes (`EURUSD=X`) | No | No | No | No |
707
+ | Historical Depth | ~20yr | ~20yr | ~30yr | ~30yr | ~20yr |
708
+ | Batch Fetching | Yes (1 call) | ⚠️ 1 call/symbol | Yes (1 call) | ⚠️ 1 call/symbol | ⚠️ 1 call/symbol |
709
+
710
+ ### Fundamentals
711
+
712
+ | Feature | YFinance | StockFit (SEC XBRL) | AlphaVantage | Tiingo | FMP |
713
+ |---|---|---|---|---|---|
714
+ | Historical Depth | 4yr (8 quarters) | **15–20yr (70+ quarters)** | ~5yr | None | Locked |
715
+ | Rate Limit | Unlimited | 500k/day (Pro) | 25/day | 500/hr | 250/day |
716
+ | `get_info()` | Yes Rich (80+ fields) | Yes (SEC details) | Yes Good | Yes Basic | Yes Good |
717
+ | `statement="income"` | Yes (4yr) | **Yes (20yr XBRL)** | Yes (5yr+) | No | No Paid |
718
+ | `statement="balance"` | Yes (4yr) | **Yes (20yr XBRL)** | Yes (5yr+) | No | No Paid |
719
+ | `statement="cashflow"` | Yes (4yr) | **Yes (20yr XBRL)** | Yes (5yr+) | No | No Paid |
720
+ | Health Scores | No | **Yes (Piotroski F-Score)** | No | No | No |
721
+ | SEC Traceability | No | **Yes (10-K/Q Accession #)** | No | No | No |
722
+ | `statement="shares_outstanding"` | Warning (in balance sheet) | Yes Historical | Yes Historical | No | No Paid |
723
+ | `statement="earnings"` | Warning (basic) | Yes | Yes + Analyst Est. | No | No Paid |
724
+ | `get_earnings_calendar()` | No | Yes | Yes | No | No |
725
+ | News | Yes | Yes | No | Yes (Best) | No |
726
+
727
+ ---
728
+
729
+ ## Roadmap
730
+
731
+ - [x] SQLite Caching (24h TTL, OS temp folder)
732
+ - [x] Concurrent batch fetching (ThreadPoolExecutor)
733
+ - [x] Auto Rate Limit protection (AlphaVantage)
734
+ - [x] Custom Exception hierarchy
735
+ - [x] Fundamentals Facade (Income, Balance, Cashflow, Earnings, Shares Outstanding)
736
+ - [x] Universal Adjusted Prices — all providers return full `Adj Open/High/Low/Close/Volume`
737
+ - [x] Local Back-Adjustment Math Engine (`finance_math.calculate_adjusted_prices`)
738
+ - [x] Hybrid Provider (`hybrid_av_tiingo`) — deepest history + accurate dividends/splits
739
+ - [x] `Price.compare()` — concurrent multi-provider comparison (MultiIndex DataFrame)
740
+ - [x] `Price.consensus()` — mean/median aggregation across providers
741
+ - [x] Custom external DataFrame injection (`custom_data` parameter)
742
+ - [x] StockFit Integration (20-year SEC XBRL Statements, Piotroski Scores, Snapshot Utility)
743
+ - [ ] Data Quality Checks (anomaly detection, missing data handling)
744
+ - [ ] Technical Indicators Module (RSI, MACD, Bollinger Bands)
745
+ - [ ] Streaming / real-time price feed
746
+
747
+ ---
748
+
749
+ ## Testing
750
+
751
+ This project utilizes `pytest-vcr` and `unittest.mock` to ensure robust and comprehensive test coverage without consuming actual API rate limits.
752
+
753
+ ### VCR Cassettes (`pytest-vcr`)
754
+ VCR records real HTTP interactions and saves them as `.yaml` files (cassettes) within the `tests/cassettes/` directory.
755
+
756
+ - **Rate Limit Protection**: By replaying recorded responses, our test suite can execute reliably in CI/CD environments (e.g., GitHub Actions) without exhausting API provider quotas.
757
+ - **Security**: Sensitive query parameters, such as API keys, are automatically sanitized and replaced with placeholder strings (`DUMMY`) before being persisted to the cassettes.
758
+ - **Version Control**: The `.yaml` cassette files **must be committed** to version control. They act as the definitive mock data for automated CI pipelines.
759
+
760
+ ### Generating New Cassettes
761
+ When adding new tests or modifying existing API requests, you will need to generate new cassettes:
762
+ 1. Ensure your valid API keys are configured in the `.env` file.
763
+ 2. Execute `pytest` locally. If a cassette for a specific test does not exist, VCR will automatically perform a real HTTP request and record the sanitized response.
764
+ 3. Commit the newly generated `.yaml` files to Git.
765
+
766
+ ### Running Tests & Checking Coverage
767
+ To run the test suite and see a detailed coverage report for all fetchers:
768
+ ```bash
769
+ pytest --cov=src/alphadataforge --cov-report=term-missing
770
+ ```