fintech-algorithms
Using a coding agent? Give it the skill: npx skills add IslamBaraka90/Fintech-Algorithms-Library What it does →

Historical Constituent Reconstruction

Rebuild the Roster Without Looking Ahead

Install and import#

bash
npm install fintech-algorithms
ts
import { 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#

NameTypeNotes
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#

data
{
  "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#

Historical Constituent Reconstruction — article hero
Historical Constituent Reconstruction — failure guard
Historical Constituent Reconstruction — worked example

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.

Read the article →

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

The rest of the Point-in-Time Universe family#