Files
Pratik Bhadane 436954138f chore: prepare v1.1.0 release
Update version numbers across Rust, Python, and documentation files to 1.1.0. Enhance the .gitignore to include macOS dSYM files and plans directory. Introduce new dependencies in the Rust core library and update the README to reflect recent performance benchmarks and backtesting engine capabilities. Add new artifacts to the benchmarks manifest and improve documentation for the backtesting engine API.
2026-03-30 12:45:52 +05:30

195 lines
6.9 KiB
Python

"""
Corporate action price adjustment utilities.
adjust_for_splits(close, split_factors, split_indices)
Apply split adjustments to a close price series (backward-adjusted).
adjust_for_dividends(close, dividends, ex_dates)
Apply dividend adjustments to a close price series (backward-adjusted).
adjust_ohlcv(open_, high, low, close, volume, split_factors=None, split_indices=None,
dividends=None, ex_date_indices=None)
Apply both split and dividend adjustments to a full OHLCV dataset.
Returns (adj_open, adj_high, adj_low, adj_close, adj_volume).
"""
from typing import Optional
import numpy as np
from numpy.typing import ArrayLike, NDArray
__all__ = ["adjust_for_splits", "adjust_for_dividends", "adjust_ohlcv"]
def adjust_for_splits(
close: ArrayLike,
split_factors: ArrayLike, # e.g. [2.0, 3.0] means 2-for-1 then 3-for-1
split_indices: ArrayLike, # bar indices of each split (must be sorted ascending)
) -> NDArray:
"""Backward-adjust close prices for stock splits.
All prices BEFORE a split are divided by the split factor.
e.g. a 2-for-1 split at bar 100: prices[0:100] are halved.
Parameters
----------
close : array-like
Raw close prices.
split_factors : array-like
Split factor for each split event (e.g. 2.0 for a 2-for-1 split).
split_indices : array-like
Bar index of each split event (0-based, must be sorted ascending).
Returns
-------
NDArray of adjusted close prices.
"""
c = np.asarray(close, dtype=np.float64).copy()
factors = np.asarray(split_factors, dtype=np.float64)
indices = np.asarray(split_indices, dtype=np.intp)
# Process splits in chronological order; apply backward adjustment
# (all bars before the split are divided by the factor)
for idx, factor in zip(indices, factors):
if factor <= 0:
raise ValueError(f"split_factor must be > 0, got {factor}")
c[:idx] /= factor
return c
def adjust_for_dividends(
close: ArrayLike,
dividends: ArrayLike, # dividend amount per ex-date
ex_date_indices: ArrayLike, # bar indices of ex-dividend dates
) -> NDArray:
"""Backward-adjust close prices for cash dividends (proportional method).
Adjustment factor at ex-date i = (close[i-1] - dividend) / close[i-1].
All bars before ex-date are multiplied by the cumulative adjustment.
Parameters
----------
close : array-like
Raw close prices.
dividends : array-like
Dividend amount (in currency units) at each ex-dividend date.
ex_date_indices : array-like
Bar index of each ex-dividend date (0-based, sorted ascending).
Returns
-------
NDArray of adjusted close prices.
"""
c = np.asarray(close, dtype=np.float64).copy()
divs = np.asarray(dividends, dtype=np.float64)
indices = np.asarray(ex_date_indices, dtype=np.intp)
# Process in chronological order
for idx, div in zip(indices, divs):
if idx == 0:
# No prior bar; skip adjustment (nothing to adjust)
continue
prev_close = c[idx - 1]
if prev_close <= 0:
continue
adj_factor = (prev_close - div) / prev_close
if adj_factor <= 0:
continue
# All prices before ex-date are multiplied by adj_factor
c[:idx] *= adj_factor
return c
def adjust_ohlcv(
open_: ArrayLike,
high: ArrayLike,
low: ArrayLike,
close: ArrayLike,
volume: ArrayLike,
split_factors: Optional[ArrayLike] = None,
split_indices: Optional[ArrayLike] = None,
dividends: Optional[ArrayLike] = None,
ex_date_indices: Optional[ArrayLike] = None,
) -> tuple[NDArray, NDArray, NDArray, NDArray, NDArray]:
"""Apply split and dividend adjustments to full OHLCV data.
Price arrays are multiplied by cumulative adjustment factor.
Volume is divided by split factors (shares outstanding adjust inversely).
Returns (adj_open, adj_high, adj_low, adj_close, adj_volume).
Parameters
----------
open_, high, low, close : array-like
Raw OHLCV price arrays.
volume : array-like
Raw volume array.
split_factors : array-like, optional
Split factors for each split event.
split_indices : array-like, optional
Bar indices of split events (required if split_factors provided).
dividends : array-like, optional
Dividend amounts for each ex-date.
ex_date_indices : array-like, optional
Bar indices of ex-dividend dates (required if dividends provided).
Returns
-------
(adj_open, adj_high, adj_low, adj_close, adj_volume)
"""
o = np.asarray(open_, dtype=np.float64).copy()
h = np.asarray(high, dtype=np.float64).copy()
low_arr = np.asarray(low, dtype=np.float64).copy()
c = np.asarray(close, dtype=np.float64).copy()
v = np.asarray(volume, dtype=np.float64).copy()
n = len(c)
# Build a per-bar cumulative adjustment factor for prices (starts at 1.0)
price_adj = np.ones(n, dtype=np.float64)
# Separate inverse adjustment for volume (splits only)
vol_adj = np.ones(n, dtype=np.float64)
# -----------------------------------------------------------------------
# Apply split adjustments
# -----------------------------------------------------------------------
if split_factors is not None and split_indices is not None:
sf = np.asarray(split_factors, dtype=np.float64)
si = np.asarray(split_indices, dtype=np.intp)
for idx, factor in zip(si, sf):
if factor <= 0:
raise ValueError(f"split_factor must be > 0, got {factor}")
# Prices before split are divided by factor
price_adj[:idx] /= factor
# Volume before split is multiplied by factor (more shares pre-split)
vol_adj[:idx] *= factor
# -----------------------------------------------------------------------
# Apply dividend adjustments (prices only)
# -----------------------------------------------------------------------
if dividends is not None and ex_date_indices is not None:
divs = np.asarray(dividends, dtype=np.float64)
ei = np.asarray(ex_date_indices, dtype=np.intp)
# We need the split-adjusted close at (idx-1) for each dividend event.
# Instead of recomputing the full array each iteration, read the single
# element we need: c[idx-1] * price_adj[idx-1].
for idx, div in zip(ei, divs):
if idx == 0:
continue
prev_close = c[idx - 1] * price_adj[idx - 1]
if prev_close <= 0:
continue
adj_factor = (prev_close - div) / prev_close
if adj_factor <= 0:
continue
price_adj[:idx] *= adj_factor
adj_open = o * price_adj
adj_high = h * price_adj
adj_low = low_arr * price_adj
adj_close = c * price_adj
adj_volume = v * vol_adj
return adj_open, adj_high, adj_low, adj_close, adj_volume