commodity — Physical Pricing Primitives

The commodity module provides the physical pricing building blocks used throughout the SIP index and OTC stack. All functions are commodity-agnostic — replace spot prices, storage costs, convenience yields, and grade factors with the target cluster’s market data.

Primary use clusters: A (Seasonal Ag), B (Weather-Event), C (Specialty Food), D (Industrial Biomaterials), E (Circular Economy), F (Aquatic/Marine), H (Animal Inputs), L (Emerging Protein), P (Soil Inputs).

import sipQuant as sq
import numpy as np

seasonality()

STL-like additive decomposition of a price series into trend, seasonal, and residual components. Uses a centred moving average for the trend and group means of the detrended series for the seasonal component.

decomp = sq.commodity.seasonality(
    dates=np.arange(104),   # 2 years of weekly data
    values=weekly_prices,
    period=52,              # 52-week annual harvest cycle
    method='stl',
)

trend    = decomp['trend']
seasonal = decomp['seasonal']
residual = decomp['residual']

Parameters

  • dates — array-like, length n. Time index (used for ordering only; can be integers).

  • values — array-like of float, length n.

  • period — int. Seasonal period. Use 52 for weekly data (annual cycle), 12 for monthly, 4 for quarterly.

  • method — str. 'stl' or 'additive' (both use the same additive algorithm).

Returns — dict with keys: trend, seasonal, residual, values, period, method.

Note

