On June 5, 2020, Apple stock closed at $275.15. The next day — June 8, 2020 — its closing price was $33.74. Nothing catastrophic happened overnight. No earnings disaster, no market crash. Apple had executed a 4-for-1 stock split, and every historical price in every database on earth quietly changed, unless the database had already been adjusted for splits. If you had downloaded closing prices the day before the split, and then compared them to prices from a different source the day after, you would have concluded that the stock lost 88% of its value in a single trading session. It did not.

This is the problem that price adjustment solves. It is also a problem that most retail investors and many quantitative practitioners get wrong, leading to systematic errors in backtests, portfolio analytics, and strategy comparisons that are invisible until they blow up a live portfolio.

This article dissects the mathematics of stock price adjustment — forward adjustment and backward adjustment — explains the CRSP standard that governs how adjusted prices are computed, shows what goes wrong when adjustment is done incorrectly, and provides production-grade Python code for working with adjusted price series.


1. Why Prices Need Adjusting: The Intuition

Stocks undergo two corporate actions that mechanically alter their nominal price without reflecting any change in underlying value: stock splits and dividends.

1.1 Stock Splits

A stock split divides each existing share into multiple shares. A 2-for-1 split means every shareholder receives two shares for every one they held, and the price is halved. The total market capitalization of the company does not change.

Event Shares outstanding Share price Market cap
Before 2-for-1 split 1,000,000 $200 $200,000,000
After 2-for-1 split 2,000,000 $100 $200,000,000

If you are building a price series for a backtest and you fail to adjust for this split, every pre-split price appears to be twice as valuable as it actually was. A strategy that "bought Apple at $180" before the split would appear profitable in a naive backtest, but $180 was never a price anyone could have traded at in the post-split world.

1.2 Cash Dividends

A cash dividend distributes a portion of the company's cash to shareholders. The share price typically drops by approximately the dividend amount on the ex-dividend date, because the company's assets decrease by the dividend payout.

Event Share price (before distribution) Dividend Share price (after distribution, theory)
Pre-dividend $100.00 $1.00 ~$99.00

The price drop is not a loss — shareholders received $1.00 in cash. But if your price series records $100 before the dividend and $99 after, and you compute returns naively, you will record a −1% loss that never actually occurred in the portfolio, because the $1 dividend was a compensating cash flow.

The fundamental principle: an unadjusted price series is not a consistent measure of value. It mixes pre-action and post-action prices that are expressed in different units, just as mixing temperatures in Celsius and Fahrenheit without converting them produces nonsense.


2. The Adjustment Factor: The Core Mathematics

2.1 Definition

An adjustment factor (复权因子) is a multiplicative scalar applied to historical prices to express them in a consistent, comparable unit. The adjustment factor absorbs the cumulative effect of all corporate actions that have occurred since the reference date.

Let $F_t$ denote the adjustment factor at time $t$, where larger values correspond to higher nominal prices in the past relative to the current price. If $P_t^{raw}$ is the raw (unadjusted) price at time $t$, then the forward-adjusted price (前复权) at a reference time $T$ is:

$$P_t^{fwd} = P_t^{raw} \times \frac{F_T}{F_t}$$

Where $F_T = 1.0$ by convention (the current period has an adjustment factor of 1.0). This means:

$$P_t^{fwd} = \frac{P_t^{raw}}{F_t}$$

The factor $F_t$ accumulates the effects of all splits and dividends that occurred between time $t$ and the present:

$$F_t = \prod_{i=t}^{T-1} s_i \times d_i$$

Where:

  • $s_i$ is the split ratio at event $i$ (e.g., 0.5 for a 2-for-1 split, because shares double and price halves)
  • $d_i$ is the dividend adjustment factor at event $i$

2.2 Split Ratio Convention

The split ratio convention matters and varies across data providers:

Split event CRSP convention Effect on $F_t$
2-for-1 split $s = 0.5$ Multiplies $F_t$ by 0.5 (price halves, factor decreases)
1-for-2 reverse split $s = 2.0$ Multiplies $F_t$ by 2.0 (price doubles, factor increases)
3-for-1 split $s = \frac{1}{3} \approx 0.333$ Price drops to one-third

