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

#> 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