private static (double StorageNpv, double CmdtyConsumed, double PeriodPv) StorageValueForDecision( ICmdtyStorage <T> storage, T period, double inventory, double inventoryLoss, double injectWithdrawVolume, double cmdtyPrice, Func <double, double> continuationValueInterpolated, double discountFactorFromCmdtySettlement, Func <Day, double> discountFactors) { double inventoryAfterDecision = inventory + injectWithdrawVolume - inventoryLoss; double continuationFutureNpv = continuationValueInterpolated(inventoryAfterDecision); // TODO use StorageHelper.StorageImmediateNpvForDecision double injectWithdrawNpv = -injectWithdrawVolume * cmdtyPrice * discountFactorFromCmdtySettlement; IReadOnlyList <DomesticCashFlow> inventoryCostCashFlows = storage.CmdtyInventoryCost(period, inventory); double inventoryCostNpv = inventoryCostCashFlows.Sum(cashFlow => cashFlow.Amount * discountFactors(cashFlow.Date)); IReadOnlyList <DomesticCashFlow> decisionCostCashFlows = injectWithdrawVolume > 0.0 ? storage.InjectionCost(period, inventory, injectWithdrawVolume) : storage.WithdrawalCost(period, inventory, -injectWithdrawVolume); double decisionCostNpv = decisionCostCashFlows.Sum(cashFlow => cashFlow.Amount * discountFactors(cashFlow.Date)); double cmdtyUsedForInjectWithdrawVolume = injectWithdrawVolume > 0.0 ? storage.CmdtyVolumeConsumedOnInject(period, inventory, injectWithdrawVolume) : storage.CmdtyVolumeConsumedOnWithdraw(period, inventory, Math.Abs(injectWithdrawVolume)); // Note that calculations assume that decision volumes do NOT include volumes consumed, and that these volumes are purchased in the market double cmdtyUsedForInjectWithdrawNpv = -cmdtyUsedForInjectWithdrawVolume * cmdtyPrice * discountFactorFromCmdtySettlement; double periodPv = injectWithdrawNpv - decisionCostNpv + cmdtyUsedForInjectWithdrawNpv - inventoryCostNpv; double storageNpv = continuationFutureNpv + periodPv; return(StorageNpv : storageNpv, CmdtyConsumed : cmdtyUsedForInjectWithdrawVolume, PeriodPv : periodPv); }
OptimalDecisionAndValue(ICmdtyStorage <T> storage, T period, double inventory, double nextStepInventorySpaceMin, double nextStepInventorySpaceMax, double cmdtyPrice, Func <double, double> continuationValueByInventory, double discountFactorFromCmdtySettlement, Func <Day, double> discountFactors, double numericalTolerance) { InjectWithdrawRange injectWithdrawRange = storage.GetInjectWithdrawRange(period, inventory); double inventoryLoss = storage.CmdtyInventoryPercentLoss(period) * inventory; double[] decisionSet = StorageHelper.CalculateBangBangDecisionSet(injectWithdrawRange, inventory, inventoryLoss, nextStepInventorySpaceMin, nextStepInventorySpaceMax, numericalTolerance); var valuesForDecision = new double[decisionSet.Length]; var cmdtyConsumedForDecision = new double[decisionSet.Length]; var periodPvForDecision = new double[decisionSet.Length]; for (var j = 0; j < decisionSet.Length; j++) { double decisionInjectWithdraw = decisionSet[j]; (valuesForDecision[j], cmdtyConsumedForDecision[j], periodPvForDecision[j]) = StorageValueForDecision(storage, period, inventory, inventoryLoss, decisionInjectWithdraw, cmdtyPrice, continuationValueByInventory, discountFactorFromCmdtySettlement, discountFactors); } (double storageNpv, int indexOfOptimalDecision) = StorageHelper.MaxValueAndIndex(valuesForDecision); return(StorageNpv : storageNpv, OptimalInjectWithdraw : decisionSet[indexOfOptimalDecision], CmdtyConsumedOnAction : cmdtyConsumedForDecision[indexOfOptimalDecision], InventoryLoss : inventoryLoss, PeriodPv : periodPvForDecision[indexOfOptimalDecision]); }
private LsmcValuationParameters(T currentPeriod, double inventory, TimeSeries <T, double> forwardCurve, ICmdtyStorage <T> storage, Func <T, Day> settleDateRule, Func <Day, Day, double> discountFactors, IDoubleStateSpaceGridCalc gridCalc, double numericalTolerance, SimulateSpotPrice regressionSpotSims, SimulateSpotPrice valuationSpotSims, IEnumerable <BasisFunction> basisFunctions, CancellationToken cancellationToken, bool discountDeltas, int extraDecisions, Action <double> onProgressUpdate = null) { CurrentPeriod = currentPeriod; Inventory = inventory; ForwardCurve = forwardCurve; Storage = storage; SettleDateRule = settleDateRule; DiscountFactors = discountFactors; GridCalc = gridCalc; NumericalTolerance = numericalTolerance; RegressionSpotSimsGenerator = () => regressionSpotSims(CurrentPeriod, storage.StartPeriod, storage.EndPeriod, forwardCurve); ValuationSpotSimsGenerator = () => valuationSpotSims(CurrentPeriod, storage.StartPeriod, storage.EndPeriod, forwardCurve); BasisFunctions = basisFunctions.ToArray(); CancellationToken = cancellationToken; DiscountDeltas = discountDeltas; ExtraDecisions = extraDecisions; OnProgressUpdate = onProgressUpdate; }
public static FixedSpacingStateSpaceGridCalc CreateForFixedNumberOfPointsOnGlobalInventoryRange <T>( [NotNull] ICmdtyStorage <T> storage, int numGridPointsOverGlobalInventoryRange) where T : ITimePeriod <T> { if (storage == null) { throw new ArgumentNullException(nameof(storage)); } if (numGridPointsOverGlobalInventoryRange < 3) { throw new ArgumentException($"Parameter {nameof(numGridPointsOverGlobalInventoryRange)} value must be at least 3.", nameof(numGridPointsOverGlobalInventoryRange)); } T[] storagePeriods = storage.StartPeriod.EnumerateTo(storage.EndPeriod).ToArray(); double globalMaxInventory = storagePeriods.Max(storage.MaxInventory); double globalMinInventory = storagePeriods.Min(storage.MinInventory); double gridSpacing = (globalMaxInventory - globalMinInventory) / (numGridPointsOverGlobalInventoryRange - 1); return(new FixedSpacingStateSpaceGridCalc(gridSpacing)); }
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)); }
public static ITreeAddStartingInventory <T> ForStorage([NotNull] ICmdtyStorage <T> storage) { return(new TreeStorageValuation <T>(storage)); }
private TreeStorageValuation([NotNull] ICmdtyStorage <T> storage) { _storage = storage ?? throw new ArgumentNullException(nameof(storage)); }
public static IIntrinsicAddStartingInventory <T> ForStorage([NotNull] ICmdtyStorage <T> storage) { return(new IntrinsicStorageValuation <T>(storage)); }
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))); }