public static TimeSeries <T, InventoryRange> CalculateInventorySpace <T>(ICmdtyStorage <T> storage, double startingInventory, T currentPeriod) where T : ITimePeriod <T> { if (currentPeriod.CompareTo(storage.EndPeriod) > 0) // TODO should condition be >= 0? { throw new ArgumentException("Storage has expired"); // TODO change to return empty TimeSeries? } T startActiveStorage = storage.StartPeriod.CompareTo(currentPeriod) > 0 ? storage.StartPeriod : currentPeriod; int numPeriods = storage.EndPeriod.OffsetFrom(startActiveStorage); // Calculate the inventory space range going forward var forwardCalcMaxInventory = new double[numPeriods]; var forwardCalcMinInventory = new double[numPeriods]; double minInventoryForwardCalc = startingInventory; double maxInventoryForwardCalc = startingInventory; for (int i = 0; i < numPeriods; i++) { T periodLoop = startActiveStorage.Offset(i); T nextPeriod = periodLoop.Offset(1); double inventoryPercentLoss = storage.CmdtyInventoryPercentLoss(periodLoop); double injectWithdrawMin = storage.GetInjectWithdrawRange(periodLoop, minInventoryForwardCalc).MinInjectWithdrawRate; double inventoryLossAtMin = inventoryPercentLoss * minInventoryForwardCalc; double storageMin = storage.MinInventory(nextPeriod); minInventoryForwardCalc = Math.Max(minInventoryForwardCalc - inventoryLossAtMin + injectWithdrawMin, storageMin); forwardCalcMinInventory[i] = minInventoryForwardCalc; double injectWithdrawMax = storage.GetInjectWithdrawRange(periodLoop, maxInventoryForwardCalc).MaxInjectWithdrawRate; double inventoryLossAtMax = inventoryPercentLoss * maxInventoryForwardCalc; double storageMax = storage.MaxInventory(nextPeriod); maxInventoryForwardCalc = Math.Min(maxInventoryForwardCalc - inventoryLossAtMax + injectWithdrawMax, storageMax); forwardCalcMaxInventory[i] = maxInventoryForwardCalc; } // Calculate the inventory space range going backwards var backwardCalcMaxInventory = new double[numPeriods]; var backwardCalcMinInventory = new double[numPeriods]; T periodBackLoop = storage.EndPeriod; backwardCalcMaxInventory[numPeriods - 1] = storage.MustBeEmptyAtEnd ? 0 : storage.MaxInventory(storage.EndPeriod); backwardCalcMinInventory[numPeriods - 1] = storage.MustBeEmptyAtEnd ? 0 : storage.MinInventory(storage.EndPeriod); for (int i = numPeriods - 2; i >= 0; i--) { periodBackLoop = periodBackLoop.Offset(-1); backwardCalcMaxInventory[i] = storage.InventorySpaceUpperBound(periodBackLoop, backwardCalcMinInventory[i + 1], backwardCalcMaxInventory[i + 1]); backwardCalcMinInventory[i] = storage.InventorySpaceLowerBound(periodBackLoop, backwardCalcMinInventory[i + 1], backwardCalcMaxInventory[i + 1]); } // Calculate overall inventory space and check for consistency var inventoryRanges = new InventoryRange[numPeriods]; for (int i = 0; i < numPeriods; i++) { double inventorySpaceMax = Math.Min(forwardCalcMaxInventory[i], backwardCalcMaxInventory[i]); double inventorySpaceMin = Math.Max(forwardCalcMinInventory[i], backwardCalcMinInventory[i]); if (inventorySpaceMin > inventorySpaceMax) { throw new InventoryConstraintsCannotBeFulfilledException(); } inventoryRanges[i] = new InventoryRange(inventorySpaceMin, inventorySpaceMax); } return(new TimeSeries <T, InventoryRange>(startActiveStorage.Offset(1), inventoryRanges)); }
private static IntrinsicStorageValuationResults <T> Calculate(T currentPeriod, double startingInventory, TimeSeries <T, double> forwardCurve, ICmdtyStorage <T> storage, Func <T, Day> settleDateRule, Func <Day, Day, double> discountFactors, Func <ICmdtyStorage <T>, IDoubleStateSpaceGridCalc> gridCalcFactory, IInterpolatorFactory interpolatorFactory, double numericalTolerance) { if (startingInventory < 0) { throw new ArgumentException("Inventory cannot be negative.", nameof(startingInventory)); } if (currentPeriod.CompareTo(storage.EndPeriod) > 0) { return(new IntrinsicStorageValuationResults <T>(0.0, TimeSeries <T, StorageProfile> .Empty)); } if (currentPeriod.Equals(storage.EndPeriod)) { if (storage.MustBeEmptyAtEnd) { if (startingInventory > 0) // TODO allow some tolerance for floating point numerical error? { throw new InventoryConstraintsCannotBeFulfilledException("Storage must be empty at end, but inventory is greater than zero."); } return(new IntrinsicStorageValuationResults <T>(0.0, TimeSeries <T, StorageProfile> .Empty)); } double terminalMinInventory = storage.MinInventory(storage.EndPeriod); double terminalMaxInventory = storage.MaxInventory(storage.EndPeriod); if (startingInventory < terminalMinInventory) { throw new InventoryConstraintsCannotBeFulfilledException("Current inventory is lower than the minimum allowed in the end period."); } if (startingInventory > terminalMaxInventory) { throw new InventoryConstraintsCannotBeFulfilledException("Current inventory is greater than the maximum allowed in the end period."); } double cmdtyPrice = forwardCurve[storage.EndPeriod]; double npv = storage.TerminalStorageNpv(cmdtyPrice, startingInventory); return(new IntrinsicStorageValuationResults <T>(npv, TimeSeries <T, StorageProfile> .Empty)); } TimeSeries <T, InventoryRange> inventorySpace = StorageHelper.CalculateInventorySpace(storage, startingInventory, currentPeriod); // TODO think of method to put in TimeSeries class to perform the validation check below in one line if (forwardCurve.IsEmpty) { throw new ArgumentException("Forward curve cannot be empty.", nameof(forwardCurve)); } if (forwardCurve.Start.CompareTo(inventorySpace.Start) > 0) { throw new ArgumentException("Forward curve starts too late.", nameof(forwardCurve)); } if (forwardCurve.End.CompareTo(inventorySpace.End) < 0) { throw new ArgumentException("Forward curve does not extend until storage end period.", nameof(forwardCurve)); } // Calculate discount factor function Day dayToDiscountTo = currentPeriod.First <Day>(); // TODO IMPORTANT, this needs to change // Memoize the discount factor var discountFactorCache = new Dictionary <Day, double>(); // TODO do this in more elegant way and share with Tree calc double DiscountToCurrentDay(Day cashFlowDate) { if (!discountFactorCache.TryGetValue(cashFlowDate, out double discountFactor)) { discountFactor = discountFactors(dayToDiscountTo, cashFlowDate); discountFactorCache[cashFlowDate] = discountFactor; } return(discountFactor); } // Perform backward induction var storageValueByInventory = new Func <double, double> [inventorySpace.Count]; double cmdtyPriceAtEnd = forwardCurve[storage.EndPeriod]; storageValueByInventory[inventorySpace.Count - 1] = finalInventory => storage.TerminalStorageNpv(cmdtyPriceAtEnd, finalInventory); int backCounter = inventorySpace.Count - 2; IDoubleStateSpaceGridCalc gridCalc = gridCalcFactory(storage); foreach (T periodLoop in inventorySpace.Indices.Reverse().Skip(1)) { (double inventorySpaceMin, double inventorySpaceMax) = inventorySpace[periodLoop]; double[] inventorySpaceGrid = gridCalc.GetGridPoints(inventorySpaceMin, inventorySpaceMax) .ToArray(); var storageValuesGrid = new double[inventorySpaceGrid.Length]; double cmdtyPrice = forwardCurve[periodLoop]; Func <double, double> continuationValueByInventory = storageValueByInventory[backCounter + 1]; Day cmdtySettlementDate = settleDateRule(periodLoop); double discountFactorFromCmdtySettlement = DiscountToCurrentDay(cmdtySettlementDate); (double nextStepInventorySpaceMin, double nextStepInventorySpaceMax) = inventorySpace[periodLoop.Offset(1)]; for (int i = 0; i < inventorySpaceGrid.Length; i++) { double inventory = inventorySpaceGrid[i]; storageValuesGrid[i] = OptimalDecisionAndValue(storage, periodLoop, inventory, nextStepInventorySpaceMin, nextStepInventorySpaceMax, cmdtyPrice, continuationValueByInventory, discountFactorFromCmdtySettlement, DiscountToCurrentDay, numericalTolerance).StorageNpv; } storageValueByInventory[backCounter] = interpolatorFactory.CreateInterpolator(inventorySpaceGrid, storageValuesGrid); backCounter--; } // Loop forward from start inventory choosing optimal decisions int numStorageProfiles = inventorySpace.Count + 1; var storageProfiles = new StorageProfile[numStorageProfiles]; var periods = new T[numStorageProfiles]; double inventoryLoop = startingInventory; T startActiveStorage = inventorySpace.Start.Offset(-1); for (int i = 0; i < numStorageProfiles; i++) { T periodLoop = startActiveStorage.Offset(i); double spotPrice = forwardCurve[periodLoop]; StorageProfile storageProfile; if (periodLoop.Equals(storage.EndPeriod)) { double endPeriodNpv = storage.MustBeEmptyAtEnd ? 0.0 : storage.TerminalStorageNpv(spotPrice, inventoryLoop); storageProfile = new StorageProfile(inventoryLoop, 0.0, 0.0, 0.0, 0.0, endPeriodNpv); } else { Day cmdtySettlementDate = settleDateRule(periodLoop); double discountFactorFromCmdtySettlement = DiscountToCurrentDay(cmdtySettlementDate); Func <double, double> continuationValueByInventory = storageValueByInventory[i]; (double nextStepInventorySpaceMin, double nextStepInventorySpaceMax) = inventorySpace[periodLoop.Offset(1)]; (double _, double optimalInjectWithdraw, double cmdtyConsumedOnAction, double inventoryLoss, double optimalPeriodPv) = OptimalDecisionAndValue(storage, periodLoop, inventoryLoop, nextStepInventorySpaceMin, nextStepInventorySpaceMax, spotPrice, continuationValueByInventory, discountFactorFromCmdtySettlement, DiscountToCurrentDay, numericalTolerance); inventoryLoop += optimalInjectWithdraw - inventoryLoss; double netVolume = -optimalInjectWithdraw - cmdtyConsumedOnAction; storageProfile = new StorageProfile(inventoryLoop, optimalInjectWithdraw, cmdtyConsumedOnAction, inventoryLoss, netVolume, optimalPeriodPv); } storageProfiles[i] = storageProfile; periods[i] = periodLoop; } double storageNpv = storageProfiles.Sum(profile => profile.PeriodPv); return(new IntrinsicStorageValuationResults <T>(storageNpv, new TimeSeries <T, StorageProfile>(periods, storageProfiles))); }