25. Commodity Prices#
25.1. Outline#
For more than half of all countries around the globe, commodities account for the majority of total exports.
Examples of commodities include copper, diamonds, iron ore, lithium, cotton and coffee beans.
In this lecture we give an introduction to the theory of commodity prices.
The lecture is quite advanced relative to other lectures in this series.
We need to compute an equilibrium, and that equilibrium is described by a price function.
We will solve an equation where the price function is the unknown.
This is harder than solving an equation for an unknown number, or vector.
The lecture will discuss one way to solve a “functional equation” for an unknown function
For this lecture we need the yfinance
library.
!pip install yfinance
Show output
Collecting yfinance
Obtaining dependency information for yfinance from https://files.pythonhosted.org/packages/1b/0f/77716aa9dd84bb1aa5e93c87122af1de89697b6231f6d01d58d4e7c03c14/yfinance0.2.36py2.py3noneany.whl.metadata
Downloading yfinance0.2.36py2.py3noneany.whl.metadata (11 kB)
Requirement already satisfied: pandas>=1.3.0 in /usr/share/miniconda3/envs/quantecon/lib/python3.11/sitepackages (from yfinance) (2.0.3)
Requirement already satisfied: numpy>=1.16.5 in /usr/share/miniconda3/envs/quantecon/lib/python3.11/sitepackages (from yfinance) (1.24.3)
Requirement already satisfied: requests>=2.31 in /usr/share/miniconda3/envs/quantecon/lib/python3.11/sitepackages (from yfinance) (2.31.0)
Collecting multitasking>=0.0.7 (from yfinance)
Downloading multitasking0.0.11py3noneany.whl (8.5 kB)
Requirement already satisfied: lxml>=4.9.1 in /usr/share/miniconda3/envs/quantecon/lib/python3.11/sitepackages (from yfinance) (4.9.3)
Requirement already satisfied: appdirs>=1.4.4 in /usr/share/miniconda3/envs/quantecon/lib/python3.11/sitepackages (from yfinance) (1.4.4)
Requirement already satisfied: pytz>=2022.5 in /usr/share/miniconda3/envs/quantecon/lib/python3.11/sitepackages (from yfinance) (2023.3.post1)
Collecting frozendict>=2.3.4 (from yfinance)
Downloading frozendict2.4.0.tar.gz (314 kB)
?25l ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 0.0/314.6 kB ? eta ::
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 314.6/314.6 kB 15.5 MB/s eta 0:00:00
?25h
Installing build dependencies ... ?25l
\

/
done
?25h Getting requirements to build wheel ... ?25l done
?25h Preparing metadata (pyproject.toml) ... ?25l
done
?25hCollecting peewee>=3.16.2 (from yfinance)
Downloading peewee3.17.1.tar.gz (3.0 MB)
?25l ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 0.0/3.0 MB ? eta ::
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 3.0/3.0 MB 93.6 MB/s eta 0:00:00
?25h
Installing build dependencies ... ?25l
\

done
?25h Getting requirements to build wheel ... ?25l
done
?25h Preparing metadata (pyproject.toml) ... ?25l
done
?25hRequirement already satisfied: beautifulsoup4>=4.11.1 in /usr/share/miniconda3/envs/quantecon/lib/python3.11/sitepackages (from yfinance) (4.12.2)
Collecting html5lib>=1.1 (from yfinance)
Downloading html5lib1.1py2.py3noneany.whl (112 kB)
?25l ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 0.0/112.2 kB ? eta ::
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 112.2/112.2 kB 48.4 MB/s eta 0:00:00
?25hRequirement already satisfied: soupsieve>1.2 in /usr/share/miniconda3/envs/quantecon/lib/python3.11/sitepackages (from beautifulsoup4>=4.11.1>yfinance) (2.4)
Requirement already satisfied: six>=1.9 in /usr/share/miniconda3/envs/quantecon/lib/python3.11/sitepackages (from html5lib>=1.1>yfinance) (1.16.0)
Requirement already satisfied: webencodings in /usr/share/miniconda3/envs/quantecon/lib/python3.11/sitepackages (from html5lib>=1.1>yfinance) (0.5.1)
Requirement already satisfied: pythondateutil>=2.8.2 in /usr/share/miniconda3/envs/quantecon/lib/python3.11/sitepackages (from pandas>=1.3.0>yfinance) (2.8.2)
Requirement already satisfied: tzdata>=2022.1 in /usr/share/miniconda3/envs/quantecon/lib/python3.11/sitepackages (from pandas>=1.3.0>yfinance) (2023.3)
Requirement already satisfied: charsetnormalizer<4,>=2 in /usr/share/miniconda3/envs/quantecon/lib/python3.11/sitepackages (from requests>=2.31>yfinance) (2.0.4)
Requirement already satisfied: idna<4,>=2.5 in /usr/share/miniconda3/envs/quantecon/lib/python3.11/sitepackages (from requests>=2.31>yfinance) (3.4)
Requirement already satisfied: urllib3<3,>=1.21.1 in /usr/share/miniconda3/envs/quantecon/lib/python3.11/sitepackages (from requests>=2.31>yfinance) (1.26.16)
Requirement already satisfied: certifi>=2017.4.17 in /usr/share/miniconda3/envs/quantecon/lib/python3.11/sitepackages (from requests>=2.31>yfinance) (2023.7.22)
Downloading yfinance0.2.36py2.py3noneany.whl (72 kB)
?25l ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 0.0/72.4 kB ? eta ::
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 72.4/72.4 kB 32.2 MB/s eta 0:00:00
?25h
Building wheels for collected packages: frozendict, peewee
Building wheel for frozendict (pyproject.toml) ... ?25l
done
?25h Created wheel for frozendict: filename=frozendict2.4.0py3noneany.whl size=15425 sha256=9b8af233b0d797481d2e55d12fe823f39dbe5b2f9ebd009fb7012c7c7841fe70
Stored in directory: /home/runner/.cache/pip/wheels/31/dd/81/a814e6f8cde8a1bbc1f088fdc273943371f10478b91a605e14
Building wheel for peewee (pyproject.toml) ... ?25l
\

