Historical Constituent Reconstruction
Rebuild the Roster Without Looking Ahead
Install and import#
npm install fintech-algorithmsimport { calculate } from "fintech-algorithms/corporate-actions-and-security-master-data/point-in-time-universe/historical-constituent-reconstruction";Signature#
calculate(data)Rebuilds an index's membership as it stood on a past date, from a base snapshot plus the change events since. Testing a strategy on today's members instead guarantees flattering results, because today's members are the survivors.
Parameters#
| Name | Type | Notes |
|---|---|---|
data | { indexId: string; effectiveAt: string; knownAt: string; snapshots: Snapshot[]; events: Event[] } | effectiveAt is the date whose roster you want; knownAt is when you are asking. They differ whenever a membership change is announced before it takes effect, or corrected afterwards — and conflating them is the bias this exists to prevent. |
Returns#
{ status, indexId, effectiveAt, knownAt, baseSnapshotId, securityIds, membershipIntervals, … }
The roster with the interval each member was in the index, and which snapshot it was rolled forward from.
Errors#
- When no snapshot precedes effectiveAt — reported as a status rather than thrown
Complexity: time O(events),
space O(members).
Worked example#
verified This is the worked example published in the article, replayed by the test suite on every run. The output cannot drift.
Input#
{
"indexId": "IDX:SYNTH:ALPHA",
"effectiveAt": "2024-08-15T00:00:00Z",
"knownAt": "2024-08-15T12:00:00Z",
"snapshots": [
{
"snapshotId": "BASE-2024",
"indexId": "IDX:SYNTH:ALPHA",
"revision": 1,
"effectiveAt": "2024-01-01T00:00:00Z",
"availableAt": "2024-01-02T09:00:00Z",
"lastSequence": 0,
"status": "active",
"securityIds": ["SEC:A", "SEC:B", "SEC:C", "SEC:D"],
"sourceId": "SYNTH-BASE"
}
],
"events": [
{
"eventId": "EV-001",
"indexId": "IDX:SYNTH:ALPHA",
"revision": 1,
"sequence": 1,
"eventType": "add",
"effectiveOrder": 0,
"effectiveAt": "2024-02-01T00:00:00Z",
"announcedAt": "2024-01-15T12:00:00Z",
"availableAt": "2024-01-15T12:05:00Z",
"status": "active",
"changes": [
{
"action": "add",
"securityId": "SEC:E"
}
],
"sourceId": "SYNTH-NOTICE-001"
},
{
"eventId": "EV-002",
"indexId": "IDX:SYNTH:ALPHA",
"revision": 1,
"sequence": 2,
"eventType": "replace",
"effectiveOrder": 0,
"effectiveAt": "2024-03-01T00:00:00Z",
"announcedAt": "2024-02-20T16:00:00Z",
"availableAt": "2024-02-20T16:03:00Z",
"status": "active",
"changes": [
{
"action": "delete",
"securityId": "SEC:B"
},
{
"action": "add",
"securityId": "SEC:F"
}
],
"sourceId": "SYNTH-NOTICE-002"
},
{
"eventId": "EV-003",
"indexId": "IDX:SYNTH:ALPHA",
"revision": 1,
"sequence": 3,
"eventType": "rebalance",
"effectiveOrder": 0,
"effectiveAt": "2024-04-01T00:00:00Z",
"announcedAt": "2024-03-20T15:00:00Z",
"availableAt": "2024-03-20T15:04:00Z",
"status": "active",
"changes": [],
"sourceId": "SYNTH-NOTICE-003"
}
]
}Call#
calculate(data)Returns#
object with 4 fields: status, securityIds, appliedEventVersions, cancelledEventIds
{
"status": "resolved",
"securityIds": ["SEC:E", "SEC:F", "SEC:G", "SEC:H", "SEC:I", "SEC:J"],
"appliedEventVersions": ["EV-001@1", "EV-002@1", "EV-003@1", "EV-004@1", "EV-005@1", "EV-006@1"],
"cancelledEventIds": []
}Diagrams#
Calculation flow#
Bitemporal reconstruction flow
flowchart LR
Q["Index ID, effectiveAt, knownAt"] --> B["Latest eligible base revision"]
Q --> E["Latest eligible revision per event"]
B --> G{"Complete ledger?"}
E --> G
G -->|"No base or gap"| I["incomplete"]
G -->|"Overlap or conflict"| A["ambiguous"]
G -->|"Unknown event semantics"| U["unsupported"]
G -->|"Yes"| P["Replay ordered changes"]
P --> R["resolved roster and audit trace"]
Resolution state lifecycle
stateDiagram-v2
[*] --> Validating
Validating --> Selecting: contract valid
Selecting --> Incomplete: no base or sequence gap
Selecting --> Ambiguous: competing evidence
Selecting --> Unsupported: unknown event type
Selecting --> Replaying: one complete ledger
Replaying --> Ambiguous: impossible add or delete
Replaying --> Resolved: ordered replay succeeds
Incomplete --> [*]
Ambiguous --> [*]
Unsupported --> [*]
Resolved --> [*]
How it works#
This page states the contract — how to call it correctly. The article explains the concept: why it works, and where it breaks.
References#
- Tesla Set to Join S&P 500
- S&P DJI Announces Implementation of Tesla’s Addition to S&P 500
- Tesla Set to Join S&P 500 & 100; Apartment Income REIT to Join S&P MidCap 400
- FTSE Russell Corporate Actions and Events Guide
- Subscribe to FTSE Russell index data
- Evidence boundary