D07-F02-A04 / Complete engineering topic

Directional Movement: From True Range to +DI, −DI, and DX

Wilder's Directional Movement system assigns each bar's winning outside move to +DM or −DM, normalizes smoothed movement by smoothed true range, and turns the separation between +DI and −DI into DX.

D07 · TECHNICAL INDICATORS
D07-F02-A04Canonical / Tested / Open
D07 / D07-F02

A close-to-close return misses gaps and does not distinguish upward range expansion from downward range expansion. Directional Movement builds those ingredients explicitly before any ADX averaging.

Wilder's Directional Movement system assigns each bar's winning outside move to +DM or −DM, normalizes smoothed movement by smoothed true range, and turns the separation between +DI and −DI into DX. By the end of this tutorial, you will be able to calculate the line, audit its hidden state, reproduce its warm-up and boundary conventions, and explain why another platform can disagree.

Data note: every chart and number in this article uses deterministic synthetic teaching data. No historical return or investment-performance claim is made.

Start with the question the indicator actually answers

A trend system is useful only when its output has a precise meaning. The current package asks one bounded measurement question and refuses to turn a chart state into a forecast.

Wilder's Directional Movement system assigns each bar's winning outside move to +DM or −DM, normalizes smoothed movement by smoothed true range, and turns the separation between +DI and −DI into DX.

Directional Movement mechanism map

The map is the implementation checklist: validate the bar, calculate the intermediate state, apply the exact boundary rule, then publish an aligned output with its diagnostic evidence.

Formula and selected convention

