Skip to content

feat(forecast): fast-resolving commodity price bets — Gate-1 accelerator (#5233)#5243

Merged
koala73 merged 4 commits into
mainfrom
feat/forecast-reengine-fast-commodity-bets
Jul 12, 2026
Merged

feat(forecast): fast-resolving commodity price bets — Gate-1 accelerator (#5233)#5243
koala73 merged 4 commits into
mainfrom
feat/forecast-reengine-fast-commodity-bets

Conversation

@koala73

@koala73 koala73 commented Jul 12, 2026

Copy link
Copy Markdown
Owner

Why

The energy pilot's EIA bets resolve weekly (deadline 2026-07-19), so bet_engine scorecard evidence was a week out. This adds a fast lane on the daily-updating commodities feed — bets resolve within the week (~4-day horizon → 2026-07-16) — so Gate-1 evidence lands sooner, and it proves the engine generalizes to a second feed with a different metric fn.

What

  • _bet-templates-commodities.mjs — bets over market:commodities-bootstrap:v1 for WTI (CL=F), Brent (BZ=F), nat-gas (NG=F), gold (GC=F). Directional (from the daily change), 2.5% threshold, 2-decimal prices, resolvable via the resolver's existing price(symbol==X) fn.
  • shapeResolutionFeed — the commodities feed is {_seed, data:{quotes:[...]}}, and the eval's iterateRecords only descends into array children, so the doubly-nested quotes were unreadable. Now exposed as the array. This also unblocks the pre-existing market commodity-price forecast path (it had the same read problem).
  • seed-forecast-bets.mjs — reads both feeds, emits both families. Commodity bets use the honest thin-history prior (0.4) until their own series accrues (Phase 2's ensemble replaces the probability regardless).

Evidence

  • 8 new tests (templates, the shaper unwrap, and end-to-end YES/NO/pending through the real resolver). 870/870 forecast+bet suites green; api tsc exit 0.
  • Live prod preview — 8 bets total (4 energy @07-19, 4 commodity @07-16), all crisp + resolvable:
    • Will the WTI crude oil price fall to at most 69.62 USD/bbl by 2026-07-16?
    • Will the US natural gas price fall to at most 2.87 USD/MMBtu by 2026-07-16?
    • Will the gold price fall to at most 4010.86 USD/oz by 2026-07-16?

Scope

Shadow lane only (invisible to users; held out of the skill Brier). Short-horizon price bets are ~coin-flip on direction by design — Gate 1 tests resolves + crisp + not-silly, not skill; calibration is Phase 2. A per-symbol commodity base-rate accumulator is a fast-follow (they're at the prior until then).

Refs #5233, #4930

https://claude.ai/code/session_01StNurp4TGC3JLHbTtKJhbp

…or, #5233)

The EIA energy bets resolve weekly (deadline 07-19). This adds a fast lane on
the daily-updating commodities feed so bet_engine evidence lands within the week
(~4-day horizon → 07-16) and proves the engine generalizes to a 2nd feed.

- _bet-templates-commodities.mjs: crude (CL=F/BZ=F), nat-gas (NG=F), gold (GC=F)
  bets over market:commodities-bootstrap:v1. Directional (from the daily change),
  2.5% threshold, 2-dp prices, resolvable via the existing price(symbol==X) fn.
- seed-forecast-resolutions.mjs: shapeResolutionFeed now exposes the nested
  {data:{quotes:[...]}} array — the eval's iterateRecords only descends into
  array children, so the doubly-nested quotes were unreadable. This ALSO
  unblocks the pre-existing market commodity-price forecast resolution path.
- seed-forecast-bets.mjs: reads both feeds, emits both families. Commodity bets
  use the honest thin-history prior (0.4) until their own series accrues.

Prod preview: 8 bets total (4 energy @07-19, 4 commodity @07-16), all crisp +
resolvable, e.g. "Will the WTI crude oil price fall to at most 69.62 USD/bbl by
2026-07-16?". 8 new tests (templates, shaper, end-to-end YES/NO/pending);
870/870 forecast+bet green; api tsc exit 0.

Refs #5233, #4930
Claude-Session: https://claude.ai/code/session_01StNurp4TGC3JLHbTtKJhbp
@strix-security

strix-security Bot commented Jul 12, 2026

Copy link
Copy Markdown

Strix Security Review

No security issues found.

Updated for 518add3.


Reviewed by Strix
Re-run review · Configure security review settings

@vercel

vercel Bot commented Jul 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
worldmonitor Ignored Ignored Preview Jul 12, 2026 1:03pm

Request Review

@greptile-apps

greptile-apps Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a fast-resolving commodity price bet lane to the shadow bet_engine, complementing the existing weekly EIA energy bets with daily-updating WTI, Brent, nat-gas, and gold bets that resolve within 4 days. It also fixes a feed-shape bug in shapeResolutionFeed that was silently blocking resolution for the pre-existing commodity-price forecast path.

  • New _bet-templates-commodities.mjs: four commodity symbols, directional bets derived from the feed's daily change field, 2.5% move threshold, 2-decimal rounding, metricKey compatible with the resolver's existing price() function.
  • shapeResolutionFeed fix: unwraps the doubly-nested {_seed, data:{quotes:[...]}} envelope to the bare quotes array so iterateRecords can reach individual quote objects; without this the price(symbol==X) path would always VOID.
  • seed-forecast-bets.mjs: merges both template families and both feed keys; commodity bets receive the honest thin-history prior (0.4) until a per-symbol accumulator is built in Phase 2.

Confidence Score: 4/5

Shadow-lane only change with no user-facing writes; the core fix is correct and well-tested.

The shapeResolutionFeed fix and template wiring are solid. Two non-blocking issues: stale energy labels in the log path and the silent default of absent/zero change to rising direction, which could skew the Gate-1 evidence pool on days the commodity feed omits the field.

scripts/_bet-templates-commodities.mjs (zero/absent change handling) and the log messages in scripts/seed-forecast-bets.mjs.

Important Files Changed

Filename Overview
scripts/_bet-templates-commodities.mjs New commodity bet template file defining 4 symbols with directional price bets over a 4-day horizon; direction derived from daily change field, defaulting to rising when absent or zero.
scripts/seed-forecast-bets.mjs Adds commodity bet family to the seeder; merges both template families and feed keys; stale energy labels remain in two log lines.
scripts/seed-forecast-resolutions.mjs Adds shapeResolutionFeed branch for the commodities feed, unwrapping the double-nested envelope into the bare quotes array so price(symbol==X) resolution works correctly.
tests/bet-templates-commodities.test.mjs 8 new tests covering template generation, shaper unwrapping, end-to-end YES/NO/pending resolution, and seeder integration; fixture and assertions are accurate.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Cron as Railway Cron
    participant Seeder as seed-forecast-bets.mjs
    participant Redis as Redis (Upstash)
    participant EIATemplate as ENERGY_BET_TEMPLATES
    participant CommodityTemplate as COMMODITY_BET_TEMPLATES
    participant BaseRate as baseRateProbability
    participant Resolver as seed-forecast-resolutions.mjs

    Cron->>Seeder: trigger daily run
    Seeder->>Redis: GET energy:eia-petroleum:v1
    Seeder->>Redis: GET market:commodities-bootstrap:v1
    Seeder->>Redis: GET forecast:bets:eia-series:v1
    Redis-->>Seeder: raw feeds + prior series
    Seeder->>Seeder: unwrapFeeds() strips envelope
    Seeder->>EIATemplate: extractMetric(unwrapped EIA feed)
    EIATemplate-->>Seeder: 4 energy bets
    Seeder->>CommodityTemplate: extractMetric(unwrapped commodities feed)
    CommodityTemplate-->>Seeder: 4 commodity bets
    Seeder->>BaseRate: baseRateProbability(EIA series, resolution)
    BaseRate-->>Seeder: accumulator-backed probability
    Seeder->>BaseRate: baseRateProbability([], resolution)
    BaseRate-->>Seeder: 0.4 thin-history prior
    Seeder->>Redis: LPUSH forecast:bets:history:v1
    Seeder->>Redis: SET forecast:bets:eia-series:v1
    Note over Resolver: Resolution run at/after deadline
    Resolver->>Redis: GET market:commodities-bootstrap:v1
    Resolver->>Resolver: shapeResolutionFeed() unwraps to quotes[]
    Resolver->>Resolver: "price(symbol==CL=F) crossesThreshold?"
    Resolver-->>Redis: YES/NO/VOID
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Cron as Railway Cron
    participant Seeder as seed-forecast-bets.mjs
    participant Redis as Redis (Upstash)
    participant EIATemplate as ENERGY_BET_TEMPLATES
    participant CommodityTemplate as COMMODITY_BET_TEMPLATES
    participant BaseRate as baseRateProbability
    participant Resolver as seed-forecast-resolutions.mjs

    Cron->>Seeder: trigger daily run
    Seeder->>Redis: GET energy:eia-petroleum:v1
    Seeder->>Redis: GET market:commodities-bootstrap:v1
    Seeder->>Redis: GET forecast:bets:eia-series:v1
    Redis-->>Seeder: raw feeds + prior series
    Seeder->>Seeder: unwrapFeeds() strips envelope
    Seeder->>EIATemplate: extractMetric(unwrapped EIA feed)
    EIATemplate-->>Seeder: 4 energy bets
    Seeder->>CommodityTemplate: extractMetric(unwrapped commodities feed)
    CommodityTemplate-->>Seeder: 4 commodity bets
    Seeder->>BaseRate: baseRateProbability(EIA series, resolution)
    BaseRate-->>Seeder: accumulator-backed probability
    Seeder->>BaseRate: baseRateProbability([], resolution)
    BaseRate-->>Seeder: 0.4 thin-history prior
    Seeder->>Redis: LPUSH forecast:bets:history:v1
    Seeder->>Redis: SET forecast:bets:eia-series:v1
    Note over Resolver: Resolution run at/after deadline
    Resolver->>Redis: GET market:commodities-bootstrap:v1
    Resolver->>Resolver: shapeResolutionFeed() unwraps to quotes[]
    Resolver->>Resolver: "price(symbol==CL=F) crossesThreshold?"
    Resolver-->>Redis: YES/NO/VOID
Loading

Comments Outside Diff (1)

  1. scripts/seed-forecast-bets.mjs, line 142-148 (link)

    P2 Stale "energy" labels in log output

    The two log lines still refer to "energy bet(s)" after adding the commodity family. If the EIA feed is absent (so only commodity bets are produced), ops will see "published N shadow energy bet(s)" for what are purely commodity forecasts — and the warning path says "no energy bets generated" even when the real reason is that both feeds were empty. Inaccurate labels make on-call triage harder.

Reviews (1): Last reviewed commit: "feat(forecast): fast-resolving commodity..." | Re-trigger Greptile

Comment on lines +62 to +64
kind: 'hard',
metricKey: `${COMMODITY_FEED}|price(symbol==${symbol})`,
operator: 'crosses',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Zero change always generates a "rising" bet

wantUp = changePct >= 0, so when change is absent from the feed (mapped to 0 by extractMetric) or exactly 0.0 (a flat day), the template always mints a "rise to at least" bet. A missing or zero daily-change field is a plausible feed state — Yahoo Finance futures data occasionally omits change on non-trading sessions — and the resulting bias silently skews the Gate-1 evidence pool toward bullish direction for that day's run. Consider using a null sentinel to skip bet generation when the daily direction is undetermined rather than defaulting to up.

koala73 added 3 commits July 12, 2026 14:20
…d (PR #5243 review)

Greptile: wantUp = changePct >= 0 defaulted an absent or exactly-flat `change`
to a "rise" bet, biasing the shadow pool bullish on non-trading sessions (Yahoo
omits `change` then). Now extractMetric keeps a null sentinel for absent change,
and buildResolutionSpec emits no bet when direction is undetermined (null or 0)
— no silent bullish default. Test: absent/flat change → 0 bets.

9 commodity tests, 871/871 forecast+bet green; api tsc exit 0.

Refs #5233, #5243
Claude-Session: https://claude.ai/code/session_01StNurp4TGC3JLHbTtKJhbp
…eeder logs (PR #5243 review)

P2 (correctness) — stale kept-warm quote could resolve as a deadline price. The
commodities feed uses extendExistingTtl on fetch failure, preserving the OLD
_seed.fetchedAt, but shapeResolutionFeed discarded it — so a quote fetched days
before the deadline scored a false YES/NO (reproduced). Now shapeResolutionFeed
stamps each quote with the envelope fetchedAt as `asOf`, and the eval's
settlement gate (previously value-only) also covers the `price` fn: a quote
dated before the deadline day pends, and VOIDs after the 10d grace. This also
hardens the pre-existing market commodity-price forecast path.

P3 (ops) — the combined seeder logged "shadow energy bet(s)" / "no energy bets"
even on commodity-only runs. Now family-neutral with a per-domain breakdown
(e.g. "published 8 shadow bet(s) [energy:4, market:4]").

Tests: stale-envelope pending + VOID-after-grace regressions, asOf stamping.
873/873 forecast+bet green; api tsc exit 0.

Refs #5233, #5243
Claude-Session: https://claude.ai/code/session_01StNurp4TGC3JLHbTtKJhbp
…5243 review)

P1 (correctness) — a stale post-deadline sample survived feed recovery and was
scored over the fresh quote. samplePendingEntries stamped samples with the CYCLE
time, so a stale kept-warm reading got a post-deadline ts; the next fresh cycle
then (a) skipped sampling (a post-deadline sample already existed) and (b) had
the at-deadline path prefer that stored stale value. Now samples are stamped
with the SOURCE observation time (asOf) via a new extractMetricObservation, so a
stale reading carries a pre-deadline ts, is never selected, and never blocks the
fresh quote. Two-cycle stale→fresh regression added.

P2 #2 — a partial refresh that dropped a symbol VOIDed the bet immediately
(missing record → gate fell through → no_establishable_metric). Now a present
feed with an absent matched record pends through the settlement grace and VOIDs
only if it never returns.

P2 #3 — the seeder could mint a newly dated bet from a stale kept-warm envelope.
A per-feed generation freshness contract (commodities: 5d, tolerates any
weekend/holiday) drops a feed whose _seed.fetchedAt is beyond it, so no bet is
generated from stale prices.

Also: the settlement gate is now scoped to POINT windows (at-deadline) — a
within-horizon spec resolves from the asOf-stamped sample timeline, not the
current feed record, and must not be gated on it (caught by an existing test).

876/876 forecast+bet green; api tsc exit 0.

Refs #5233, #5243
Claude-Session: https://claude.ai/code/session_01StNurp4TGC3JLHbTtKJhbp
@koala73
koala73 merged commit 16c31bc into main Jul 12, 2026
26 checks passed
koala73 added a commit that referenced this pull request Jul 15, 2026
…regression (#5327)

* fix(forecast): present() observation must be 0 (not NaN) on absence — VOID-rate regression

While triaging the 68% resolution VOID rate, found a regression I introduced in
the sampling refactor (#5243 P1): extractMetricObservation returned NaN for a
missing matched record, but present() is a boolean existence check — an absent
subject means the event did NOT occur → 0, not NaN. samplePendingEntries then
stored an error sample instead of a valid 0, so a legitimately-NO within-horizon
present spec (e.g. infra:outages present(country==X) for a quiet country)
stranded with an empty timeline and VOIDed as no_establishable_metric instead of
resolving NO. Now mirrors extractMetricValue's `record ? 1 : 0`.

Tests: extractMetricObservation present-absent → 0 (matches extractMetricValue);
present-within-horizon resolves NO on a sampled absence. 908/908 forecast+bet
green; api tsc exit 0.

Refs #5233, #4930
Claude-Session: https://claude.ai/code/session_01StNurp4TGC3JLHbTtKJhbp

* fix(review): assert present observation timestamp
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant