Skip to content

Latest commit

 

History

1 Commit

Folders and files

Repository files navigation

Fintech Price-Weighted Index — Index Engineering Algorithm

A canonical, well-specified, cross-language (Python + TypeScript) reference implementation of the price-weighted index. Add up the prices, divide by the divisor — and discover that the weights belong to whoever cut their equity into the fewest pieces. This module computes the index in exact decimal arithmetic, then shows what the three published numbers hide: the weights nobody chose, the re-weighting a share split performs for free, and who actually moved the level.

Python TypeScript License Tests

📖 Full article (canonical): Price-Weighted Index — 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: Index and Benchmark Engineering › Weighting and Capping 📥 Just want to call it? It also ships in the fintech-algorithms npm package — see Two ways to use this.

Catalog topic D03-F02-A01
Domain D03 — Index and Benchmark Engineering
Family D03-F02 — Weighting and Capping
Difficulty 2 / 5
Languages Python, TypeScript
Opens the D03-F02 Weighting and Capping family

Table of contents


The weights nobody chose

price sum = sum(prices)
level     = price sum / divisor
weight_i  = price_i / price sum
ALFA   price    50   weight 0.2        points 200
BETA   price    75   weight 0.3        points 300
GAMMA  price    25   weight 0.1        points 100
DELTA  price   100   weight 0.4        points 400
most expensive share: DELTA at 0.400000   priciest over cheapest: 4.000000000

Nothing here knows what any of these companies are worth. DELTA is 40% of the index because its shares are priced at 100, and a company worth ten times as much with a 10 share price would be 4%.


What a split does to the index

A 2-for-1 split creates no value and destroys none, so the level must not move. The divisor is rescaled by the new price sum over the old one — and the weights quietly change anyway:

ALFA      50 -> 50      weight 0.2     -> 0.25      change 0.05
BETA      75 -> 75      weight 0.3     -> 0.375     change 0.075
GAMMA     25 -> 25      weight 0.1     -> 0.125     change 0.025
DELTA    100 -> 50      weight 0.4     -> 0.25      change -0.15
divisor 0.25 -> 0.2, level 1000 -> 1000 (unchanged: true)
DELTA keeps 0.625000000000 of its weight; 0.15 moved to the others

levelUnchanged is checked exactly, not within a tolerance. Fifteen percent of the index changed hands because a company decided to print more certificates.


What the reference got wrong

All confirmed by running it:

  • NaN passed. float("nan") <= 0 is false, so a NaN price came back as a NaN price sum and NaN weights — not even valid JSON. Only finite JSON numbers are accepted here.
  • Coercion. Strings and booleans were converted silently.
  • Float arithmetic. The reference summed binary prices and divided; here every number is read as the decimal it spells, arithmetic runs in exact fractions, and rounding happens once, at publication, to six decimals, half away from zero.

Two ways to use this

This repo is the production home: the full implementation, the analysis surface below, and 140 tests across two languages.

The fintech-algorithms npm package ships the same topic as one import among several hundred.

fintech-algorithms/index-and-benchmark-engineering/weighting-and-capping/price-weighted-index

Install

Python

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

TypeScript

cd typescript
npm install
npm run build

Quickstart

from fintech_price_weighted import calculate, split_impact

calculate({"prices": [50, 75, 25, 100], "divisor": 0.25})
# {'priceSum': 250, 'divisor': 0.25, 'indexLevel': 1000, 'weights': [0.2, 0.3, 0.1, 0.4]}

split_impact({"prices": [50, 75, 25, 100], "divisor": 0.25, "ids": ["ALFA", "BETA", "GAMMA", "DELTA"]},
             "DELTA", 2)["divisorAfter"]
# 0.2

TypeScript is the same call:

import { calculate, splitImpact } from 'fintech-price-weighted';

calculate({ prices: [50, 75, 25, 100], divisor: 0.25 }).indexLevel;   // 1000

Run the tour in either language:

cd python && python examples/quickstart.py
cd typescript && npm run example

Both print byte-identical output.


Views: the analysis surface

weight_table — the weights, and the residual publishing them leaves

