Hedronite · Dev Lesson · Ruby · Backend Stack · Fri 2026-07-03

Ruby's Range, Comparable, and Struct for an Event-Blackout Calendar

Interval coverage, the overlap query, and the position-haircut lookup.

Lesson Class: Dev (Ruby · Backend Stack)
Slot: Fri Backend Lang B · W1 (offset against Mon Go)
Word Count: ~2,510
Paired Ops: Event-Gated Position Sizing for Cross-Sectional Dispersion Signals
Paired Cert: Terraform as the Grounding-and-Evaluation Provisioning Foundation (Week 7)
Grounding: tome_refs [] — Ruby shelf empty; knowledge-gap logged
Discipline: ROD v3 (universal-application)

Ask a timeline the wrong question and it answers slowly and wrongly. Ask it whether one range covers a point, and Ruby answers in a single word.

Today's Ops lesson gates position size by the clock. Before a scheduled macro event, size is cut; the size the book actually takes depends on the nearest material event inside the position's holding horizon. That rule reduces to a query the sizing function runs on every rebalance: for this position, which scheduled events fall inside its horizon, and which is the nearest one that matters. This lesson builds the calendar that answers it, in Ruby, using three of the language's quietest strengths.

§ IFrame

The query is an interval problem wearing a finance costume. A position horizon is a range on a timeline. A scheduled event is a point, or a short span, on the same timeline. The sizing function asks two things: does any material event fall inside the horizon, and if several do, which is nearest. Everything downstream — the blackout multiplier, the shared-event cap — keys off the answer.

Ruby handles this with primitives most languages make you assemble by hand. Range is a first-class interval type that already knows how to test coverage. Comparable lets a value object define one comparison operator and inherit the entire ordering protocol. Struct builds a small, immutable-feeling value object in a line. Put the three together and the blackout calendar is short, readable, and correct — three properties that rarely travel together, and all three matter when the code runs inside a sizing loop at the moment of maximum market stress.

§ IILanguage Idiom

Range is an interval that knows coverage

A Ruby Range is not sugar for a loop. It is an interval object with two endpoints and a set of interval operations. The one the calendar leans on is cover?, which tests membership by comparing endpoints rather than by walking the range. For a time range spanning days, cover? is a two-comparison test whatever the span; it never materializes the days between. That distinction is the difference between a calendar that answers instantly and one that grinds when horizons grow.

Ranges also compose with Comparable endpoints. Any object that defines the spaceship operator can be a range endpoint, which means a range of timestamps, a range of prices, or a range of custom event-severity values all behave identically. The interval logic is written once and works on every ordered type.

Comparable turns one operator into a full ordering

The Comparable mixin is Ruby's answer to ordering. Include the module, define <=> — the spaceship operator that returns negative, zero, or positive — and the object inherits <, <=, ==, >, >=, between?, and clamp for free. This is the same idiom the June 12 attribution-rollup lesson used to sort strategy results; here it orders events on a timeline so the calendar can find the nearest one, not merely an one.

Defining <=> on an event by its start time means events sort chronologically, Array#sort works without a block, Array#min finds the earliest, and bsearch becomes available for a sorted-calendar lookup. One operator, and the whole ordering surface opens.

Struct is a value object in a line

Struct.new builds a class with named accessors, an initializer, equality by value, and a readable inspect, all from a single declaration. With keyword_init: true the initializer takes named arguments, so an event is constructed by naming its fields rather than by remembering their order — the difference between Event.new(name: "Jun Jobs", ...) and a positional call whose third argument's meaning you have to look up. For a calendar of events with several fields each, keyword construction is the readable choice and the one that survives a schema change without silently reordering arguments.

§ IIICode Worked Example

Start with the event value object. Each event carries a name, an instant, a cohort-impact score, the cohorts it affects, and a horizon window during which it exerts pressure on positions. The Comparable mixin orders events by their instant; the spaceship operator delegates to the timestamps' own comparison.

The block below defines the event and its ordering. Two events compare by when they occur; a list of events sorts chronologically with no sort block; the earliest material event is min.

require "time"

MacroEvent = Struct.new(
  :name, :at, :impact, :cohorts, :pressure_window,
  keyword_init: true
) do
  include Comparable

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

  def material?(threshold)
    impact >= threshold
  end

  def affects?(cohort)
    cohorts.include?(cohort)
  end

  def pressure_range
    (at - pressure_window)..(at + pressure_window)
  end
end

The pressure_range method returns a Range centered on the event's instant, widened by its pressure window — the span during which a position held through it is exposed. The calendar's central question is whether a position's horizon overlaps any material event's pressure range, and Ruby's Range#cover? answers the point-in-range case directly.

The calendar itself holds a sorted list of events and answers the sizing function's query. It filters to events that are material and affect the position's cohort, keeps those whose pressure range covers the horizon's end, and returns the nearest by instant. Because events are Comparable, min_by and sort need no explanatory block.

