Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Fintech Intraday Index-Level Calculation — Index Engineering Algorithm

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.

Python TypeScript License Tests

📖 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

Table of contents


One weighted sum per tick

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.


What the reference got wrong

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, so 10:00:00Z followed by 11:00:00+01:00 is 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.


Two ways to use this

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

Install

Python

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

TypeScript

cd typescript
npm install
npm run build

Quickstart

from 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 example

Both print byte-identical output.


Views: the analysis surface

constituent_attribution — why the level moved

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.

stale_price_report — what did not trade

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.

session_summary — the session in one object

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.

verify_intraday — six checks, with the snapshot that failed

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.


Input shape

{
  "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
  }]
}

Snapshots use one timestamp form and are strictly increasing. An empty snapshots list is a valid session with no levels.


API reference

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.


Edge cases & limitations

  • 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_report flags 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.

Testing

cd python && pytest -q          # 76 tests
cd typescript && npm test       # 76 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 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

Related algorithms

Same family — D03-F01 Index Initialization and Continuity

🧭 Browse all algorithms →


License

MIT — see LICENSE.