Giter VIP home page Giter VIP logo

cmdty / storage Goto Github PK

View Code? Open in Web Editor NEW
70.0 8.0 28.0 110.08 MB

Multi-Factor Least Squares Monte Carlo energy storage valuation model (Python and .NET).

License: MIT License

C# 82.09% PowerShell 0.08% Shell 0.06% Python 17.77%
commodities quantitative-finance storage csharp oil gas power energy-storage stochastic-optimization dynamic-programming stochastic-dynamic-programming python excel

storage's Introduction

Cmdty Storage

Build Status NuGet PyPI

Multi-Factor Least Squares Monte Carlo energy storage valuation model. Usable from C#, Python and Excel.

Table of Contents

Overview

A collection of models for the valuation and optimisation of commodity storage, either virtual or physical. The models can be used for any commodity, although are most suitable for natural gas storage valuation and optimisation.

Calculations take into account many of the complex features of physical storage including:

  • Inventory dependent injection and withdrawal rates, otherwise known as ratchets. For physical storage it is often the case that maximum withdrawal rates will increase, and injection rates will decrease as the storage inventory increases. For natural gas, this due to the increased pressure within the storage cavern.
  • Time dependent injection and withdrawal rates, including the ability to add outages when no injection or withdrawal is allowed.
  • Forced injection/withdrawal, as can be enforced by regulatory or physical constraints.
  • Commodity consumed on injection/withdrawal, for example where natural gas is consumed by the motors that power injection into storage.
  • Time dependent minimum and maximum inventory, necessary if different notional volumes of a storage facility are leased for different consecutive years.
  • Optional time and inventory dependent loss of commodity in storage. For example this assumption is necessary for electricity storage which isn't 100% efficient.
  • Ability to constrain the storage to be empty at the end of it's life, or specify a value of commodity inventory left in storage.

Models Implemented

Currently the following models are implemented in this repository:

  • Intrinsic valuation, i.e. optimal value assuming the commodity price remains static.
  • One-factor trinomial tree, with seasonal spot volatility.
  • Least-Squares Monte Carlo with a multi-factor price process including the flexibility for callers to provide own price simulations.

Getting Started

Installing C# API

For use from C# install the NuGet package Cmdty.Storage.

PM> Install-Package Cmdty.Storage

Installing Python Package

> pip install cmdty-storage

If running on an OS other than Windows see the section .NET Dependency below.

Installing the Excel Add-In

  • First determine if your installed Excel is 32-bit or 64-bit. A Google search can tell you how to do this.
  • Download the Excel add-in zip file from the releases page for the latest Excel release.
    • If your Excel is 32-bit, download Cmdty.Storage-x86.zip.
    • If your Excel is 64-bit, download Cmdty.Storage-x64.zip.
  • Create a folder on your local drive to hold the add-in files. You might want to create this within a folder specifically to hold Excel add-ins.
  • Unzip the contents of the zip file into the folder created in the previous step. Some issues have been observed during development with anti-malware software flagging the .xll as malicious. A similar warning could occur at this step of installation. This is a known issue with Excel-DNA. Please raise an issue in this repo if this happens.
  • Open Excel and go to the File > Options dialogue box.
  • Open the Add-ins tab on the left. At the bottom there is “Manage:” label next to a drop-down which should be selected to “Excel Add-ins”. Press the Go button next to this. A new dialogue box will open.
  • Press the Browse button which should open a file selector dialogue box in which you should select the Cmdty.Storage xll file which was previously saved to your local disk. Press OK.
  • This should take you back to the previous Add-ins dialogue box where you should press OK.
  • Back in Excel you can confirm that the add-in has been installed by:
    • In the Add-ins tab of the Ribbon there should now be a drop-down menu labelled "Cmdty.Storage".
    • New Excel functions prefixed with "cmdty." should be available. These can be seen in the Excel Insert Function dialogue box, within the Cmdty.Storage category.

.NET Dependency

As Cmdty.Storage is mostly written in C# it requires the .NET runtime to be installed to execute. The dlls are targetting .NET Standard 2.0 which is compatible with .NET Framework versions 4.6.1 upwards. A version of .NET Framework meeting this restriction should be installed on most Windows computers, so nothing extra is required.

If running on a non-Windows OS then the runtime of a cross-platform type of .NET will be required. .NET Standard is compatible with .NET and Mono, with the former being recommended. For the Python package, by default it will try to use .NET, and if this isn't installed it will try Mono. See the Microsoft documentation on installing the .NET runtime on Linux and on macOS.

Using the Python API

Creating the Storage Object

The first step is to create an instance of the class CmdtyStorage which represents the storage facility. This section gives two examples of how to do this. For full details on how to create CmdtyStorage instances see the Jupyter notebook creating_storage_instances.ipynb.

Storage with Constant Parameters

The following code creates a simple storage object with constant constraints.

from cmdty_storage import CmdtyStorage, RatchetInterp
import pandas as pd
storage_simple = CmdtyStorage(
    freq='D',
    storage_start = '2021-04-01',
    storage_end = '2022-04-01',
    injection_cost = 0.01,
    withdrawal_cost = 0.025,
    min_inventory = 0.0,
    max_inventory = 1500.0,
    max_injection_rate = 25.5,
    max_withdrawal_rate = 30.9
)

Storage with Time and Inventory Varying Inject/Withdraw Rates

The second examples creates a storage object with inventory-varying injection and withdrawal rates, commonly known as "ratchets".

storage_with_ratchets = CmdtyStorage(
    freq='D',
    storage_start = '2021-04-01',
    storage_end = '2022-04-01',
    injection_cost = 0.01,
    withdrawal_cost = 0.025,
    ratchets = [
                ('2021-04-01', # For days after 2021-04-01 (inclusive) until 2022-10-01 (exclusive):
                       [
                            (0.0, -150.0, 250.0),    # At min inventory of zero, max withdrawal of 150, max injection 250
                            (2000.0, -200.0, 175.0), # At inventory of 2000, max withdrawal of 200, max injection 175
                            (5000.0, -260.0, 155.0), # At inventory of 5000, max withdrawal of 260, max injection 155
                            (7000.0, -275.0, 132.0), # At max inventory of 7000, max withdrawal of 275, max injection 132
                        ]),
                  ('2022-10-01', # For days after 2022-10-01 (inclusive):
                       [
                            (0.0, -130.0, 260.0),    # At min inventory of zero, max withdrawal of 130, max injection 260
                            (2000.0, -190.0, 190.0), # At inventory of 2000, max withdrawal of 190, max injection 190
                            (5000.0, -230.0, 165.0), # At inventory of 5000, max withdrawal of 230, max injection 165
                            (7000.0, -245.0, 148.0), # At max inventory of 7000, max withdrawal of 245, max injection 148
                        ]),
                 ],
    ratchet_interp = RatchetInterp.LINEAR
)

Storage Optimisation Using LSMC

The following is an example of valuing the storage using LSMC and a three-factor seasonal model of price dynamics. For comprehensive documentation of invoking the LSMC model, using the three-factor price model, a more general multi-factor model, or externally generated price simulations, see the notebook multifactor_storage.ipynb.

from cmdty_storage import three_factor_seasonal_value

# Creating the Inputs
monthly_index = pd.period_range(start='2021-04-25', periods=25, freq='M')
monthly_fwd_prices = [16.61, 15.68, 15.42, 15.31, 15.27, 15.13, 15.96, 17.22, 17.32, 17.66, 
                      17.59, 16.81, 15.36, 14.49, 14.28, 14.25, 14.32, 14.33, 15.30, 16.58, 
                      16.64, 16.79, 16.64, 15.90, 14.63]
fwd_curve = pd.Series(data=monthly_fwd_prices, index=monthly_index).resample('D').fillna('pad')