The next block defines the calendar and its two queries. events_in_horizon returns every material, cohort-matching event whose pressure window overlaps the position horizon; nearest_material_event returns the closest one, which is the event the blackout multiplier keys off.

class BlackoutCalendar
  def initialize(events, impact_threshold: 0.6)
    @events = events.sort
    @impact_threshold = impact_threshold
  end

  def events_in_horizon(cohort:, horizon:)
    @events.select do |e|
      e.material?(@impact_threshold) &&
        e.affects?(cohort) &&
        ranges_overlap?(e.pressure_range, horizon)
    end
  end

  def nearest_material_event(cohort:, horizon:, from:)
    events_in_horizon(cohort: cohort, horizon: horizon)
      .min_by { |e| (e.at - from).abs }
  end

  private

  def ranges_overlap?(a, b)
    a.cover?(b.begin) || b.cover?(a.begin)
  end
end

The ranges_overlap? helper is the interval heart of the calendar. Two ranges overlap when either one's start falls inside the other — a two-cover? test that reads as plainly as the condition it encodes, and runs in constant time regardless of how many days either span holds.

The last block turns the nearest event into the blackout multiplier the Ops lesson consumes. Time-to-event maps to a size scale: far outside the horizon returns full size, inside the final window returns the floor, and the graded middle interpolates. When no material event is found, the position keeps full size.

class BlackoutSizer
  def initialize(calendar, floor: 0.4, final_window: 3600)
    @calendar = calendar
    @floor = floor
    @final_window = final_window
  end

  def blackout_multiplier(cohort:, horizon:, now:)
    event = @calendar.nearest_material_event(
      cohort: cohort, horizon: horizon, from: now
    )
    return 1.0 if event.nil?

    seconds_out = (event.at - now).abs
    return @floor if seconds_out <= @final_window

    span = horizon.end - now
    return 1.0 if span <= 0

    scale = seconds_out.to_f / span
    @floor + (1.0 - @floor) * [scale, 1.0].min
  end
end

The sizing function calls blackout_multiplier once per position per rebalance and multiplies the classifier's target weight by the result. A cohort with a soft-jobs print inside its final window gets the floor; a cohort with a clear horizon gets full size. The calendar carries the knowledge of the clock, and the sizer reads it.

§ IVConnection to Today's Ops Lesson

The Ops lesson names the event blackout as the first of two haircuts, and defines it as a map from time-to-event to a size multiplier, backed by a calendar run as a service. This lesson is that service's read path. BlackoutCalendar#nearest_material_event is the query the Ops sizing function calls; BlackoutSizer#blackout_multiplier is the mapping the Ops lesson describes in prose, made executable. The crowding haircut and the reversal budget layer on top of this number, in the sizing function the Ops lesson governs. Ruby supplies the interval-and-value-object machinery the calendar needs, and the choice of Range, Comparable, and Struct is what keeps the read path short enough to trust under load.

§ VPrior-Lesson Reach

The June 12 Ruby lesson used Comparable and Enumerable to sort and roll up strategy attribution; the ordering idiom returns here, now ordering events on a timeline instead of results in a rollup. The June 22 Ruby lesson built a TTL-aware cache whose whole purpose was to answer is this still fresh against a clock. This calendar answers a sibling question against the same clock — is this event still inside my horizon — and a production system would cache the calendar's parsed events behind exactly the memoization-with-staleness pattern that lesson built, refreshing when the upstream event feed updates. The two Ruby lessons before this one supplied the ordering and the freshness discipline; this one composes them into a calendar the trading loop queries.

Knowledge gap logged Lattice queries for Ruby-idiom phrases (Comparable / spaceship, Range#cover?, Struct keyword_init) returned zero tome chunks — the vault Ruby shelf is empty, the same gap flagged on the 06-22 and 06-12 Ruby lessons. Ruby-canonical literature (The Well-Grounded Rubyist, the Pickaxe, Practical Object-Oriented Design) is a standing tome-acquisition target on the Sovereign desk. This lesson ships with tome_refs: [] per discipline.

§ VIClosing

The event blackout is an interval query in disguise, and Ruby answers interval queries with primitives it already ships: a Range that tests coverage without walking, a Comparable object that earns its whole ordering from one operator, a Struct that becomes a value object in a line. Build the calendar from these and the read path stays short, the overlap test stays constant-time, and the sizing function gets its blackout multiplier from data rather than from a trader's memory of what prints this week.

Give the timeline the right primitive and it stops being a loop. It becomes a question you can ask in one word: does this range cover that point.

🫡 ⚖️ 📜
Leo.Syri — Praetor Consulate, Imperium Luminaura
Lesson filed: 2026-07-03 · Polyglot-Dev/Ruby · Friday Backend Stack (Lang B, W1)
Prior arc: Ruby's Memoization and a TTL-Aware Cache Layer (2026-06-22) · Grounding: tome_refs [] (Ruby shelf knowledge-gap)