The key insight is that the factor moves in the opposite direction of the price. When the split causes prices to drop, the historical factor must drop proportionally so that the adjusted historical price (computed as raw price divided by historical factor) comes out higher, expressed in current-unit terms.

2.3 Dividend Adjustment

Dividends require a continuous adjustment, not a one-time event. The dividend adjustment factor is computed as:

$$d_i = 1 - \frac{dividend_per_share}{pre_dividend_price}$$

For example, if a stock trades at $100 before a $0.50 dividend, the dividend adjustment factor is:

$$d = 1 - \frac{0.50}{100.00} = 0.995$$

This means every historical price before this dividend needs to be multiplied by 0.995 to account for the value that was "paid out" and is now reflected as cash in the investor's portfolio rather than equity price appreciation.

Dividend adjustments compound over time. A stock that paid $1 in annual dividends for 10 years with no price appreciation would need a cumulative dividend adjustment factor of approximately 0.99^10 ≈ 0.904 in its historical price series.


3. CRSP Adjustment Standards

The Center for Research in Security Prices (CRSP), housed at the University of Chicago Booth School of Business, established the definitive methodology for price adjustment in the 1960s and has maintained it since. CRSP's methodology is the de facto standard for academic research and is widely adopted by institutional data providers.

3.1 CRSP's Key Principles

Principle 1: Cumulative multiplicative adjustment. All adjustments are multiplicative, never additive. An additive adjustment (subtracting dividends from prices) fails when dividends are large relative to prices and produces inconsistent results across different dividend patterns.

Principle 2: Total return orientation. CRSP adjusts prices to be consistent with a total return series. This means the adjusted price series, combined with dividend cash flows, should reproduce the true total return of the security. A price-only adjusted series that ignores dividends understates the true economic return.

Principle 3: Reference to the most recent observation. The adjustment factor is normalized to 1.0 at the most recent available date. All historical prices are adjusted relative to this reference point.

Principle 4: Event-date precision. The adjustment factor changes at the open of the ex-dividend date or the split effective date — never intraday. This ensures that a historical researcher can compute the correct factor for any date boundary.

3.2 CRSP's Treatment of Special Dividends

Regular cash dividends (quarterly dividends from operating earnings) are treated with the standard dividend adjustment factor.

Special dividends — one-time payments from non-recurring events like asset sales or litigation settlements — present a harder problem. CRSP has historically included special dividends in the adjustment factor, but this is controversial. The argument for including them: a shareholder who held through the special dividend received cash, so the price drop reflects a real economic transfer. The argument against: special dividends are non-recurring and distort historical return comparisons if included, because no future investor can expect to receive them again.

Many modern data providers (including TickDB) treat special dividends as price-neutral events that do not require adjustment, because they are not expected to recur and are not part of the normal earnings distribution mechanism. This is a deliberate departure from classical CRSP methodology and represents a judgment call that affects long-horizon return comparisons.


4. Forward vs. Backward Adjustment: Two Views of the Same Reality

This is where most confusion originates. Both forward adjustment (前复权) and backward adjustment (后复权) are mathematically equivalent — they are the same operation viewed from opposite ends of the time series. But they produce different numbers and serve different purposes.

4.1 Forward Adjustment (前复权)

Forward adjustment adjusts historical prices to the current price level. The most recent observation has an adjustment factor of 1.0, and every historical price is scaled up by the cumulative adjustment factor.

The forward-adjusted price at time $t$ is:

$$P_t^{fwd} = P_t^{raw} \times F_t^{fwd}$$

Where $F_t^{fwd} = \prod_{i=t}^{current} \frac{1}{s_i \times d_i}$ — the cumulative factor from time $t$ to the present.

Intuitive reading: "In today's units, what was this stock worth on date $t$?"

Practical use case: Live trading systems, real-time dashboards, intraday monitoring. Forward-adjusted prices are directly comparable to the current market price without any mental translation.

Problem: The historical prices in a forward-adjusted series are not the prices that actually traded. If you are analyzing historical bid-ask spreads, order book dynamics, or microstructure, forward-adjusted prices will show spreads of $0.05 on a stock that traded at $2,500 in 1980 — which is not a meaningful microstructure observation.

