IntIndex & Lookup Policy
The adaptive integer-key directory behind every typed store. It maps each key to a slot, chooses dense addressing, a map, binary search or a scan from the keys it sees, and leaves the payload to you.
A key-to-slot directory
A typed store holds one value per key. When a key owns several fields, such as a price, a side and a label, an IntIndex resolves the key once and every payload array uses the same slot. Slots follow insertion order from 0, so slot n belongs to the nth key added.
Build and query
A build has three steps: buildBegin(), then add() or addMany(), then buildFinish(), which seals the index and chooses the lookup.
var op.IntIndex trades = op.intIndex()
var array<float> entryPrices = array.new<float>()
var array<string> sides = array.new<string>()
if barstate.isfirst
trades.buildBegin()
trades.addMany(array.from(7005, 7001, 7042))
trades.buildFinish()
// Slots follow insertion order: 7005 is slot 0, 7001 slot 1, 7042 slot 2.
entryPrices.push(101.5)
entryPrices.push(99.0)
entryPrices.push(104.25)
sides.push("long")
sides.push("short")
sides.push("long")
int slot = trades.find(7001)
float entryPrice = slot >= 0 ? entryPrices.get(slot) : na
string side = slot >= 0 ? sides.get(slot) : na
// Output: trade 7001 resolves to slot 1, entry price 99.0, side "short".find(key)returns the slot, or-1on a miss.findMany(queryKeys, output)resolves a batch in query order with one dispatch.keyAt(slot)reads a slot's key, andkeysView()is every key in slot order, as a read-only view.add()is O(1).buildFinish()is O(entries) for most builds, and O(entries log entries) when unordered keys need sorting.- Collections are reused across rebuilds, so rebuilding an index every bar allocates nothing new.
buildFinish(), or an add() after it, stops the script. Call buildBegin() to rebuild. With strictLifecycle = false, the lookup returns -1 and the add starts a new build instead.Automatic selection rules
In automatic mode, the default, buildFinish() checks these rules in order and takes the first that matches:
| Keys | Lookup | find() cost |
|---|---|---|
| None, or no more than linearMaxEntries (0 by default) | Linear scan | O(n) |
| Consecutive and ascending | Arithmetic dense: the slot is the key minus the smallest key | O(1) |
| Span within 16,384 and within twice the entry count | Dense slot table | O(1) |
| Not ascending, up to 50,000 keys | Map | O(1) |
| Ascending, up to 50,000 keys, and expectedQueries at least max(32, 10% of the entries) | Map | O(1) |
| Anything else | Sorted binary search | O(log n) |
- The span is the largest key minus the smallest, plus one.
- A strictly ascending build searches the key array directly, with no extra storage. Any other order sorts a copy once.
- With
autoPromoteon, a sorted index of up to 50,000 keys promotes itself to a map once its counted queries reach the same threshold as the hint.op.indexConfigDynamic()turns this on.
Forcing a lookup
A LookupPolicy in the configuration overrides automatic mode. Each preset sets one:
| Preset | Policy | Suits |
|---|---|---|
op.indexConfigStaticSorted() | sorted | A directory built once and queried across many bars. No map is ever allocated. |
op.indexConfigDense() | dense | Validated dense integer keys. Raises when the span exceeds maxDenseSpan. |
op.indexConfigDictionary() | dictionary | A persistent map for lookup-heavy work, up to 50,000 keys. |
op.indexConfigTiny() | linear | A very small directory rebuilt far more often than it is queried. No build cost at all. |
op.indexConfigDynamic() | automatic | Online promotion and counters, for query volume that is unknown in advance. |
mode() reports the lookup in use. It is never automatic.
Duplicate keys
A DuplicatePolicy decides what a repeated key means:
| Policy | Effect |
|---|---|
op.DuplicatePolicy.raiseError | The default. buildFinish() stops the script and reports how many duplicates it found. Every lookup representation verifies uniqueness. |
op.DuplicatePolicy.firstWins | Every lookup resolves to the slot of the first occurrence. |
op.DuplicatePolicy.lastWins | Every lookup resolves to the slot of the last occurrence. |
IndexConfig fields
op.indexConfig() takes every field as a named argument and clamps each one to a valid range. One configuration can be shared by several indexes; change it between builds, not during a query loop.
| Field | Default | Meaning |
|---|---|---|
policy | automatic | The requested lookup. |
duplicates | raiseError | What a repeated key means. |
promoteQueryRatio | 0.10 | Queries per entry that justify building a map. |
promoteMinQueries | 32 | Minimum queries before a map is justified. |
autoPromote | false | Let a sorted index promote itself to a map while queried. |
validateAscending | true | Check key order during the build. When false, buildBegin(expectedAscending) is trusted. |
maxDenseSpan | 100000 | Largest span a forced dense lookup accepts. |
autoDenseSpanRatio | 2.0 | Automatic dense tables need span ≤ entries × this. 0 disables them. |
autoDenseMaxSpan | 16384 | Absolute span ceiling for automatic dense tables. |
linearMaxEntries | 0 | Builds this small use a linear scan in automatic mode. |
duplicateScanLimit | 64 | Largest unordered linear build checked pair by pair for duplicates before sorting instead. |
collectStats | false | Count queries, hits and misses. |
strictLifecycle | true | Stop the script on calls out of order. |
Inspecting an index
stats() returns an IndexStats snapshot: the requested and active lookups, entries, queries, hits and misses, builds, promotions, duplicates, the key span, and whether the keys were ascending or consecutive. Queries are counted when collectStats is on or a promotion is armed; hits and misses only with collectStats.
Clearing and resetting
clear()empties the index, keeps every allocation and the configuration, and keeps the cumulative build and promotion counters.reset(settings)also replaces the configuration and zeroes those counters.
Calls
| Call | What it does |
|---|---|
op.intIndex(settings = na) | Creates an index with automatic defaults or the given configuration. |
index.buildBegin(expectedAscending = true) | Starts a build and clears the previous keys. |
index.add(key), index.addMany(keyBatch) | Add keys. Each returns the slot of the first key it added. |
index.buildFinish(expectedQueries = na) | Seals the index and returns the lookup it chose. |
index.find(key), index.contains(key) | Resolve one key to its slot, or test it. |
index.findMany(queryKeys, output = na) | Resolve a batch in query order. -1 marks a miss. |
index.keyAt(slot), index.keysView() | Read keys by slot. |
index.entryCount(), index.mode(), index.sealed(), index.stats() | Size, active lookup, build state and telemetry. |
index.clear(), index.reset(settings = na) | Empty the index, or empty it and replace the configuration. |
op.indexConfig(...) | A configuration. Also the five presets above. |
Every signature is in the API Reference, and configuration in IndexConfig. Next: IntBuckets.