The seasonal component is centred so it sums to zero over one period. Edge values (first and last period//2 observations) of trend are filled with the nearest valid moving-average value.

Cluster guide

  • Cluster A (Hay, Straw, Silage): period=52, weekly observations

  • Cluster B (Road salt): period=52, demand spikes in winter months

  • Cluster F (Seaweed, Fish meal): period=52, harvest season dependency

  • Cluster T (Horticultural): period=52 with event-spike overlays (Valentine’s Day, etc.)

convenienceYield()

Extracts the implied convenience yield from the cost-of-carry relationship between a spot price and a futures price. The convenience yield represents the benefit of holding the physical commodity rather than the futures contract.

\[y = r + u - \frac{\ln(F/S)}{T}\]

where \(y\) is the convenience yield, \(r\) is the risk-free rate, \(u\) is storage cost, \(F\) is the futures price, \(S\) is spot, and \(T\) is tenor in years.

cy = sq.commodity.convenienceYield(
    spotPrice=187.50,
    futuresPrice=192.00,
    tenor=0.25,         # 3-month
    r=0.046,
    storageCost=0.02,   # 2% per year
)

print(f"Convenience yield: {cy['convenienceYield']:.4f}")
print(f"Net carry:         {cy['netCarry']:.4f}")

Parameters

  • spotPrice — float. Current spot price S.

  • futuresPrice — float. Observed futures price F.

  • tenor — float. Time to delivery in years.

  • r — float. Risk-free rate (continuous).

  • storageCost — float. Annualised storage cost rate.

Returns — dict with keys: convenienceYield, carryAdjustedForward, netCarry.

basis()

Cash price minus reference (benchmark) price. The basis captures local supply/demand imbalances, transport differentials, and grade differences relative to a benchmark or composite index.

b = sq.commodity.basis(
    cashPrice=185.00,
    referencePrice=188.00,     # SIP-AHI-001 composite
    market='lethbridge_ab',
    grade='premium_bale_14pct_moisture',
)

print(f"Basis:     {b['basis']:.2f} $/tonne")
print(f"Basis bps: {b['basisBps']:.0f}")

Parameters

  • cashPrice — float. Observed local cash price.

  • referencePrice — float. Benchmark/futures/composite price.

  • market — str, optional. Market label stored in output.

  • grade — str, optional. Grade label stored in output.

Returns — dict with keys: basis, basisBps, cashPrice, referencePrice, market, grade.

gradeAdjustment()

Quality-adjusted price computed as base price plus the sum of all grade factor adjustments. Grade factors can be passed as a dict (with descriptive keys) or as a plain list/array of float adjustments.

grade_factors = {
    'moisture_premium_14pct':  +2.50,
    'dockage_discount_2pct':   -1.80,
    'test_weight_premium':     +1.20,
}

adj = sq.commodity.gradeAdjustment(
    basePrice=185.0,
    gradeFactors=grade_factors,
)

print(f"Adjusted price:    {adj['adjustedPrice']:.2f}")
print(f"Total adjustment:  {adj['totalAdjustment']:+.2f}")

Parameters

  • basePrice — float. Unadjusted reference price.

  • gradeFactors — dict or array-like. Dict keys are labels; values (or array elements) are float adjustments in price units.

Returns — dict with keys: adjustedPrice, totalAdjustment, gradeFactors.

Cluster guide

  • Cluster A: moisture %, test weight, dockage, mould

  • Cluster C: certification premium (organic, fair trade, ceremonial)

  • Cluster L: protein %, amino acid profile, GMO status

  • Cluster Q: genetic performance metrics (EPD, EBV)

  • Cluster V: certified vs uncertified seed premium

transportDifferential()

Delivered price at destination by adding logistics costs to the origin price. Transport cost is 30–60% of value in Cluster A markets — this function is critical for basis calculations between origin and destination.

delivered = sq.commodity.transportDifferential(
    originPrice=185.0,
    freightCost=8.50,     # $/tonne truck freight Lethbridge → Calgary
    handlingCost=1.20,    # feedlot receiving fee
    insuranceCost=0.30,
)

print(f"Delivered price:       {delivered['deliveredPrice']:.2f}")
print(f"Total logistics cost:  {delivered['totalLogisticsCost']:.2f}")

Parameters

  • originPrice — float. Price at origin.

  • freightCost — float. Freight/shipping cost per unit.

  • handlingCost — float. Terminal/handling charges per unit.

  • insuranceCost — float. Cargo insurance per unit.

Returns — dict with keys: deliveredPrice, originPrice, totalLogisticsCost, breakdown.

localForwardCurve()

Builds a local forward price curve using the cost-of-carry model adjusted for local basis. This is the primary input to OTC pricing via schema.ForwardCurve.

\[F(t) = (S + \text{basis}) \cdot e^{(r + u - y) \cdot t}\]

where \(y\) is the convenience yield, \(u\) is storage cost, and basis adjustment shifts the spot price to reflect the local market.

tenors = np.array([0.0, 0.25, 0.50, 0.75, 1.0])

fwd = sq.commodity.localForwardCurve(
    spotPrice=187.50,
    tenor=tenors,
    r=0.046,
    convYield=0.028,
    storageCost=0.020,
    basisAdjustment=-2.50,  # local market at -$2.50 to benchmark
)

# Wrap in ForwardCurve schema for OTC pricing
curve = sq.schema.ForwardCurve(
    tenors=fwd['tenors'],
    prices=fwd['forwards'],
    baseDate='2026-03-14',
    market='alberta_hay_premium',
)

Parameters

  • spotPrice — float. Current spot price S.

  • tenor — array-like. Forward tenors in years.

  • r — float. Risk-free rate.

  • convYield — float. Convenience yield (from convenienceYield() or assumed).

  • storageCost — float. Annualised storage cost rate.

  • basisAdjustment — float. Local basis adjustment to spot.

Returns — dict with keys: tenors, forwards, netCarry, impliedConvenienceYield.

rollingRollCost()

Estimates roll costs from the shape of the forward curve at specified roll indices. Used in index methodology to quantify the cost of maintaining a rolling futures exposure.

roll_dates = np.array([0, 1, 2, 3])  # quarterly roll indices

roll = sq.commodity.rollingRollCost(fwd, roll_dates)

print(f"Quarterly roll costs:  {roll['rollCosts']}")
print(f"Annualised roll cost:  {roll['annualizedRollCost']:.4f}")

Parameters

  • forwardCurve — dict. Must contain 'forwards' or 'prices' array and 'tenors'.

  • rollDates — array-like of int. Indices into the forwards array at which rolls occur.

Returns — dict with keys: rollCosts, totalRollCost, annualizedRollCost.