Gold#
…
#> Libraries
import numpy as np
import pandas as pd
# from IPython.display import display # import not required, maybe
import plotly.express as px
import plotly.graph_objects as go
import plotly.io as pio
from plotly.subplots import make_subplots
import yfinance as yf
#> Mount GDrive if required - for Colab editing
from pathlib import Path
# Detect Colab and mount Drive if needed
try:
from google.colab import drive
IN_COLAB = True
pio.renderers.default = "colab"
except ImportError:
IN_COLAB = False
pio.renderers.default = "iframe"
if IN_COLAB:
drive.mount('/content/drive')
%cd /content/drive/MyDrive/basics-books/repos/financial-edu/bbooks-financial-edu/ch/history
FILE_ROOT = Path.cwd()
print("Repo root:", FILE_ROOT)
Repo root: /home/davide/Documents/basics-books/books/bbooks-financial-edu/ch/history
#> Import Shiller data (.xls)
shiller_filen = str(FILE_ROOT) + "/../../code/data/shiller/ie_data.xlsx"
df_shiller = pd.read_excel(shiller_filen, sheet_name="Data", skiprows=7) #, engine="xlrd")
#> Import gold data: monthly prices (.csv)
gold_filen = str(FILE_ROOT) + "/../../code/data/gold/monthly.csv"
df_gold = pd.read_csv(gold_filen,)
#> Clean data
#> Shiller
#> Remove non-data last row
df_shiller = df_shiller.iloc[:-1].copy()
#> Convert Data to proper datetime type
df_shiller["Date"] = df_shiller["Date"].map(lambda x: f"{x:.2f}")
df_shiller["Date"] = pd.to_datetime( df_shiller["Date"], format="%Y.%m")
#> Gold
df_gold["Date"] = pd.to_datetime( df_gold["Date"], format="%Y-%m")
#> Inspection
display(df_shiller.head())
display(df_shiller.tail())
display(df_gold.head())
display(df_gold.tail())
| Date | P | D | E | CPI | Fraction | Rate GS10 | Price | Dividend | Price.1 | ... | CAPE | Unnamed: 13 | TR CAPE | Unnamed: 15 | Yield | Returns | Returns.1 | Real Return | Real Return.1 | Returns.2 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1871-01-01 | 4.44 | 0.26 | 0.4 | 12.464061 | 1871.041667 | 5.32 | 114.692610 | 6.716234 | 114.692610 | ... | NaN | NaN | NaN | NaN | NaN | 1.004177 | 1.000000 | 0.130609 | 0.092504 | 0.038106 |
| 1 | 1871-02-01 | 4.5 | 0.26 | 0.4 | 12.844641 | 1871.125000 | 5.323333 | 112.798304 | 6.517235 | 113.341406 | ... | NaN | NaN | NaN | NaN | NaN | 1.004180 | 0.974424 | 0.130858 | 0.094635 | 0.036224 |
| 2 | 1871-03-01 | 4.61 | 0.26 | 0.4 | 13.034972 | 1871.208333 | 5.326667 | 113.868307 | 6.422074 | 114.954311 | ... | NaN | NaN | NaN | NaN | NaN | 1.004183 | 0.964209 | 0.130951 | 0.096186 | 0.034765 |
| 3 | 1871-04-01 | 4.74 | 0.26 | 0.4 | 12.559226 | 1871.291667 | 5.33 | 121.514327 | 6.665343 | 123.233997 | ... | NaN | NaN | NaN | NaN | NaN | 1.004185 | 1.004919 | 0.122056 | 0.090972 | 0.031084 |
| 4 | 1871-05-01 | 4.86 | 0.26 | 0.4 | 12.273812 | 1871.375000 | 5.333333 | 127.487866 | 6.820339 | 129.868479 | ... | NaN | NaN | NaN | NaN | NaN | 1.004188 | 1.032591 | 0.122638 | 0.089488 | 0.033150 |
5 rows × 22 columns
| Date | P | D | E | CPI | Fraction | Rate GS10 | Price | Dividend | Price.1 | ... | CAPE | Unnamed: 13 | TR CAPE | Unnamed: 15 | Yield | Returns | Returns.1 | Real Return | Real Return.1 | Returns.2 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1850 | 2025-03-01 | 5683.983333 | 76.145301 | 216.69 | 319.799 | 2025.208333 | 4.28 | 5722.525411 | 76.661629 | 3845325.219 | ... | 34.784750 | NaN | 37.348615 | NaN | 0.016749 | 1.003567 | 39.625790 | NaN | NaN | NaN |
| 1851 | 2025-04-01 | 5369.495714 | 76.546867 | NaN | 320.795 | 2025.291667 | 4.28 | 5389.121125 | 76.826645 | 3625591.938 | ... | 32.621270 | NaN | 35.015341 | NaN | 0.018767 | 0.992343 | 39.643654 | NaN | NaN | NaN |
| 1852 | 2025-05-01 | 5810.919524 | 76.948434 | NaN | 321.465 | 2025.375000 | 4.42 | 5820.002899 | 77.068716 | 3919793.241 | ... | 35.172023 | NaN | 37.769849 | NaN | 0.014834 | 1.006896 | 39.258095 | NaN | NaN | NaN |
| 1853 | 2025-06-01 | 6029.9515 | 77.350000 | NaN | 321.8 | 2025.458333 | 4.38 | 6033.090148 | 77.390261 | 4067651.835 | ... | 36.394510 | NaN | 39.098992 | NaN | 0.014026 | 1.010099 | 39.487674 | NaN | NaN | NaN |
| 1854 | 2025-07-01 | 6279.35 | NaN | NaN | 321.9675 | 2025.541667 | 4.3 | 6279.350000 | NaN | 4233686.042 | ... | 37.806545 | NaN | 40.590130 | NaN | 0.013847 | NaN | 39.865727 | NaN | NaN | NaN |
5 rows × 22 columns
| Date | Price | |
|---|---|---|
| 0 | 1833-01-01 | 18.93 |
| 1 | 1833-02-01 | 18.93 |
| 2 | 1833-03-01 | 18.93 |
| 3 | 1833-04-01 | 18.93 |
| 4 | 1833-05-01 | 18.93 |
| Date | Price | |
|---|---|---|
| 2312 | 2025-09-01 | 3836.23 |
| 2313 | 2025-10-01 | 4002.77 |
| 2314 | 2025-11-01 | 4217.36 |
| 2315 | 2025-12-01 | 4322.26 |
| 2316 | 2026-01-01 | 5024.17 |
#> Merge Shiller and gold data
df_gold.rename(columns={"Price": "Gold"}, inplace=True)
df = df_gold.merge(df_shiller, on="Date", how="left")
#> Filter by date
mask = df['Date'].between('1871-01', '2025-07')
df = df[mask]
#> Add a column with gold real value (today value)
base_cpi = df['CPI'].iloc[-1]
print(base_cpi)
df['Gold Real'] = (df['Gold'] / df['CPI']) * base_cpi
321.9675
display(df.head())
display(df.tail(10))
| Date | Gold | P | D | E | CPI | Fraction | Rate GS10 | Price | Dividend | ... | Unnamed: 13 | TR CAPE | Unnamed: 15 | Yield | Returns | Returns.1 | Real Return | Real Return.1 | Returns.2 | Gold Real | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 456 | 1871-01-01 | 18.93 | 4.44 | 0.26 | 0.4 | 12.464061 | 1871.041667 | 5.32 | 114.692610 | 6.716234 | ... | NaN | NaN | NaN | NaN | 1.004177 | 1.000000 | 0.130609 | 0.092504 | 0.038106 | 488.993491 |
| 457 | 1871-02-01 | 18.93 | 4.5 | 0.26 | 0.4 | 12.844641 | 1871.125000 | 5.323333 | 112.798304 | 6.517235 | ... | NaN | NaN | NaN | NaN | 1.004180 | 0.974424 | 0.130858 | 0.094635 | 0.036224 | 474.504863 |
| 458 | 1871-03-01 | 18.93 | 4.61 | 0.26 | 0.4 | 13.034972 | 1871.208333 | 5.326667 | 113.868307 | 6.422074 | ... | NaN | NaN | NaN | NaN | 1.004183 | 0.964209 | 0.130951 | 0.096186 | 0.034765 | 467.576365 |
| 459 | 1871-04-01 | 18.93 | 4.74 | 0.26 | 0.4 | 12.559226 | 1871.291667 | 5.33 | 121.514327 | 6.665343 | ... | NaN | NaN | NaN | NaN | 1.004185 | 1.004919 | 0.122056 | 0.090972 | 0.031084 | 485.28823 |
| 460 | 1871-05-01 | 18.93 | 4.86 | 0.26 | 0.4 | 12.273812 | 1871.375000 | 5.333333 | 127.487866 | 6.820339 | ... | NaN | NaN | NaN | NaN | 1.004188 | 1.032591 | 0.122638 | 0.089488 | 0.033150 | 496.573109 |
5 rows × 24 columns
| Date | Gold | P | D | E | CPI | Fraction | Rate GS10 | Price | Dividend | ... | Unnamed: 13 | TR CAPE | Unnamed: 15 | Yield | Returns | Returns.1 | Real Return | Real Return.1 | Returns.2 | Gold Real | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 2301 | 2024-10-01 | 2690.08 | 5792.319565 | 73.877418 | 203.570000 | 315.664 | 2024.791667 | 4.1 | 5907.986497 | 75.352678 | ... | NaN | 39.375226 | NaN | 0.015221 | 0.982514 | 40.006731 | NaN | NaN | NaN | 2743.798255 |
| 2302 | 2024-11-01 | 2651.13 | 5929.916 | 74.354836 | 206.870000 | 315.493 | 2024.875000 | 4.36 | 6051.608846 | 75.880735 | ... | NaN | 40.176632 | NaN | 0.012564 | 1.001225 | 39.328482 | NaN | NaN | NaN | 2705.536092 |
| 2303 | 2024-12-01 | 2648.01 | 6010.908571 | 74.832255 | 210.170000 | 315.605 | 2024.958333 | 4.39 | 6132.086645 | 76.340850 | ... | NaN | 40.544845 | NaN | 0.012630 | 0.984603 | 39.362679 | NaN | NaN | NaN | 2701.393069 |
| 2304 | 2025-01-01 | 2709.69 | 5979.5155 | 75.269937 | 212.343333 | 317.671 | 2025.041667 | 4.63 | 6060.388442 | 76.287963 | ... | NaN | 39.899969 | NaN | 0.011804 | 1.018269 | 38.504542 | NaN | NaN | NaN | 2746.338555 |
| 2305 | 2025-02-01 | 2894.73 | 6038.69 | 75.707619 | 214.516667 | 319.082 | 2025.125000 | 4.45 | 6093.298659 | 76.392253 | ... | NaN | 39.943400 | NaN | 0.013572 | 1.017426 | 39.034610 | NaN | NaN | NaN | 2920.90742 |
| 2306 | 2025-03-01 | 2983.25 | 5683.983333 | 76.145301 | 216.690000 | 319.799 | 2025.208333 | 4.28 | 5722.525411 | 76.661629 | ... | NaN | 37.348615 | NaN | 0.016749 | 1.003567 | 39.625790 | NaN | NaN | NaN | 3003.478886 |
| 2307 | 2025-04-01 | 3217.64 | 5369.495714 | 76.546867 | NaN | 320.795 | 2025.291667 | 4.28 | 5389.121125 | 76.826645 | ... | NaN | 35.015341 | NaN | 0.018767 | 0.992343 | 39.643654 | NaN | NaN | NaN | 3229.400417 |
| 2308 | 2025-05-01 | 3309.49 | 5810.919524 | 76.948434 | NaN | 321.465 | 2025.375000 | 4.42 | 5820.002899 | 77.068716 | ... | NaN | 37.769849 | NaN | 0.014834 | 1.006896 | 39.258095 | NaN | NaN | NaN | 3314.66325 |
| 2309 | 2025-06-01 | 3352.66 | 6029.9515 | 77.350000 | NaN | 321.8 | 2025.458333 | 4.38 | 6033.090148 | 77.390261 | ... | NaN | 39.098992 | NaN | 0.014026 | 1.010099 | 39.487674 | NaN | NaN | NaN | 3354.405092 |
| 2310 | 2025-07-01 | 3340.15 | 6279.35 | NaN | NaN | 321.9675 | 2025.541667 | 4.3 | 6279.350000 | NaN | ... | NaN | 40.590130 | NaN | 0.013847 | NaN | 39.865727 | NaN | NaN | NaN | 3340.15 |
10 rows × 24 columns
Price history#
Piecewise gold standard resisted until 1971, when the US abandoned the Bretton Woods system and ended the direct convertibility between gold and the USD.
Before this “Nixon shock”, the price of gold was anchored to the USD for long periods of timethough this anchor was occasionally reset. The most dramatic shifts occurred during WWI to account for war-time currency devaluation, and again in 1933-34. In the latter, Executive Order 6102 and the Gold Reserve Act effectively nationalized private gold and revalued it from \(20.67 to \)35.00 per ounce—a deliberate act of monetary policy to combat the Great Depression by devaluing the dollar.
References
H.McCulloch, World War I, Gold, and the Great Depression, Cato Institute, https://www.cato.org/blog/world-war-i-gold-great-depression
National Mining Association, Historical Gold Prices - 1833 to Present
#> Nominal and real value
fig = make_subplots(rows=2, cols=1, shared_xaxes=True, # vertical_spacing=0.1,
subplot_titles=("Gold price - Nominal", "Gold price - Real",)
)
#> S&P500 index
fig.add_trace(go.Scatter(x=df["Date"], y=df["Gold"], name="Gold price - Nominal", line=dict(color="black")),
row=1, col=1
)
fig.add_trace(go.Scatter(x=df["Date"], y=df["Gold Real"]/df["Gold Real"].iloc[0], name="Gold price - Real, 1871 = 1", line=dict(color="black")),
row=2, col=1
)
fig.update_yaxes(type="log", row=1, col=1)
fig.update_yaxes(type="log", row=2, col=1)
fig.update_layout(showlegend=False,)
fig.show()
# fig = go.Figure()
# fig.add_trace(go.Scatter(x=df_gold['Date'], y=df_gold['Price']))
# fig.update_yaxes(type="log")
# fig.show()
Quantitative Characteristics#
Some statistics…
Return and Volatility#
Since 1971, from the Nixon shock and the end of the convertibility between USD and gold.
#> Get the df since 1971
month_window = 1
year_window = 12
df_1971 = df.copy()
mask = df['Date'].between('1971-01', '2025-07')
df_1971 = df_1971[mask]
#> Evaluate nominal returns
df_1971['Gold Return 1m'] = df_1971['Gold'].pct_change(month_window)
df_1971['Gold Return 1m Annual'] = ( 1. + df_1971['Gold Return 1m'] )**(12/month_window) - 1
df_1971['Gold Return 1y'] = df_1971['Gold'].pct_change( year_window)
df_1971['Gold Vol 1y'] = df_1971['Gold Return 1y'].rolling(window= year_window).std() * np.sqrt(12)
#> Evaluate real returns
df_1971['Gold Real Return 1m'] = df_1971['Gold Real'].pct_change(month_window)
df_1971['Gold Real Return 1m Annual'] = ( 1. + df_1971['Gold Real Return 1m'] )**(12/month_window) - 1
df_1971['Gold Real Return 1y'] = df_1971['Gold Real'].pct_change( year_window)
df_1971['Gold Real Vol 1y'] = df_1971['Gold Real Return 1y'].rolling(window= year_window).std() * np.sqrt(12)
# display(df.tail(10))
#> Nominal returns
fig = make_subplots(rows=2, cols=1, shared_xaxes=True, # vertical_spacing=0.1,
subplot_titles=("Monthly returns", "1-year rolling statistics",)
)
#> S&P500 index
colors_1m = np.where(df_1971['Gold Return 1m'] >= 0, 'green', 'red')
fig.add_trace(
go.Bar(
x=df_1971["Date"], y=df_1971["Gold Return 1m"],
name="1m return (annualized)",
marker_color=colors_1m,
showlegend=False
),
row=1, col=1
)
colors_1y = np.where(df_1971['Gold Return 1y'] >= 0, 'green', 'red')
fig.add_trace(
go.Bar(
x=df_1971["Date"], y=df_1971["Gold Return 1y"],
name="1y return",
marker_color=colors_1y,
showlegend=False
),
row=2, col=1
)
fig.add_trace(go.Scatter(x=df_1971["Date"], y=df_1971["Gold Vol 1y"], name="1y vol",),
row=2, col=1
)
fig.update_layout(title="Nominal Return")
fig.show()
#> Nominal returns
fig = make_subplots(rows=2, cols=1, shared_xaxes=True, # vertical_spacing=0.1,
subplot_titles=("Monthly returns", "1-year rolling statistics",)
)
#> S&P500 index
colors_1m = np.where(df_1971['Gold Real Return 1m'] >= 0, 'green', 'red')
fig.add_trace(
go.Bar(
x=df_1971["Date"], y=df_1971["Gold Real Return 1m"],
name="1m return (annualized)",
marker_color=colors_1m,
showlegend=False
),
row=1, col=1
)
colors_1y = np.where(df_1971['Gold Real Return 1y'] >= 0, 'green', 'red')
fig.add_trace(
go.Bar(
x=df_1971["Date"], y=df_1971["Gold Real Return 1y"],
name="1y return",
marker_color=colors_1y,
showlegend=False
),
row=2, col=1
)
fig.add_trace(go.Scatter(x=df_1971["Date"], y=df_1971["Gold Real Vol 1y"], name="1y vol",),
row=2, col=1
)
fig.update_layout(title="Real Return")
fig.show()
Drawdown history#
from textwrap import fill
#> Calculate drawdowns with rolling max
rolling_max = df_1971['Gold'].cummax()
df_1971['Drawdown'] = (df_1971['Gold'] / rolling_max) - 1
rolling_max = df_1971['Gold Real'].cummax()
df_1971['Drawdown Real'] = (df_1971['Gold Real'] / rolling_max) - 1
#> Plot nominal and real drawdowns
fig_dd = go.Figure()
colors = {
'gold': '255, 215, 0',
'black': '0, 0, 0',
'firebrick': '178, 34, 34',
'royalblue': '65, 105, 225'
}
fig_dd.add_trace(go.Scatter(
x=df_1971['Date'],
y=df_1971['Drawdown Real'],
fill='tozeroy',
fillcolor=f'rgba({colors["black"]}, 0.2)',
mode='lines',
line_color='black',
name='Real'
),
)
fig_dd.add_trace(go.Scatter(
x=df_1971['Date'],
y=df_1971['Drawdown'],
fill='tozeroy',
fillcolor=f'rgba({colors["firebrick"]}, 0.2)',
mode='lines',
line=dict(color='firebrick', dash='dot'),
name='Nominal'
),
)
# 4. Styling
fig_dd.update_layout(
title='Gold Price Drawdown (%)',
yaxis_title='Percentage Decline',
yaxis_tickformat='.1%', # Formats as -5.0%, -10.0%, etc.
template='plotly_white',
# hovermode='x unified'
)
fig_dd.show()
Correlation with other assets#
low correlation with equities and bonds. Always? Example: evaluate the rolling 36-month correlation between gold and other assets.
df_1971["SP500 Total Return 1m"] = df_1971["Price.1"].pct_change(month_window)
#> Calculate 12-Month Rolling Correlation
window = 12 # Starting from monthly data
df_1971['Corr(Gold,SP500)'] = df_1971['Gold Return 1m'].rolling(window).corr(df_1971['SP500 Total Return 1m'])
#> Define equity stress (S&P500 drawdown > 10%)
sp500_max = df_1971['Price.1'].cummax()
df_1971['SP500 Total Drawdown'] = (df_1971['Price.1'] / sp500_max) - 1
stress_mask = df_1971['SP500 Total Drawdown'] < -0.1
starts = df_1971["Date"][stress_mask & ~stress_mask.shift(1).fillna(False)]
ends = df_1971["Date"][stress_mask & ~stress_mask.shift(-1).fillna(False)]
# 3. Plotting with Plotly
fig_corr = go.Figure()
# Gold vs S&P 500 (Solid line)
fig_corr.add_trace(go.Scatter(
x=df_1971['Date'], y=df_1971['Corr(Gold,SP500)'],
mode='lines', name='Gold vs S&P 500',
line=dict(color='rgba(65, 105, 225, 1)', width=2)
))
# # Gold vs DXY (Dashed line for accessibility)
# fig_corr.add_trace(go.Scatter(
# x=df.index, y=df['Corr_Gold_DXY'],
# mode='lines', name='Gold vs US Dollar (DXY)',
# line=dict(color='rgba(178, 34, 34, 1)', width=2, dash='dash')
# ))
# # Add a zero-line for reference
# fig_corr.add_hline(y=0, line_dash="dot", line_color="black", opacity=0.5)
# fig_corr.update_layout(
# title='12-Month Rolling Correlation of Gold',
# yaxis=dict(title='Correlation Coefficient', range=[-1, 1]),
# template='plotly_white',
# hovermode='x unified'
# )
# 4. Add the Background Shading for Equity Drawdowns
# We can loop through the index to find 'stress' clusters
for (start, end) in zip(starts, ends):
fig_corr.add_vrect(
x0=start, x1=end,
fillcolor="rgba(128, 128, 128, 0.2)", # Light gray
layer="below", line_width=0,
# annotation_text="Equity Stress"
)
fig_corr.update_layout(
title='12-month Cross Correlation between gold and other assets',
showlegend=True,
)
fig_corr.show()
Gold as a macro indicator#
relationship with real rates (yield - inflation)
safe heaven in major crisis
currency hedge (correlation between USD and gold)
The role in a portfolio#
Low correlation, see section above
“Safe Heaven”, see section above