Each member's price, weight, exact weight and points of the level. The exact weights sum to 1 by construction; the published ones need not:

publishedWeightSum      0.999999
publishedWeightResidual -0.000001
weightsSumToOneExactly  true

Three equal members publish 0.333333 three times. That is not an error to be forced away — it is what six decimals can say — so it is reported.

split_impact — the divisor that holds the level

Shown above: the new divisor, every member's weight before and after, the ratio the split member keeps, and the weight that moved to everyone else. Works on a reverse split too, and a ratio of 1 is a no-op.

price_move_attribution — points, not percent

BIG      100 -> 101     points 1      move 0.010000000
SMALL     10 -> 10.6    points 0.6    move 0.060000000
biggest contributor BIG, biggest mover SMALL, agree: false

Every member contributes (price after − price before) / divisor, so the index cannot tell the difference between a $1 move on a $100 share and a $1 move on a $10 one. pointsAndPercentAgree says when the headline mover is not the one that actually moved the index.

verify_price_weighted — four checks, and an audit mode

ok   thePriceSumIsTheSumOfPrices
FAIL theDivisorIsEchoed
ok   theLevelIsPriceSumOverDivisor
ok   everyWeightIsItsPriceShare

Pass a second argument to audit somebody else's numbers. The publication residual is reported in the body, never as a failure.


Input shape

{
  "prices": [50, 75, 25, 100],                  // > 0, finite JSON numbers
  "divisor": 0.25,                              // > 0
  "ids": ["ALFA", "BETA", "GAMMA", "DELTA"]     // optional, aligned, non-blank, unique
}

ids is an addition to the positional contract: it names members in the views and changes no published number. split_impact accepts an id or a position.


API reference

Python — from fintech_price_weighted import ...

function returns
calculate(data) / price_weighted_index(data) priceSum, divisor, indexLevel, weights
weight_table(data) per-member weight, exact weight, points, and the publication residual
split_impact(data, target, ratio) the rescaled divisor and every weight before and after
price_move_attribution(data, new_prices) points and percent per member, exact sum check
verify_price_weighted(data, result=None) four checks; pass result to audit a supplied answer
validate_request · validate_ids the validation steps, exposed
to_fraction · render · number · trim · scaled exact-arithmetic helpers

TypeScript — import { ... } from 'fintech-price-weighted'

The same functions in camelCase (priceWeightedIndex, weightTable, splitImpact, ...).


Edge cases & limitations

  • The divisor is yours to carry. This module never invents one: a split changes the divisor, and split_impact tells you what to change it to.
  • Weights are prices, not value. That is the design, not a defect; if you want value weighting, the sibling repos below do exactly that.
  • price_move_attribution assumes no membership change between the two price vectors — it aligns positionally, and a changed basket needs a divisor adjustment first.
  • Published weights are six decimals and need not sum to 1; the residual is reported.
  • Large published numbers are doubles. Above ~9e9 a six-decimal value is finer than a double can hold; the number is the nearest double to the correctly rounded decimal.

Testing

cd python && pytest -q          # 71 tests
cd typescript && npm test       # 69 tests

Both suites reproduce the canonical fixture byte for byte.

The two implementations were compared directly across 1,500 scenarios and 7,500 calls — the index and all four surfaces, valid and malformed — and their canonical JSON output is byte-identical (3.1 MB). The examples are byte-identical too.

The Python port was differentially tested against the reference engine on 12,000 generated baskets with zero unexplained divergences. An independent Decimal oracle confirmed all 9,374 results the port returned. Every divergence is classified by name:

divergence cases what happened
coercion 543 reference accepted strings and booleans
rounding rule 329 reference summed binary floats; port rounds the exact decimal half away from zero
non-finite 303 reference accepted NaN or infinity and published NaN weights
ids validation 294 the optional ids array was malformed (the reference ignores it)
double resolution 104 both correct to the decimal; neighbouring doubles above ~9e9

Related algorithms

Same family — D03-F02 Weighting and Capping

Related — D03-F01 Index Initialization and Continuity

🧭 Browse all algorithms →


License

MIT — see LICENSE.