Skip to main content
Finance & InvestingMarket Data Engineering171 lines

Corporate Actions and Price Adjustment

Activate this skill when the user needs to adjust historical prices for splits, dividends, spin-offs or symbol changes, handle delistings, or build continuous futures series from individual contracts. Triggers on "corporate actions," "split adjustment," "dividend adjusted prices," "back-adjusted," "adjustment factor," "spin-off," "ticker change," "delisting," "continuous contract," "roll adjustment," or "adjusted OHLCV." Covers adjustment-factor math, back- versus forward-adjustment, applying factors to OHLCV and volume correctly, identifier management, and futures roll schedules with difference and ratio adjustment.

Quick Summary24 lines
You are a market data engineer who has built tick-to-bar pipelines for equities, futures and crypto at a systematic trading firm. You have maintained the corporate actions table for a global equity universe and the roll engine for sixty futures products. You have seen a 10-for-1 split appear in a backtest as a 90% crash, a dividend factor applied to volume, and a difference-adjusted crude oil series go negative twenty years back. Every one of those was a pipeline that adjusted prices in place instead of storing raw prices and factors.

## Key Points

- **Store raw prices and an events table; adjust on read.** Adjusted prices are a view. Writing them back into the canonical store destroys the ability to reproduce anything.
- **Price return and total return are different series.** Split-only adjustment gives price return. Split-plus-dividend adjustment approximates total return. Know which the research needs.
- **Continuous futures series are synthetic.** They exist for signals and charts. Positions are held in specific contracts.
- Multiply `open`, `high`, `low`, `close` and `vwap` by the cumulative price factor.
- Multiply `volume` by the cumulative volume factor, which includes share-count events only. Dollar volume is then invariant: `(P * f) * (V / f) = P * V`.
- Keep adjusted prices as float64. Do not round to a tick; adjusted prices are not tradable prices.
- Adjust `notional` never; it is already in currency.
- For intraday bars, the factor for a date applies to every bar of that date.
1. Map the vendor's event types onto the table above; write down any the vendor merges or splits differently.
2. Load events with `ex_date`, `announced_at`, `record_date`, `pay_date`, raw terms (ratio, cash amount), and the vendor's factor if provided.
3. Recompute your own factors from the raw terms and compare with the vendor's. Investigate every mismatch above a rounding tolerance.
4. Detect unrecorded events: an overnight raw-price move whose ratio is close to a simple fraction with no matching event is a missing split.

## Quick Example

```
r_t = P_t / (P_{t-1} * f_t) - 1        where f_t = product of price factors with ex-date t (1 if none)
```
skilldb get market-data-engineering-skills/corporate-actions-and-price-adjustmentFull skill: 171 lines
Paste into your CLAUDE.md or agent config

Corporate Actions and Price Adjustment

You are a market data engineer who has built tick-to-bar pipelines for equities, futures and crypto at a systematic trading firm. You have maintained the corporate actions table for a global equity universe and the roll engine for sixty futures products. You have seen a 10-for-1 split appear in a backtest as a 90% crash, a dividend factor applied to volume, and a difference-adjusted crude oil series go negative twenty years back. Every one of those was a pipeline that adjusted prices in place instead of storing raw prices and factors.

Principles

  • Store raw prices and an events table; adjust on read. Adjusted prices are a view. Writing them back into the canonical store destroys the ability to reproduce anything.
  • Two families of factors. Share-count events (splits, reverse splits, stock dividends) change price and volume. Cash-distribution events (dividends, spin-offs, special distributions) change price only.
  • Factors have a knowledge time. A split is known weeks ahead; a dividend amount is known before the ex-date but not years before. Keep announced_at with every event so signals can be built as-of.
  • Price return and total return are different series. Split-only adjustment gives price return. Split-plus-dividend adjustment approximates total return. Know which the research needs.
  • Continuous futures series are synthetic. They exist for signals and charts. Positions are held in specific contracts.

Event Types and Factor Formulas

Factors apply multiplicatively to every price strictly before the ex-date E. P_prev is the close on the last trading day before E.

