Reuse & Selection

Weighted Sampling

Choose entries in proportion to their weights, for simulations, randomized search and priority sampling. Weights can change between draws without rebuilding everything.

Updated Publisher Alien_AlgorithmsPine Script v6

WeightedSampler

WeightedSampler is the high-level interface. Each slot holds a weight; sample() turns a fraction into the slot whose share of the total weight covers it.

Three weighted slotspine
var op.WeightedSampler sampler = op.weightedSampler(512)

if barstate.isfirst
    sampler.setWeight(10, 0.25)
    sampler.setWeight(11, 0.80)
    sampler.setWeight(12, 0.10)

float fraction = 0.50
int selected = sampler.sample(fraction)

// Output: selected is 11 for the supplied fraction of 0.50.

The total is 1.15, so slot 10 covers the first 22% of the range, slot 11 the next 70%, and slot 12 the rest. A fraction of 0.50 lands in slot 11. Each slot can stand for a candidate model, a simulation outcome or a work item: update its weight when its score changes, then keep sampling through the same interface.

  • Every slot starts at weight 0, and a slot with weight 0 is never selected.
  • sample() returns -1 when every weight is 0.
  • setWeights(weights) replaces every weight from an array of exactly size() values. setMany(slots, weights) writes chosen slots and returns how many were in range.
  • probability(slot) is a slot's chance of selection, and total() the total weight.

Where fractions come from

The sampler does not generate randomness. Pass math.random() for a random draw, or a fixed sequence of fractions for draws you can reproduce. Any real value is wrapped into the range from 0 up to 1, so 1.0 selects the same slot as 0.0.

Eight draws with one refreshpine
// Pseudocode: modelScores holds 64 non-negative scores, one per model.
var op.WeightedSampler models = op.weightedSampler(64)
var array<float> fractions = array.new<float>(8, 0.0)
var array<int> picks = array.new<int>()

models.setWeights(modelScores)

for i = 0 to 7
    fractions.set(i, math.random())

models.sampleMany(fractions, picks)

// Output: picks holds eight model slots, each chosen in proportion to its score.

sampleMany() refreshes the selection structure once for the whole batch, then draws each fraction in order.

The exponent

Every weight is raised to the sampler's exponent before selection, which is 1 by default. Above 1, high weights dominate more strongly; below 1, the choice flattens towards uniform. Change it with setExponent(), or pass it to op.weightedSampler(capacity, exponent). Negative values become 0.

At exponent 0, every weight becomes 1 and the choice is uniform, including slots whose weight is 0. With ExponentZeroPolicy.preserveZeros in the configuration, zero weights stay unselectable at every exponent.

Prefix sum or Fenwick tree

Selection needs running totals of the weights. OptiPine keeps them in one of two ways:

RepresentationWeight changeDrawSuits
Linear prefix sum (default)O(1), then O(n) once on the next drawO(log n)Weights that are rebuilt together or change rarely. Bit-reproducible.
Fenwick treeO(log n)O(log n)A few weights changing between many draws.

A WeightConfig chooses between them. Four presets cover the usual cases:

PresetWhat it does
op.weightConfigDeterministic()Forces the linear prefix sum, whose left-to-right addition order never changes. This is also the default.
op.weightConfigStatic()Forces a Fenwick tree, for weights that rarely change under a stable exponent and many draws.
op.weightConfigSparseUpdates()Automatic: starts with the prefix sum and moves to a Fenwick tree once the exponent is stable and updates stay sparse, such as a prioritized replay buffer whose priorities change a few at a time.
op.weightConfigStrict()Rejects na and negative weights instead of clamping them.
A sampler for sparse updatespine
var op.WeightedSampler replay = op.weightedSampler(4096, 1.0, op.weightConfigSparseUpdates())

In automatic mode the tree is used once two preparations in a row have kept the same exponent, as long as the weight updates since the last preparation number no more than 5% of the range. Denser updates fall back to the prefix sum. Both thresholds are fields of op.weightConfig(): promoteAfterCycles and sparseUpdateRatio.