rates = [0.005, 0.006, 0.0072, 0.0087, 0.0101, 0.0115, 0.0126]
rates_pillars = pd.PeriodIndex(freq='D', data=['2021-04-25', '2021-06-01', '2021-08-01', '2021-12-01', '2022-04-01', 
                                              '2022-12-01', '2023-12-01'])
ir_curve = pd.Series(data=rates, index=rates_pillars).resample('D').asfreq('D').interpolate(method='linear')

def settlement_rule(delivery_date):
    return delivery_date.asfreq('M').asfreq('D', 'end') + 20

# Call the three-factor seasonal model
three_factor_results = three_factor_seasonal_value(
    cmdty_storage = storage_with_ratchets,
    val_date = '2021-04-25',
    inventory = 1500.0,
    fwd_curve = fwd_curve,
    interest_rates = ir_curve,
    settlement_rule = settlement_rule,
    num_sims = 2000,
    seed = 12,
    spot_mean_reversion = 91.0,
    spot_vol = 0.85,
    long_term_vol =  0.30,
    seasonal_vol = 0.19,
    basis_funcs = '1 + x_st + x_sw + x_lt + s + x_st**2 + x_sw**2 + x_lt**2 + s**2 + s * x_st',
    discount_deltas = True
)

# Inspect the NPV results
print('Full NPV:\t{0:,.0f}'.format(three_factor_results.npv))
print('Intrinsic NPV: \t{0:,.0f}'.format(three_factor_results.intrinsic_npv))
print('Extrinsic NPV: \t{0:,.0f}'.format(three_factor_results.extrinsic_npv))

Prints the following.

Full NPV:	78,175
Intrinsic NPV: 	40,976
Extrinsic NPV: 	37,199

Inspecting Valuation Results

The object returned from the calling three_factor_seasonal_value has many properties containing useful information. The code below give examples of a few of these. See the Valuation Results section of multifactor_storage.ipynb for more details.

Plotting the daily Deltas and projected inventory:

%matplotlib inline
ax_deltas = three_factor_results.deltas.plot(title='Daily Deltas vs Projected Inventory', legend=True, label='Delta')
ax_deltas.set_ylabel('Delta')
inventory_projection = three_factor_results.expected_profile['inventory']
ax_inventory = inventory_projection.plot(secondary_y=True, legend=True, ax=ax_deltas, label='Expected Inventory')
h1, l1 = ax_deltas.get_legend_handles_labels()
h2, l2 = ax_inventory.get_legend_handles_labels()
ax_inventory.set_ylabel('Inventory')
ax_deltas.legend(h1+h2, l1+l2, loc=1)

Delta Chart

The trigger_prices property contains information on "trigger prices" which are approximate spot price levels at which the exercise decision changes.

  • The withdraw trigger price is the spot price level, at time of nomination, above which the optimal decision will change to withdraw.
  • The inject trigger price is the spot price level, at time of nomination, below which the optimal decision will change to inject.

Plotting the trigger prices versus the forward curve:

%matplotlib inline
ax_triggers = three_factor_results.trigger_prices['inject_trigger_price'].plot(
    title='Trigger Prices vs Forward Curve', legend=True)
three_factor_results.trigger_prices['withdraw_trigger_price'].plot(legend=True)
fwd_curve['2021-04-25' : '2022-04-01'].plot(legend=True)
ax_triggers.legend(['Inject Trigger Price', 'Withdraw Trigger', 'Forward Curve'])

Trigger Prices Chart

Ancillary Python Classes for Model Covariance and Spot Simulation

The following (currently undocumented) Python classes provide helper functionality around the multi-factor model:

  • MultiFactorModel calculates forward covariances given multi-factor parameters. This can be used to understand the dynamics of the model for various purposes, including calibration.
  • MultiFactorSpotSim simulations the spot prices using the multi-factor model. This can be used to build other Monte Carlo models.

Example Python GUI

An example GUI notebook created using Jupyter Widgets can be found here.

Demo GUI

Workaround for Crashing Python Interpreter

In some environments the valuation calculations have been observed to crash the Python interpretter. This is due to the use of Intel MKL, which itself loads libiomp5md.dll, the OpenMP threading library. The crash occurs during the initialisation of libiomp5md.dll, due to this dll already having been initialised, presumably by Intel MKL usage from NumPy. The below code is a
workaround suggested by mattslezak-shell to fix to fix this by setting the KMP_DUPLICATE_LIB_OK environment variable to true.

import os
os.environ['KMP_DUPLICATE_LIB_OK']='True'

The code should be run at the start of any notebook or program.

Python Version Compatibility

The cmdty-storage package should be compatible with the Python interpreter up to version 3.11.

Limitations on the Python version which the cmdty-storage package can be used are largely driven by the pythonnet package dependency. The latest version depends on pythonnet version 3.0.1, which itself works with Python up to version 3.11. Hence this is also the maximum version with which cmdty-storage works.

Using the C# API

This section introduces how to use the C# API. See /samples/csharp for a solution containing C# code which can directly be compiled and executed.

Creating the Storage Object

In order for storage capacity to be valued, first an instance of the class CmdtyStorage needs to be created. The code samples below shows how the fluent builder API can be used to achieve this. Once the Cmdty.Storage package has been installed, a good way to discover the flexibility in the API is to look at the IntelliSense suggestions in Visual Studio.

Storage with Constant Parameters

The code below shows simple storage facility with constant parameters.

const double constantMaxInjectRate = 5.26;
const double constantMaxWithdrawRate = 14.74;
const double constantMaxInventory = 1100.74;
const double constantMinInventory = 0.0;
const double constantInjectionCost = 0.48;
const double constantWithdrawalCost = 0.74;

CmdtyStorage<Day> storage = CmdtyStorage<Day>.Builder
    .WithActiveTimePeriod(new Day(2019, 9, 1), new Day(2019, 10, 1))
    .WithConstantInjectWithdrawRange(-constantMaxWithdrawRate, constantMaxInjectRate)
    .WithConstantMinInventory(constantMinInventory)
    .WithConstantMaxInventory(constantMaxInventory)
    .WithPerUnitInjectionCost(constantInjectionCost, injectionDate => injectionDate)
    .WithNoCmdtyConsumedOnInject()
    .WithPerUnitWithdrawalCost(constantWithdrawalCost, withdrawalDate => withdrawalDate)
    .WithNoCmdtyConsumedOnWithdraw()
    .WithNoCmdtyInventoryLoss()
    .WithNoInventoryCost()
    .MustBeEmptyAtEnd()
    .Build();

Storage with Time and Inventory Varying Inject/Withdraw Rates

The code below shows how to create a more complicated storage object with injection/withdrawal rates being dependent on time and the inventory level.This is much more respresentative of real physical gas storage capacity.

const double constantInjectionCost = 0.48;
const double constantWithdrawalCost = 0.74;

var injectWithdrawConstraints = new List<InjectWithdrawRangeByInventoryAndPeriod<Day>>
{
    (period: new Day(2019, 9, 1), injectWithdrawRanges: new List<InjectWithdrawRangeByInventory>
    {
        (inventory: 0.0, (minInjectWithdrawRate: -44.85, maxInjectWithdrawRate: 56.8)), // Inventory empty, highest injection rate
        (inventory: 100.0, (minInjectWithdrawRate: -45.01, maxInjectWithdrawRate: 54.5)),
        (inventory: 300.0, (minInjectWithdrawRate: -45.78, maxInjectWithdrawRate: 52.01)),
        (inventory: 600.0, (minInjectWithdrawRate: -46.17, maxInjectWithdrawRate: 51.9)),
        (inventory: 800.0, (minInjectWithdrawRate: -46.99, maxInjectWithdrawRate: 50.8)),
        (inventory: 1000.0, (minInjectWithdrawRate: -47.12, maxInjectWithdrawRate: 50.01)) // Inventory full, highest withdrawal rate
    }),
    (period: new Day(2019, 9, 20), injectWithdrawRanges: new List<InjectWithdrawRangeByInventory>
    {
        (inventory: 0.0, (minInjectWithdrawRate: -31.41, maxInjectWithdrawRate: 48.33)), // Inventory empty, highest injection rate
        (inventory: 100.0, (minInjectWithdrawRate: -31.85, maxInjectWithdrawRate: 43.05)),
        (inventory: 300.0, (minInjectWithdrawRate: -31.68, maxInjectWithdrawRate: 41.22)),
        (inventory: 600.0, (minInjectWithdrawRate: -32.78, maxInjectWithdrawRate: 40.08)),
        (inventory: 800.0, (minInjectWithdrawRate: -33.05, maxInjectWithdrawRate: 39.74)),
        (inventory: 1000.0, (minInjectWithdrawRate: -34.80, maxInjectWithdrawRate: 38.51)) // Inventory full, highest withdrawal rate
    })
};

CmdtyStorage<Day> storage = CmdtyStorage<Day>.Builder
    .WithActiveTimePeriod(new Day(2019, 9, 1), new Day(2019, 10, 1))
    .WithTimeAndInventoryVaryingInjectWithdrawRatesPiecewiseLinear(injectWithdrawConstraints)
    .WithPerUnitInjectionCost(constantInjectionCost, injectionDate => injectionDate)
    .WithNoCmdtyConsumedOnInject()
    .WithPerUnitWithdrawalCost(constantWithdrawalCost, withdrawalDate => withdrawalDate)
    .WithNoCmdtyConsumedOnWithdraw()
    .WithNoCmdtyInventoryLoss()
    .WithNoInventoryCost()
    .MustBeEmptyAtEnd()
    .Build();

Calculating Optimal Storage Value

Calculating the Intrinsic Value

The following example shows how to calculate the intrinsic value of the storage, including the optimal intrinsic inject/withdraw decision profile.

var currentPeriod = new Day(2019, 9, 15);

const double lowerForwardPrice = 56.6;
const double forwardSpread = 87.81;

double higherForwardPrice = lowerForwardPrice + forwardSpread;

var forwardCurveBuilder = new TimeSeries<Day, double>.Builder();

foreach (var day in new Day(2019, 9, 15).EnumerateTo(new Day(2019, 9, 22)))
    forwardCurveBuilder.Add(day, lowerForwardPrice);

foreach (var day in new Day(2019, 9, 23).EnumerateTo(new Day(2019, 10, 1)))
    forwardCurveBuilder.Add(day, higherForwardPrice);

const double startingInventory = 50.0;

IntrinsicStorageValuationResults<Day> valuationResults = IntrinsicStorageValuation<Day>
    .ForStorage(storage)
    .WithStartingInventory(startingInventory)
    .ForCurrentPeriod(currentPeriod)
    .WithForwardCurve(forwardCurveBuilder.Build())
    .WithCmdtySettlementRule(day => day.First<Month>().Offset(1).First<Day>().Offset(5)) // Commodity is settled on the 5th day of the next month
    .WithDiscountFactorFunc((valDate, cfDate) => 1.0) // Assumes no discounting (don't do this in practice)
    .WithFixedGridSpacing(10.0)
    .WithLinearInventorySpaceInterpolation()
    .WithNumericalTolerance(1E-12)
    .Calculate();

Console.WriteLine("Calculated intrinsic storage NPV: " + valuationResults.Npv.ToString("N2"));

When run, the above code prints the following to the console.

Calculated intrinsic storage NPV: 10,827.21

Calculating the Extrinsic Value: Least Squares Monte Carlo with Three-Factor Model

The code sample below shows how to calculate the optimal storage value, including extrinsic option value, using the Least Squares Monte Carlo valuation technique, and a seasonal 3-factor model of price dynamics.

const double constantInjectionCost = 0.48;
const double constantWithdrawalCost = 0.74;

var injectWithdrawConstraints = new List<InjectWithdrawRangeByInventoryAndPeriod<Day>>
            {
                (period: new Day(2019, 9, 1), injectWithdrawRanges: new List<InjectWithdrawRangeByInventory>
                {
                    (inventory: 0.0, (minInjectWithdrawRate: -44.85, maxInjectWithdrawRate: 56.8)), // Inventory empty, highest injection rate
                    (inventory: 100.0, (minInjectWithdrawRate: -45.01, maxInjectWithdrawRate: 54.5)),
                    (inventory: 300.0, (minInjectWithdrawRate: -45.78, maxInjectWithdrawRate: 52.01)),
                    (inventory: 600.0, (minInjectWithdrawRate: -46.17, maxInjectWithdrawRate: 51.9)),
                    (inventory: 800.0, (minInjectWithdrawRate: -46.99, maxInjectWithdrawRate: 50.8)),
                    (inventory: 1000.0, (minInjectWithdrawRate: -47.12, maxInjectWithdrawRate: 50.01)) // Inventory full, highest withdrawal rate
                }),
                (period: new Day(2019, 9, 20), injectWithdrawRanges: new List<InjectWithdrawRangeByInventory>
                {
                    (inventory: 0.0, (minInjectWithdrawRate: -31.41, maxInjectWithdrawRate: 48.33)), // Inventory empty, highest injection rate
                    (inventory: 100.0, (minInjectWithdrawRate: -31.85, maxInjectWithdrawRate: 43.05)),
                    (inventory: 300.0, (minInjectWithdrawRate: -31.68, maxInjectWithdrawRate: 41.22)),
                    (inventory: 600.0, (minInjectWithdrawRate: -32.78, maxInjectWithdrawRate: 40.08)),
                    (inventory: 800.0, (minInjectWithdrawRate: -33.05, maxInjectWithdrawRate: 39.74)),
                    (inventory: 1000.0, (minInjectWithdrawRate: -34.80, maxInjectWithdrawRate: 38.51)) // Inventory full, highest withdrawal rate
                })
            };

var storageCapacityStart = new Day(2019, 9, 1);
var storageCapacityEnd = new Day(2019, 10, 1);

CmdtyStorage<Day> storage = CmdtyStorage<Day>.Builder
    .WithActiveTimePeriod(storageCapacityStart, storageCapacityEnd)
    .WithTimeAndInventoryVaryingInjectWithdrawRatesPiecewiseLinear(injectWithdrawConstraints)
    .WithPerUnitInjectionCost(constantInjectionCost, injectionDate => injectionDate)
    .WithNoCmdtyConsumedOnInject()
    .WithPerUnitWithdrawalCost(constantWithdrawalCost, withdrawalDate => withdrawalDate)
    .WithNoCmdtyConsumedOnWithdraw()
    .WithNoCmdtyInventoryLoss()
    .WithNoInventoryCost()
    .MustBeEmptyAtEnd()
    .Build();

const double lowerForwardPrice = 56.6;
const double forwardSpread = 87.81;

double higherForwardPrice = lowerForwardPrice + forwardSpread;

var forwardCurveBuilder = new TimeSeries<Day, double>.Builder();

foreach (var day in storageCapacityStart.EnumerateTo(new Day(2019, 9, 22)))
    forwardCurveBuilder.Add(day, lowerForwardPrice);

foreach (var day in new Day(2019, 9, 23).EnumerateTo(storageCapacityEnd))
    forwardCurveBuilder.Add(day, higherForwardPrice);


const double flatInterestRate = 0.055;

// 3-Factor Seasonal Model Parameters
const double longTermVol = 0.17;
const double seasonalVol = 0.32;
const double spotFactorVol = 0.7;
const double spotFactorMeanReversionRate = 90.6;

MultiFactorParameters<Day> threeFactorParameters = MultiFactorParameters.For3FactorSeasonal(spotFactorMeanReversionRate, spotFactorVol,
    longTermVol, seasonalVol, storage.StartPeriod, storage.EndPeriod);

