Corporate Actions¶
Handling stock splits, dividends, and other corporate actions in your data.
Overview¶
Corporate actions affect price and volume data:
| Action | Effect on Price | Effect on Volume |
|---|---|---|
| Stock Split | Divided | Multiplied |
| Reverse Split | Multiplied | Divided |
| Dividend | Reduced (ex-date) | No change |
| Spin-off | Reduced | No change |
| Merger | Converted/Delisted | Varies |
Adjusted vs Unadjusted Prices¶
Unadjusted (Raw) Prices¶
What actually traded on each day:
Text Only
Date | AAPL Price
2014-06-06 | $645.57 <- Before 7:1 split
2014-06-09 | $93.70 <- After split
Adjusted Prices¶
Retroactively adjusted for corporate actions:
Text Only
Date | AAPL Adjusted Price
2014-06-06 | $92.22 <- Adjusted back
2014-06-09 | $93.70 <- Same
Always use adjusted prices for backtesting.
Using Adjusted Prices¶
Data Provider¶
Most data providers offer adjusted prices:
Text Only
data:
source = "adjusted_prices.parquet"
format = parquet
columns:
date: Date
ticker: Symbol
adj_close: Numeric as prices # Use adjusted close
volume: Numeric
Manual Adjustment¶
If you have split data:
Text Only
signal adjust_for_splits:
// Cumulative adjustment factor
adj_factor = cumprod(split_ratio)
// Adjust prices backward
adjusted_price = prices / adj_factor
emit adjusted_price
Stock Splits¶
Detection¶
Detect splits in unadjusted data:
Text Only
signal detect_splits:
daily_return = ret(prices, 1)
// Large drop (potential split)
potential_split = daily_return < -0.4
// Check if round ratio
ratio = lag(prices, 1) / prices
is_round = abs(round(ratio) - ratio) < 0.01
split_detected = potential_split and is_round
emit where(split_detected, ratio, 1)
Common Split Ratios¶
| Ratio | Description |
|---|---|
| 2:1 | Price halves |
| 3:1 | Price divides by 3 |
| 4:1 | Price divides by 4 |
| 7:1 | Apple 2014 split |
| 10:1 | NVIDIA 2024 split |
| 20:1 | Google/Amazon 2022 splits |
Reverse Splits¶
Text Only
signal detect_reverse_split:
daily_return = ret(prices, 1)
// Large jump (potential reverse split)
potential_reverse = daily_return > 1.0
// Check if round ratio
ratio = prices / lag(prices, 1)
is_round = abs(round(ratio) - ratio) < 0.01
reverse_detected = potential_reverse and is_round
emit where(reverse_detected, ratio, 1)
Dividends¶
Dividend Adjustment¶
Prices drop by dividend amount on ex-date:
Text Only
signal adjust_for_dividends:
// Dividend yield on ex-date
div_yield = dividend / lag(prices, 1)
// Cumulative adjustment factor
adj_factor = cumprod(1 - div_yield)
// Adjust prices backward
adjusted_price = prices / adj_factor
emit adjusted_price
Total Return¶
Include dividends in return calculation:
Text Only
signal total_return:
price_return = ret(prices, 1)
div_return = dividend / lag(prices, 1)
total = price_return + div_return
emit total
Spin-offs¶
When a company splits into two:
Text Only
signal handle_spinoff:
// Parent company price drops
// Spinoff creates new security
// Track combined value
parent_value = parent_price * parent_shares
spinoff_value = spinoff_price * spinoff_shares
total_value = parent_value + spinoff_value
emit total_value
Mergers and Acquisitions¶
Cash Acquisition¶
Target is acquired for cash:
Text Only
signal handle_cash_acquisition:
// Stock delists at acquisition price
was_trading = lag(prices, 1) > 0
not_trading = prices == 0 or is_nan(prices)
acquired = was_trading and not_trading
// Last price should be ~acquisition price
emit where(acquired, 0, prices)
Stock-for-Stock Merger¶
Target converted to acquirer shares:
Text Only
signal handle_stock_merger:
// Target shareholders receive exchange_ratio shares of acquirer
target_value = target_price * exchange_ratio
emit target_value
Data Quality with Corporate Actions¶
Validation¶
Text Only
signal validate_corporate_actions:
daily_return = ret(prices, 1)
// Flag suspicious large moves
large_move = abs(daily_return) > 0.3
// Check if explained by corporate action
has_action = corporate_action_flag > 0
// Unexplained moves are data errors
suspicious = large_move and not(has_action)
emit where(suspicious, 1, 0)
Volume Adjustment¶
When prices are adjusted, volume should be adjusted inversely:
Best Practices¶
1. Use Pre-Adjusted Data¶
Most data providers handle adjustments:
2. Verify Adjustments¶
Text Only
signal verify_adjustments:
daily_ret = ret(prices, 1)
// No return > 100% (unadjusted splits)
no_splits = abs(daily_ret) < 1.0
// No returns exactly -50%, -67%, etc. (unhandled splits)
common_splits = [-0.5, -0.667, -0.75, -0.8, -0.857, -0.9, -0.95]
// Check distance from common split returns
not_near_split = min(abs(daily_ret - common_splits)) > 0.01
emit where(no_splits and not_near_split, 1, 0)
3. Track Adjustment Factors¶
Keep adjustment factors for audit:
Text Only
data:
source = "prices.parquet"
format = parquet
columns:
date: Date
ticker: Symbol
close: Numeric as raw_prices
adj_close: Numeric as prices
adj_factor: Numeric # For debugging
4. Handle Delistings¶
Text Only
signal handle_delisting:
// Zero out signals for delisted stocks
is_trading = prices > 0 and not(is_nan(prices))
signal = where(is_trading, raw_signal, 0)
emit signal
5. Document Your Adjustments¶
Text Only
// Data adjustments:
// - Split adjusted back to 2000
// - Dividend adjusted (total return)
// - Delisted stocks included with final prices
// - Adjusted volume for splits
data:
source = "fully_adjusted.parquet"
Common Issues¶
Missing Split Adjustment¶
Text Only
Symptom: -50%, -67% returns on split dates
Solution: Use adjusted prices or apply split factors
Missing Dividend Adjustment¶
Text Only
Symptom: Small drops on ex-dividend dates
Solution: Use dividend-adjusted prices or total return prices
Inconsistent Adjustment¶
Text Only
Symptom: Some prices adjusted, others not
Solution: Verify entire dataset uses same adjustment method
Volume Not Adjusted¶
Text Only
Symptom: Volume spikes/drops on split dates
Solution: Adjust volume inversely to price adjustment
Adjustment Timeline¶
Text Only
Original Data:
2024-01-15: $200 (before 2:1 split)
2024-01-16: $100 (after split)
Adjusted Data (looking back):
2024-01-15: $100 (adjusted)
2024-01-16: $100 (actual)
When loading in 2024:
- All historical prices adjusted to current shares outstanding
- Returns are meaningful across corporate actions
Next Steps¶
- Data Quality - General data validation
- Backtesting - Using clean data for backtests
- CSV Format - Loading data files