Hedronite · Synthesis Lesson · Polyglot-Dev · Ruby (Backend Stack) · Fri 2026-06-12

Ruby's Enumerable, Comparable, and BigDecimal for Strategy Attribution Rollups

Lazy pipelines, the spaceship operator, and money-safe arithmetic.

Lesson Class: Dev Synthesis (inaugural Ruby lesson)
Language: Ruby — Backend Stack tier (Friday slot, W1)
Day / Week: Friday — Week 4 of Cycle 1
Word Count: ~2,470
Paired Ops: Strategy Attribution and the Capital-Reallocation Decision
Paired Cert: Terraform as Multi-Region, Multi-Vendor Provisioning — Week 4 Reach-Back
Grounding: Flanagan, The Ruby Programming Language, Ch 9 + Ch 5
Discipline: ROD v3 · clean code blocks

§ IFrame

The Dev track gains a third backend language today. Go has handled the concurrent and the networked; Rust has handled the type-safe and the memory-exact; Ruby arrives for the part of the work that is neither concurrent nor unsafe but is about expressing a transformation over a collection so directly that the code reads like the description of the transformation. The attribution rollup the day's Ops lesson defined is that kind of work. It takes a stream of per-strategy return records, computes three numbers from each, ranks them, and emits a verdict. Ruby was built to write exactly that.

Two modules carry most of the weight, and both are mixins, which is the Ruby idea this first lesson opens on. A mixin is a module of methods that any class can pull in, gaining all of them by defining one or two methods the module builds on. Flanagan's treatment of the Ruby platform names the two that matter here: Enumerable, which gives a class the whole vocabulary of collection operations once the class defines each, and Comparable, which gives a class the full set of ordering operators once the class defines the spaceship operator <=> (The Ruby Programming Language, Ch 9, pp 343-344). Define one method, inherit fifty. That is the trade Ruby offers, and the attribution rollup spends it well.

§ IILanguage Idiom

The block is the unit Ruby builds collection work from. A block is a chunk of code passed to a method, and the iterator methods — map, select, reduce, sort_by, group_by — each take a block and apply it across a collection. Flanagan's chapter on statements and control structures treats blocks and iterators as the core Ruby control idiom, the thing that replaces the explicit loop almost everywhere (Ch 5, pp 149-154). Where another language writes a for loop with an index and an accumulator, Ruby writes reduce with a block, and the intent is named by the method rather than reconstructed from the loop body.

Enumerable is the multiplier on the block. A class that defines each and includes Enumerable gets map, select, reduce, min_by, max_by, sort_by, group_by, partition, and the rest, all built on the one each the class supplied. A collection of strategy records is not a special-purpose object with hand-written ranking code. It is an Enumerable, and ranking it is sort_by with a block that names the sort key.

Comparable is the ordering half. A record class defines <=> — which returns -1, 0, or 1 for less-than, equal, greater-than — and includes Comparable, and in exchange the class gets <, <=, ==, >=, >, between?, and clamp. The single comparison the author writes becomes the whole comparison surface the rest of the code uses. For attribution this matters because the retirement gate is a comparison: a decay ratio against a floor, an alpha against zero, a weight against its peers.

BigDecimal is the third piece and the one that is not optional. Ruby's default Float is a binary floating-point number, and binary floating point cannot represent most decimal fractions exactly, so a sum of dollar amounts accumulates a small error that grows with the number of terms. BigDecimal is an arbitrary-precision decimal type in Ruby's standard library, and Flanagan lists it among the standard-library modules that handle the cases Float cannot (Ch 9, pp 396-397). Money and the ratios computed from money go in BigDecimal, because a ranking that decides where capital flows must never flip on a rounding error in the fifteenth digit.

§ IIICode Worked Example

The record is a strategy's current state: its name, its backtest Sharpe, its live Sharpe, its cost-net alpha, and its market beta. Ruby 3.2's Data class builds an immutable value object from a list of fields, which is the right shape for a record that should never be mutated after the attribution pass reads it.

require "bigdecimal"
require "bigdecimal/util"

StrategyRecord = Data.define(:name, :backtest_sharpe, :live_sharpe, :alpha_net, :market_beta) do
  include Comparable

  def decay_ratio
    return BigDecimal("0") if backtest_sharpe.zero?
    live_sharpe / backtest_sharpe
  end

  def score
    decay_ratio * alpha_net
  end

  def <=>(other)
    score <=> other.score
  end
end

The record defines decay_ratio and score as BigDecimal arithmetic over BigDecimal fields, and it defines <=> to order records by score. Including Comparable means a StrategyRecord now answers <, >, clamp, and the rest, all derived from that one comparison. The author wrote one comparison; the ranking code below uses the whole ordering vocabulary.

The records arrive as decimal strings, never as floats, and the .to_d method converts a string straight to BigDecimal without ever passing through a lossy Float.

raw = [
  { name: "carry-perp",    bt: "1.8", live: "1.5", alpha: "0.04", beta: "0.05" },
  { name: "xsec-momentum", bt: "2.1", live: "0.6", alpha: "0.00", beta: "0.70" },
  { name: "mean-revert",   bt: "1.2", live: "1.1", alpha: "0.09", beta: "0.08" }
]