const double startingInventory = 50.0;

const int regressMaxDegree = 3;
const int numInventorySpacePoints = 50;
const int numSims = 500;
const int randomSeed = 11;

var valuationParameters = new LsmcValuationParameters<Day>.Builder
    {
        BasisFunctions = BasisFunctionsBuilder.Ones +
                         BasisFunctionsBuilder.AllMarkovFactorAllPositiveIntegerPowersUpTo(regressMaxDegree, 1) + Sim.Spot,
        CurrentPeriod = new Day(2019, 8, 29),
        DiscountFactors = StorageHelper.CreateAct65ContCompDiscounter(flatInterestRate),
        ForwardCurve = forwardCurveBuilder.Build(),
        GridCalc = FixedSpacingStateSpaceGridCalc.CreateForFixedNumberOfPointsOnGlobalInventoryRange(storage, numInventorySpacePoints),
        Inventory = startingInventory,
        Storage = storage,
        SettleDateRule = deliveryDate => Month.FromDateTime(deliveryDate.Start).Offset(1).First<Day>() + 19, // Settlement on 20th of following month (business days ignore for simplicity),
        SimulationDataReturned = SimulationDataReturned.None
    }
    .SimulateWithMultiFactorModelAndMersenneTwister(threeFactorParameters, numSims, randomSeed)
    .Build();

LsmcStorageValuationResults<Day> results = LsmcStorageValuation.WithNoLogger.Calculate(valuationParameters);

Console.WriteLine("Calculated storage NPV: " + results.Npv.ToString("N2"));

The above code prints the following.

Calculated storage NPV: 25,473.10

Calculating the Extrinsic Value: One-Factor Trinomial Tree

The code sample below shows how to calculate the optimal storage value, including extrinsic option value, using a one-factor trinomial tree model.

var currentPeriod = new Day(2019, 9, 15);

const double lowerForwardPrice = 56.6;
const double forwardSpread = 87.81;

double higherForwardPrice = lowerForwardPrice + forwardSpread;

var forwardCurveBuilder = new TimeSeries<Day, double>.Builder();

foreach (var day in new Day(2019, 9, 15).EnumerateTo(new Day(2019, 9, 22)))
    forwardCurveBuilder.Add(day, lowerForwardPrice);

foreach (var day in new Day(2019, 9, 23).EnumerateTo(new Day(2019, 10, 1)))
    forwardCurveBuilder.Add(day, higherForwardPrice);

TimeSeries<Month, Day> cmdtySettlementDates = new TimeSeries<Month, Day>.Builder
    {
        {new Month(2019, 9), new Day(2019, 10, 20) }
    }.Build();

const double interestRate = 0.025;

// Trinomial tree model parameters
const double spotPriceMeanReversion = 5.5;
const double onePeriodTimeStep = 1.0 / 365.0;

TimeSeries<Day, double> spotVolatility = new TimeSeries<Day, double>.Builder
    {
        {new Day(2019, 9, 15),  0.975},
        {new Day(2019, 9, 16),  0.97},
        {new Day(2019, 9, 17),  0.96},
        {new Day(2019, 9, 18),  0.91},
        {new Day(2019, 9, 19),  0.89},
        {new Day(2019, 9, 20),  0.895},
        {new Day(2019, 9, 21),  0.891},
        {new Day(2019, 9, 22),  0.89},
        {new Day(2019, 9, 23),  0.875},
        {new Day(2019, 9, 24),  0.872},
        {new Day(2019, 9, 25),  0.871},
        {new Day(2019, 9, 26),  0.870},
        {new Day(2019, 9, 27),  0.869},
        {new Day(2019, 9, 28),  0.868},
        {new Day(2019, 9, 29),  0.867},
        {new Day(2019, 9, 30),  0.866},
        {new Day(2019, 10, 1),  0.8655}
    }.Build();

const double startingInventory = 50.0;

TreeStorageValuationResults<Day> valuationResults = TreeStorageValuation<Day>
    .ForStorage(storage)
    .WithStartingInventory(startingInventory)
    .ForCurrentPeriod(currentPeriod)
    .WithForwardCurve(forwardCurveBuilder.Build())
    .WithOneFactorTrinomialTree(spotVolatility, spotPriceMeanReversion, onePeriodTimeStep)
    .WithMonthlySettlement(cmdtySettlementDates)
    .WithAct365ContinuouslyCompoundedInterestRate(settleDate => interestRate)
    .WithFixedGridSpacing(10.0)
    .WithLinearInventorySpaceInterpolation()
    .WithNumericalTolerance(1E-12)
    .Calculate();

Console.WriteLine("Calculated storage NPV: " + valuationResults.NetPresentValue.ToString("N2"));

The above code prints the following.

Calculated storage NPV: 24,799.09

Using the Excel Add-In

Add Excel functions included in the add-in have a "cmdty." prefix to their names. All functions can also be seen under the category "Cmdty.Storage" in the Excel function Wizard/

The folder /samples/excel/ of this repo contains some sample spreadsheets which can be used to learn the the functionality.

Excel Calculation Modes

There are two modes under which the Monte Carlo valuation functions (currently just cmdty.StorageValueThreeFactor) run: async and blocking. The argument Calc_mode of these functions control which mode is used for execution.

Blocking Calculation Mode

Blocking mode results in the valuation functions behaving like typical Excel functions, with all other operations in Excel freezing until the functions return. If calculating functions with blocking mode then the Excel application calculation mode should be set to manual, otherwise each time any input is changed the storagevaluation calculations will be kicked off, freezing Excel.

Async Calculation Mode

Under async mode the Excel function returns immediately upon calculation and the cell is set to Pending status. When the valuation is started, using one of the methods described below, the calculation is performed in the background with no freezing of the Excel UI during calculation. Excel-DNA RTD functionality is used to notify Excel when calculations are complete with the resulting figures pushed to the UI.

Generally async mode gives a more slick user experience with the following additional characteristics:

  • Progress updates via the cmdty.SubscribeProgress function. Progress bars in Excel cells can be achieved using conditional formatting data bars.
  • Cancellation of storage calculations before they have completed.
  • Parallel valuation of multiple storage facilities.

It is recommended to have the Excel application calculation mode set to Automatic if using async mode, otherwise you will have to keep press F9 (calculate) to check progress and whether the completed status.

Launching and Cancelling Calculations in Async Mode

Buttons in the Cmdty.Storage Excel ribbon can be used to calculate all pending, cancel all running, and reset all cancelled calculations in the current Excel instance. Excel Ribbon

Similarly Excel context menu items can be used to perform the same operations, but specifically on the selected cells. Excel Context Menu

The Excel functions cmdty.StartPending, cmdty.CancelRunning and cmdty.ResetCancelled can be used to perform the same operations. These were included with the intention to be called from VBA. COM wrapped C# classes, the conventional approach to provide VBA-callable code, was not used due to complications experienced during development.

Example Spreadsheet

An example of async calculation mode can be see in /samples/excel/three_factor_storage.xlsm .

Calibration

See this page for some ideas about calibration. Example Python code for calibrating the 3-factor seasonal model will be added to this repo later.

Building

This section describes how to run a scripted build on a cloned repo. Visual Studio 2022 is used for development, and can also be used to build the C# and run unit tests on the C# and Python APIs. However, the scripted build process also creates packages (NuGet and Python), builds the C# samples, and verifies the C# interactive documentation. Cake is used for running scripted builds. The ability to run a full scripted build on non-Windows is planned, but at the moment it can only be done on Windows.

Building on Windows

Build Prerequisites

The following are required on the host machine in order for the build to run.

  • The .NET Core SDK. Check the global.json file for the version necessary, taking into account the matching rules used.
  • The Python interpretter, accessible by being in a file location in the PATH environment variable. See Python Version Compatibility for details of which Python version should work.
  • The following Python packages installed:
    • virtualenv.
    • setuptools.
    • wheel.

Running the Build

The build is started by running the PowerShell script build.ps1 from a PowerShell console, ISE, or the Visual Studio Package Manager Console.

PM> .\build.ps1

Build Artifacts

The following results of the build will be saved into the artifacts directory (which itelf will be created in the top directory of the repo).

  • The NuGet package: Cmdty.Storage.[version].nupkg
  • The Python package files:
    • cmdty_storage-[version]-py3-none-any.whl
  • 32-bit and 64-bit versions of the Excel add-in:
    • Cmdty.Storage-x86.zip
    • Cmdty.Storage-x64.zip

Building on Linux or macOS

At the moment only building, testing and packaging the .NET components is possible on a non-Windows OS.

Build Prerequisites

The following are required on the host machine in order for the build to run.

Running the Build

Run the following commands in a cloned repo

> dotnet build src/Cmdty.Storage/ -c Release
> dotnet test tests/Cmdty.Storage.Test/ -c Release
> dotnet pack src/Cmdty.Storage -o artifacts -c Release --no-build

Build Artifacts

The following results of the build will be saved into the artifacts directory (which itelf will be created in the top directory of the repo).

  • The NuGet package: Cmdty.Storage.[version].nupkg

Why the Strange Tech Stack?

Users of the Python API might be perplexed as to the technology used: Python calling into .NET, which itself calls into native code for the Intel MKL numerical routines. This is certainly not a common structure, especially for a package focussed on complex numerical calculations. Where the Python runtime speed is an issue, as is suspected with this project, it is more usual to have a structure where Python calls into native code using ctypes, or makes use of a Numba.

However, the Cmdty project started off as a .NET only project, written in C#, due to the author being mainly a C# guy during the day-job. The Python wrapper was added later as it became apparent that there was a demand to use the models from Python. Since then it now seems that there are many more users of the Python API than the C# NuGet package, resulting in significant time being spent on the Python API, and examples.

If the project was started again from scratch, potentially it would have been written entirely in Python utilising Numba. However, due to time constraints, and not wanting to have the maintenance headache of having two independent implementations side-by-side there is no plan to work on this. That said, if others want to have a go at a pure Python implementation it would be very much welcomed and I would happily help out.

Despite the oddity in the structure it seems to work quite well with the performance of the LSMC model being decent. Although compiled C# usually does not run as quickly as native code, it's performance isn't bad at all, and the majority of the running time is spent during the QR decomposition for the regression which is itself done using Intel MKL, which does these calculations pretty much as quickly as you can get. The only real annoyances with the structure is:

  • pythonnet not currently supporting .NET Core. A fix for this is currently in the pipeline for the next pythonnet release. At current this means that Mono needs to be installed in order to use the Python API on Linux.
  • The PyPI package size.
  • If a version of the curves package is installed which has a .NET dependency with a different version to a dependency of the cmdty-storage package this can cause strange errors.

Debugging C# Code From a Jupyter Notebook

This section contains the procedure to follow in order to debug the calculations in the C# code, as invoked from Python running in a Jupyter notebook. The following steps are a prerequisite to the procedures described below.

  • Install the following software for building the C#:
    • Visual Studio 2022.
    • The .NET Core SDK version, as described in the section Build Prerequisites.
  • In Visual Studio uncheck the box for the debugging option "Enable Just My Code" in Tools > Options > Debugging > General.
  • Clone the storage repo onto your machine.

The below descriptions have been used from a Windows desktop. As Visual Studio is available for Apple computers a similar procedure might work with Apple hardware, but has never been tested.

Debugging a Released PyPI Package

This section describes how to debug the execution of the cmdty-storage package installed from PyPI.

  • Do a git checkout to the git tag associated with the version of the cmdty-storage package you are running in Jupyter. The git tags for each release are found on GitHub here.
  • In the cloned repo open Cmdty.Storage.sln in Visual Studio and build in Debug configuration.
  • Set breakpoints in the C# code. To investigate the Least Squares Monte Carlo valuation, a good place for a breakpoin is at the start of the Calculate method of the class LsmcStorageValuation, as found in LsmcStorageValuation.cs.
  • It is likely that there are many running processes for the python.exe interpretter. It is necessary to identify the PID (Process ID) of the exact python.exe process which is being used by Jupyter. One way to do this uses Sysinternals Process Explorer:
    • Launch Process Explorer and ensure PID is one of the displayed columns.
    • Order the displayed processes by process name and locate the section which contains the python.exe processes.
    • Run the Jupyter notebook and observe the specific python.exe process for which the CPU usage increases, taking a note of the PID. In the image below the PID is found to be 33568. Identifying PID
  • In the Visual Studio menu bar select Debug > Attach to Process. In the resulting dialogue box search the processes using the noted PID. Select this process and press the Attach button.
  • Execute the Jupyter notebook. The C# code should break at the placed breakpoints.

Debugging Code With Custom Modifications

This section describes the more advanced scenario of running and debugging Cmdty.Storage code which has been modified, and so is different to that used to created released PyPI packages. The process of debugging the C# code with custom modifications is identical to that described above, except that a pip local project install is required. This should be done in the Anaconda Prompt using the path of the directory src\Cmdty.Storage.Python\ within the cloned cmdty-storage repo as the path in the pip install command.

Get in Touch and/or Give a Star

It's always motivating to hear how the model is being used, especially by practitioners in the energy trading sector. Much progress on the LSMC model would not have been possible without the input and motivation provided by collaborators from industry. So don't hesitate to get in touch to discuss storage modelling or suggest future enhancements. Also, show your appreciation by giving this repo a star!

License

This project is licensed under the MIT License - see the LICENSE file for details.

storage's People

Contributors

cmdty avatar dependabot[bot] avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

storage's Issues

Error with a min_inventory value different from 0

Hi,

I do not manage to run the model since I change the value of the min_inventory... it only works with a value equal to 0.

It throws the following error :