Why the default is the prefix sum
A Fenwick tree adds the weights in a different order. The totals are the same mathematically, but floating-point rounding can move a draw that falls exactly on a boundary into the neighbouring slot. When results must reproduce bit for bit, keep the prefix sum.

na and negative weights

By default an na weight is stored as 0, and a negative weight is clamped to 0; both make the slot unselectable. NaWeightPolicy.raiseError and NegativeWeightPolicy.raiseError stop the script instead, which is what op.weightConfigStrict() sets.

WeightedIndex over a circular range

The sampler draws from all of its slots. WeightedIndex, the Tier 2 structure underneath it, draws from a circular range of physical slots, which is exactly the layout of a RingCursor. prepare(exponent, startPhysical, count) fixes the exponent, the slot holding logical index 0, and the length of the range.

Draw from the retained events in a ringpine
// Pseudocode: newEvent and eventStrength come from the script.
var op.RingCursor events = op.ringCursor(200)
var op.WeightedIndex eventWeights = op.weightedIndex(200)

if newEvent
    int slot = events.reserve()
    // Overwrites the weight of the event the ring just dropped.
    eventWeights.setWeight(slot, eventStrength)

int picked = -1
if events.count() > 0
    // Logical index 0 is the oldest retained event.
    eventWeights.prepare(1.0, events.physical(0), events.count())
    picked := eventWeights.draw(math.random())

// Output: picked is the physical slot of one retained event, chosen by strength.
  • draw() and drawMany() return physical slots, so they index your arrays directly. They return -1 before the first prepare() or when the range has no positive weight.
  • prepareAll(exponent) prepares every slot, which is what the sampler does.
  • setWeight() with the value already stored does nothing and invalidates nothing. weightRevision() advances on every effective change, for callers that cache results derived from the weights.
  • mode(), currentExponent(), preparedRange() and pendingUpdates() describe the prepared state.
  • A sampler's indexView() returns its WeightedIndex, for forced policies and scoped editing.

Editing the weight array directly

For a measured hot path, Tier 3 calls write the weights without per-call checks. weightsEditBegin() opens a scope, weightsUnsafe() returns the backing array for direct writes, and weightsEditFinish() clamps invalid values and rebuilds every derived structure. Never keep the array past the end of the scope. setWeightUnchecked() writes one weight without bounds checks or clamping, and touchWeights() repairs the derived state without a scope.

WeightedPrefix and FloatFenwick

The two representations are also exported on their own:

  • WeightedPrefix builds cumulative weights over a plain or circular range in O(n), with an optional exponent, and answers lowerBound(threshold) in O(log n): the first index whose running total reaches the threshold.
  • FloatFenwick is a Fenwick tree over up to 99,999 non-negative weights. set(), add(), prefix(), rangeSum() and lowerBound() are O(log n), and lowerBoundCircular() selects within a circular range. na and negative values become 0.

Use them when you need prefix sums for something other than sampling, such as cumulative volume between two indices.

Calls

CallWhat it does
op.weightedSampler(capacity, exponent = 1.0, settings = na)Creates a sampler with every weight at 0.
sampler.setWeight(slot, weight), sampler.setWeights(weights), sampler.setMany(slots, weights), sampler.fill(weight)Write weights.
sampler.sample(fraction), sampler.sampleMany(fractions, output = na)Draw one slot or a batch.
sampler.weight(slot), sampler.probability(slot), sampler.total()Read weights and chances.
sampler.setExponent(exponent), sampler.size(), sampler.mode(), sampler.indexView(), sampler.clear()Exponent, size, representation, the underlying index, and reset.
op.weightedIndex(capacity, settings = na)Creates a circular-range index.
index.prepare(exponent, startPhysical, count), index.prepareAll(exponent = 1.0)Fix the exponent and range.
index.draw(fraction), index.drawMany(fractions, output = na)Draw physical slots.
op.weightConfig(...)A configuration. Also the four presets above.
op.weightedPrefix(), op.floatFenwick(size = 0)The two cumulative-weight representations, on their own.

Every signature is in the API Reference. Next: Optimizing Pine Script.

OptiPine is a performance library for Pine Script. TradingView's memory, execution and collection limits still apply. © Alien_Algorithms.