A canonical, well-specified, cross-language (Python + TypeScript) reference implementation of intraday index levels. Between rebalances an index is a fixed basket with moving prices — index shares, float factors and the divisor do not change through the session — so every price snapshot is one weighted sum. This module computes it in exact decimal arithmetic with validated, ordered timestamps, then answers what the levels alone cannot: why the index moved, which part of it was frozen, and whether a feed's published levels add up.
📖 Full article (canonical): Intraday Index-Level Calculation — 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 › Index Initialization and Continuity
📥 Just want to call it? It also ships in the fintech-algorithms npm package — see Two ways to use this.
| Catalog topic | D03-F01-A05 |
| Domain | D03 — Index and Benchmark Engineering |
| Family | D03-F01 — Index Initialization and Continuity |
| Difficulty | 3 / 5 |
| Languages | Python, TypeScript |
| Completes | the D03-F01 Index Initialization and Continuity family |
- One weighted sum per tick
- What the reference got wrong
- Two ways to use this
- Install
- Quickstart
- Views: the analysis surface
- Input shape
- API reference
- Edge cases & limitations
- Testing
- Related algorithms
- License
numerator(t) = sum(price_i(t) x indexShares_i x floatFactor_i x fx_i(t))
level(t) = numerator(t) / divisor
Levels, divisor 120
09:30 numerator 134680 level 1122.333333
09:45 numerator 134630 level 1121.916667
10:00 numerator 135020 level 1125.166667
10:15 numerator 135060 level 1125.5
10:30 numerator 135615 level 1130.125
The quantities were fixed at the last rebalance, which is why an intraday level is cheap: nothing but prices and fx rates has to be read on each tick.
All confirmed by running it:
- A float factor of 0 was accepted. The member silently vanished from the level while staying in
the basket. Here every float factor must be in
(0, 1]. - NaN. The Python reference let a NaN price through its positivity check and returned a NaN level;
its TypeScript twin never checked prices at all and returned
null. Here only finite JSON numbers are accepted. - Timestamps were never validated — any JSON value, in any order. Here a timestamp is a session
clock
HH:MM[:SS[.ffffff]]or an instant with a UTC offset, one form per request, and snapshots must be strictly increasing. Instants are compared in UTC, so10:00:00Zfollowed by11:00:00+01:00is refused: it is the same instant. - Coercion. Strings and booleans were converted silently.
Constituents are positional, as in the fixture. The catalog metadata describes keyed records; the
fixture and both reference twins use arrays, and so does this port. An optional ids array names the
constituents for the views.
This repo is the production home: the full implementation, the analysis surface below, and 152 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/index-initialization-and-continuity/intraday-index-level-calculation
Python
cd python
pip install -e ".[dev]"TypeScript
cd typescript
npm install
npm run buildfrom fintech_intraday_index import calculate, constituent_attribution
request = {
"indexShares": [800, 450, 1080],
"floatFactors": [1, 1, 1],
"divisor": 120,
"snapshots": [
{"timestamp": "09:30", "prices": [50, 80, 40], "fxRates": [1, 1, 1]},
{"timestamp": "10:00", "prices": [50.5, 79, 40.2], "fxRates": [1, 1, 1]},
{"timestamp": "10:30", "prices": [51, 79.5, 40.6], "fxRates": [1, 1, 1]},
],
}
calculate(request)["levels"]
# [{'timestamp': '09:30', 'numerator': 119200, 'level': 993.333333},
# {'timestamp': '10:00', 'numerator': 119366, 'level': 994.716667},
# {'timestamp': '10:30', 'numerator': 120423, 'level': 1003.525}]TypeScript is the same call:
import { calculate, constituentAttribution } from 'fintech-intraday-index';
calculate(request).levels;Run the tour in either language:
cd python && python examples/quickstart.py
cd typescript && npm run exampleBoth print byte-identical output.
Each interval's change, member by member, split exactly into a price effect (p1 − p0) × x0 × q / D
and an fx effect p1 × (x1 − x0) × q / D:
Why the level moved from 09:45 to 10:00
ALFA price -2 fx 0 points -2
BETA price 2.25 fx 0 points 2.25
GAMMA price 0 fx 0 points 0
DELTA price 0 fx 3 points 3
level change 3.25, sums exactly: true
DELTA's price never moved; its three points are the currency. attributionSumsExactly is checked on
every interval, and the view also returns session points per member and the most influential one.
ALFA price changes 4 fx changes 0 moved every tick
BETA price changes 4 fx changes 0 moved every tick
GAMMA price changes 0 fx changes 0 unchanged 09:30-10:30 (5 snapshots)
DELTA price changes 0 fx changes 1 unchanged 10:00-10:30 (3 snapshots)
never moved: GAMMA, 0.286694 of the last numerator
A level computed from a frozen feed looks calm because part of it is not moving. staleWeight says how
much of the published level that is.
open 09:30 1122.333333
high 10:30 1130.125
low 09:45 1121.916667
close 10:30 1130.125
return 0.006942382 path 8.625 points efficiency 0.903381643
largest drawdown 0.416667 points from 09:30 to 09:45
efficiency is net move over path length: 1 for a session that only went one way. High and low report
their first occurrence.
ok theDivisorIsEchoed
ok thereIsALevelForEverySnapshot
ok timestampsMatchTheSnapshots
ok everyNumeratorIsTheWeightedSum
FAIL everyLevelIsNumeratorOverDivisor at snapshot 2
FAIL theReportedLevelsAreConsistentWithTheirNumerators at snapshot 2
That is a feed that published 10:00 one cent high. The last check needs nothing but the feed's own
numbers: a level and a numerator both published to six decimals can differ from numerator / divisor
by at most h + h / divisor, with h half a unit in the sixth decimal — so honest rounding passes and
a wrong level does not.
Snapshots use one timestamp form and are strictly increasing. An empty snapshots list is a valid
session with no levels.
Python — from fintech_intraday_index import ...
| function | returns |
|---|---|
calculate(data) / intraday_levels(data) |
levels (timestamp, numerator, level) and divisor |
constituent_attribution(data) |
per-interval price and fx points per member, exact sums |
stale_price_report(data) |
changes per member, longest unchanged run, stale weight |
session_summary(data) |
open, high, low, close, return, path, efficiency, drawdown |
verify_intraday(data, result=None) |
six checks with mismatching snapshot indices |
validate_request · parse_timestamp · days_from_civil |
validation and the timestamp grammar |
to_fraction · render · number · trim · scaled |
exact-arithmetic helpers |
TypeScript — import { ... } from 'fintech-intraday-index'
The same functions in camelCase (intradayLevels, constituentAttribution, ...). Timestamp keys are
bigint microseconds.
- Quantities are fixed for the session. A corporate action or rebalance during the session needs a new divisor and a new request from that point on.
- Session clock times do not cross midnight. A session spanning midnight should use instants.
- The divisor is echoed at six decimals. A divisor supplied with more decimals is used exactly in every level but published rounded, as in the contract.
- Stale means unchanged, not missing. A price that genuinely did not trade and a feed that stopped
look the same from the numbers;
stale_price_reportflags both. - 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.
cd python && pytest -q # 76 tests
cd typescript && npm test # 76 testsBoth suites reproduce the canonical fixture byte for byte.
The two implementations were compared directly across 1,500 scenarios and 7,500 calls — the levels and all four surfaces, valid and malformed, including 513 sessions with stale members and 695 with drawdowns — and their canonical JSON output is byte-identical (3.9 MB). The examples are byte-identical too.
The Python port was differentially tested against the reference engine on 12,000 generated sessions
with zero unexplained divergences. An independent Decimal oracle confirmed all 9,040 results the
port returned. Every divergence is classified by name:
| divergence | cases | what happened |
|---|---|---|
| rounding rule | 2,110 | reference rounded a binary float; port rounds the exact decimal half away from zero |
| double resolution | 1,789 | both correct to the decimal; neighbouring doubles above ~9e9 |
| timestamp validation | 911 | reference echoed any timestamp, in any order |
| coercion | 564 | reference accepted strings and booleans |
| non-finite | 401 | reference accepted NaN or infinity |
| float bound | 325 | reference accepted a float factor of 0 |
| ids validation | 254 | the optional ids array was malformed (the reference ignores it) |
| reference float error | 25 | reference's binary sum drifted; port matches the oracle |
Same family — D03-F01 Index Initialization and Continuity
- Base-Date/Base-Value Initialization — where the basket and the first divisor come from.
- Index Divisor Initialization — how precisely to publish the divisor every tick divides by.
- Divisor Continuity Adjustment — what changes when the basket does.
- Corporate-Action Divisor Bridge — the divisor across an ex-date.
MIT — see LICENSE.
{ "indexShares": [800, 450, 1080], // > 0, fixed since the last rebalance "floatFactors": [1, 1, 1], // in (0, 1], aligned with indexShares "divisor": 120, // > 0 "ids": ["ALFA", "BETA", "GAMMA"], // optional, aligned, unique; names members in the views "snapshots": [{ "timestamp": "09:30", // HH:MM[:SS[.ffffff]] or YYYY-MM-DDTHH:MM:SS[.ffffff](Z|±HH:MM) "prices": [50, 80, 40], // > 0, aligned "fxRates": [1, 1, 1] // > 0, aligned, into the index currency }] }