<InventoryConstraintsCannotBeFulfilledException: Valuation calculation cannot be performed as storage inventory constraints cannot be fulfilled.
à Cmdty.Storage.StorageHelper.CalculateInventorySpace[T](ICmdtyStorage1 storage, Double startingInventory, T currentPeriod) à Cmdty.Storage.IntrinsicStorageValuation1.Calculate(T currentPeriod, Double startingInventory, TimeSeries2 forwardCurve, ICmdtyStorage1 storage, Func2 settleDateRule, Func3 discountFactors, Func`2 gridCalcFactory, IInterpolatorFactory interpolatorFactory, Double numericalTolerance)>

You can reproduce the error by using the given python example in the repository https://github.com/cmdty/storage#storage-with-time-and-inventory-varying-injectwithdraw-rates and changing the min_inventory value

More Vectorisations with MKL

A decent performance improvement could be achieve by more vectorisation of calculations of arrays of numbers, e.g. add decision volume to inventory to get next step inventory over the vector of per-simulation inventories.

Should be done after #28 to avoid a divergence in performance over different OS.

Fix Warnings on Creating Python Package

Warnings seen when running the Pack-Python cake build step with setuptools version 65.5.0, but not version 56.0.0.

The first warning related to the use of the package_data parameter to setuptools.setup and is fixed in the branch put_package_data_into_packages.

Another warning seen, but yet to be investigated is the following:
SetuptoolsDeprecationWarning: setup.py install is deprecated. Use build and pip and other standards-based tools.

This sort of issue should be helped (made more predictable) when #35 is implemented by fixing the version of all dependency packages.

Fix C# Coverage

Coverlet stopped working a while ago, I think after updating .NET SDK version.

Separate Nomination Dates From Delivery Dates

Currently the model assumes that injection/withdrawal nomination occurs in the same time period as delivery. In reality virtual storage deals could enforce nomination to be on the previous business day, for example the weekend volume is nominated on Friday.

The storage object needs to be enriched to encapsulate the set of nomination dates, plus a mapping to the delivery dates each nomination date corresponds to. The valuation code will also need to be updated to simulate not just the spot price, but the prompt forward prices for nomination delivery dates as of the nomination date.

Improve Build Process on Non-Windows Platforms

Currently the full build can only be executed on Windows machines. This is due to two reasons:

  • The dependence on the pythonnet Python package which doesn't support calling .NET assemblies running on .NET Core from Python. This should be fixed in version 2.5.0 of pythonnet.
  • The project Cmdty.Storage.Excel having a dependence on Excel-DNA, which itself targets the full .NET Framework.

The solution to the first reason is to just wait for version 2.5.0 of pythonnet. For the second reason, although there has been some discussion on GitHub for a .NET Core compatible version of Excel-DNA, it doesn't appear to be coming any time soon. So probably the solution to this is to create a second "non-Windows friendly" .sln file which doesn't include the Excel project. Some changes to build.cake would also probably be necessary.

Fix for libiomp5md.dll Crash Issue

As highlighted in issue 13 , multiple loading of libiomp5md.dll can cause the Python interpreter to crash. The workaround suggested in the same issue, being added to the Cmdty.Storage documentation, has been seen to fix this but according to the error message printed when the crash occurs, this workaround is 'unsafe, unsupported, undocumented' and 'may cause crashes or silently produce incorrect results'. As such a proper long-term solution should be devised.

Create .NET Console App

A console app which accepts a file on disk as the inputs and writes the result to another file on disk. The Python API is updated to be a wrapper around this console app. Different platform specific self-contained console apps would be created containing all .NET dependencies.

This design has the following benefits:

  • Remove pythonnet dependency.
  • No need to install .NET runtime.
  • Issues users have with unexpected results can be easily debugged. They just need to send the input file, the console app version and the platform for any calculation to be replay-able. This is the biggest benefit.
  • Similarly applications which call the storage model can store an archive of input files to understand what exactly has been calculated.
  • Calling applications can easily switch between storage model versions.
  • Makes it easier to set up in a HPC environment.

Issue installing Cmdty-storage Python (spyder)

HI Team,

i'm having this issue when i try to install python package , i have tried to upgrade pip / wheel / setuptools as well
however still getting the following error message.

Collecting cmdty_storage Using cached https://files.pythonhosted.org/packages/cc/1e/da3d37c8afb036ef121514515006a32c2d9cf6c35ff4d44f674c8447f220/cmdty_storage-0.1.0a7-py3-none-any.whl Requirement already satisfied: pandas>=0.24.2 in /usr/local/lib/python3.6/dist-packages (from cmdty_storage) (1.1.5) Collecting pythonnet==2.4.0 Using cached https://files.pythonhosted.org/packages/4a/0a/5964a5a1fdf207fab8e718edd1f8d3578e0681577fe9abe0d9006f9718c2/pythonnet-2.4.0.tar.gz Requirement already satisfied: pytz>=2017.2 in /usr/local/lib/python3.6/dist-packages (from pandas>=0.24.2->cmdty_storage) (2018.9) Requirement already satisfied: python-dateutil>=2.7.3 in /usr/local/lib/python3.6/dist-packages (from pandas>=0.24.2->cmdty_storage) (2.8.1) Requirement already satisfied: numpy>=1.15.4 in /usr/local/lib/python3.6/dist-packages (from pandas>=0.24.2->cmdty_storage) (1.19.5) Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.6/dist-packages (from python-dateutil>=2.7.3->pandas>=0.24.2->cmdty_storage) (1.15.0) Building wheels for collected packages: pythonnet Building wheel for pythonnet (setup.py) ... error ERROR: Failed building wheel for pythonnet Running setup.py clean for pythonnet Failed to build pythonnet Installing collected packages: pythonnet, cmdty-storage Running setup.py install for pythonnet ... error ERROR: Command errored out with exit status 1: /usr/bin/python3 -u -c 'import sys, setuptools, tokenize; sys.argv[0] = '"'"'/tmp/pip-install-r07633fo/pythonnet/setup.py'"'"'; __file__='"'"'/tmp/pip-install-r07633fo/pythonnet/setup.py'"'"';f=getattr(tokenize, '"'"'open'"'"', open)(__file__);code=f.read().replace('"'"'\r\n'"'"', '"'"'\n'"'"');f.close();exec(compile(code, __file__, '"'"'exec'"'"'))' install --record /tmp/pip-record-80vpah_w/install-record.txt --single-version-externally-managed --compile Check the logs for full command output.

Thank you for your help

Mean reversion for three-factor seasonal model

Hello,
I would like to understand how to calculate the mean reversion as I dont understand the figure you use in your exemple (mean reversion = 91)
When I used the Ornstein-Uhlenbeck for my historical spot prices, I obtained alfa = 0.017 and average = 35.76.

What does this 91 represent in your example? How do you calculate it ?

Many thanks

Move Delta Calculation out of Core Valuation Code

Delta is calculated using pathwise differentiation alongside the core Least Squares Monte Carlo valuation calculations in LsmcStorageValuation.cs. These calculations makes an assumption that simulated spot price is calculated as forward prices times some stochastic term. This is fine for the multifactor model in Cmdty.Core, but will not be the case for all models, e.g. a shifted lognormal model to account for negative prices. As such, the delta calculation code should be moved out of the core C# valuation class. Such a pathwise differentiation could be calculated by callers use the simulated data.

This limitation should be considered by anyone calling the Python value_from_sims function.

Example results in a kernel crash due to multiple instances of the OpenMP library libiomp5md.dll loaded

The easy fix for this is just a simple:

import os
os.environ['KMP_DUPLICATE_LIB_OK']='True'

Anyhow, on an Anaconda Python installation you get a crash from multiple OpenMP DLLs being loaded in 1 session. It seems the C# code is calling the libraries and Python doesn't like multiple copies of the same DLL. Not a huge deal when you know the "fix" I posted above. This results from just running the examples in the Readme.md for natural gas storage with ratchets.

Negative Prices

Hi,

I am trying to use the 3 factor model to simulate random hourly electricity price cuves based on a volatility curve and a forward curve. However my forward curve has négative prices…which is not allowed in the model.
Do you know how I can deal with this issue?

thanks

issues with extrinsic expected profile

Hi, I have some questions regarding the expected_profile that is returned from the extrinsic valuation methods. Most of my code uses the value_from_sims method as I have written my own implementation of the multifactor price process simulator, but I also see this issue when I use your three_factor_seasonal_value method that performs price simulations for example.

Basically I am trying to better understand where the model's extrinsic NPV actually comes from in a more concrete sense. I have been looking at the "period_pv" column / "net_volume" column to try and derive an understanding of the price that the model is using to assign a PV to the expectation of the volume decision at the point in time. This has lead to some very interesting observations, where I see cases that the model is buying 20000 units of a gas in a month at a negative price for a profit, or selling a small amount of gas at a very high price, say 100 units of gas at $100/unit for $10k PV.

These cases occur in situations where my distribution of simulated prices does not include negative prices, or prices even above $14 for example. I have tried playing with a lot of the knobs to see if I can affect this issue in a meaningful way, but don't believe that I have yet. Perhaps I am just misunderstanding the interpretability of the values in the expected_profile. I can provide more context if necessary.

Any thoughts?

Example Python code for calibrating the 3-factor seasonal model will be added to this repo later.

Please provide the Python code mentioned in the documentation to calibrate the 3-factor seasonal model. An example script which takes the historical prices as input and calculates: spot_mean_reversion, spot_vol, long_term_vol, seasonal_vol. That would make this project much more user friendly.

I can organize a validation of your model if you wish, and report to you the results.

Thanks for all the modeling! Great project!

Allow Excel Add-in Two Calculation Modes

The current Excel add-in function asynchronous calculations doesn't work as well as hoped for because Excel DNA RTD updates do not update when Excel calculations model is manual. This means that:

  • If Excel calculation model mode is manual, the users has to keep on pressing F9 (calculating) to see progress % updates and see if the calculation has finished.
  • If Excel calculation mode is automatic, progress % updates and calculation completion work as hoped for, except that the full storage calculation gets kicked off every single time that users changes any of the inputs. This is undesirable as usually users would want to change many inputs and then kick off the calculations.

The proposes solution is for the Cmdty.Storage add-in to have two calculation modes itself, controlled via a drop-down in the Excel Ribbon:

  • Async calculation mode, would be the default if Excel calculation mode is automatic. Under this model Cmdty.Storage calculations would be started by pressing a custom calculation button in the Cmdty.Storage Ribbon. Multiple storage valuations could be calculated in parallel and progress % updates should work.
  • Blocking calculation model, would be the default if Excel calculation mode is manual. Storage valuation calculations in Excel would block until complete, multiple valuations could not run concurrently (due to Excel DNA/RTD restriction), and no progress % update would be possible.

Use SVD for Regression in LSMC

Currently regression uses QR decomposition is used for regression in LSMC valuation. Previous experiments showed that the performance of using SVD was much worse than QR, especially as the number of simulations increased. However, some more recent reading implies that SVD performance for regression, relative to QR, is not as bad a observed. I suspect this is because the previous code using SVD had some inefficiencies. If similar performance using SVD can be achieved, it is preferable to QR due to being more robust to collinear regressors.

Problems specifying complicated ratchet constraints

Hi! I'm trying to test the model (in python) with the below constraints:

InjectWithdrawByInventoryAndPeriod(date(2020, 8, 28),
                                   [
                                       InjectWithdrawByInventory(0.0, -8300, 7900),
                                       InjectWithdrawByInventory(0.1505 * 1e6, -10300, 7900),
                                       InjectWithdrawByInventory(0.237 * 1e6, -10300, 6700),
                                       InjectWithdrawByInventory(0.305 * 1e6, -12300, 6700),
                                       InjectWithdrawByInventory(0.428 * 1e6, -14300, 6700),
                                       InjectWithdrawByInventory(0.438 * 1e6, -14300, 5500),
                                       InjectWithdrawByInventory(0.77 * 1e6, -14300, 4000),
                                       InjectWithdrawByInventory(0.97 * 1e6, -14300, 2500),
                                       InjectWithdrawByInventory(1.0 * 1e6, -14300, 2500),
                                   ])

Yes, the facility I have to value has kind of ridiculous ratchet constraints. That said, upon plugging these constraints into your test code, I got the following stacktrace:

Traceback (most recent call last):
  File "value_bistineau.py", line 76, in <module>
    interest_rates=interest_rate_curve, num_inventory_grid_points=1000)
  File "C:\miniconda\envs\cmdty-storage\lib\site-packages\cmdty_storage\intrinsic.py", line 74, in intrinsic_value
    net_val_results = net_cs.IIntrinsicCalculate[time_period_type](intrinsic_calc).Calculate()
System.IndexOutOfRangeException: Index was outside the bounds of the array.
   at Cmdty.TimeSeries.TimeSeries`2.get_Item(TIndex index)
   at Cmdty.Storage.CmdtyStorageBuilderExtensions.<>c__DisplayClass5_0`1.<AddInjectWithdrawRanges>g__GetMinInventory|1(T period)
   at Cmdty.Storage.CmdtyStorage`1.GetInjectWithdrawRange(T date, Double inventory)
   at Cmdty.Storage.StorageHelper.CalculateInventorySpace[T](ICmdtyStorage`1 storage, Double startingInventory, T currentPeriod)
   at Cmdty.Storage.IntrinsicStorageValuation`1.Calculate(T currentPeriod, Double startingInventory, TimeSeries`2 forwardCurve, ICmdtyStorage`1 storage, Func`2 settleDateRule, Func`3 discountFactors, Func`2 gridCalcFactory, IInterpolatorFactory interpolatorFactory, Double numericalTolerance)
   at Cmdty.Storage.IntrinsicStorageValuation`1.Cmdty.Storage.IIntrinsicCalculate<T>.Calculate()

Is it possible that I've misunderstood how to specify the constraints?

Rounding Error using time-varying ratchets

Hi,

we've implemented your model in a context where is necessary to have time varying ratchets across the simulation period, in order to reach target filling levels on specific dates.

Depending on the starting inventory level and the ratchets for that time frame, we are incurring into this strange error which seems to be related to some non-well managed rounding operations behind the scenes:

self.results: MultiFactorValuationResults = three_factor_seasonal_value( File "/azureml-envs/azureml_56ff17e21e8cba7e88bd7ac0a5656619/lib/python3.10/site-packages/cmdty_storage/multi_factor.py", line 361, in three_factor_seasonal_value return _net_multi_factor_calc(cmdty_storage, fwd_curve, interest_rates, inventory, add_multi_factor_sim, File "/azureml-envs/azureml_56ff17e21e8cba7e88bd7ac0a5656619/lib/python3.10/site-packages/cmdty_storage/multi_factor.py", line 460, in _net_multi_factor_calc intrinsic_result = cs_intrinsic.net_intrinsic_calc(cmdty_storage, net_current_period, net_interest_rate_time_series, File "/azureml-envs/azureml_56ff17e21e8cba7e88bd7ac0a5656619/lib/python3.10/site-packages/cmdty_storage/intrinsic.py", line 82, in net_intrinsic_calc net_val_results = net_cs.IIntrinsicCalculate[time_period_type](intrinsic_calc).Calculate() System.ArgumentException: Inventory of 6872.954213832093 is below minimum allowed value of 6873 during period 2021-11-01. (Parameter 'inventory') at Cmdty.Storage.CmdtyStorage1.GetInjectWithdrawRange(T date, Double inventory) in C:\Users\Jake\source\repos\storage\src\Cmdty.Storage\StorageEntity\CmdtyStorage.cs:line 94
at Cmdty.Storage.StorageHelper.CalculateInventorySpace[T](ICmdtyStorage1 storage, Double startingInventory, T currentPeriod) in C:\Users\Jake\source\repos\storage\src\Cmdty.Storage\StorageHelper.cs:line 42 at Cmdty.Storage.IntrinsicStorageValuation1.Calculate(T currentPeriod, Double startingInventory, TimeSeries2 forwardCurve, ICmdtyStorage1 storage, Func2 settleDateRule, Func3 discountFactors, Func2 gridCalcFactory, IInterpolatorFactory interpolatorFactory, Double numericalTolerance) in C:\Users\Jake\source\repos\storage\src\Cmdty.Storage\IntrinsicValuation\IntrinsicStorageValuation.cs:line 154 at Cmdty.Storage.IntrinsicStorageValuation1.Cmdty.Storage.IIntrinsicCalculate.Calculate() in C:\Users\Jake\source\repos\storage\src\Cmdty.Storage\IntrinsicValuation\IntrinsicStorageValuation.cs:line 116
`
We tried to use RatchetInterp.STEP, but this is apparently failing whenever ratchets are time-varying.

Any idea to prevent this error?

Power Storage Simulation

Hi all,

Thank you for this great job. I am trying to use it to optimize and value power storage facilities hourly, but I am facing two issues.

  1. Cycling constraint: I have to limit the absolute overall amount of energy injected and withdrawn (sum of both) over a period of time, e.g., 24 hrs or a year (8760 hrs). Any idea how I can model it?

Example: t1: injection = 1; t2: withdrawing = 1 means the total absolute energy used over the two periods is 2

  1. Hourly Forward Curve in the Multifactor model: I have an hourly forward curve, but I do not know how to calibrate the model and which basis function to use since all the examples use a daily curve. Do you know where I can find an example using an hourly power curve?

Thank you for the help

InventoryConstraintsCannotBeFulfilledException on minimal min_inventory

InventoryConstraintsCannotBeFulfilledException: Valuation calculation cannot be performed as storage inventory constraints cannot be fulfilled.
at Cmdty.Storage.StorageHelper.CalculateInventorySpace[T](ICmdtyStorage1 storage, Double startingInventory, T currentPeriod) at Cmdty.Storage.IntrinsicStorageValuation1.Calculate(T currentPeriod, Double startingInventory, TimeSeries2 forwardCurve, ICmdtyStorage1 storage, Func2 settleDateRule, Func3 discountFactors, Func2 gridCalcFactory, IInterpolatorFactory interpolatorFactory, Double numericalTolerance) at Cmdty.Storage.IntrinsicStorageValuation1.Cmdty.Storage.IIntrinsicCalculate.Calculate()

With following time-varying inventory, setting min_inventory to even 3% of max_inventory, 6 month after storage_start is failing the constraints.

Code:
max_inventory = 1.0 # MWh

injection_duration = 100.0 # days
withdrawal_duration = injection_duration / 2.0 # days

max_injection_rate = max_inventory / injection_duration # MWh / day
max_withdrawal_rate = max_inventory / withdrawal_duration # MWh / day

yearly_period_index = pd.PeriodIndex(freq='D', data = ['2023-04-01', '2023-10-01', '2024-04-01'])

storage_time_varying = CmdtyStorage(
freq='D',
storage_start = '2023-04-01',
storage_end = '2024-04-01',
injection_cost = 0.0,
withdrawal_cost = 0.0,
min_inventory = pd.Series([0.0, 0.03 * max_inventory, 0.0], yearly_period_index).resample('D').fillna('pad'),
max_inventory = pd.Series([max_inventory, max_inventory, max_inventory], yearly_period_index).resample('D').fillna('pad'),
max_injection_rate = max_injection_rate,
max_withdrawal_rate = max_withdrawal_rate
)

iv_results = intrinsic_value(
cmdty_storage = storage_time_varying,
val_date = storage_time_varying.start,
inventory = 0 * max_inventory,
forward_curve = fwd_curve,
interest_rates = ir_curve,
settlement_rule = settlement_rule)

Standard Error Bug

Some testing found that the standard error calculation contains a bug. As antithetic variance reduction is used, all PV sims are not independent hence the standard error calculation is incorrect. The solution is to take the average of all pairs of PVs calculated from the same random numbers and then calculate standard error as usual.

Volatility and Correlation of Traded Forward Contracts

The documentation, and implementation in MultiFactorModel, only derive the integrated covariance of (log of) forwards at a uniform granularity, which will generally be the highest possible granularity of traded contracts assumed by the model, and the granularity at which physical storage nomination occurs. For example for natural gas, when valuing storage with day-ahead nomination, the granularity will be daily. However, for calibration and simulation purposes it would be useful to approximate the covariances for contracts at the actual traded granularity, e.g. monthly, quarter, seasonal and yearly.

One approach would be to use moment-matching, i.e. find the parameters of a single GBM (Geometric Brownian Motion) which matches the moments of a basket of GBMs.

For 1 and 2 factor models, something like Jamshidian Trick could be used to calculate the PV of options, which in turn could be used to back out the implied volatility of the traded contract which are implied by a specific set of model parameters.

Remove .NET Dependency

The dependency on .NET, is in turn causing a dependency on the Python.NET package for Python to .NET interop. However, this is getting in the way of running cmdty-storage on Linux as it requires Mono (and last I checked, an old version of Mono). Perversely, running the Cmdty.Storage C# package on Linux is easier, as it just requires .NET Core installed, than running the Python package!

Probably the storage calculations would run too slowly if implemented in CPython, which means the following options should be considered:

  • Writing the intensive calculations in C and using the Python ctypes model.
  • Cython.
  • Numba.

Tidy Up Python Build/CI/Test Dependencies and Fix Versions

The python dependencies for build/ci/test is a bit of a mess, being found in a requirements.txt, build.cake and azure-pipelines.yml. This should be consolidated and tidied up into requirement.txt files with specific versions specified.

Fixing numerical tolerance

Hi and thx for this model.

I am making a simpel model when I change max_injection rate from xxx to xxxx i.e 2500

min_inventory = 0.0,
max_inventory = 500000.0,
max_injection_rate = 2500.5,
max_withdrawal_rate = 3000.9

then I get following error but it says it can be fixed by increasing numerical tolerance but where can I fix it ?

Inventory constraints cannot be fulfilled. This could potentially be fixed by increasing the numerical tolerance.
ved Cmdty.Storage.StorageHelper.CalculateBangBangDecisionSet(InjectWithdrawRange injectWithdrawRange, Double currentInventory, Double inventoryLoss, Double nextStepMinInventory, Double nextStepMaxInventory, Double numericalTolerance, Int32 numExtraDecisions)
ved Cmdty.Storage.IntrinsicStorageValuation1.OptimalDecisionAndValue(ICmdtyStorage1 storage, T period, Double inventory, Double nextStepInventorySpaceMin, Double nextStepInventorySpaceMax, Double cmdtyPrice, Func2 continuationValueByInventory, Double discountFactorFromCmdtySettlement, Func2 discountFactors, Double numericalTolerance)
ved Cmdty.Storage.IntrinsicStorageValuation1.Calculate(T currentPeriod, Double startingInventory, TimeSeries2 forwardCurve, ICmdtyStorage1 storage, Func2 settleDateRule, Func3 discountFactors, Func2 gridCalcFactory, IInterpolatorFactory interpolatorFactory, Double numericalTolerance)
ved Cmdty.Storage.IntrinsicStorageValuation`1.Cmdty.Storage.IIntrinsicCalculate.Calculate()

storage_gui has injection and withdrawal rate reverse to the ratchet storage definition

### In storage_gui.py:

RatchetRow = namedtuple('RatchetRow', ['date', 'inventory', 'inject_rate', 'withdraw_rate'])

def enumerate_ratchets():
    ratchet_row = 0
    while ratchet_row < num_ratch_rows and ratchet_input_sheet.cells[1].value[ratchet_row] != '':
        yield RatchetRow(ratchet_input_sheet.cells[0].value[ratchet_row], ratchet_input_sheet.cells[1].value[ratchet_row],
                         ratchet_input_sheet.cells[2].value[ratchet_row], ratchet_input_sheet.cells[3].value[ratchet_row])
        ratchet_row += 1


def read_ratchets():
    ratchets = []
    for ratchet in enumerate_ratchets():
        if ratchet.date != '':
            dt_item = (pd.Period(ratchet.date, freq=freq), [(ratchet.inventory, -ratchet.inject_rate, ratchet.withdraw_rate)])
            ratchets.append(dt_item)
        else:
            dt_item[1].append((ratchet.inventory, -ratchet.inject_rate, ratchet.withdraw_rate))
    return ratchets

### In creating_storage_instance notebook:
The second element of the tuple as an iterable of tuples, each with three elements:

The first element is the inventory.
The second element is the maximum withdrawal rate.
The third element is the maximum injection rate.

I am able to match the results to simple storage by flipping them around in the storage_gui.

Create Separate Platform Specific Python Packages With Correct Native Binaries

Currently there is one Python package which will be used for all OS and architecture. However, the MathNet (including MKL) binaries included are Windows-specific. This won't cause an error on non-Windows OS, it just means that on non-Windows the matrix routines will fall back to those implemented in C#, so will be slower.

Use Conda Package Manager

Update build.cake to create Python Conda package during CI, and publish to conda-forge on release. Also updated Python testing task in build.cake to perform tests in a Conda environment.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.