Add adjust_ohlcv.py
Browse files- adjust_ohlcv.py +39 -19
adjust_ohlcv.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
"""Split/dividend adjustment for a user's raw 1-minute OHLCV bars.
|
| 2 |
|
| 3 |
-
Reproduces the convention the model was trained under (
|
| 4 |
-
|
| 5 |
|
| 6 |
* factors are cumulative and backward-looking, anchored at 1.0 on the latest bar date;
|
| 7 |
* a split with ex-date D multiplies price by denominator/numerator and volume by
|
|
@@ -9,7 +9,8 @@ data_pull/data/adjustments.py and preprocessing/quality.py in the source repo):
|
|
| 9 |
* a cash dividend with ex-date D multiplies price by (close_prior - amount) / close_prior
|
| 10 |
on every bar dated strictly before D, where close_prior is the regular-session close on
|
| 11 |
the last bar date before D; volume is unchanged;
|
| 12 |
-
* dividends with amount >= close_prior are skipped (price left under-adjusted)
|
|
|
|
| 13 |
|
| 14 |
Only pandas and numpy are required.
|
| 15 |
"""
|
|
@@ -29,32 +30,43 @@ def clean_bars(bars: pd.DataFrame) -> pd.DataFrame:
|
|
| 29 |
"""Apply the training pipeline's raw-bar quality checks.
|
| 30 |
|
| 31 |
Drops duplicate timestamps (keeps first), bars with high < low, bars whose close lies
|
| 32 |
-
outside [low, high], and bars with a non-positive price; negative
|
|
|
|
| 33 |
"""
|
| 34 |
df = bars.drop_duplicates(subset="timestamp", keep="first")
|
| 35 |
ok = (df["high"] >= df["low"]) & (df["close"] <= df["high"]) & (df["close"] >= df["low"])
|
| 36 |
ok &= (df[["open", "high", "low", "close"]] > 0).all(axis=1)
|
| 37 |
df = df[ok.fillna(False)].copy()
|
| 38 |
-
df["volume"] = df["volume"].clip(lower=0)
|
| 39 |
return df.sort_values("timestamp").reset_index(drop=True)
|
| 40 |
|
| 41 |
|
| 42 |
def _eastern(ts: pd.Series) -> pd.Series:
|
| 43 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
return ts.dt.tz_localize(EASTERN) if ts.dt.tz is None else ts.dt.tz_convert(EASTERN)
|
| 45 |
|
| 46 |
|
| 47 |
def _split_events(splits: pd.DataFrame | None) -> dict[dt.date, tuple[float, float]]:
|
| 48 |
-
"""{ex_date: (numerator, denominator)}; accepts `ratio` (
|
|
|
|
|
|
|
|
|
|
| 49 |
if splits is None or len(splits) == 0:
|
| 50 |
return {}
|
| 51 |
s = splits.copy()
|
| 52 |
if "numerator" not in s:
|
| 53 |
s["numerator"], s["denominator"] = s["ratio"].astype(float), 1.0
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
|
|
|
|
|
|
| 58 |
|
| 59 |
|
| 60 |
def _close_prior(df: pd.DataFrame, ex_date: dt.date) -> float | None:
|
|
@@ -67,14 +79,20 @@ def _close_prior(df: pd.DataFrame, ex_date: dt.date) -> float | None:
|
|
| 67 |
|
| 68 |
|
| 69 |
def _dividend_events(df: pd.DataFrame, dividends: pd.DataFrame | None) -> dict[dt.date, tuple[float, float]]:
|
| 70 |
-
"""{ex_date: (amount, close_prior)}; close_prior is taken from the bars unless given.
|
|
|
|
|
|
|
|
|
|
| 71 |
if dividends is None or len(dividends) == 0:
|
| 72 |
return {}
|
| 73 |
-
events = {}
|
| 74 |
given = dividends["close_prior"] if "close_prior" in dividends else [None] * len(dividends)
|
| 75 |
for d, amount, cp in zip(dividends["date"], dividends["amount"], given, strict=True):
|
| 76 |
ex_date = pd.Timestamp(d).date()
|
| 77 |
-
|
|
|
|
|
|
|
|
|
|
| 78 |
if close_prior is not None and close_prior > 0:
|
| 79 |
events[ex_date] = (float(amount), float(close_prior))
|
| 80 |
return events
|
|
@@ -120,10 +138,12 @@ def adjust_ohlcv(
|
|
| 120 |
"""Return cleaned bars with adjusted prices ready for `make_features`.
|
| 121 |
|
| 122 |
Args:
|
| 123 |
-
bars: one security; columns `timestamp` (tz-aware, or naive
|
| 124 |
-
`high`, `low`, `close`, `volume`, all unadjusted.
|
| 125 |
-
splits: columns `date` (ex-date) and either `ratio` or `numerator`+`denominator`
|
| 126 |
-
|
|
|
|
|
|
|
| 127 |
|
| 128 |
Returns:
|
| 129 |
DataFrame with `timestamp_eastern`, `open_adj`, `high_adj`, `low_adj`, `close_adj`,
|
|
@@ -150,4 +170,4 @@ def adjust_ohlcv(
|
|
| 150 |
"adj_factor_price",
|
| 151 |
"adj_factor_volume",
|
| 152 |
]
|
| 153 |
-
return df[keep]
|
|
|
|
| 1 |
"""Split/dividend adjustment for a user's raw 1-minute OHLCV bars.
|
| 2 |
|
| 3 |
+
Reproduces the convention the model was trained under (ported from the authors' training
|
| 4 |
+
pipeline):
|
| 5 |
|
| 6 |
* factors are cumulative and backward-looking, anchored at 1.0 on the latest bar date;
|
| 7 |
* a split with ex-date D multiplies price by denominator/numerator and volume by
|
|
|
|
| 9 |
* a cash dividend with ex-date D multiplies price by (close_prior - amount) / close_prior
|
| 10 |
on every bar dated strictly before D, where close_prior is the regular-session close on
|
| 11 |
the last bar date before D; volume is unchanged;
|
| 12 |
+
* dividends with amount >= close_prior are skipped (price left under-adjusted);
|
| 13 |
+
* two actions on the same ex-date are combined: amounts summed, split ratios multiplied.
|
| 14 |
|
| 15 |
Only pandas and numpy are required.
|
| 16 |
"""
|
|
|
|
| 30 |
"""Apply the training pipeline's raw-bar quality checks.
|
| 31 |
|
| 32 |
Drops duplicate timestamps (keeps first), bars with high < low, bars whose close lies
|
| 33 |
+
outside [low, high], and bars with a non-positive or missing price; negative or
|
| 34 |
+
missing volume is set to 0.
|
| 35 |
"""
|
| 36 |
df = bars.drop_duplicates(subset="timestamp", keep="first")
|
| 37 |
ok = (df["high"] >= df["low"]) & (df["close"] <= df["high"]) & (df["close"] >= df["low"])
|
| 38 |
ok &= (df[["open", "high", "low", "close"]] > 0).all(axis=1)
|
| 39 |
df = df[ok.fillna(False)].copy()
|
| 40 |
+
df["volume"] = df["volume"].fillna(0).clip(lower=0)
|
| 41 |
return df.sort_values("timestamp").reset_index(drop=True)
|
| 42 |
|
| 43 |
|
| 44 |
def _eastern(ts: pd.Series) -> pd.Series:
|
| 45 |
+
"""Parse if needed, then localize naive as Eastern or convert aware to Eastern."""
|
| 46 |
+
if not pd.api.types.is_datetime64_any_dtype(ts):
|
| 47 |
+
try:
|
| 48 |
+
ts = pd.to_datetime(ts)
|
| 49 |
+
except ValueError: # strings with mixed UTC offsets, e.g. across a DST change
|
| 50 |
+
ts = pd.to_datetime(ts, utc=True)
|
| 51 |
return ts.dt.tz_localize(EASTERN) if ts.dt.tz is None else ts.dt.tz_convert(EASTERN)
|
| 52 |
|
| 53 |
|
| 54 |
def _split_events(splits: pd.DataFrame | None) -> dict[dt.date, tuple[float, float]]:
|
| 55 |
+
"""{ex_date: (numerator, denominator)}; accepts `ratio` (10.0 = 10-for-1, 0.5 = 1-for-2).
|
| 56 |
+
|
| 57 |
+
Same-date splits are multiplied together.
|
| 58 |
+
"""
|
| 59 |
if splits is None or len(splits) == 0:
|
| 60 |
return {}
|
| 61 |
s = splits.copy()
|
| 62 |
if "numerator" not in s:
|
| 63 |
s["numerator"], s["denominator"] = s["ratio"].astype(float), 1.0
|
| 64 |
+
events: dict[dt.date, tuple[float, float]] = {}
|
| 65 |
+
for d, n, m in zip(s["date"], s["numerator"], s["denominator"], strict=True):
|
| 66 |
+
ex_date = pd.Timestamp(d).date()
|
| 67 |
+
n0, m0 = events.get(ex_date, (1.0, 1.0))
|
| 68 |
+
events[ex_date] = (n0 * float(n), m0 * float(m))
|
| 69 |
+
return events
|
| 70 |
|
| 71 |
|
| 72 |
def _close_prior(df: pd.DataFrame, ex_date: dt.date) -> float | None:
|
|
|
|
| 79 |
|
| 80 |
|
| 81 |
def _dividend_events(df: pd.DataFrame, dividends: pd.DataFrame | None) -> dict[dt.date, tuple[float, float]]:
|
| 82 |
+
"""{ex_date: (amount, close_prior)}; close_prior is taken from the bars unless given.
|
| 83 |
+
|
| 84 |
+
Same-date dividends are summed against one close_prior.
|
| 85 |
+
"""
|
| 86 |
if dividends is None or len(dividends) == 0:
|
| 87 |
return {}
|
| 88 |
+
events: dict[dt.date, tuple[float, float]] = {}
|
| 89 |
given = dividends["close_prior"] if "close_prior" in dividends else [None] * len(dividends)
|
| 90 |
for d, amount, cp in zip(dividends["date"], dividends["amount"], given, strict=True):
|
| 91 |
ex_date = pd.Timestamp(d).date()
|
| 92 |
+
if ex_date in events:
|
| 93 |
+
events[ex_date] = (events[ex_date][0] + float(amount), events[ex_date][1])
|
| 94 |
+
continue
|
| 95 |
+
close_prior = cp if not pd.isna(cp) else _close_prior(df, ex_date)
|
| 96 |
if close_prior is not None and close_prior > 0:
|
| 97 |
events[ex_date] = (float(amount), float(close_prior))
|
| 98 |
return events
|
|
|
|
| 138 |
"""Return cleaned bars with adjusted prices ready for `make_features`.
|
| 139 |
|
| 140 |
Args:
|
| 141 |
+
bars: one security; columns `timestamp` (bar start; tz-aware, or naive taken as
|
| 142 |
+
Eastern), `open`, `high`, `low`, `close`, `volume`, all unadjusted.
|
| 143 |
+
splits: columns `date` (ex-date) and either `ratio` or `numerator`+`denominator`
|
| 144 |
+
(numerator = shares after the split).
|
| 145 |
+
dividends: columns `date` (ex-date), `amount` (cash per share as declared),
|
| 146 |
+
optional `close_prior`.
|
| 147 |
|
| 148 |
Returns:
|
| 149 |
DataFrame with `timestamp_eastern`, `open_adj`, `high_adj`, `low_adj`, `close_adj`,
|
|
|
|
| 170 |
"adj_factor_price",
|
| 171 |
"adj_factor_volume",
|
| 172 |
]
|
| 173 |
+
return df[keep]
|