+DMt={ΔHt,ΔHt>ΔLt and ΔHt>00,otherwise+DM_t=\begin{cases}\Delta H_t,&\Delta H_t>\Delta L_t\text{ and }\Delta H_t>0\\0,&\text{otherwise}\end{cases} DMt={ΔLt,ΔLt>ΔHt and ΔLt>00,otherwise-DM_t=\begin{cases}\Delta L_t,&\Delta L_t>\Delta H_t\text{ and }\Delta L_t>0\\0,&\text{otherwise}\end{cases}

where ΔHt=HtHt1\Delta H_t=H_t-H_{t-1} and ΔLt=Lt1Lt\Delta L_t=L_{t-1}-L_t. True range is the maximum of the current range and the two previous-close gap distances. Wilder sums update as Xt=Xt1Xt1/n+xtX_t=X_{t-1}-X_{t-1}/n+x_t.

+DI=100+DMnTRn,DI=100DMnTRn,DX=100+DI(DI)+DI+(DI)+DI=100\frac{+DM_n}{TR_n},\quad -DI=100\frac{-DM_n}{TR_n},\quad DX=100\frac{|+DI-(-DI)|}{+DI+(-DI)}

When both DIs sum to zero, this package returns DX equal to zero.

Defaults are teaching choices rather than universal laws:

ParameterPackage default
period14

Worked numerical example

If smoothed +DM is 8, smoothed −DM is 2, and smoothed TR is 20, +DI is 40, −DI is 10, and DX is 60.

The hand result is deliberately small enough to recalculate without either implementation. The canonical fixture then extends the same rules across five long, topic-specific paths.

Directional Movement exact canonical trace

From data contract to executable state

Use finalized, chronological observations with one declared source field, session calendar, time zone, and adjustment basis. Reject non-finite values, malformed high/low geometry, and invalid parameters. Do not sort inside the numeric kernel, fill missing bars with zero, or splice adjusted and unadjusted history.

The implementation returns one aligned entry per input row. Warm-up stays None in Python and null in TypeScript. A revision to historical input invalidates the recursive or rolling suffix from the earliest changed row.

Rendering system map…

Implementation walkthrough

The Python and TypeScript files favor direct state variables over clever vectorization. That makes seed, tie, clamp, displacement, and reversal behavior reviewable. Both languages read the same fixture and preserve the same null, numeric, string, and boolean semantics.

Complexity is linear in the number of observations. The reference code is optimized for readability; a production streaming implementation can retain only the active rolling/recursive state after validating parity.

Reconcile a platform disagreement systematically

DimensionThis packageMaintained-platform contextWhat to compare
Equal outside moveBoth DM values are zeroWilder strict-winner ruleUsing >= on either side creates false direction.
Zero DI sumDX is zeroCurrent TA-Lib DX carries the prior DX after the first zero stateThe aligned suffix can differ even after values resume.
Seed visibilityFirst DI/DX at index periodLibrary lookback and internal seed staging varyCompare first-ready index before the last row.

Start at the first row where the two outputs diverge. Compare source fields and parameters first, then the previous intermediate state, and only then the published line. This avoids treating a documented convention difference as a numerical defect.

Explore the exact state

Open the guided Directional Movement playground. The initial state is already informative. Choose a scenario, scrub or step to a named checkpoint, compare the visible diagnostics, and inspect the last 12 published rows.

The lab uses 180 observations in each of five scenarios, not a tiny decorative sample. A recent-60, recent-120, or complete-history focus keeps the denser path readable. Reduced-motion Play advances one observation without starting a timer.

Use the five-scenario atlas

LessonScenario IDWhat the controlled path isolates
Directional expansiondirectional-expansionAlternating range expansion exposes raw +DM, −DM, smoothed DI shares, and DX.
Equal outside movesequal-outside-movesEngineered bars expand equally upward and downward so both one-bar DM values must be zero.
Gap and true rangegap-true-rangeSynthetic gaps make previous-close distances dominate true range while DM still uses high/low changes.
Zero-direction statezero-direction-stateA long flat shelf drives both directional shares toward zero and exercises the package's zero-DX convention.
Seven-period comparisonfast-smoothing-comparisonA shorter Wilder period reacts more quickly to alternating directional winners.

Each path contains five checkpoints: first guided state, transition, boundary, platform reconciliation, and mature-state audit. These labels explain deterministic calculation state; they do not classify future market behavior.

A production debugging ladder

  1. Verify finalized input fields, chronological order, calendar, time zone, and adjustment basis.
  2. Verify parameter values and the first-ready index.
  3. Audit one-bar TR/+DM/−DM first, then the Wilder sums, DI shares, and zero-denominator DX rule.
  4. Compare the shared fixture at the first divergent row.
  5. Recalculate one checkpoint independently before changing code.
  6. Record the convention version with every persisted output.

Boundaries that cause real implementation drift

  • Equal upward and downward outside moves assign neither side; both DM values are zero.
  • True range includes previous-close gaps, while raw directional movement uses high/low changes.
  • DX measures the magnitude of DI separation and discards which side is larger.

Directional Movement boundary map

The strongest reconciliation workflow compares the first valid index, a steady-state row, an equality boundary, a reversal or reset, and the complete aligned suffix—not merely the last visible chart point.

Compare the family question, not the chart color

Aroon asks when the rolling extremes occurred. Directional Movement asks which side won each bar's range expansion and how large its smoothed share is relative to true range.

Neighboring indicators can display a similar bullish/bearish state while measuring different inputs. Agreement is not independent confirmation when the systems reuse the same prices and smoothing primitives.

Testing proves calculation, not profitability

The release checks cover:

  • all-five-scenario Python/TypeScript parity;
  • exact first-ready behavior;
  • invalid values and parameters;
  • equality, zero, tie, displacement, clamp, or reversal semantics;
  • SVG accessibility and fixture-derived values;
  • deterministic playground controls and reduced motion;
  • responsive reader and standalone rendering.

None of those checks estimates future returns. A strategy study would still need point-in-time constituents, execution clocks, fees, slippage, survivorship controls, and out-of-sample evaluation.

Historical-example decision

A named historical chart is not useful for this mechanism lesson. It would introduce vendor data, adjustment, identifier, session, licensing, and hindsight ambiguity without strengthening the arithmetic. The synthetic paths isolate the causal rule and can be redistributed with the package.

Common questions

Is Directional Movement a prediction?

No. It is a deterministic transformation of observed bars under the selected convention.

Can I compare values across platforms?

Only after aligning the source field, price basis, windows, seed, boundary rules, and display displacement.

What should I log in production?

Log the parameters, first-ready index, current intermediate state, input revision identifier, and the exact convention version.

What is the next tutorial?

Continue to Average Directional Index, which changes the trend-system question and makes a different state or normalization visible.

Directional Movement calculation flow

Rendering system map…

Takeaway: the displayed line is reproducible only when the hidden state and its boundary convention are preserved.

References5 primary sources and evidence notes

Expand the source trail, evidence role, and limitations behind the engineering choices.

Access date for web sources: 2026-07-26. Public artifacts use only deterministic synthetic data.

R1 — J. Welles Wilder, New Concepts in Technical Trading Systems

  • Organization or authors: J. Welles Wilder
  • Source type: Original or origin-attribution publication record
  • Publication or effective date: See catalog record
  • Version: Bibliographic record accessed 2026-07-26
  • URL or DOI: https://books.google.com/books?vid=ISBN0894590278
  • Accessed: 2026-07-26
  • Jurisdiction: General technical analysis; no regulatory jurisdiction
  • Evidence role: Origin and historical-definition context
  • Supports: Authorship/origin and the conceptual purpose of the method.
  • Limitations: The public record does not by itself freeze every modern platform seed, tie, plotting, or rounding convention.
  • Publication decision: Cite for origin; use maintained documentation and the package contract for executable semantics.

R2 — TA-Lib DX

  • Organization or authors: TA-Lib project or TradingView, as identified by the linked page
  • Source type: Maintained official technical documentation
  • Publication or effective date: Current page
  • Version: Page accessed 2026-07-26
  • URL or DOI: https://ta-lib.org/functions/dx
  • Accessed: 2026-07-26
  • Jurisdiction: General technical analysis; platform applicability stated in the package
  • Evidence role: Formula and maintained implementation-context evidence
  • Supports: Wilder's Directional Movement system assigns each bar's winning outside move to +DM or −DM, normalizes smoothed movement by smoothed true range, and turns the separation between +DI and −DI into DX.
  • Limitations: Documentation cannot establish predictive power, profitability, or universal platform parity.
  • Publication decision: Publish formula facts with the package's selected conventions visibly separated.

R3 — Pinned TA-Lib ta_DX.c implementation

  • Organization or authors: TA-Lib project or TradingView
  • Source type: Pinned maintained source or maintained calculation guide
  • Publication or effective date: Repository commit or current guide
  • Version: e203f7c436a9c21fd08246661971cfcb7ee37517
  • URL or DOI: https://github.com/TA-Lib/ta-lib/blob/e203f7c436a9c21fd08246661971cfcb7ee37517/src/ta_func/ta_DX.c
  • Accessed: 2026-07-26
  • Jurisdiction: General technical analysis
  • Evidence role: Executable or platform-convention evidence
  • Supports: Executable loop order, warm-up behavior, and maintained reference semantics used to compare the package convention.
  • Limitations: Source parity is not claimed where this package explicitly selects a clearer seed or zero-state convention.
  • Publication decision: Use to regression-check semantics; document intentional differences instead of implying universal equivalence.

R4 — Canonical synthetic fixture and independent arithmetic

  • Organization or authors: The Fintech Builder
  • Source type: Author-derived calculation from synthetic teaching inputs
  • Publication or effective date: 2026-07-26
  • Version: Fixture schema 2.0
  • URL or DOI: datasets/directional-movement-fixtures.json
  • Accessed: 2026-07-26
  • Jurisdiction: Not applicable
  • Evidence role: Reproducibility and Python/TypeScript parity
  • Supports: Published worked values, warm-up, equality, reset, and scenario behavior.
  • Limitations: Synthetic observations prove calculation behavior only; they are not market evidence.
  • Publication decision: Redistributable with the package; label every use synthetic.

R5 — TA-Lib TRANGE function documentation

  • Organization or authors: TA-Lib project or TradingView
  • Source type: Maintained official technical documentation
  • Publication or effective date: Current page
  • Version: Page accessed 2026-07-26
  • URL or DOI: https://ta-lib.org/functions/trange
  • Accessed: 2026-07-26
  • Jurisdiction: General technical analysis; platform applicability stated in the package
  • Evidence role: Neighboring-function and platform-reconciliation evidence
  • Supports: Maintained previous-close true-range definition.
  • Limitations: A maintained platform record documents its own convention and does not make the package convention universal.
  • Publication decision: Use in the reconciliation matrix; retain package choices and intentional differences explicitly.
directional_movement.ts
export type Numeric = number | null;

    function validateSeries(values: number[], name: string): void {
      if (!Array.isArray(values) || values.length === 0) throw new RangeError(`${name} must be a non-empty array`);
      if (values.some((value) => typeof value !== "number" || !Number.isFinite(value))) {
throw new TypeError(`${name} must contain only finite numbers`);
      }
    }

    function validateHL(high: number[], low: number[]): void {
      validateSeries(high, "high");
      validateSeries(low, "low");
      if (high.length !== low.length) throw new RangeError("high and low lengths must match");
      if (high.some((value, index) => value < low[index])) throw new RangeError("high must be >= low");
    }

    function validateHLC(high: number[], low: number[], close: number[]): void {
      validateHL(high, low);
      validateSeries(close, "close");
      if (close.length !== high.length) throw new RangeError("high, low, and close lengths must match");
      if (close.some((value, index) => value < low[index] || value > high[index])) {
throw new RangeError("close must lie inside each high-low range");
      }
    }
export function directional_movement(high: number[], low: number[], close: number[], period = 14) {
  validateHLC(high, low, close);
  if (!Number.isInteger(period) || period < 1) throw new RangeError("period must be a positive integer");
  const size = high.length;
  const true_range: Numeric[] = Array(size).fill(null);
  const plus_dm: Numeric[] = Array(size).fill(null);
  const minus_dm: Numeric[] = Array(size).fill(null);
  true_range[0] = high[0] - low[0]; plus_dm[0] = 0; minus_dm[0] = 0;
  for (let index = 1; index < size; index += 1) {
    const upMove = high[index] - high[index - 1];
    const downMove = low[index - 1] - low[index];
    plus_dm[index] = upMove > downMove && upMove > 0 ? upMove : 0;
    minus_dm[index] = downMove > upMove && downMove > 0 ? downMove : 0;
    true_range[index] = Math.max(high[index] - low[index], Math.abs(high[index] - close[index - 1]), Math.abs(low[index] - close[index - 1]));
  }
  const smoothed_tr: Numeric[] = Array(size).fill(null);
  const smoothed_plus_dm: Numeric[] = Array(size).fill(null);
  const smoothed_minus_dm: Numeric[] = Array(size).fill(null);
  const atr: Numeric[] = Array(size).fill(null);
  const plus_di: Numeric[] = Array(size).fill(null);
  const minus_di: Numeric[] = Array(size).fill(null);
  const dx: Numeric[] = Array(size).fill(null);
  if (size > period) {
    let smoothedTr = true_range.slice(1, period + 1).reduce<number>((sum, value) => sum + (value as number), 0);
    let smoothedPlus = plus_dm.slice(1, period + 1).reduce<number>((sum, value) => sum + (value as number), 0);
    let smoothedMinus = minus_dm.slice(1, period + 1).reduce<number>((sum, value) => sum + (value as number), 0);
    for (let index = period; index < size; index += 1) {
      if (index > period) {
        smoothedTr = smoothedTr - smoothedTr / period + (true_range[index] as number);
        smoothedPlus = smoothedPlus - smoothedPlus / period + (plus_dm[index] as number);
        smoothedMinus = smoothedMinus - smoothedMinus / period + (minus_dm[index] as number);
      }
      smoothed_tr[index] = smoothedTr; smoothed_plus_dm[index] = smoothedPlus; smoothed_minus_dm[index] = smoothedMinus;
      atr[index] = smoothedTr / period;
      const plus = smoothedTr === 0 ? 0 : (100 * smoothedPlus) / smoothedTr;
      const minus = smoothedTr === 0 ? 0 : (100 * smoothedMinus) / smoothedTr;
      plus_di[index] = plus; minus_di[index] = minus;
      dx[index] = plus + minus === 0 ? 0 : (100 * Math.abs(plus - minus)) / (plus + minus);
    }
  }
  return { true_range, plus_dm, minus_dm, smoothed_tr, smoothed_plus_dm, smoothed_minus_dm, atr, plus_di, minus_di, dx };
}
Full-height labdirectional movement labOpen full screen