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,125 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
def calculate_adjusted_prices(
|
|
5
|
+
raw_df: pd.DataFrame,
|
|
6
|
+
dividends_df: pd.DataFrame,
|
|
7
|
+
splits_df: pd.DataFrame
|
|
8
|
+
) -> pd.DataFrame:
|
|
9
|
+
"""
|
|
10
|
+
Calculates backward-adjusted OHLCV prices using raw daily data, dividends, and splits.
|
|
11
|
+
|
|
12
|
+
Expected inputs:
|
|
13
|
+
- raw_df: DataFrame with DatetimeIndex and columns ['Open', 'High', 'Low', 'Close', 'Volume']
|
|
14
|
+
- dividends_df: DataFrame with DatetimeIndex (ex-dividend date) and column 'Dividend'
|
|
15
|
+
- splits_df: DataFrame with DatetimeIndex (effective date) and column 'SplitFactor'
|
|
16
|
+
(SplitFactor > 1 means a forward split, e.g., 4.0 for a 4-for-1 split)
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
- DataFrame with same index and columns ['Open', 'High', 'Low', 'Close', 'Volume', 'Adj Close', ...]
|
|
20
|
+
"""
|
|
21
|
+
if raw_df.empty:
|
|
22
|
+
return raw_df
|
|
23
|
+
|
|
24
|
+
required_cols = {'Open', 'High', 'Low', 'Close', 'Volume'}
|
|
25
|
+
missing = required_cols - set(raw_df.columns)
|
|
26
|
+
if missing:
|
|
27
|
+
raise ValueError(
|
|
28
|
+
f"calculate_adjusted_prices requires columns {required_cols}. "
|
|
29
|
+
f"Missing: {missing}. Got: {list(raw_df.columns)}"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
df = raw_df.copy()
|
|
33
|
+
|
|
34
|
+
# Ensure index is sorted ascending (oldest to newest)
|
|
35
|
+
df = df.sort_index(ascending=True)
|
|
36
|
+
|
|
37
|
+
# 1. Join Dividends and Splits onto the main DataFrame
|
|
38
|
+
# Ensure df index is tz-naive
|
|
39
|
+
if df.index.tz is not None:
|
|
40
|
+
df.index = df.index.tz_localize(None)
|
|
41
|
+
|
|
42
|
+
df['Dividend'] = 0.0
|
|
43
|
+
if dividends_df is not None and not dividends_df.empty:
|
|
44
|
+
if dividends_df.index.tz is not None:
|
|
45
|
+
dividends_df.index = dividends_df.index.tz_localize(None)
|
|
46
|
+
# Align dividends by date
|
|
47
|
+
div_series = dividends_df['Dividend'] if 'Dividend' in dividends_df.columns else dividends_df.iloc[:, 0]
|
|
48
|
+
divs = div_series.groupby(dividends_df.index).sum()
|
|
49
|
+
df['Dividend'] = df.index.map(divs).fillna(0.0)
|
|
50
|
+
|
|
51
|
+
df['SplitFactor'] = 1.0
|
|
52
|
+
if splits_df is not None and not splits_df.empty:
|
|
53
|
+
if splits_df.index.tz is not None:
|
|
54
|
+
splits_df.index = splits_df.index.tz_localize(None)
|
|
55
|
+
# Align splits by date (if multiple splits on same day, multiply them)
|
|
56
|
+
split_series = splits_df['SplitFactor'] if 'SplitFactor' in splits_df.columns else splits_df.iloc[:, 0]
|
|
57
|
+
splits = split_series.groupby(splits_df.index).prod()
|
|
58
|
+
df['SplitFactor'] = df.index.map(splits).fillna(1.0)
|
|
59
|
+
|
|
60
|
+
# 2. Calculate Cumulative Split Multiplier (Backward)
|
|
61
|
+
# If a split is effective ON day t, prices on day t are ALREADY split.
|
|
62
|
+
# Prices on day t-1 must be divided by SplitFactor to be comparable.
|
|
63
|
+
# So CumSplit on day t-1 should multiply SplitFactor from day t.
|
|
64
|
+
df['CumSplit'] = df['SplitFactor'].iloc[::-1].cumprod().shift(1).fillna(1.0).iloc[::-1]
|
|
65
|
+
|
|
66
|
+
# 3. Calculate Split-Adjusted Close (needed for dividend adjustment)
|
|
67
|
+
df['SplitAdjClose'] = df['Close'] / df['CumSplit']
|
|
68
|
+
|
|
69
|
+
# 4. Calculate Dividend Adjustment Factor
|
|
70
|
+
# DivFactor_t = (PrevClose - Dividend) / PrevClose
|
|
71
|
+
# This factor calculated on ex-date applies to all days BEFORE ex-date.
|
|
72
|
+
prev_close = df['SplitAdjClose'].shift(1)
|
|
73
|
+
|
|
74
|
+
# Avoid division by zero
|
|
75
|
+
div_factor = np.where(
|
|
76
|
+
(df['Dividend'] > 0) & (prev_close > 0),
|
|
77
|
+
(prev_close - df['Dividend']) / prev_close,
|
|
78
|
+
1.0
|
|
79
|
+
)
|
|
80
|
+
df['DivFactor'] = div_factor
|
|
81
|
+
|
|
82
|
+
# 5. Calculate Cumulative Dividend Multiplier (Backward)
|
|
83
|
+
df['CumDiv'] = df['DivFactor'].iloc[::-1].cumprod().shift(1).fillna(1.0).iloc[::-1]
|
|
84
|
+
|
|
85
|
+
# 6. Apply Total Adjustment Factor
|
|
86
|
+
df['AdjFactor'] = df['CumDiv'] / df['CumSplit']
|
|
87
|
+
|
|
88
|
+
# We create standard adjusted columns
|
|
89
|
+
df['Adj Close'] = df['Close'] * df['AdjFactor']
|
|
90
|
+
df['Adj Open'] = df['Open'] * df['AdjFactor']
|
|
91
|
+
df['Adj High'] = df['High'] * df['AdjFactor']
|
|
92
|
+
df['Adj Low'] = df['Low'] * df['AdjFactor']
|
|
93
|
+
|
|
94
|
+
# Volume is adjusted by splits only, not dividends
|
|
95
|
+
df['Adj Volume'] = df['Volume'] * df['CumSplit']
|
|
96
|
+
|
|
97
|
+
# Drop temporary calculation columns
|
|
98
|
+
drop_cols = ['Dividend', 'SplitFactor', 'CumSplit', 'SplitAdjClose', 'DivFactor', 'CumDiv', 'AdjFactor']
|
|
99
|
+
df = df.drop(columns=drop_cols)
|
|
100
|
+
|
|
101
|
+
return df
|
|
102
|
+
|
|
103
|
+
def backfill_adjusted_columns(df: pd.DataFrame) -> pd.DataFrame:
|
|
104
|
+
"""
|
|
105
|
+
Backfills 'Adj Open', 'Adj High', 'Adj Low' by calculating the ratio of 'Adj Close' / 'Close'.
|
|
106
|
+
This ensures a consistent Full Adjusted OHLC schema across providers like YFinance or FMP
|
|
107
|
+
that only natively return 'Adj Close'.
|
|
108
|
+
"""
|
|
109
|
+
if df.empty:
|
|
110
|
+
return df
|
|
111
|
+
|
|
112
|
+
df = df.copy()
|
|
113
|
+
|
|
114
|
+
if 'Close' in df.columns and 'Adj Close' in df.columns:
|
|
115
|
+
# Calculate ratio, defaulting to 1.0 where Close is 0 or NaN
|
|
116
|
+
ratio = np.where(df['Close'] != 0, df['Adj Close'] / df['Close'], 1.0)
|
|
117
|
+
|
|
118
|
+
if 'Open' in df.columns and 'Adj Open' not in df.columns:
|
|
119
|
+
df['Adj Open'] = df['Open'] * ratio
|
|
120
|
+
if 'High' in df.columns and 'Adj High' not in df.columns:
|
|
121
|
+
df['Adj High'] = df['High'] * ratio
|
|
122
|
+
if 'Low' in df.columns and 'Adj Low' not in df.columns:
|
|
123
|
+
df['Adj Low'] = df['Low'] * ratio
|
|
124
|
+
|
|
125
|
+
return df
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def setup_logger(name: str, level: int = logging.INFO) -> logging.Logger:
|
|
6
|
+
"""
|
|
7
|
+
Create and return a logger with a consistent format for the library.
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
from alphadataforge.utils.logger import setup_logger
|
|
11
|
+
logger = setup_logger(__name__)
|
|
12
|
+
logger.info("message")
|
|
13
|
+
|
|
14
|
+
End-users can control verbosity via:
|
|
15
|
+
logging.getLogger("alphadataforge").setLevel(logging.DEBUG)
|
|
16
|
+
"""
|
|
17
|
+
logger = logging.getLogger(name)
|
|
18
|
+
|
|
19
|
+
# Avoid adding duplicate handlers when modules are re-imported
|
|
20
|
+
if not logger.handlers:
|
|
21
|
+
handler = logging.StreamHandler(sys.stdout)
|
|
22
|
+
formatter = logging.Formatter(
|
|
23
|
+
fmt="%(asctime)s | %(name)s | %(levelname)s | %(message)s",
|
|
24
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
25
|
+
)
|
|
26
|
+
handler.setFormatter(formatter)
|
|
27
|
+
logger.addHandler(handler)
|
|
28
|
+
|
|
29
|
+
logger.setLevel(level)
|
|
30
|
+
return logger
|