Aperiodic rate of return

The aperiodic rate of return (RoR) measures the return of adding one extra unit of inventory under uncertain demand and uncertain lead time. It comes from Joannes Vermorel’s Introduction to Supply Chain. Instead of choosing an arbitrary horizon (month, quarter, year), the aperiodic RoR slides a cursor along the cash-flow timeline and keeps the best fastest-compounding return. The day that achieves it becomes the option’s implicit horizon.

Table of contents

Economic definition

The baseline RoR in the book is:

$$ \mathtt{RoR}=\frac{\text{revenues}-\text{spending}}{\text{spending}\times\text{period}} $$

The period is expressed in time units (days here). This makes RoR comparable across items with different lead times.

The aperiodic cursor

For each day $t \ge 1$, compute the return as if the position were liquidated at day $t$.

Discounted spending:

$$ S(t)=\sum_{u=0}^{t} \mathbb{E}[\text{Spend}(u)]\cdot DF(u) $$

Discounted sales revenue:

$$ R_{\text{sell}}(t)=\sum_{u=0}^{t} \mathbb{E}[\text{SellRev}(u)]\cdot DF(u) $$

Liquidation value at $t$:

$$ R_{\text{salv}}(t)=\mathbb{E}[\text{InvEnd}(t)]\cdot \text{Salvage}\cdot DF(t) $$

Close-out revenue:

$$ R(t)=R_{\text{sell}}(t)+R_{\text{salv}}(t) $$

Then:

$$ \mathtt{RoR}(t)=\frac{R(t)-S(t)}{S(t)\cdot t} $$

Finally, the aperiodic RoR is:

$$ \mathtt{RoR}^{\star}=\max_{t\in{1..H}} \mathtt{RoR}(t) \quad,\quad t^{\star}=\arg\max_{t} \mathtt{RoR}(t) $$

This “max over cursor positions” is the operational definition used below.

Uncertainty model

Demand uses a minimal ISSM (negative binomial innovations) and lead time is bimodal:

This keeps the model simple while preserving tail risk in lead times.

Reading the output

Each row is a marginal +1 unit for a SKU:

Full Envision script

/// Aperiodic RoR of a +1 inventory unit (no base arrivals)
// Demand: ISSM (negative binomial innovations)
// Lead time: bimodal (perfect mode) + uniform tail (stockout)
// Uses: montecarlo + sample accumulators

present = date(2025, 1, 1)
horizonDays = 60
keep span date = [present .. present + horizonDays]

// Day index and discount factor
Day.t = date - present
dailyDiscount = 0.0005
Day.DF = (1 + dailyDiscount) ^ (-Day.t)

///////////////////////////////////////////////////////////
// Tiny master data: 3 SKUs (kept from previous tuned version)
///////////////////////////////////////////////////////////

table Sku[sku] = with
  [| as sku,   as Buy, as Sell, as Salvage, as Hold, as Init, as MeanDaily, as LtMode, as LtTail, as PPerfect |]
  [| "SKU-A",  18,     23,      8,          0.07,    3,       1.5,         22,       18,       0.75      |]
  [| "SKU-B",  20,     32,      10,         0.10,    6,       2.2,         9,        30,       0.65      |]
  [| "SKU-C",  18,     24,      4,          0.08,    12,      1.0,         12,       40,       0.55      |]

///////////////////////////////////////////////////////////
// Baseline ("theta") per SKU per day for ISSM
///////////////////////////////////////////////////////////

table SkuDay = cross(Sku, Day)
SkuDay.Theta = Sku.MeanDaily * random.uniform(0.8 into SkuDay, 1.2)

///////////////////////////////////////////////////////////
// Monte-Carlo valuation of the marginal unit (+1)
///////////////////////////////////////////////////////////

scenarios = 1000
alpha = 0.30
dispersion = 2.0
minLevel = 0.10

