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,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