4.2 Backward Adjustment (后复权)

Backward adjustment adjusts the most recent prices down to the historical price level. The oldest observation (or a fixed historical anchor date) has an adjustment factor of 1.0, and every subsequent price is scaled down.

The backward-adjusted price at time $t$ is:

$$P_t^{bwd} = P_t^{raw} \times F_t^{bwd}$$

Where $F_t^{bwd} = \prod_{i=anchor}^{t} s_i \times d_i$ — the cumulative factor from the anchor date to time $t$.

Intuitive reading: "At the prices that actually traded, what was this stock worth on date $t$?"

Practical use case: Backtesting, historical strategy evaluation, academic research. Backward-adjusted prices are the prices that actually existed and reflect genuine historical market conditions including bid-ask spreads and microstructure dynamics.

Problem: The most recent price in a backward-adjusted series is not the current market price. You cannot directly compare a backward-adjusted price to today's market without converting it.

4.3 Mathematical Equivalence

Both methods produce identical return series:

$$R_t^{fwd} = \frac{P_t^{fwd} - P_{t-1}^{fwd}}{P_{t-1}^{fwd}} = \frac{P_t^{bwd} - P_{t-1}^{bwd}}{P_{t-1}^{bwd}} = R_t^{bwd}$$

The returns are identical because both are derived from the same underlying price movements, just expressed in different units. This is the critical property that makes adjustment mathematically sound: return calculations are invariant to the choice of adjustment direction, as long as both prices in the return calculation use the same adjustment convention.

The confusion arises when practitioners mix adjustment conventions — computing returns with a forward-adjusted entry price and a backward-adjusted exit price, or comparing a forward-adjusted current price against a backward-adjusted historical baseline.

4.4 A Concrete Example

Consider a stock with the following corporate actions:

  • Day 0: Price = $100, factor = 1.0
  • Day 1: 2-for-1 split occurs. Post-split price = $50, factor = 0.5 (current)
  • Day 2: Price = $51

Forward adjustment (current = Day 2, factor = 1.0):

Day Raw price Factor Forward-adjusted price
0 $100 0.5 $50
1 $50 1.0 $50
2 $51 1.0 $51

Backward adjustment (anchor = Day 0, factor = 1.0):

Day Raw price Factor Backward-adjusted price
0 $100 1.0 $100
1 $50 0.5 $100
2 $51 0.5 $102

Notice that Day 2 shows $51 (forward) and $102 (backward). The return from Day 0 to Day 2:

  • Forward: ($51 − $50) / $50 = 2%
  • Backward: ($102 − $100) / $100 = 2%

Identical. But the price levels are different, and using one when you should use the other will produce systematic errors.


5. Common Pitfalls in Price Adjustment

5.1 The Split-Adjusted Close Trap

Many free data sources (Yahoo Finance, for example) provide "split-adjusted close" — which adjusts for splits but not for dividends. This creates a hybrid series that is:

  • Correct for split adjustment
  • Incorrect for dividend reinvestment returns
  • Misleadingly useful-sounding

If you compute returns from a split-adjusted-close series on a dividend-paying stock and compare them to buy-and-hold returns, you will systematically understate the true return of the strategy. The dividend-paying stock looks worse than it actually was because the price drops from dividend payments are not compensated.

5.2 Survivorship Bias in Adjustment

Adjustment factors must be computed as of the current composition of the security. When a company is acquired, delisted, or goes bankrupt, its historical prices still exist in the dataset. If the adjustment factor for that delisted company is computed using only data available before delisting (forward-looking information would be unavailable in a real backtest), the factor will be incorrect.

This is a form of look-ahead bias — the researcher is using information (the future delisting) that would not have been available when the historical decision was made.

CRSP addresses this through its survivor bias file, which provides separately computed adjustment factors based on information available at each historical date, not corrected for future events.

5.3 The Reverse Split Problem

Reverse splits (1-for-N) amplify the adjustment factor problem. A 1-for-10 reverse split means 10 shares become 1 share, and the price multiplies by 10. In a forward-adjusted series, the pre-reverse-split prices get multiplied by 10, producing very large historical prices that can overflow float32 precision in some numerical systems.