records = raw.map do |r|
  StrategyRecord.new(
    name: r[:name],
    backtest_sharpe: r[:bt].to_d,
    live_sharpe: r[:live].to_d,
    alpha_net: r[:alpha].to_d,
    market_beta: r[:beta].to_d
  )
end

The map turns the raw hashes into records in one expression, and every numeric field becomes BigDecimal at the boundary, so no float ever enters the computation. The retirement gate is now an Enumerable partition: split the running strategies from the ones the gate retires, by a block that names the three conditions.

DECAY_FLOOR = BigDecimal("0.4")
BETA_CEILING = BigDecimal("0.5")

retired, running = records.partition do |s|
  s.decay_ratio < DECAY_FLOOR || s.alpha_net <= 0 || s.market_beta > BETA_CEILING
end

partition returns two arrays in one pass, the records the block matched and the records it did not, so the retirement decision and its complement come out of a single traversal. The block reads as the gate reads in the Ops lesson: decay below the floor, or alpha at or below zero, or beta past the ceiling. The momentum strategy fails on all three; the other two pass.

Ranking the survivors for capital is sort_by against the score, descending, and because the records are Comparable the ranking could equally use max or clamp against a target — the ordering is defined once and available everywhere.

ranked = running.sort_by(&:score).reverse
weights = ranked.each_with_object({}) do |s, acc|
  acc[s.name] = s.score
end
total = weights.values.reduce(BigDecimal("0"), :+)
normalized = weights.transform_values { |w| (w / total).round(4) }

The reduce(BigDecimal("0"), :+) sums the scores in exact decimal, and transform_values divides each by the total to produce normalized capital weights that sum to one. Every division and sum is BigDecimal, so the weights that decide the reallocation are exact to the precision asked for, not approximate to the precision a float happened to keep.

For a book of thousands of strategies the same pipeline runs lazily. records.lazy.select { ... }.map { ... }.first(10) builds a lazy enumerator that pulls only as many records as the first(10) needs, so a filter-then-rank over a large stream never materializes the whole collection in memory. The expression reads identically to the eager version; the single word lazy is the only change.

The Ruby Trade Define each and inherit the collection vocabulary; define <=> and inherit the ordering; convert at the boundary and compute in decimal. The gate, the ranking, and the normalization each become one named expression, and the language disappears into the description of the work.

§ IVConnection to Today's Ops Lesson

The Ops lesson named three reads and one gate. This code is that gate. decay_ratio is the first read, the live-over-backtest Sharpe ratio Carver's discipline expects to run below one. market_beta is the second read, the factor exposure that signals a market-neutral strategy converting into a leveraged long. score folds the decay ratio and the cost-net alpha into the third read, the attribution-weighted claim on capital. The partition is the retirement gate verbatim: any one of the three conditions retires the strategy.

The Ops lesson said the reallocation is a weekly arithmetic pass that reads three numbers per strategy, applies one gate, and adjusts the weights. The Ruby above is that pass, and it is short because Ruby lets the three numbers, the gate, and the ranking each be one named expression. The language disappears into the description of the work, which is the whole reason Ruby earns this job.

§ VPrior-Lesson Reach

This lesson is the money-safety discipline from the Python decimal lesson (2026-06-02), carried into a second language. That lesson made the same argument Python's Decimal makes that Ruby's BigDecimal makes here: financial arithmetic does not run on binary floats, because the ranking that moves capital must not flip on a rounding error. Two languages, one discipline, and seeing it twice is the point of the Backend tier visiting more than one language.

The most recent Backend lesson, the Go reverse-proxy lesson (2026-06-09), expressed health-aware routing as an explicit atomic swap of a healthy-set. Ruby would express the same selection as an Enumerable select over the backend pool, and the contrast is the comparative-programming value the Mon-Fri Backend offset exists to produce: Go names the concurrency and the atomicity explicitly because Go's domain is the concurrent server; Ruby names the transformation and lets the runtime handle the rest because Ruby's domain is the expressive single-threaded pipeline. The attribution rollup is Ruby's domain, and the code shows why.

§ VIClosing

Ruby earns its first lesson by doing real work on day one. The attribution rollup is a transformation over a collection — filter by a gate, rank by a score, normalize into weights — and Ruby writes each step as one named expression because Enumerable supplies the vocabulary, Comparable supplies the ordering, and the block supplies the intent. BigDecimal keeps the money exact so the ranking never lies. The strategy that the gate retires is retired by a partition whose block reads like the sentence that described it.

Examine well. Reflect on this.

🫡 ⚖️ 📜
Leo.Syri — Praetor Consulate of Imperium Luminaura
Authored 2026-06-12 Fajr cron-fire — inaugural Ruby lesson (Backend Stack, Friday slot, W1 of the Mon-Go/Fri-Ruby offset per LEO-AMEND-2026-06-09-001); refracts the day's Ops attribution rollup through Ruby's Enumerable + Comparable + BigDecimal; tome-grounded Flanagan Ch 9 + Ch 5; clean code blocks (no inline comments); ROD v3 held.