done
?25h Created wheel for peewee: filename=peewee3.17.1cp311cp311linux_x86_64.whl size=272799 sha256=c9b4512a7ae3149087f5af539ce1958e577ca008f25cff77479f8a87f8ccb9a3
Stored in directory: /home/runner/.cache/pip/wheels/33/d2/ca/79b9807826bc7ef0b86a1ee28c372daaf073f9aa8756eedd7f
Successfully built frozendict peewee
Installing collected packages: peewee, multitasking, html5lib, frozendict, yfinance
Successfully installed frozendict2.4.0 html5lib1.1 multitasking0.0.11 peewee3.17.1 yfinance0.2.36
We will use the following imports
import numpy as np
import yfinance as yf
import matplotlib.pyplot as plt
from scipy.interpolate import interp1d
from scipy.optimize import brentq
from scipy.stats import beta
25.2. Data#
The figure below shows the price of cotton in USD since the start of 2016.
Show source
s = yf.download('CT=F', '201611', '202341')['Adj Close']
Show output
[*********************100%%**********************] 1 of 1 completed
Show source
fig, ax = plt.subplots()
ax.plot(s, marker='o', alpha=0.5, ms=1)
ax.set_ylabel('price', fontsize=12)
ax.set_xlabel('date', fontsize=12)
plt.show()
The figure shows surprisingly large movements in the price of cotton.
What causes these movements?
In general, prices depend on the choices and actions of
suppliers,
consumers, and
speculators.
Our focus will be on the interaction between these parties.
We will connect them together in a dynamic model of supply and demand, called the competitive storage model.
This model was developed by [Sam71], [WW82], [SS83], [DL92], [DL96], and [CB96].
25.3. The competitive storage model#
In the competitive storage model, commodities are assets that
can be traded by speculators and
have intrinsic value to consumers.
Total demand is the sum of consumer demand and demand by speculators.
Supply is exogenous, depending on “harvests”.
Note
These days, goods such as basic computer chips and integrated circuits are often treated as commodities in financial markets, being highly standardized, and, for these kinds of commodities, the word “harvest” is not appropriate.
Nonetheless, we maintain it for simplicity.
The equilibrium price is determined competitively.
It is a function of the current state (which determines current harvests and predicts future harvests).
25.4. The model#
Consider a market for a single commodity, whose price is given at \(t\) by \(p_t\).
The harvest of the commodity at time \(t\) is \(Z_t\).
We assume that the sequence \(\{ Z_t \}_{t \geq 1}\) is IID with common density function \(\phi\).
Speculators can store the commodity between periods, with \(I_t\) units purchased in the current period yielding \(\alpha I_t\) units in the next.
Here \(\alpha \in (0,1)\) is a depreciation rate for the commodity.
For simplicity, the risk free interest rate is taken to be zero, so expected profit on purchasing \(I_t\) units is
Here \(\mathbb{E}_t \, p_{t+1}\) is the expectation of \(p_{t+1}\) taken at time \(t\).
25.5. Equilibrium#
In this section we define the equilibrium and discuss how to compute it.
25.5.1. Equilibrium conditions#
Speculators are assumed to be risk neutral, which means that they buy the commodity whenever expected profits are positive.
As a consequence, if expected profits are positive, then the market is not in equilibrium.
Hence, to be in equilibrium, prices must satisfy the “noarbitrage” condition
Profit maximization gives the additional condition
We also require that the market clears in each period.
We assume that consumers generate demand quantity \(D(p)\) corresponding to price \(p\).
Let \(P := D^{1}\) be the inverse demand function.
Regarding quantities,
supply is the sum of carryover by speculators and the current harvest
demand is the sum of purchases by consumers and purchases by speculators.
Mathematically,
supply \( = X_t = \alpha I_{t1} + Z_t\), which takes values in \(S := \mathbb R_+\), while
demand \( = D(p_t) + I_t\)
Thus, the market equilibrium condition is
The initial condition \(X_0 \in S\) is treated as given.
25.5.2. An equilibrium function#
How can we find an equilibrium?
Our path of attack will be to seek a system of prices that depend only on the current state.
In other words, we take a function \(p\) on \(S\) and set \(p_t = p(X_t)\) for every \(t\).
Prices and quantities then follow
We choose \(p\) so that these prices and quantities satisfy the equilibrium conditions above.
More precisely, we seek a \(p\) such that (25.1) and (25.2) hold for the corresponding system (25.4).
To this end, suppose that there exists a function \(p^*\) on \(S\) satisfying
where
It turns out that such a \(p^*\) will suffice, in the sense that (25.1) and (25.2) hold for the corresponding system (25.4).
To see this, observe first that
Thus (25.1) requires that
This inequality is immediate from (25.5).
Second, regarding (25.2), suppose that
Then by (25.5) we have \(p^*(X_t) = P(X_t)\)
But then \(D(p^*(X_t)) = X_t\) and \(I_t = I(X_t) = 0\).
As a consequence, both (25.1) and (25.2) hold.
We have found an equilibrium.
25.5.3. Computing the equilibrium#
We now know that an equilibrium can be obtained by finding a function \(p^*\) that satisfies (25.5).
It can be shown that, under mild conditions there is exactly one function on \(S\) satisfying (25.5).
Moreover, we can compute this function using successive approximation.
This means that we start with a guess of the function and then update it using (25.5).
This generates a sequence of functions \(p_1, p_2, \ldots\)
We continue until this process converges, in the sense that \(p_k\) and \(p_{k+1}\) are very close together.
Then we take the final \(p_k\) that we computed as our approximation of \(p^*\).
To implement our update step, it is helpful if we put (25.5) and (25.6) together.
This leads us to the update rule
In other words, we take \(p_k\) as given and, at each \(x\), solve for \(q\) in
Actually we can’t do this at every \(x\), so instead we do it on a grid of points \(x_1, \ldots, x_n\).
Then we get the corresponding values \(q_1, \ldots, q_n\).
Then we compute \(p_{k+1}\) as the linear interpolation of the values \(q_1, \ldots, q_n\) over the grid \(x_1, \ldots, x_n\).
Then we repeat, seeking convergence.
25.6. Code#
The code below implements this iterative process, starting from \(p_0 = P\).
The distribution \(\phi\) is set to a shifted Beta distribution (although many other choices are possible).
The integral in (25.8) is computed via Monte Carlo.
α, a, c = 0.8, 1.0, 2.0
beta_a, beta_b = 5, 5
mc_draw_size = 250
gridsize = 150
grid_max = 35
grid = np.linspace(a, grid_max, gridsize)
beta_dist = beta(5, 5)
Z = a + beta_dist.rvs(mc_draw_size) * c # Shock observations
D = P = lambda x: 1.0 / x
tol = 1e4
def T(p_array):
new_p = np.empty_like(p_array)
# Interpolate to obtain p as a function.
p = interp1d(grid,
p_array,
fill_value=(p_array[0], p_array[1]),
bounds_error=False)
# Update
for i, x in enumerate(grid):
h = lambda q: q  max(α * np.mean(p(α * (x  D(q)) + Z)), P(x))
new_p[i] = brentq(h, 1e8, 100)
return new_p
fig, ax = plt.subplots()
price = P(grid)
ax.plot(grid, price, alpha=0.5, lw=1, label="inverse demand curve")
error = tol + 1
while error > tol:
new_price = T(price)
error = max(np.abs(new_price  price))
price = new_price
ax.plot(grid, price, 'k', alpha=0.5, lw=2, label=r'$p^*$')
ax.legend()
ax.set_xlabel('$x$', fontsize=12)
plt.show()
The figure above shows the inverse demand curve \(P\), which is also \(p_0\), as well as our approximation of \(p^*\).
Once we have an approximation of \(p^*\), we can simulate a time series of prices.
# Turn the price array into a price function
p_star = interp1d(grid,
price,
fill_value=(price[0], price[1]),
bounds_error=False)
def carry_over(x):
return α * (x  D(p_star(x)))
def generate_cp_ts(init=1, n=50):
X = np.empty(n)
X[0] = init
for t in range(n1):
Z = a + c * beta_dist.rvs()
X[t+1] = carry_over(X[t]) + Z
return p_star(X)
fig, ax = plt.subplots()
ax.plot(generate_cp_ts(), label="price")
ax.set_xlabel("time")
ax.legend()
plt.show()