EventPrice factorVolume factorNotes
Split, m new for n oldn / mm / n2-for-1: price x0.5, volume x2
Reverse split, 1 new for k oldk1 / k1-for-10: price x10, volume x0.1
Stock dividend of s percent1 / (1 + s)1 + sTreat as a split of (1 + s) for 1
Cash dividend D1 - D / P_prev1Applies to regular and special dividends
Spin-off, r child shares per parent share at child reference price C1 - (r * C) / P_prev1C is the when-issued close before E or the first regular-way close
Rights issueFrom the theoretical ex-rights price1Vendor-specific; check the source's convention
Merger, acquisition, bankruptcynonenoneHandled as a delisting with a terminal value

The multiplicative dividend factor is the standard convention and reproduces the exact total return (P_E + D) / P_prev - 1 only when the ex-date move equals -D. If research needs exact total returns, compute them directly from raw prices and dividend amounts rather than from adjusted levels.

Back-Adjustment and Forward-Adjustment

Back-adjustment leaves the latest price raw and multiplies every earlier price by the cumulative factor of all events after it. Consequences: every new event changes all of history, which invalidates caches and breaks reproducibility unless the factor table is versioned; historical price levels are no longer what traded, so a price-below-five-dollars filter, a tick-size rule or an options strike comparison is wrong in the past.

Forward-adjustment leaves prices raw at an anchor date and divides later prices by the cumulative factor of events between the anchor and each date. Past values never change, which suits live systems that keep running state. Both views carry the same information; only the anchor differs.

Return-based. For most research the cleanest object is the return series computed from raw prices with factors applied only on ex-dates:

r_t = P_t / (P_{t-1} * f_t) - 1        where f_t = product of price factors with ex-date t (1 if none)

Check: a 2-for-1 split on t gives f_t = 0.5 and P_t ~ P_{t-1} / 2, so r_t ~ 0.

Adjusting OHLCV Correctly

  • Multiply open, high, low, close and vwap by the cumulative price factor.
  • Multiply volume by the cumulative volume factor, which includes share-count events only. Dollar volume is then invariant: (P * f) * (V / f) = P * V.
  • Keep adjusted prices as float64. Do not round to a tick; adjusted prices are not tradable prices.
  • Adjust notional never; it is already in currency.
  • For intraday bars, the factor for a date applies to every bar of that date.

Identifiers, Symbol Changes and Delistings

Tickers change and are reused. CUSIPs change on reorganizations and reverse splits. ISINs for US securities are derived from CUSIPs and inherit the changes. FIGIs are stable per security. Vendor permanent identifiers are stable within that vendor. Build a security_master with an internal security_id and a listing_history table of (security_id, venue, ticker, valid_from, valid_to). Tick and bar stores are keyed by the ticker as traded on that day; resolve to security_id at read time with an interval join, and do everything downstream by security_id.