Sku.RorDay, Sku.RorYearLin, Sku.RorYearComp, Sku.BestHorizonDays,
    Sku.TotalSpendPV, Sku.TotalRevPV, Sku.ProfitPV, Sku.SaleProb,
    Sku.ExpSaleDay, Sku.ExpLeadTime = each Sku

  // Slice theta series for current SKU
  Day.Theta = SkuDay.Theta

  montecarlo scenarios with

    // Lead time: bimodal
    lead = if random.binomial(Sku.PPerfect) then
             Sku.LtMode
           else
             Sku.LtMode + random.integer(1, Sku.LtTail)
    lead = if lead > horizonDays then horizonDays else lead
    sample avgLead = avg(lead)

    // ISSM demand trajectory
    level = 1.0
    Day.Demand = each Day scan date
      keep level
      mean = level * Day.Theta
      dev = random.negativeBinomial(mean, dispersion)
      level = alpha * dev / Day.Theta + (1 - alpha) * level
      level = max(minLevel, level)
      return dev

    ///////////////////////////////////////////////////////
    // Inventory simulation (lost sales), marginal unit
    // No base arrivals at lead time.
    ///////////////////////////////////////////////////////

    baseInv = Sku.Init
    extraInv = 0
    Day.SoldExtra = each Day scan date
      keep baseInv
      keep extraInv

      // the +1 unit arrives at 'lead'
      extraInv = extraInv + (if Day.t == lead then 1 else 0)

      dem = Day.Demand
      soldBase = min(baseInv, dem)
      baseInv = baseInv - soldBase
      rem = dem - soldBase
      soldExtra = min(extraInv, rem)
      extraInv = extraInv - soldExtra

      return soldExtra

    // Second pass: remaining extra inventory
    baseInv = Sku.Init
    extraInv = 0
    Day.ExtraInvEnd = each Day scan date
      keep baseInv
      keep extraInv

      extraInv = extraInv + (if Day.t == lead then 1 else 0)

      dem = Day.Demand
      soldBase = min(baseInv, dem)
      baseInv = baseInv - soldBase
      rem = dem - soldBase
      soldExtra = min(extraInv, rem)
      extraInv = extraInv - soldExtra

      return extraInv

    // Cashflows (present-valued later)
    Day.Spend = (if Day.t == 0 then Sku.Buy else 0) + Sku.Hold * Day.ExtraInvEnd
    Day.Rev = Sku.Sell * Day.SoldExtra
            + (if Day.t == horizonDays then Sku.Salvage * Day.ExtraInvEnd else 0)

    // Expected discounted profiles
    sample Day.ExpSpendPV = avg(Day.Spend * Day.DF)
    sample Day.ExpRevPV   = avg(Day.Rev * Day.DF)
    sample Day.ExpSold    = avg(Day.SoldExtra)

  // Scalars from expected profiles
  totalSpendPV = sum(Day.ExpSpendPV)
  totalRevPV   = sum(Day.ExpRevPV)
  profitPV     = totalRevPV - totalSpendPV

  // Aperiodic RoR (safe: no /0)
  Day.CumRevPV = sum(Day.ExpRevPV) scan Day.t
  safeSpendPV  = max(0.000001, totalSpendPV)
  Day.t1       = max(1, Day.t)

  Day.Speed = (Day.CumRevPV / safeSpendPV - 1) / Day.t1
  Day.Speed = Day.Speed - (if Day.t == 0 then 1e9 else 0)

  rorDay  = max(Day.Speed)
  bestH   = argmax(Day.Speed, Day.t)

  // Annualizations
  rorYearLin  = 365 * rorDay
  safePlus    = max(0.000001, 1 + rorDay)
  rorYearComp = safePlus ^ 365 - 1

  // Diagnostics
  pSold = sum(Day.ExpSold)
  expSaleDay = sum(Day.t * Day.ExpSold) / max(0.000001, pSold)

  return (rorDay, rorYearLin, rorYearComp, bestH,
          totalSpendPV, totalRevPV, profitPV,
          pSold, expSaleDay, avgLead)

///////////////////////////////////////////////////////////
// Output (inventory investment table)
///////////////////////////////////////////////////////////

show table "Inventory investment: +1 unit per SKU (aperiodic RoR)" a1f10 with
  Sku.sku
  Sku.ExpLeadTime
  Sku.SaleProb
  Sku.ExpSaleDay
  Sku.TotalSpendPV
  Sku.TotalRevPV
  Sku.ProfitPV
  Sku.BestHorizonDays
  Sku.RorDay
  Sku.RorYearLin
  Sku.RorYearComp
  order by Sku.RorDay desc
User Contributed Notes
0 notes + add a note