Library/Market Breadth and Internals/High/Low and Trend Breadth

D04-F03-A02 / Complete engineering topic

High-Low Ratio

A production-minded guide to High-Low Ratio.

D04 · MARKET BREADTH AND INTERNALS
D04-F03-A02Canonical / Tested / Open
D04 / D04-F03
Key concepts

The governed definitions this build depends on. Read them first if a term is unfamiliar.

An index level can look healthy while participation underneath it is narrow. High-Low Ratio makes one precise internal question auditable: How many fresh highs occur for each fresh low, and what must the system publish when no low exists? The output is descriptive evidence, not a forecast or trading instruction.

The decision this measure owns

Choose the ratio when multiplicative balance matters more than raw count scale. Preserve the denominator state beside the value.

The family contains nearby measures because “breadth” is not one calculation. A raw difference, a ratio, a bounded smoother, and three moving-average horizons can all be correct while answering different questions. The implementation must name the universe, session, method, and knowledge cutoff before it publishes any of them.

Freeze the evidence before the formula

Use stable security identifiers and membership effective for the evaluated session. Record both effective_at and available_at: the first says when the market state applies; the second says when a system could know it. A correction received tomorrow cannot enter today's decision retroactively.

StockCharts' maintained high/low documentation documents the selected provider-style transform. TradingView's support article shows why interval and equality conventions must remain part of series identity rather than being assumed universal.

Every number in this tutorial is labelled synthetic. No provider observation is republished.

Formula and unit

HLRt=NHt/NLtHLR_t = NH_t / NL_t

The output unit is new highs per new low. Calculate at full precision and round only for display. Keep the diagnostics that explain the result—counts, denominator, window, comparison flags, status, reason, and series identity.

Work the canonical state

With 2 highs and 10 lows, 2 / 10 = 0.2 highs per low. Scaling to 4 and 20 leaves 0.2 unchanged; changing only the denominator does not.

Canonical worked state

The first visual preserves the existing parity fixture. It separates ready inputs, author-derived arithmetic, and the display-rounded output.

Inspect the decision boundary

Positive highs divided by zero lows is named unbounded, not serialized as infinity. Zero highs and zero lows is incomplete: no new extremes, because the ratio has no direction.

Topic-specific decision boundary

The second visual makes the non-obvious branch visible. It is deliberately synthetic, so the learner can inspect the state without mistaking it for a historical market claim.

Follow the algorithm's branch

Rendering system map…

The formula runs only after identity, causal availability, and completeness checks pass. An incomplete, warming, unbounded, or unsupported result is part of the algorithm—not an exception to hide in a log.

Reference implementation

The Python and TypeScript modules use the same fixture and return the same value, null state, reason code, and diagnostics. Their order is intentional:

  1. validate session, identifiers, method, and causal cutoff;
  2. validate the topic-specific count or price-window contract;
  3. compute the smallest readable transform;
  4. publish value and evidence state together.

The implementations are dependency-free. The official pandas rolling-mean documentation is recorded only for standard rolling terminology where relevant; the code does not delegate correctness to pandas.

Run the signature experiment

Open visuals/animated/playground.html. The signature demonstration is: Denominator topology: animate 12/3, 12/1, 12/0, and 0/0 into resolved, resolved, unbounded, and incomplete states.

Use Step and Back to inspect one transition, then Play/Pause to follow the same transition function. Switch to the comparison scenario and read the diagnostic, not just the line. Finally choose the failure scenario and confirm that Reset restores the canonical state. Reduced-motion Play advances one deterministic step without starting a timer.

Compare the sibling, not just the number

The signed difference remains finite at a zero-low boundary; the High-Low Index uses NH + NL and becomes undefined only when both counts are zero.

That comparison is the purpose of the family map:

High/Low and Trend Breadth family map

Use the map to choose an input object first—new-extreme events or price relative to a moving baseline—then choose transform and memory. Do not select a measure merely because a charting platform exposes it.

