Skip to content

Latest commit

 

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Fintech Net Advances — Market Breadth Algorithm

A canonical, well-specified, cross-language (Python + TypeScript) reference implementation of Net Advances (advances − declines) — the foundational market-breadth primitive — built around point-in-time revision selection (no look-ahead) and a hard rule: never publish a number the evidence does not support. Ships an as-of replay surface that makes revision risk visible.

Python TypeScript License Tests

📖 Full article (canonical): Net Advances — The Fintech Builder

This repository is the runnable, production-oriented companion to that article. The article teaches the concept; this repo is the code you install and build on.

🧭 Browse all algorithms: Awesome FinTech Algorithms — the full index of the library. 🗂️ This algorithm's domain: Market Breadth and Internals › Advance-Decline Breadth

Catalog topic D04-F01-A01
Domain D04 — Market Breadth and Internals
Family D04-F01 — Advance-Decline Breadth
Difficulty 2 / 5
Languages Python, TypeScript
Feeds A/D Ratio · Cumulative A/D Line · McClellan Oscillator

Table of contents


What is Net Advances?

net_advances = advances − declines

It's the count of issues that closed up minus the count that closed down — the primitive underneath the A/D Ratio, the Cumulative A/D Line, and the McClellan Oscillator. Breadth answers "how many stocks are participating?", which a cap-weighted index can hide entirely.

Subtracting two counts is trivial. Producing a number you can defend six months later is not — and that's what this package is actually about.

The two things that actually go wrong

1. Look-ahead through revisions

Breadth inputs get corrected. A vendor publishes a provisional close at 21:05, then a correction at 22:00 that flips one issue from advance to decline. If your backtest reads today's database, it silently uses the corrected value for a decision made at 21:30 — and your results are fiction.

So a request here carries the whole revision chain, and the calculator selects the latest revision that was both effective and available at calculation_as_of. The supersession chain is validated first (no sequence gaps, no duplicate ids, links intact). A correction that lands later cannot change an earlier query — that's a test, not a promise.

2. Publishing a plausible-but-unsupported number

Every issue is partitioned:

advances + declines + unchanged + excluded + unclassified  ==  universe_size

That invariant is enforced. If even one issue can't be classified, the result does not quietly report the sum of the rest — it returns status: "incomplete" and leaves net_advances as null.

Status: the refusal to guess

status meaning net_advances
ready counts reconcile to the universe the number
no_movers complete universe, nothing advanced or declined 0 (active zero)
empty_universe the declared universe is empty 0 (active zero)
incomplete an issue lacks evidence to classify null
ambiguous broken revision chain / duplicate listing ids null
unsupported nothing was causally available at the as-of time null

The distinction that matters: no_movers and empty_universe are active zeros — genuinely "nothing moved". incomplete is not zero, because there a zero would be a lie. Most implementations collapse all four into 0 and lose the difference.