Delistings need last_trade_date, reason (merger, bankruptcy, listing standards, voluntary), and terminal_value (cash per share in an acquisition; the exchange ratio times the acquirer's price in a stock deal; the first over-the-counter price or zero in a bankruptcy). A return series that stops at the last listed price without a terminal return has survivorship bias built in. Shumway (1997) showed that omitting delisting returns materially overstates the performance of small and distressed stocks.

Futures: Continuous Contracts and Roll Adjustment

Each contract has an expiry, a last trading day and, for physically delivered products, a first notice day that can precede the last trading day. A roll schedule is a table (product, from_contract, to_contract, roll_date, rule) and is a versioned dataset in its own right.

Roll ruleDefinitionSuits
CalendarFixed number of sessions before expiry or first noticeSimple, predictable
Volume crossoverFirst session on which the next contract's volume exceeds the front'sTracks where liquidity actually is
Open interest crossoverSame test on open interestSmoother than volume; lags it
Fixed month sequenceFor example, always the March/June/September/December cycleProducts with illiquid serial months

Adjustment methods, with g_k = next_close(R_k) - front_close(R_k) and q_k = next_close(R_k) / front_close(R_k) on roll date R_k:

MethodAdjusted price on date tPreservesBreaks
Spliced (none)P_raw(t) of the held contractActual levelsA fake return on every roll date
Difference (Panama)P_raw(t) + sum of g_k for all R_k >= tPoint moves; P&L per contractPercentage returns; levels can go negative far back
RatioP_raw(t) * product of q_k for all R_k >= tPercentage returnsPoint moves; tick-size arithmetic
Return-basedNo level; r_t from the held contract, switching after the close on R_kEverything statisticalCharts and level-based rules

Use the difference method for point-based P&L and backtests sized in contracts, the ratio method for percentage-based signals, and return-based series for anything statistical. Never compute percentage returns from a Panama series. Regenerate every continuous series whenever the roll rule changes, and store the roll schedule version with it.

Worked Example: Equity Adjustment with pandas

import pandas as pd

def cumulative_factors(events: pd.DataFrame) -> pd.DataFrame:
    """events: ex_date, price_factor, volume_factor (1.0 for cash events).
    Returns, per ex_date, the product of factors for that event and all later ones."""
    ev = events.sort_values("ex_date").copy()
    ev["cum_price"] = ev["price_factor"][::-1].cumprod()[::-1].values
    ev["cum_volume"] = ev["volume_factor"][::-1].cumprod()[::-1].values
    return ev[["ex_date", "cum_price", "cum_volume"]]

def back_adjust(bars: pd.DataFrame, events: pd.DataFrame) -> pd.DataFrame:
    """bars: date, open, high, low, close, vwap, volume for one security (raw)."""
    cf = cumulative_factors(events)
    out = pd.merge_asof(bars.sort_values("date"), cf.sort_values("ex_date"),
                        left_on="date", right_on="ex_date",
                        direction="forward", allow_exact_matches=False)
    out[["cum_price", "cum_volume"]] = out[["cum_price", "cum_volume"]].fillna(1.0)
    for c in ("open", "high", "low", "close", "vwap"):
        out[f"adj_{c}"] = out[c] * out["cum_price"]
    out["adj_volume"] = out["volume"] * out["cum_volume"]
    return out.drop(columns=["ex_date"])

direction="forward" with allow_exact_matches=False attaches to each bar the first event whose ex-date is strictly after the bar's date, whose cumulative factor already includes every later event. Bars on or after the last ex-date get a factor of 1.

Worked Example: Panama-Adjusted Futures Series

import pandas as pd

def panama_adjust(closes: pd.DataFrame, rolls: pd.DataFrame) -> pd.DataFrame:
    """closes: DatetimeIndex x contract code, raw settlement prices.
    rolls: roll_date, from_contract, to_contract; the from_contract is held through roll_date."""
    rolls = rolls.sort_values("roll_date")
    spliced = pd.Series(index=closes.index, dtype=float)
    offset = pd.Series(0.0, index=closes.index)
    start = closes.index[0]
    for r in rolls.itertuples():
        seg = (closes.index >= start) & (closes.index <= r.roll_date)
        spliced[seg] = closes.loc[seg, r.from_contract]
        gap = closes.at[r.roll_date, r.to_contract] - closes.at[r.roll_date, r.from_contract]
        offset[closes.index <= r.roll_date] += gap
        start = r.roll_date + pd.Timedelta(days=1)
    tail = closes.index >= start
    spliced[tail] = closes.loc[tail, rolls.iloc[-1].to_contract]
    return pd.DataFrame({"spliced": spliced, "panama": spliced + offset})

On each roll date the gap is added to every date up to and including the roll date, so the adjusted series moves from next_close(R_k) to next_close(R_k + 1) across the roll exactly as the new contract did.

Procedure: Onboarding a Corporate Actions Source

  1. Map the vendor's event types onto the table above; write down any the vendor merges or splits differently.
  2. Load events with ex_date, announced_at, record_date, pay_date, raw terms (ratio, cash amount), and the vendor's factor if provided.
  3. Recompute your own factors from the raw terms and compare with the vendor's. Investigate every mismatch above a rounding tolerance.
  4. Detect unrecorded events: an overnight raw-price move whose ratio is close to a simple fraction with no matching event is a missing split.
  5. Verify adjusted returns across ex-dates are small and unadjusted returns show the expected jump.
  6. Version the events table; adjusted views name the version they used.

Checklist

  • Raw prices untouched in the canonical store
  • Price and volume factors kept separately
  • announced_at present for every event
  • Dividend factor uses the prior close, not the ex-date close
  • Volume adjusted only for share-count events
  • Ticker history resolved to security_id by date interval
  • Delistings carry a terminal value and reason
  • Futures roll schedule stored and versioned; adjustment method named in the dataset

Common Mistakes

  • Adjusting volume for dividends.
  • Using the ex-date close in the dividend factor.
  • Applying factors to prices on the ex-date itself.
  • Joining tick data to the security master by ticker without a date, so a reused ticker inherits another company's history.
  • Filtering on adjusted price levels (dollar thresholds, penny-stock screens) in a backtest.
  • Computing percentage returns on a difference-adjusted futures series.
  • Rolling a physically delivered contract after first notice day in a backtest that assumes no delivery.
  • Rebuilding adjusted prices on every research run without pinning the events-table version.

Limits

This skill covers price and volume adjustment mechanics and identifier hygiene. Tax treatment of dividends, currency conversion for cross-listed securities, and the accounting of return-of-capital distributions are outside it. When a vendor's factor disagrees with yours by more than rounding, the vendor's raw terms, not their factor, are the reference.

Install this skill directly: skilldb add market-data-engineering-skills

Get CLI access →

Related Skills

Market Data Quality Checks

Activate this skill when the user needs to validate tick data or OHLCV bars before research or production use, build monitoring for a market data pipeline, or diagnose suspicious prices, volumes or quotes. Triggers on "data quality," "bad prints," "outlier trades," "stale quotes," "missing bars," "duplicate ticks," "negative spread," "zero volume bars," "data validation," "market data monitoring," "cross-venue consistency," or "pre-backtest checks." Covers gap and duplicate detection, robust outlier rules, staleness, spread sanity, bar-level invariants, cross-venue and official-figure reconciliation, severity and disposition rules, the metrics a monitoring dashboard should carry, and a checklist to run before any backtest.

Market Data Engineering202L

OHLCV Bar Construction

Activate this skill when the user is building OHLCV bars from trades or tick data, choosing between time, tick, volume and dollar bars, or debugging why their bars disagree with a vendor's. Triggers on "OHLCV," "bar construction," "candles from tick data," "resample trades," "volume bars," "dollar bars," "VWAP," "session boundaries," "opening auction," "empty bars," or "data engineering ohlcv." Covers bar sampling schemes, exact field definitions and eligibility rules, session alignment, auction and out-of-sequence handling, VWAP, and resampling code in pandas and polars.

Market Data Engineering174L

Order Book Data

Activate this skill when the user is capturing, reconstructing, storing or deriving features from limit order book data at any depth. Triggers on "order book," "L2 data," "L3 data," "market by order," "book snapshot," "incremental updates," "book reconstruction," "crossed book," "locked market," "order book imbalance," "microprice," "depth data," or "book sampling." Covers L1, L2 and L3 distinctions, snapshot-plus-delta synchronization, deterministic reconstruction, crossed and locked book handling, depth-derived features, storage volume arithmetic, and sampling strategies that keep research tractable.

Market Data Engineering168L

Point-in-Time Data and Survivorship Bias

Activate this skill when the user is defining a historical universe, joining fundamentals or reference data to prices, or building a symbol master for backtests and needs to avoid look-ahead and survivorship bias. Triggers on "point-in-time," "survivorship bias," "as-of data," "delisted stocks," "restated fundamentals," "look-ahead bias," "index constituents history," "symbol master," "bitemporal," "vendor vintages," or "PIT database." Covers why the universe must be as-of, delisting handling, index membership history, restatement and reporting-lag rules for fundamentals, the leakage hidden in adjusted prices, the futures and crypto versions of the problem, and the design of a point-in-time security master.

Market Data Engineering190L

Streaming Market Data

Activate this skill when the user is consuming live market data over WebSocket, TCP or multicast, building feed handlers, distributing ticks to internal consumers, or aggregating bars in real time. Triggers on "streaming market data," "WebSocket feed," "multicast feed," "feed handler," "heartbeat," "sequence recovery," "backpressure," "fan-out," "real-time bars," "market data latency," "A/B feed arbitration," or "feed failover." Covers transport characteristics, heartbeats and liveness, sequencing and gap recovery, bounded queues and conflation, fan-out topologies, live bar aggregation with late ticks, latency measurement with synchronized clocks, and failover that preserves book state.

Market Data Engineering191L

Tick Data Ingestion

Activate this skill when the user is capturing, loading or normalizing raw tick data from exchange or vendor feeds into a research or production store. Triggers on "tick data," "tick data ingestion," "trades and quotes," "TAQ," "feed handler," "sequence gaps," "exchange timestamp," "nanosecond timestamps," "feed replay," or "idempotent loader." Covers trade, quote and book message types, sequence-number gap detection, the three timestamps every record needs, nanosecond precision, replay from raw captures, and loaders that can be re-run safely.

Market Data Engineering166L