Example #1
0
        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)));
        }