fix(spreads,indicators): send required params and type the payloads production returns (#112) - #119
Merged
Merged
Conversation
…roduction returns (#112) client.spreads and client.indicators could not send the parameters their routes require (basis, curve-structure, price-context and annotations answer HTTP 400 without them) and were typed against fields no route sends. The /all routes nest their list under a named key, cftc-positioning returns one object, and the core client's latest-price heuristic wrapped the price-context and annotations payloads in a one-element array. - Required selectors are required arguments; blank values, unparseable dates, inverted windows and >20 batch codes raise ValidationError before any request (the routes otherwise serve a default window or an empty 200). - Every method returns a type taken from verbatim production bodies and raises unexpected_response_shape on a malformed 200. - Adds the history, /all, gasoil-crack and annotations/batch routes. - Removes get/historical/all, indicators.get and the never-populated trades route; old type names stay as @deprecated aliases. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ao5paex73xXvuM424Libo
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #112
What was wrong (re-verified 2026-09-13/14)
Checked against production (paid test account), oilpriceapi-api
origin/main(config/routes.rb,V1::SpreadsController,app/services/calculated_metrics/), and python-sdk #146.basis(),curveStructure(),priceContext(),annotations()could not sendpair/commodity/codeMISSING_PARAMETER;basis/historicalandannotations/batchdo the samecrack(),margin(),physicalPremium(),fuelSwitching(),storageAnalytics(),cftcPositioning()could not sendtype,crude,index,commodity,gas,locationSpreadValue.type,components: string[]; margin sendsmargin_usd_bbl, notvalue;PriceContextIndicator.percentileiscontext.percentile_1yall()returnedundefinedspreads/commodities/margins/premiums/locations, notdatacftcPositioning()typed as an array/cftc-positioning/allpriceContext()andannotations()returned a one-element array even with a codeOilPriceAPI#shapeResponseDatawraps anydatawith a top-levelpricein an array (the/v1/prices/latestconvention). Proven by mutation belowcongressionalTrades()DATA_NOT_AVAILABLE, has never returned data (api#8478)Silent-input behavior measured live, which is why validation happens locally:
start_date=banana→ 200 with the default windowstart_dateafterend_date→ 200,count: 0paironbasis/historical, unknowntypeoncrack/historical→ 200,count: 0annotations/batch→total_codes: 21, 20 annotated (already api#8448)Fix
src/resources/calculated-metrics.ts: one place for selector validation (reusesrequireFilterfrom fix(ei): make the six by-* methods send their required filter and return the page (#105) #110), YYYY-MM-DD date and window checks, envelope unwrapping, named-collection unwrapping, andunexpected_response_shapewithrawBody.client.requestgains an internalunshapedoption that skipsshapeResponseData; the metrics resources use it and validate the envelope themselves.tests/fixtures/calculated-metrics/) and cross-checked against the Ruby serializers for conditional keys and nulls.CrackSpread.data_stale?: true(absent = "not flagged", api#8477);CftcPositioningPoint.spec_net_pct_oi: number | null(a fabricated 0 today, api#8476).Observed wire shapes (production, 2026-09-13)
dataspreads/crack{spread_type, crude_benchmark, value, unit, components:{crude, product | gasoline+diesel}, timestamp, changes, data_stale?, stale_warning?}spreads/crack/historical{spread_type, crude_benchmark, period, coverage:{from,to,observations,complete}, data_revised_at, count, data:[{date,value,crude,product?|gasoline?,diesel?}]}spreads/crack/all{crude_benchmark, spreads:[crack]}spreads/gasoil-crack{spread_type, name, value, unit, components:{product,crude}:{code,contract_month,updated_at,price,unit}, conversion, timestamp, updated_at, data_stale?}spreads/basis(pairrequired){pair, spread_name, value, unit, components:{CODE:price}, signal, timestamp, percentile_1y|null, changes, negative_streak_days?, data_stale?}spreads/basis/historical(pairrequired){pair, period, count, data:[{date,value,code_a,code_b}]}spreads/basis/all{spreads:[basis]}spreads/curve-structure(commodityrequired){commodity, display_name, structure, severity, term_slope_pct, spreads:{m1_m3?,m1_m6,m1_m12?}, front_month, back_month_6, curve_points, signal, timestamp}spreads/curve-structure/all{commodities:[curve]}spreads/margin{index, name, margin_usd_bbl, crude_input, product_basket:{name:{yield_pct,price,code}}, signal, percentile_1y|null, changes, timestamp}spreads/margin/historical{index, period, count, data:[{date,margin,crude,revenue}]}spreads/margin/all{margins:[margin]}spreads/physical-premium{commodity, name, premium, premium_pct, unit, components:{spot,futures:{code,price,contract?}}, signal, elevated_streak_days, percentile_1y|null, timestamp, data_stale?}spreads/physical-premium/historical{commodity, period, count, data:[{date,premium,premium_pct,spot,futures}]}spreads/physical-premium/all{premiums:[premium]}indicators/fuel-switching{oil_parity:{ratio_pct,threshold_pct,signal,parity_price,current_gas,headroom_pct}, components:{gas,crude}, energy_equivalent, historical_context:{}|{...}, timestamp}indicators/fuel-switching/historical{gas_benchmark, crude_benchmark, period, count, data:[{date,ratio_pct,above_parity,gas_price,crude_price}]}indicators/price-context(coderequired){code, price, timestamp, context:{anomaly, anomaly_reason?, change_*?, high_52w?, low_52w?, percentile_1y?, percentile_5y?}, related_spreads?}indicators/storage-analytics{location, name, current:{volume_mmbbl,utilization_pct,operational_capacity_mmbbl,data_date,timestamp}, draw_rate, seasonal, anomalies, range_52w, signal, trading_implication}indicators/storage-analytics/all{locations:[storage]}indicators/annotations(coderequired){code, price, timestamp, annotation_count, annotations:[{type,severity,message,...}]}indicators/annotations/batch(codesrequired){annotated:[annotations], total_codes, codes_with_annotations}indicators/cftc-positioning{commodity, name, report_date, positioning:{speculative:{net,long,short,net_pct_of_oi}, commercial:{net}, open_interest}, signal, percentile_1y, week_change, timestamp}indicators/cftc-positioning/historical{commodity, period, count, data:[{date,spec_net,open_interest,spec_net_pct_oi}]}indicators/cftc-positioning/all{commodities:[cftc]}403
PREMIUM_REQUIRED: taken fromV1::SpreadsController#check_analytics_access→render_standard_error, same envelope as the live 400/404 bodies. Unverified live: every key available (test and smoke) holds the entitlement.TDD evidence
Red: new tests against unmodified
origin/mainsourceThe 16 that passed on old code are transport guards (429, timeout, abort) that must keep passing.
Mutation: remove only the
unshapedopt-out(file restored afterwards)
Green
Baseline on
7fd722bwas 812 passed | 1 skipped.npx tsc --noEmit: 0npm run lint: 0npx tsc --noEmit -p tsconfig.typecheck.json: 0npm run build: 0npm run storefront:check: validated 40 Node public surfacesnpm run check:secrets: 0npm run snippets:check: 4 passed, no type errorsNo version bump and no CHANGELOG heading (
release-readiness.test.ts); release-note text is below.Breaking changes (for 2.0.0 notes)
client.spreadsget(type),historical(type, options),all(type). They could not send the parameters the routes require. Use the named methods.basis()→basis(pair: string), required; returnsBasisSpread.curveStructure()→curveStructure(commodity: string), required; returnsCurveStructure.crack()→crack(options?: { type?, crude? }); returnsCrackSpread(spread_type, nottype;componentsis an object of priced legs).margin()→margin(options?: { index? }); returnsRefineryMargin(margin_usd_bbl, novalue).physicalPremium()→physicalPremium(options?: { commodity? }); returnsPhysicalPremium(premium,premium_pct).crackHistorical,crackAll,gasoilCrack,basisHistorical,basisAll,curveStructureAll,marginHistorical,marginAll,physicalPremiumHistorical,physicalPremiumAll.SpreadValueis now a@deprecatedunion ofCrackSpread | BasisSpread | CurveStructure | RefineryMargin | PhysicalPremium.HistoricalSpreadValueis now a@deprecatedunion of the real row types.HistoricalSpreadOptionsis a@deprecatedalias ofMetricsDateRangeOptions;SpreadTypeis@deprecated(no method takes it).client.indicatorsget(type)(untyped, could not send parameters).congressionalTrades()and typeCongressionalTradeIndicator. The route has never returned data (api#8478).priceContext()→priceContext(code: string, options?: { relatedSpreads? }), required; returnsPriceContext(metrics undercontext:percentile_1y,percentile_5y,high_52w...).annotations()→annotations(code: string), required; returns aMarketAnnotationsobject (wasAnnotationIndicator[]).cftcPositioning()→cftcPositioning(options?: { commodity? }); returns oneCftcPositioningobject (was an array). The list iscftcPositioningAll().fuelSwitching()→fuelSwitching(options?: { gas?, crude? }); returnsFuelSwitching(oil_parity.ratio_pct,oil_parity.signal).storageAnalytics()→storageAnalytics(options?: { location? }); returnsStorageAnalytics(current.volume_mmbbl,draw_rate.days_to_depletion).fuelSwitchingHistorical,storageAnalyticsAll,annotationsBatch,cftcPositioningHistorical,cftcPositioningAll, and the constantANNOTATIONS_BATCH_MAX_CODES(20).@deprecatedaliases:FuelSwitchingIndicator=FuelSwitching,PriceContextIndicator=PriceContext,StorageAnalyticsIndicator=StorageAnalytics,AnnotationIndicator=MarketAnnotation(one annotation),CFTCPositioningIndicator=CftcPositioning.IndicatorTypeis@deprecatedand no longer includes"congressional-trades".Behavior (both resources)
ValidationErrorbefore any request for a missing or blank required selector, a present-but-blank optional selector, a date that is not a realYYYY-MM-DD,startDateafterendDate, or anannotationsBatchlist that is empty, has a blank or comma-containing code, or has more than 20 codes.OilPriceAPIErrorwithcode: "unexpected_response_shape"andrawBody, instead of returningundefinedfields.PREMIUM_REQUIREDsurfaces asisEntitlementError(status 403, codePREMIUM_REQUIRED).🤖 Generated with Claude Code
https://claude.ai/code/session_015ao5paex73xXvuM424Libo