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:
- with probability
PPerfect:LtMode(on-time mode) - otherwise:
LtMode + U, whereUis uniform on[1..LtTail]
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:
SaleProb: expected unit sold within the horizon.BestHorizonDays: implicit horizon $t^{\star}$ selected by the cursor.RorDay: $\mathtt{RoR}^{\star}$ per day (positive means profitable).ProfitPV: expected discounted profit over the fixed horizon.
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