Failure and misuse boundaries

  • Never backfill today's roster into an older session.
  • Never combine provider totals whose security types, sessions, intervals, equality rules, or adjustments differ.
  • Never turn absent or future evidence into zero.
  • Never infer predictive power, causation, or profitability from implementation correctness.
  • Never hide a changed denominator behind a plausible percentage.

For this topic, the decisive rule is: Positive highs divided by zero lows is named unbounded, not serialized as infinity. Zero highs and zero lows is incomplete: no new extremes, because the ratio has no direction.

Historical-example decision

The decision remains deferred. A named reconstruction would require point-in-time membership, stable security identity, exact methodology, adjustment or high/low lineage, availability and correction times, and redistribution rights. A screenshot or current provider symbol cannot establish those facts. The synthetic case teaches the mechanism without manufacturing historical certainty.

What the learner can now audit

You can calculate HLR_t = NH_t / NL_t, explain its unit, reproduce its canonical value, distinguish its invalid states, and choose it over a sibling for a reason. Review New Highs–New Lows if the immediate input concept is unfamiliar. Continue with High-Low Index to see the next transform or horizon.

Primary and provider-method records, applicability, versions, and limitations are maintained in ../REFERENCES.md.

Topic decision flow — High-Low Ratio

Purpose: expose the branch that is unique to HLR_t = NH_t / NL_t after identity and availability have passed.

Rendering system map…

Takeaway: Positive highs divided by zero lows is named unbounded, not serialized as infinity. Zero highs and zero lows is incomplete: no new extremes, because the ratio has no direction.

ReferencesPrimary sources and evidence notes

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

Access date for web sources: 2026-07-25. Source roles are deliberately limited to the claims each record can support.

NASDAQ-FUNDAMENTAL — Nasdaq Fundamental Data

  • Organization: Nasdaq
  • Source type: Official exchange data-product documentation
  • Publication/effective date: Current product page
  • Version: Web page accessed 2026-07-25
  • URL: https://www.nasdaqtrader.com/Trader.aspx?id=FD
  • Accessed: 2026-07-25
  • Jurisdiction/applicability: Nasdaq-listed U.S. equities; provider field availability
  • Supports: Security-master attributes plus daily and 52-week high/low market-summary fields are distributed on a T+1 basis.
  • Limitations: Field availability does not define this package's eligible universe, equality rule, adjustment policy, or redistribution rights.

TRADINGVIEW-HIGHLOW — How High/Low and New High/New Low Are Calculated

  • Organization: TradingView
  • Source type: Official provider support documentation
  • Publication/effective date: Current support article
  • Version: Web page accessed 2026-07-25
  • URL: https://www.tradingview.com/support/solutions/43000753745-how-are-high-low-and-new-high-new-low-calculated/
  • Accessed: 2026-07-25
  • Jurisdiction/applicability: TradingView screener convention
  • Supports: One provider compares the latest daily high with a period high using a greater-than-or-equal rule.
  • Limitations: The interval, date-range, session, and equality choices are provider-specific and are not universal market rules.

STOCKCHARTS-HIGHLOW — New 52-Week Highs and Lows for Exchanges

  • Organization: StockCharts ChartSchool
  • Source type: Maintained provider-method documentation
  • Publication/effective date: Current documentation
  • Version: Web page accessed 2026-07-25
  • URL: https://chartschool.stockcharts.com/table-of-contents/index-and-market-indicator-catalog/new-52-week-highs-and-lows-for-exchanges
  • Accessed: 2026-07-25
  • Jurisdiction/applicability: U.S. exchange breadth series
  • Supports: Net New Highs use highs minus lows; the High-Low Ratio uses highs divided by lows; provider universes and adjustments can produce different totals.
  • Limitations: This package does not reproduce proprietary observations or interpretive thresholds.

Evidence decision

