ITreeAddNumericalTolerance <T> ITreeAddInterpolator <T> .WithInterpolatorFactory([NotNull] IInterpolatorFactory interpolatorFactory) { _interpolatorFactory = interpolatorFactory ?? throw new ArgumentNullException(nameof(interpolatorFactory)); return(this); }
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))); }