Date Raw price Forward factor Forward-adjusted price
Pre-reverse split $1.20 10.0 $12.00
Post-reverse split $12.00 1.0 $12.00
Later $15.00 1.0 $15.00

Python code handling price arrays in float32 will silently overflow or lose precision at these boundaries. Use float64 (Python's default float) for all adjusted price calculations.

5.4 Inconsistent Adjustment Across Markets

Chinese A-shares and US equities handle adjustment differently in some commercial databases. A-shares have historically experienced extreme adjustments due to rights offerings (配股) and bonus share issues (送股), where free shares are distributed to existing shareholders. The CRSP-derived framework works for splits and dividends but requires extension for rights offerings, which involve a subscription price that is below market — creating a more complex adjustment scenario.

TickDB's kline data uses CRSP-standard forward adjustment, providing a consistent methodology across all covered markets.


6. Building a Production Adjustment Factor Calculator

The following Python module implements a CRSP-style adjustment factor calculator. It handles splits, cash dividends, and the generation of both forward-adjusted and backward-adjusted price series.

"""
adjustment_factor.py
CRSP-standard price adjustment factor calculator.

Implements forward adjustment (historical → current price level) and
backward adjustment (current → historical price level) for stock splits
and cash dividends.

⚠️ For production use with large datasets, consider using pandas groupby
   operations or a vectorized NumPy implementation for 10–100x speedup.
"""

import os
import time
from datetime import date, datetime
from dataclasses import dataclass
from typing import Optional
import requests


@dataclass
class CorporateAction:
    """Represents a single corporate action event."""
    event_date: date
    action_type: str  # 'split', 'dividend'
    value: float      # split ratio (e.g., 0.5 for 2-for-1) or dividend per share


@dataclass
class PriceRecord:
    """A single price observation."""
    trade_date: date
    close_raw: float


def fetch_raw_prices(
    symbol: str,
    start_date: date,
    end_date: date,
    api_key: Optional[str] = None,
) -> list[PriceRecord]:
    """
    Fetch raw (unadjusted) daily close prices for a symbol.

    In production, this would call a market data API. Here we demonstrate
    the interface pattern using the TickDB kline endpoint with adjustment=1
    to retrieve raw, unadjusted closes.

    Args:
        symbol: Exchange symbol, e.g., "AAPL.US"
        start_date: Start of the date range (inclusive)
        end_date: End of the date range (inclusive)
        api_key: API key loaded from TICKDB_API_KEY env var if not provided

    Returns:
        List of PriceRecord sorted by date ascending

    Raises:
        ValueError: If API key is missing or symbol is invalid
        RuntimeError: On unexpected API errors (non-3001 codes)
    """
    api_key = api_key or os.environ.get("TICKDB_API_KEY")
    if not api_key:
        raise ValueError(
            "TICKDB_API_KEY not set. "
            "Set it via: export TICKDB_API_KEY='your_key_here'"
        )

    params = {
        "symbol": symbol,
        "interval": "1d",
        "start": int(start_date.strftime("%Y%m%d")),
        "end": int(end_date.strftime("%Y%m%d")),
        "adjustment": "1",      # Raw, unadjusted prices
        "limit": 500,
    }

    max_retries = 3
    retry_count = 0

    while retry_count < max_retries:
        try:
            response = requests.get(
                "https://api.tickdb.ai/v1/market/kline",
                headers={"X-API-Key": api_key},
                params=params,
                timeout=(3.05, 10),
            )
            data = response.json()

            code = data.get("code", 0)
            if code == 0:
                klines = data["data"]
                return [
                    PriceRecord(
                        trade_date=datetime.fromtimestamp(k["t"] / 1000).date(),
                        close_raw=float(k["c"]),
                    )
                    for k in klines
                ]
            elif code in (1001, 1002):
                raise ValueError(
                    "Invalid API key — check your TICKDB_API_KEY env var"
                )
            elif code == 2002:
                raise KeyError(
                    f"Symbol {symbol} not found — verify via /v1/symbols/available"
                )
            elif code == 3001:
                retry_after = int(response.headers.get("Retry-After", 5))
                print(f"Rate limited. Retrying after {retry_after}s...")
                time.sleep(retry_after)
                continue
            else:
                raise RuntimeError(
                    f"Unexpected error {code}: {data.get('message', 'Unknown')}"
                )

        except requests.exceptions.Timeout:
            retry_count += 1
            backoff = min(2 ** retry_count + 0.1, 30)
            print(f"Request timed out. Retrying in {backoff:.1f}s (attempt {retry_count})...")
            time.sleep(backoff)
        except requests.exceptions.RequestException as e:
            raise RuntimeError(f"Network error: {e}")

    raise RuntimeError("Max retries exceeded")


def fetch_corporate_actions(
    symbol: str,
    start_date: date,
    end_date: date,
    api_key: Optional[str] = None,
) -> list[CorporateAction]:
    """
    Fetch corporate actions (splits, dividends) for a symbol.

    ⚠️ Note: TickDB does not currently expose a dedicated corporate actions
    endpoint. In production, this would need to be sourced from a financial
    data provider (CRSP, Compustat, or a commercial alternative). The
    implementation below demonstrates the expected interface and data model.
    """
    api_key = api_key or os.environ.get("TICKDB_API_KEY")

    # Placeholder: Corporate actions would come from CRSP, Compustat,
    # or a dedicated corporate actions data provider.
    # Example endpoint structure (not implemented in TickDB):
    # response = requests.get(
    #     "https://api.tickdb.ai/v1/reference/corporate-actions",
    #     headers={"X-API-Key": api_key},
    #     params={"symbol": symbol, "start": start_date, "end": end_date},
    #     timeout=(3.05, 10),
    # )
    return []  # Return empty list until endpoint is available


def compute_forward_adjustment_factor(
    actions: list[CorporateAction],
    price_records: list[PriceRecord],
) -> dict[date, float]:
    """
    Compute forward adjustment factors (historical → current level).

    Returns a dict mapping each trade_date to its forward adjustment factor.
    The most recent observation has factor = 1.0.

    CRSP convention:
    - Split factor: ratio of new shares to old shares (0.5 for 2-for-1)
    - Dividend factor: 1 - dividend / pre_dividend_price
    """
    if not price_records:
        return {}

    sorted_actions = sorted(actions, key=lambda a: a.event_date)
    most_recent_date = price_records[-1].trade_date
    most_recent_price = price_records[-1].close_raw

    # Build a dict of adjustment factors on action dates
    # Forward: factor decreases as we go backward in time
    factors = {most_recent_date: 1.0}

    cumulative_factor = 1.0

    # Iterate backward from the most recent date
    for action in reversed(sorted_actions):
        if action.action_type == "split":
            # CRSP convention: split_ratio = new_shares / old_shares
            # For 2-for-1: ratio = 2 → price halves → factor multiplies by 0.5
            cumulative_factor /= action.value
        elif action.action_type == "dividend":
            # Find the price before the dividend for dividend factor computation
            pre_div_prices = [
                p for p in price_records if p.trade_date < action.event_date
            ]
            if pre_div_prices:
                pre_price = pre_div_prices[-1].close_raw
                div_factor = 1 - (action.value / pre_price)
                cumulative_factor /= div_factor
        factors[action.event_date] = cumulative_factor

    # Fill in dates between action dates with the most recent factor
    filled_factors = {}
    last_factor = 1.0
    for record in price_records:
        if record.trade_date in factors:
            last_factor = factors[record.trade_date]
        filled_factors[record.trade_date] = last_factor

    return filled_factors


def compute_backward_adjustment_factor(
    actions: list[CorporateAction],
    price_records: list[PriceRecord],
) -> dict[date, float]:
    """
    Compute backward adjustment factors (current → historical level).

    Returns a dict mapping each trade_date to its backward adjustment factor.
    The oldest observation has factor = 1.0.

    Backward factors multiply as we move forward in time from the anchor.
    """
    if not price_records:
        return {}

    sorted_actions = sorted(actions, key=lambda a: a.event_date)
    anchor_date = price_records[0].trade_date

    factors = {anchor_date: 1.0}
    cumulative_factor = 1.0

    for action in sorted_actions:
        if action.action_type == "split":
            cumulative_factor *= action.value
        elif action.action_type == "dividend":
            pre_div_prices = [
                p for p in price_records if p.trade_date < action.event_date
            ]
            if pre_div_prices:
                pre_price = pre_div_prices[-1].close_raw
                div_factor = 1 - (action.value / pre_price)
                cumulative_factor *= div_factor
        factors[action.event_date] = cumulative_factor

    # Fill in dates between action dates
    filled_factors = {}
    last_factor = 1.0
    for record in price_records:
        if record.trade_date in factors:
            last_factor = factors[record.trade_date]
        filled_factors[record.trade_date] = last_factor

    return filled_factors


def build_adjusted_series(
    price_records: list[PriceRecord],
    factors: dict[date, float],
    adjustment_type: str,
) -> list[tuple[date, float]]:
    """
    Apply adjustment factors to raw prices.

    Args:
        price_records: Raw price records
        factors: Pre-computed adjustment factors
        adjustment_type: 'forward' or 'backward'

    Returns:
        List of (date, adjusted_close) tuples
    """
    adjusted = []
    for record in price_records:
        factor = factors.get(record.trade_date, 1.0)
        if adjustment_type == "forward":
            adjusted_price = record.close_raw * factor
        else:
            adjusted_price = record.close_raw * factor
        adjusted.append((record.trade_date, adjusted_price))
    return adjusted


def compute_returns(adjusted_series: list[tuple[date, float]]) -> list[float]:
    """
    Compute daily returns from an adjusted price series.

    Returns:
        List of daily returns (percentage), aligned with the input dates.
        First element is always None (no return for the first observation).
    """
    returns = [None]
    for i in range(1, len(adjusted_series)):
        prev_price = adjusted_series[i - 1][1]
        curr_price = adjusted_series[i][1]
        ret = (curr_price - prev_price) / prev_price
        returns.append(ret)
    return returns


# Example usage
if __name__ == "__main__":
    import pprint

    # Fetch raw prices for AAPL around its 2020 4-for-1 split
    aapl_raw = fetch_raw_prices(
        symbol="AAPL.US",
        start_date=date(2020, 6, 1),
        end_date=date(2020, 8, 31),
    )

    # AAPL 4-for-1 split effective June 5, 2020
    # Split ratio = 4 (new shares / old shares) → factor = 0.25
    aapl_actions = [
        CorporateAction(
            event_date=date(2020, 8, 31),
            action_type="split",
            value=0.25,  # CRSP convention: ratio = new/old = 4/1 → 0.25
        )
    ]

    fwd_factors = compute_forward_adjustment_factor(aapl_actions, aapl_raw)
    bwd_factors = compute_backward_adjustment_factor(aapl_actions, aapl_raw)

    fwd_series = build_adjusted_series(aapl_raw, fwd_factors, "forward")
    bwd_series = build_adjusted_series(aapl_raw, bwd_factors, "backward")

    print("=== Forward-Adjusted Prices (historical → current level) ===")
    pprint.pprint(fwd_series)

    print("\n=== Backward-Adjusted Prices (current → historical level) ===")
    pprint.pprint(bwd_series)

    # Verify: daily returns should be identical
    fwd_returns = compute_returns(fwd_series)
    bwd_returns = compute_returns(bwd_series)

    print("\n=== Return Comparison (should be identical) ===")
    for i, (f, b) in enumerate(zip(fwd_returns[1:], bwd_returns[1:])):
        if f is not None and b is not None:
            diff = abs(f - b)
            assert diff < 1e-10, f"Return mismatch at index {i}: {f} vs {b}"
    print("✓ Returns verified identical across adjustment methods")

6.1 Key Implementation Notes

The code above demonstrates several production-critical patterns:

Environment variable authentication — The API key is loaded from TICKDB_API_KEY and never hardcoded. This is essential for any code that may be committed to version control.

Timeout enforcement — Every requests.get call includes a (connect_timeout, read_timeout) tuple. Without this, a network issue can cause the entire process to hang indefinitely.

Rate-limit handling — The 3001 error code triggers a read of the Retry-After header and a pause before retry. This is not optional for any production data pipeline.

Assertion-based return verification — The final check that forward and backward returns are identical is a regression test that will catch any implementation errors in the factor computation. Run this after any change to the factor logic.


7. Practical Implications for Backtesting

The choice of adjustment method has measurable consequences for backtest results, particularly over long time horizons with many corporate actions.

7.1 Long-Horizon Equity Studies

Consider a backtest over 30 years of S&P 500 stocks. A stock like Johnson & Johnson (JNJ) has paid uninterrupted quarterly dividends since 1944 and has executed at least 9 stock splits. The cumulative dividend adjustment for JNJ over 30 years is approximately 0.65 — meaning unadjusted prices overstate the value of a buy-and-hold position by about 35% relative to the true total return.

A momentum strategy that rebalances monthly and reinvests dividends will show different performance characteristics depending on whether:

  • Raw (unadjusted) prices are used (systematic overstatement of entry costs)
  • Split-adjusted close is used (correct for splits, wrong for dividends)
  • Fully adjusted total return series is used (correct for both)

7.2 Cross-Sectional Strategy Comparisons

When comparing a dividend-paying stock (e.g., a utility) to a non-dividend-paying stock (e.g., a high-growth tech company), using a price-only adjusted series will systematically favor the non-payer, because the dividend-paying stock's price is artificially deflated by the dividend adjustment while the non-payer's price is unaffected.

This creates a systematic bias against dividend-paying stocks in any backtest that uses price-only adjustment — a subtle but persistent error that can lead to strategies that are inadvertently underweight dividend stocks relative to their true risk-adjusted contribution.

7.3 TickDB's Approach

TickDB's kline endpoint provides pre-adjusted OHLCV data through the adjustment parameter. Setting adjustment=1 returns raw, unadjusted closes; the default provides forward-adjusted closes aligned to the most recent price level. This allows quant researchers to:

  1. Retrieve forward-adjusted closes for real-time comparison with current prices
  2. Retrieve raw closes to implement their own adjustment logic with full control over the methodology (CRSP standard, special dividend handling, rights offering treatment)
  3. Compute their own backward-adjusted series for microstructure analysis

The kline endpoint does not currently provide a dedicated corporate actions endpoint. For long-horizon backtests requiring precise CRSP-standard adjustment, combine TickDB's raw price data with a separate corporate actions feed (CRSP, Compustat, or a commercial alternative) using the framework demonstrated in the code above.


8. The Answer to the Original Question

Why do yesterday's closing prices change?

Because the prices in your database are adjusted to be consistent with the current price level. The adjustment factor absorbs all corporate actions (splits, dividends) that have occurred since those prices were recorded. When a split or dividend occurs, the adjustment factor for all historical dates changes, and with it, every historical price.

Which is correct: forward or backward adjustment?

Both are correct. They are mathematical inverses of each other, producing identical return series but different price levels. The choice depends on the use case:

Use case Recommended method
Real-time monitoring, live trading Forward adjustment
Backtesting and strategy evaluation Either — as long as you are consistent
Historical microstructure analysis Backward adjustment
Academic research on historical returns Forward adjustment (CRSP standard)
Cross-sectional stock comparisons Forward adjustment

The real error is not choosing the wrong direction — it is mixing adjustment methods within a single analysis, using unadjusted prices, or using a hybrid "split-adjusted only" series that ignores dividends. Any of these will introduce systematic biases that are difficult to detect without careful verification.


Next Steps

If you're building a backtesting system, start with raw (unadjusted) prices and implement your own adjustment factor computation using the CRSP methodology above. This gives you full control over special dividend handling and allows you to verify that your adjustment produces identical returns across forward and backward methods.

If you're comparing strategies across markets, verify that all price series use the same adjustment convention. Mixing a forward-adjusted US equity series with a raw-price crypto series will introduce cross-asset return distortions.

If you need 10+ years of cleaned, aligned US equity OHLCV data for strategy backtesting, TickDB provides this through its /v1/market/kline endpoint, covering 6 asset classes with consistent API authentication and data quality standards.

If you're using AI coding assistants, search for the tickdb-market-data SKILL in your AI tool's marketplace to get native TickDB integration in your coding workflow.


This article does not constitute investment advice. Markets involve risk; past performance does not guarantee future results. Price adjustment methodologies are technical descriptions of data processing practices and do not imply any prediction of future security performance.