Why this implementation

  • Point-in-time by construction — revision selection, chain validation, and an is_provisional flag on every result.
  • Explicit statuses over silent zeros, with diagnostics explaining why.
  • Full audit trail — the selected revision_id / sequence / effective_at / available_at, the per-state exclusion_counts, coverage_ratio, and the echoed request identity (venue, universe, comparison basis).
  • Replay helpers (this repo's addition) that turn the revision history into a timeline you can chart.
  • Cross-language parity — both suites assert all three named fixture query_expectations, field by field.

Install

Python

pip install fintech-net-advances

TypeScript / JavaScript (Node ≥ 20)

npm install fintech-net-advances

Quickstart

Python

from fintech_net_advances import calculate_net_advances

result = calculate_net_advances(request)   # request carries the revision chain
if result["status"] == "ready":
    publish(result["net_advances"], provisional=result["is_provisional"])
else:
    flag(result["status"], result["diagnostics"])

TypeScript

import { calculateNetAdvances } from "fintech-net-advances";

const result = calculateNetAdvances(request);
if (result.status === "ready") publish(result.net_advances);

Replay: making revision risk visible

The calculator answers one as-of question. The operationally interesting question is how the answer moved as corrections arrived — and whether a chart's move was a market event or a data event.

from fintech_net_advances import revision_timeline, replay_at_revision_boundaries

revision_timeline(request)          # when each revision became usable, and why
replay_at_revision_boundaries(request)   # the answer at each of those instants

Running the bundled example prints exactly that (identical in both languages):

revision timeline:
  SYNTH-R1: usable from 2026-01-05T21:05:00Z (provisional)
  SYNTH-R2: usable from 2026-01-05T22:00:00Z (final)

replay at revision boundaries:
  as of 2026-01-05T21:05:00Z: SYNTH-R1 -> net=2 (ready, provisional)
  as of 2026-01-05T22:00:00Z: SYNTH-R2 -> net=0 (ready, final)

before first publication: status=unsupported net=None

Same session, same date — breadth "moved" from +2 to 0 because a correction landed, not because the market did. usable_from is max(effective_at, available_at): a revision is usable only once it is both in force and published.

Request & result shapes

Request: session_date, session_id, session_timezone, venue_id, universe_id, comparison_basis, corporate_action_policy, price_tolerance, calculation_as_of, and revisions[].

Revision: revision_id, revision_sequence, supersedes_revision_id, effective_at, available_at, is_final, members[].

Member: listing_id, security_id, ticker, state, current_price, prior_comparable_price. Valid states: eligible, excluded_halted, excluded_suspended, excluded_delisted, excluded_new_no_prior_close, missing_price, unclassified.

Result: status, direction, net_advances, the counts (advances/declines/unchanged/excluded/unclassified), universe_size, mover_count, classified_count, coverage_ratio, exclusion_counts, diagnostics, the full selected-revision audit trail, and is_provisional.

Note the calculator does not adjust prices. prior_comparable_price must already be split/distribution-adjusted upstream; corporate_action_policy records whose convention was used.

Worked example (exact)

A 12-issue synthetic universe, price_tolerance = 0.01, two revisions:

as of selected status A D U excl net provisional
21:04:59Z — unsupported — — — — null —
21:30:00Z SYNTH-R1 ready 5 3 2 2 +2 (advances dominant) yes
22:30:00Z SYNTH-R2 ready 4 4 2 2 0 (balanced) no

Both partitions reconcile to 12 with coverage_ratio = 10/12. The correction moves one issue from advance to decline — and must not affect the 21:30 query. Every field of all three rows is asserted by both language test suites.

API reference

Purpose Python TypeScript
Point-in-time calculation calculate_net_advances(request) calculateNetAdvances(request)
Revision usability timeline revision_timeline(request) revisionTimeline(request)
Replay at chosen times replay_as_of(request, times) replayAsOf(request, times)
Replay at revision boundaries replay_at_revision_boundaries(request) replayAtRevisionBoundaries(request)
Errors BreadthValidationError BreadthValidationError

Structural problems (bad dates, malformed timestamps, unknown member states) raise; evidence problems return a non-ready status. That split is deliberate: a caller bug is not the same thing as a data gap.

Edge cases & limitations

  • available_at may not precede effective_at — that combination is a structural error, not a data gap.
  • Ties are balanced, and direction is null whenever net_advances is.
  • Tolerance is inclusive: a move of exactly price_tolerance counts as unchanged, not a mover.
  • Coverage is not quality. A high coverage_ratio with a stale universe is still wrong; universe_id and comparison_basis travel with the result so the reader can check.
  • No price adjustment happens here — see the note above.
  • Not a signal. Breadth describes participation, not future direction.

Testing

Python (39 tests)

cd python && pip install -e ".[dev]" && pytest

TypeScript (28 tests, zero runtime dependencies)

cd typescript && npm install && npm test && npm run build

Related algorithms

  • D04-F01-A02 — Advance/Decline Ratio · A03 — Cumulative A/D Line · A04 — Normalized A/D Line · A05 — Absolute Breadth Index
  • D04-F02-A01 — Traditional McClellan Oscillator (EMAs of Net Advances)
  • D07-F01-A02 — EMA (the smoothing the McClellan family uses)

Full index: Awesome FinTech Algorithms.

License

MIT © The Fintech Builder. Part of the 100 FinTech Algorithms library.