Historical example: deferred. A named count-series reconstruction requires a complete point-in-time universe, exact provider classification, availability/correction lineage, stable identifiers, and redistribution rights. The public package uses labelled synthetic teaching data and makes no prediction, causation, threshold-performance, or profitability claim.

high_low_ratio.ts
/** Point-in-time reference implementation for High-Low Ratio. */
export type HighLowRecord = {
  session_date: string;
  available_at: string;
  source_evidence_state: string;
  venue_id: string;
  universe_id: string;
  methodology_id: string;
  lookback_sessions: number;
  new_highs: number;
  new_lows: number;
  eligible_issues: number;
  overlap_issues?: number;
};

const MODE: "net" | "ratio" | "index" = "ratio";
const INDEX_WINDOW = 10;
const identityFields = ["venue_id", "universe_id", "methodology_id", "lookback_sessions"] as const;

function time(value: unknown, field: string): number {
  if (typeof value !== "string" || !/(Z|[+-]\d\d:\d\d)$/.test(value)) throw new Error(`${field} must be offset-aware`);
  const parsed = Date.parse(value);
  if (!Number.isFinite(parsed)) throw new Error(`${field} must be a valid timestamp`);
  return parsed;
}

function count(value: unknown, field: string): number {
  if (!Number.isSafeInteger(value) || (value as number) < 0) throw new Error(`${field} must be a non-negative safe integer`);
  return value as number;
}

export function calculate(records: HighLowRecord[], decisionTime: string) {
  const cutoff = time(decisionTime, "decision_time");
  if (!records.length) throw new Error("records must not be empty");
  let previousSession = "";
  let identity: string | null = null;
  const rhp: number[] = [];
  return records.map((row) => {
    if (!row.session_date || row.session_date <= previousSession) throw new Error("session_date values must be unique and strictly increasing");
    previousSession = row.session_date;
    const currentIdentity = JSON.stringify(identityFields.map((field) => row[field]));
    if (identity === null) identity = currentIdentity;
    else if (identity !== currentIdentity) return {status: "unsupported", reason: "series_identity_changed", value: null};
    const highs = count(row.new_highs, "new_highs");
    const lows = count(row.new_lows, "new_lows");
    const eligible = count(row.eligible_issues, "eligible_issues");
    const overlap = count(row.overlap_issues ?? 0, "overlap_issues");
    if (highs > eligible || lows > eligible || overlap > Math.min(highs, lows)) throw new Error("high/low counts cannot exceed the eligible universe");
    if (time(row.available_at, "available_at") > cutoff) return {session_date: row.session_date, status: "incomplete", reason: "future_evidence", value: null};
    if (row.source_evidence_state !== "ready") return {session_date: row.session_date, status: "incomplete", reason: "evidence_not_ready", value: null};
    let value: number | null = null;
    let status = "resolved";
    let reason: string | null = null;
    if (MODE === "net") value = highs - lows;
    else if (MODE === "ratio") {
      if (lows === 0) {
        status = highs > 0 ? "unbounded" : "incomplete";
        reason = highs > 0 ? "zero_new_lows" : "no_new_extremes";
      } else value = highs / lows;
    } else {
      const denominator = highs + lows;
      if (denominator === 0) {
        rhp.push(Number.NaN);
        status = "incomplete";
        reason = "no_new_extremes";
      } else {
        rhp.push(100 * highs / denominator);
        if (rhp.length < INDEX_WINDOW) {
          status = "warming";
          reason = "requires_10_sessions";
        } else if (rhp.slice(-INDEX_WINDOW).some((item) => !Number.isFinite(item))) {
          status = "incomplete";
          reason = "undefined_record_high_percent_in_window";
        } else value = rhp.slice(-INDEX_WINDOW).reduce((sum, item) => sum + item, 0) / INDEX_WINDOW;
      }
    }
    return {session_date: row.session_date, new_highs: highs, new_lows: lows, value, status, reason};
  });
}
Full-height labplaygroundOpen full screen